Saarsec

saarsec

Schwenk and pwn

ENOWARS 3 WriteUp shittr

Shittr was a Twitter clone and part of ENOWARS 3. It provides user accounts, which can be registered using a username and password. Each user can then send messages, which can either be private or public, as well as set a status message in their profile. A user can also see and like other user’s messages. Private messages of other users are visible, but only appear as long base64-encoded strings.

Vulnerability 1: Shared Secret Key

The first step was understanding how messages are encoded in the code base. After posting a message, the user sees a long base64-encoded string.

Shittr shows a base64-encoded string after posting a message.

We follow the code to see how the message is turned into a base64-encoded string. First we identified the place where the encoding happens. In db.sh there is a function called create_shit, which contains this line:

local s=$(echo "$s" | base64 -w 0 | enc | base64 -w 0)

This looks already quite promising, as we have the base64-encoding in there as well as an encryption function. We can locate the encryption and decryption routines in utils.sh:

# https://stackoverflow.com/a/11454477/8957548
enc() {
    read -r data
    echo "$data" | openssl enc -e -aes-256-ofb -K $(cat "$ENCKEY") 2>/dev/null | base64 -w 0
}

dec() {
    read -r data
    echo "$data" | base64 -d | openssl enc -d -aes-256-ofb -K $(cat "$ENCKEY") 2>/dev/null
}

The enc function also base64-encodes its output, thus our final message which we see as the user is actually double base64-encoded. The openssl call looks normal, so let’s see where the encryption key is coming from. In config.sh we spot these lines:

STATICDIR="./static"
# ...
ENCKEY="$STATICDIR/enc.key"
# ...
if [ ! -f "$ENCKEY" ]
then
    hexdump -n 16 -e '4/4 "%08X" 1 "\n"' /dev/random > "$ENCKEY"
fi

The code specifies the path to the keyfile and generates a new one, if it does not exist yet. The keyfile is located at ./ro/static/enc.key and contains 93F4E7B14FB35801BC3F460F96FA0ACE. Let’s try to decode the message V2dFQVhDWlpERjF0TVMwT1B3PT0= from above into its plaintext again:

$ echo "V2dFQVhDWlpERjF0TVMwT1B3PT0=" | base64 -d | base64 -d | openssl enc -d -aes-256-ofb -K 93F4E7B14FB35801BC3F460F96FA0ACE | base64 -d
iv undefined

The error message stems from openssl and searching for iv in the codebase does not yield any useful information. However, we find a binary named openssl in the ro/bin folder. Using this binary we receive the expected string saarsec. The openssl binary behaves differently depending on the length of its argv[0] argument. It only works, if the argument is short, but calling it with a long absolute path results in garbage output, which cannot be decoded anymore. This weird behaviour cost us a lot of time until we understood why our decoding did not work anymore. Unfortunately, the openssl binary is a large stripped executable, such that we could not check why this was happening or if there are any backdoors in the binary itself.

We noticed, that the enc.key file was not generated by the config.sh script, but instead was installed during the installation of the service from the deb packages. This means, all the teams are sharing the same secret key, and we can use our secret key to decode any private message.

Exploitation

Our first exploit simply looks for all messages on a user’s profile page. If the message looks like a flag, or we can decode the base64-encoded message to a flag, then we submit it. Accessing the profile page also gives us access to the status messages each user can set.

The main difficulty in writing the exploit was that Shittr returns the HTTP status code 1337 for its content pages, but requests does not support custom status codes and instead throws an exception. Fortunately, account registration and login was still possible by setting allow_redirects=False for requests, such that it does not hit the 1337 status code, but already receives the auth cookie. We then download the website using curl, which can handle the status code, by passing the cookie to a new curl process.

The exploit consists of these steps:

  1. Account registration
  2. Account login
  3. Retrieve a list of all users using the /shittrs endpoint.
  4. For each user, visit the profile page at /@username and try to decode all messages.

Patching

We patched the vulnerability by replacing our secret key with a new one, generated with the same command from above:

hexdump -n 16 -e '4/4 "%08X" 1 "\n"' /dev/random > "$ENCKEY"

After a restart, all new messages in the system were encrypted with a different key.

Vulnerability 2: Admin Access

After some time into the CTF, we noticed that other teams started spamming our system with usernames of the form <RANDOM>admin<RANDOM>. Tracing this pattern through the codebase, we find this function:

