关不住的“邪恶”代码,mongo-express远程代码执行漏洞分析

近日,奇安信CERT监测到mongo-express远程代码执行漏洞,漏洞编号为CVE-2019-10758。凭借着对Node.js漏洞天生好奇(也是因为我好久没有写类似文章了),便深入跟进了这个漏洞。漏洞原理虽然简单但还是比较有意思的。那么闲话少说,慢慢跟我来看吧。

漏洞简介

先介绍下mongo-express是个什么工具,简单的说,mongo-express是一个MongoDB的客户端。和传统我们经常使用的数据库客户端不同的是,mongo-express是一套基于Web的客户端。它使用Node.js+express(一个Node.js服务器框架)开发,故名mongo-express。很自然的,这个客户端能对建立连接的MongoDB数据库执行任意操作。

此漏洞是因为代码中使用的vm去执行不安全的用户可控的代码导致的远程代码执行。至此我把这个漏洞的概况先抛出来,我们继续看。

环境搭建

阅读官方GitHub的安全公告,我们发现漏洞影响0.54.0以下的所有版本,打开DockerHub搜索mongo-express,在官方镜像页面中的tag中可随意选择0.54.0以下的版本进行环境搭建。我们以0.49为例。由于此漏洞环境还需要MongoDB数据库,我们可以通过执行以下docker命令进行快速搭建:

  • 搭建MongoDB数据库
docker run --name mymongo -d mongo:3.2
  • 搭建包含漏洞的mongo-express并且连接到上面的MongoDB数据库:
docker run -it --rm -p 8081:8081 --link mymongo:mongo mongo-express:0.49

当看到以下输出即说明mongo-express成功运行并成功连接MongoDB数据库。

根据上面的输出我们可以发现,mongo-express使用了Basic认证,并且默认账号密码为admin:pass,所以此漏洞虽然是高权限才可以利用,但因为默认高权限账号密码的存在,如果不修改默认账号密码,极易受到攻击。

环境搭建完毕后我们可以访问http://localhost:8081/去看看mongo-express长什么样子了。

漏洞分析

漏洞问题出在lib/bson.js中的toBSON()函数中。熟悉MongoDB的同学应该知道,MongoDB存储和查询数据的格式是BSON,它长得像JSON但并不是JSON,数据类型和JSON是有所不同的。众所周知,JSON的数据类型包括string、number、object、array、boolean和null,而BSON的基础数据类型包括byte、int32、int64、uint64、double、decimal128。其中byte类型又可以根据不同的定义派生出更多的类型。若想详细了解BSON相关知识可查看BSON Spec,或者参考gyyyy的文章:《浅析SQL和NoSQL注入(下)》中的“关于BSON”部分。这篇文章就不再赘述了。我们只需要知道BSON和JSON在数据类型上有很大区别就好。

mongo-express中的所有和BSON相关的操作,如新建一个文档(类似其他数据库的插入操作)都需要通过toBSON()函数。如我使用mongo-express新建如下文档:

通过Chrome调试工具,我们可以发现,在我新建一个文档的时候,前端把数据处理成表单的形式提交给后端,如下图:

lib/bson.js中的toBSON()函数中断点,命中后即可看到如下字符串传入了后端:

我们先来分析一下这段代码,可以发现在toBSON()函数上面的getSandbox()定义了MongoDB相关的数据类型,然后在下面的toBSON()函数中对传入的字符串进行了转换,最终变成BSON类型的文档。

代码作者在注释中也提到了,因为JSON.parse不支持BSON类型,因此写了toBSON()函数去转换传入的字符串为BSON类型。我们可以显而易见的看到eval()函数的调用。eval is evil,eval()函数在JavaScript中本就是一个危险函数的存在,在前端中eval()可以造成XSS漏洞,而随着JavaScript被用到了后端,如果eval()中的内容可控,就可以直接造成代码执行了。那么我们是不是就可以直接构造一个类似其他Node.js代码执行漏洞的代码执行PoC即可了?没那么简单。

细心的同学想必已经发现了,toBSON()部分的代码使用了vm模块执行,它是Node.js默认就提供的一个内建模块,vm模块提供了一系列API用于在V8虚拟机环境中编译和运行代码。也就是说通过vm执行的JavaScript代码会运行在一个沙盒中。代码作者其实并没有忽略安全问题。那么在vm中执行的代码就真的安全了吗?我们来简化源码做一个实验:

function evalDirect(string) {
  eval(string);
}

function evalUseVm(string) {
  const vm = require('vm');
  const sandbox = {};
  vm.runInNewContext('eval((' + string + '));', sandbox);
}

evalDirect('require("child_process").execSync("open /System/Applications/Calculator.app")');
evalUseVm('require("child_process").execSync("open /System/Applications/Calculator.app")');

运行这段代码,在调用evalDirect()函数时弹出了计算器,而在执行evalUseVm()函数时抛出如下错误:

报错中说,require未定义。那么只要想办法找到require就能继续构造PoC了。在JavaScript中,创建一个对象,即通过这个对象的原型对象的构造函数实例化这个对象,因此,原型对象是新对象的“模版”。那么假设我们使用的vm的沙箱就是一个当前进程上下文创建的对象,那么我们能不能通过链式调用vm的沙箱的构造函数来查找到当前上下文并且拿到require呢?vm在GitHub中的一个issue:Sandbox can be broken已经给出了答案,这个问题貌似永远也不会得到修复了。

通过this.constructor.constructor("return process")即可拿到当前上下文,我们可以看到其中包含了Node.js的全部全局变量:

这样我们就可以轻松拿到require构造PoC了。

补丁分析

从GitHub补丁diff上看,修复方案是删掉了全部使用vm的代码,而将BSON的处理转为通过mongodb-query-parsermongodb-extended-json这两个库配合实现:

修复建议

建议升级到mongo-express 0.54.0及以上版本,并且修改默认密码。mongo-express虽然是一个Web项目,但只建议内网使用,或使用其他客户端代替。

总结

通过近期对Node.js漏洞代码的研究,隐约发现Node.js漏洞的一些规律,如eval()出现在代码中,require("child_process")出现在代码中,只要他们可以被控制 ,甚至如CVE-2019-7609的kibana RCE漏洞仅仅是可以控制环境变量,都可能造成较严重的后果。今后再做Node.js相关项目的代码审计、漏洞分析或漏洞挖掘时,我们应充分利用JavaScript的语言特性,或许能总结出更多规律,让思路更清晰。

参考

  1. http://bsonspec.org/spec.html
  2. https://github.com/gf3/sandbox/issues/50
  3. https://juejin.im/post/5afe3c91518825673954f718
  4. https://github.com/mongo-express/mongo-express/pull/522
  5. https://odino.org/eval-no-more-understanding-vm-vm2-nodejs/
  6. https://github.com/gyyyy/footprint/blob/master/articles/2018/sql-vs-nosql-injection-02.md
  7. https://github.com/mongo-express/mongo-express/security/advisories/GHSA-h47j-hc6x-h3qq
Comments
Write a Comment
  • 645491049 reply

    更新很频繁哈,加油