目录

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.

https://s1.ax1x.com/2022/11/22/zlfHAI.png

继承:有对象自然就有继承,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}`)
  1. 在对象son中寻找last_name
  2. 如果找不到,则在son.__proto__中寻找last_name
  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name
  4. 依次寻找,直到找到null结束。比如,Object.prototype__proto__就是null

输出结果:

Name: Melania Trump

每个实例对象(object)都有一个私有属性proto指向它的构造函数的原型对象(prototype),每个实例对象还有一个属性constructor指向原型的构造函数。该原型对象也有一个自己的原型对象proto,层层向上直到一个对象的原型对象为nullnull 没有原型,它是原型链中的最后一个环节。

https://s1.ax1x.com/2022/11/22/zlfjgS.png

总结

  1. prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
  2. 一个对象的__proto__属性,指向这个对象所在的类的prototype属性
  3. 每个构造函数constructor都有一个原型对象prototype
  4. 对象的__proto__属性,指向类的原型对象prototype
  5. 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:

  1. 下载到源码
  2. 看一下路由里面的源代码: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]
        }
    }
  }

进行了一个对象克隆的操作。

  1. 看完关键代码后答案呼之欲出:当调用secert.ctfshow这个属性不存在时,它会从它自己的__proto__里找。那么现在我们只要让它的__proto__改变就行。 这里可以通过可控的POST参数req.body对实例化对象user__proto__进行污染, 从而到达污染Object__proto__的目的,这样我们就能让secert__proto__的值为 ‘36dboy’了。

https://s1.ax1x.com/2022/11/22/z33AXQ.png

至于为什么提到了进行了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