is_admin() {
    if [[ "$USER" =~ "admin" && -n "$DEBUG" ]]
    then
        ADMIN=1
    else
        ADMIN=0
    fi
}

It tests if the username contains admin via a regex match and if the variable DEBUG contains any value. The username matches, as we have seen, and in the config.sh file DEBUG=1 is set.

Being an admin does not give you access to the private messages of a user. You only get access to the /log endpoint. It contains the list of all requested websites and information about account registration and login. The interesting information in the log are lines like Session is 708423527839774631739000000000. These contain the secret which is used to calculate the auth cookie and thus the user authentication. Everyone in possession of these values can forge session cookies.

Patching

The gameserver never used any accounts with admin in their names. We disabled all admin functionality by replacing ADMIN=1 with ADMIN=0 in the entire code base, thus blocking the log as an information leak.

Vulnerability 3: Weak Authentication Cookies

Based the on the findings of the previous vulnerability, we analyze the session code more. Session handling is part of the db.sh file, with this function being the most relevant one:

generate_session() {
    local rand=$(dd if=/dev/urandom bs=1 count=32 2>/dev/null | xxd |  tr -dc '[:digit:]\n\r' | rev | head -n1)
    local sessid=$(echo "${rand:0:3}" | md5sum | cut -d ' ' -f 1)
    echo $(echo "$1" | md5sum | cut -d ' ' -f 1) > "$SESSIONSDIR/$sessid.session"
    debug "Session is $rand"
    echo "${sessid}"
}

This code generates a random value and uses that to generate the session ID (the auth cookie), by calculating the md5 over parts of it. However, the code only takes the first three digits (${rand:0:3}). This mean that in total there are only 1000 different auth cookies, from md5("000\n") to md5("999\n").

Exploit

The exploit for this vulnerability simply brute forces all possible values for the auth cookie. We do this in a loop for every team and retrieve the page with the current user’s messages at the / endpoint. This was a very successful exploit which gave us many points over the rest of the game.

Patching

We fixed this problem by using the full value of rand as input to md5sum, thus drastically increasing the entropy.

Unknowns: Including Images

There was another functionality in the Shittr service which made us suspicious, but which we could not figure out how to exploit. While sending a message you could embed an image link. If the image link pointed to a http(s) site and contained the string .png, then the service would download the image and replace the URL with an img tag.

The suspicious code can be found in db.sh in the create_shit function:

while read l
do
    [[ ! "$l" =~ .png ]] && continue
    local f="$(echo "$l" | rev | cut -d / -f 1 | rev )"
    local p=$(urldecode "$IMAGESDIR/$f")
    debug curl --silent -k "$l" -o "$p"
    debug $l
    curl --silent -k "$l" -o "$p"
    s=$(echo $(urldecode "$s") | sed -e 's|'"$l"'|XXXIMGXXX|g')
    s=$(htmlEscape "$s")
    s=$(echo "$s" | sed -e 's|XXXIMGXXX|<img src="/images/'"$f"'">|g')
    s=$(urlencode "$s")
    e=1
done < <(echo $(urldecode "$s") |grep -oP '(http.?)?://[\S\[\]:.png]+' | head -n 1)

The code reads the first URL matching the regex (http.?)?://[\S\[\]:.png]+ and if it contains .png, then the if-condition at the top of the while loop will be passed and the file downloaded. The regex has multiple irregularities. For one, it allows you to fully omit the protocol part and start with ://. While curl does support URLs without a protocol part, but only if the :// part is also missing. As such, it did not seem possible to leverage this detail to our benefit. Second, the character set [\S\[\]:.png] contains a . which is unescaped, thus allowing any character. This makes all the other specified character unnecessary and the whole group could just be replaced with a simple .. Lastly, the .png also does not need to appear at the end of the URLs. Any URL is fine, as long as it appears somewhere, even the fragment identifier is an option.

What this feature allows us to do, is download an arbitrary file to another team’s VM. We could use it for a Denial-of-Service attack, downloading large files and filling up the disc. If we could find a way to execute them or use them instead of the included templates, we could use it as part of a remote code execution. However, we did not find any such flaw during the CTF.

Discussion

After fixing these three vulnerabilities, we fixed the flag leaks in our service. At one point, we lost three additional flags over three game rounds, but could not figure out which exploit was used against us. After the three game rounds, the exploits stopped and we did not lose any more defensive points.