前两天做了hackit的js原型链污染题目,算是首次接触,写一个笔记,做一个知识扩展。
基础知识
1.prototype
和__proto__
的概念及其区别
__proto__
:该属性是所有对象的都内置的属性,用户获取当前对象的原型。这里引入一个概念,javascript中一切皆对象。也就是说我们创建的字符串,数字等任何变量都是一个独立的对象。所谓的对象的原型,可以理解为父类。并且对象会继承原型的属性。
prototype
:在JavaScript中每个函数对象都有prototype
属性。这个属性是一个指针指向函数的原型对象。同上我也是将原型对象理解为父类。每一个原型对象在默认情况下,其构造函数属性指向prototype
属性所在的函数。这句话贼难理解,太绕了,用个例子讲解一下。
1 | test=function(){alert(1)}; |
简单点说也就是函数对象的原型对象的构造函数所指向的函数对象本身。
二者的区别:__proto__
属性任何对象都有,包括函数(函数也是对象)。但是prototype
只有函数对象有。
2.什么是原型链
在javascript中一切皆对象,并且每个对象都有__proto__
属性。这么一来就必然形成一条通过__proto__
属性链接的对象链。这串链条就是原型链。每一个对象的__proto__
属性指向当前对象的原型对象。其上下文关系可理解为继承和被继承的关系。在javascript中,获取属性时,先检查对象本身是否拥有该属性,若没有则转向该对象的原型链上寻找。也可以理解为一个对象原型链上的属性就是该对象的属性。
题目分析
题目源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106const 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('/static', express.static('static'));
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 ??*/
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is <b>flag{redacted}</b>')
user.admintoken = "";
}
else {
res.status(403).send('Forbidden');
}
}
)
app.post('/api', (req, res) => {
var client = req.body;
var winner = null;
matrix[client.row][client.col] = client.data;
console.log(matrix);
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!')
})
逻辑还是比较清晰的,主要实现的功能就是下棋。通过post网站的api接口实现落子。想要获取flag,需要访问admin路径,并且通过身份验证。但是有个问题,user.admintoken
我们是不知道的。当时一直以为是通过时序攻击,逐一比较每一个字符通过其响应时间来获取正确的哈希值,但是并没有成功。
后来看了wp,了解到是原型链污染。其实我更愿意理解为变量覆盖。问题出在这句代码:matrix[client.row][client.col] = client.data;
这里的matrix就是一个数组,client.row、client.col、client.data都是我们传入的json数据。我们可以对matrix数组的元素执行赋值操作。通过前面的原型链相关知识,我们可以知道每一个数组都是对象,有__proto__
属性。并且user也是数组,其也有__proto__
属性。也就是说,user和matrix的原型对象是同一个。在覆盖matrix对象的原型对象的admintoken属性之后,user对象在获取admintoken属性时,在当前对象中是找不到这个属性的。从而转向user的原型链上寻找该属性。
过程如下:1
2
3
4
5
6user=[];
matrix=[];
//这时user和matrix和其原型对象(一共三个对象)都没有admintoken属性
matrix['__proto__']['admintoken']=1; //这一步设置matrix的原型对象的admintoken属性,其值为1。
这样一来,我们便可控制user.admintoken
,可以轻易地通过身份验证。
知识扩展
虽然上文中我一直把对象原型和对象的关系类比为继承关系,但是在具体细节上还是有区别的。在不同语种中的继承也都不尽相同。大体上来说都是子类继承父类的公共属性。这里以python为例做一个简单的分析。
不得不说,python在安全这块做的不错,至少不会像js这样可以对基础数据类型对象的属性进行任意的操作。但是对于自定义类实例化的对象的参数在默认情况下并不是不可写的。并且父对象的一切公有属性都会被子对象继承。在没有特别声明的情况下我们创建的变量都是公有变量。通过子对象可以设置父对象的属性值。代码实例如下:
通过上述方法,我们可以通过设置子类的属性来覆盖其父类的属性,实现变量覆盖。