Prototype Pollution Attck
原型链污染攻击
JavaScript原型链
原型和原型链
JavaScript没有父类和子类这个概念,也没有类和实例的区分。其中的继承关系则是靠一种叫“原型链”模式来实现继承。
原型的定义和继承
原型的定义:
任何对象都有一个原型对象,这个原型对象由对象的内置属性
proto
指向它的构造函数的prototype
指向的对象,即任何对象都是由一个构造函数创建的
function A(name,age){
this.name = name;
this.age = age;
}
在JS中,声明了一个函数A,然后浏览器就自动在内存中创建一个对象B,A函数默认有一个属性prototype
指向了这个对象B,B就是函数A的原型对象,简称原型。同时,对象B默认有属性constructor
指向函数A。创建一个对象a,对象a会默认有一个属性proto
指向构造函数A的原型对象B.
继承:有对象自然就有继承,JS里面的继承是这样的
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中寻找last_name
- 如果找不到,则在
son.__proto__
中寻找last_name - 如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name - 依次寻找,直到找到
null
结束。比如,Object.prototype
的__proto__
就是null
输出结果:
Name: Melania Trump
每个实例对象(object)都有一个私有属性
proto
指向它的构造函数的原型对象(prototype),每个实例对象还有一个属性constructor
指向原型的构造函数。该原型对象也有一个自己的原型对象proto
,层层向上直到一个对象的原型对象为null
。null
没有原型,它是原型链中的最后一个环节。
总结
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法- 一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性 - 每个构造函数
constructor
都有一个原型对象prototype
- 对象的
__proto__
属性,指向类的原型对象prototype
- JavaScript使用prototype链实现继承机制
原型链污染
原型链的核心就是依赖对象proto
的指向,当访问的属性在该对象不存在时,就会向上从该对象构造函数的prototype
的进行查找,直至查找到Object
的原型null
为止。
由于对象之间存在继承关系,所以当我们要使用或者输出一个变量就会通过原型链向上搜索,当上层没有就会再向上上层搜索,直到指向 null
,若此时还未找到就会返回 undefined
根据prototype
链继承机制,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父类的对象。这种攻击方式就是原型链污染
。
一个例子:
let test = {var: 1}; //test 为一个简单的JS的Object
console.log(test.var); //输出此时test,var的值
test.__proto__.var = 2; //修改test的proto相当于给这Object类增加了一个属性var
console.log(test.var); //由于查找顺序的原因此时的var,仍然为1
let exp = {}; //创建一个Object
console.log(exp.var) // 此时var为2
利用原型链攻击核心思想是:更改__proto__
的值通过可控参数来进行攻击,一般的利用条件如下
- 对象合并
- 对象克隆
这里来一个简单的例子
题目为CTFShowWeb338:
- 下载到源码
- 看一下路由里面的源代码:login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
module.exports = router;
首先router.post('/', require('body-parser').json(),function(req, res, next)
对POST
上传的参数进行了一个JSION
。解析
然后明显看到只要满足secert.ctfshow==='36dboy'
就能读到flag,而secert
是一个空Object
,继续跟进代码发现还有另外一个空对象user
接下来使用了一个utils.copy()
跟进到/utils/common
关键代码如下
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
进行了一个对象克隆
的操作。
- 看完关键代码后答案呼之欲出:当调用
secert.ctfshow
这个属性不存在时,它会从它自己的__proto__
里找。那么现在我们只要让它的__proto__
改变就行。 这里可以通过可控的POST参数req.body
对实例化对象user
的__proto__
进行污染, 从而到达污染Object
的__proto__
的目的,这样我们就能让secert
的__proto__
的值为 ‘36dboy’了。
至于为什么提到了进行了json解析
是因为如果不在JSON解析的情况下递归遍历克隆不到key:__proto__
导致污染失败,在JSON解析的情况下__proto__
才会被认为是一个真正的“键名”,而不代表“原型”,所以在copy()
遍历的时候会存在这个键。感兴趣的可以看看参考文章P神也提到了这一点。
参考: https://meizjm3i.github.io/2018/09/11/JavaScript_Prototype_Pollution/ https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x04