NoSQL注入

目录

简介

核心模型

注入

语法注入

运算符注入

利用语法注入提取数据

识别字段名称

利用运算符注入提取数据

提取字段名称

基于时序的注入

防止NoSQL注入

简介

NoSQL 数据库不使用传统的关系型表格存储数据,而是专为处理大规模非结构化或半结构化数据而设计。相比 SQL 数据库,它们在关系约束和一致性检查上较少,但在可扩展性、灵活性和性能方面表现突出。

与 SQL 数据库不同,NoSQL 没有统一的查询语言标准,而是根据具体数据库类型使用不同的查询方式(如 JSON、XML 或自定义 API)。

核心模型
  • 文档型 (Document stores): 以 JSON/BSON/XML 等格式存储半结构化数据。例如:MongoDB。

  • 键值型 (Key-value stores): 通过唯一键检索值,非常高效。例如:Redis。

  • 宽列型 (Wide-column stores): 按列族组织相关数据,适合大规模数据处理。例如:Cassandra。

  • 图形数据库 (Graph databases): 使用节点(实体)和边(关系)来存储数据,专注于关联分析。例如:Neo4j。

注入

语法注入

通过注入特殊字符打破原有的查询语法,从而植入恶意的逻辑判断。

1.模糊测试 (Fuzzing):

使用一系列特殊字符(如 ', ", {, ;, \xYZ 等)尝试打破查询语法。

如果服务器返回错误,或者页面行为发生变化(例如返回了不同的数据量),则说明输入可能未经过滤。

注:在测试漏洞时,不能机械地照搬某个 Payload,必须根据注入载体(在哪里输入数据)来对 Payload 进行"包装"。

注入场景 需要做的操作 最终发送的 Payload
URL 参数 进行 URL 编码 category='%22%60%7b%0d%0a%3b%24Foo%7d%0d%0a%24Foo%20%5cxYZ%00
JSON 请求体 进行 JSON 转义 '\"{\r;Foo}\\nFoo \xYZ\u0000`

2.确认阶段

发送两个请求,分别注入"假"条件(例如 && 0 &&)和"真"条件(例如 && 1 &&)。

如果两者的响应行为不同,说明你成功地在服务器端修改了查询逻辑,而非仅仅是引发了应用层的逻辑异常。

3.利用阶段

利用始终为真的布尔逻辑(如 || '1'=='1),使得查询条件恒成立。

通过强制条件恒真,可以检索出所有符合(或不符合)原始条件的数据,甚至绕过登录验证或获取隐藏类别的信息。

也可以通过在payload后加空字符,MongoDB可能会忽略空字符之后的所有字符。

运算符注入

运算符注入是利用数据库原生提供的"功能性"指令来修改查询逻辑。

NoSQL 数据库(特别是 MongoDB)允许查询参数不仅仅是字符串,还可以是包含运算符的对象

MongoDB查询运算符的示例包括:

  • $where- 匹配满足JavaScript表达式的文档。

  • $ne- 匹配所有不等于指定值的值。

  • $in- 匹配数组中指定的所有值。

  • $regex- 选择值与指定正则表达式匹配的文档。

  • 常规查询: {"username": "admin"} ------ 匹配用户名为 "admin" 的用户。

  • 注入查询: {"username": {"$ne": "invalid"}} ------ 匹配用户名"不等于 (not equal)" "invalid" 的用户。

很多应用最初接收的是 URL 编码的数据,但后端处理时会将其转为 JSON 对象。需要掌握这种"格式转换"的攻击技巧,如果URL注入失败,转成JSON试试:

  • 将请求方法从GET转换为 POST

  • 将头Content-Type改为 application/json

  • 在消息正文中添加 JSON 代码。

  • 在 JSON 中注入查询操作符。

  • URL 编码形式: username[$ne]=invalidJSON 结构形式: {"username": {"$ne": "invalid"}}

利用语法注入提取数据

在许多 NoSQL 数据库中,一些查询运算符或函数可以运行有限的 JavaScript 代码,例如 MongoDB 的运算符和函数。数据库可能会将JavaScript作为查询的一部分进行评估,因此,可能可以使用JavaScript函数从数据库中提取数据。

  • 原始代码:{"$where":"this.username == '用户输入'"}

  • 当输入 admin' && this.password[0] == 'a' || 'a'=='b 时,后端查询变成了: this.username == 'admin' && this.password[0] == 'a' || 'a'=='b'

    ```&& this.password[0] == 'a'``:检查该用户的密码第一个字符是不是 'a'。

    || 'a'=='b':闭合掉原始代码中最后的那个单引号,使其不报错(因为 'a'=='b' 永远为假,不影响前面的结果)。

  • 如果猜对了: this.password[0] == 'a' 为真,页面会显示 admin 的信息。

    如果猜错了: 整个条件为假,页面不会显示任何信息。

  • 相当于SQL注入中的布尔盲注

  • 可以先用这个payload判断密码中是否有数字,admin' && this.password.match(/\d/) || 'a'=='b

  • 用bp的intruder爆破时注意编码问题,如果已经用ctrl+U编码过了,在intruder就取消勾选URL-encode

识别字段名称

因为 NoSQL 不像 SQL 有固定的 TABLE 结构,所以需要先识别集合中的有效字段,才能用 JavaScript 注入提取数据。

字段探测利用了 JavaScript 的对象属性访问特性。其核心原理是:

  • 如果字段存在this.fieldname 会返回一个值或空字符串,逻辑表达式可以正常计算。

  • 如果字段不存在this.fieldname 在 JavaScript 中是 undefined

响应差异:

基准请求(肯定存在的字段):admin' && this.username!=',返回 admin 的信息(因为 username 肯定存在且不为空)。

对照请求(肯定不存在的字段): admin' && this.foo!=',报错或不返回数据(因为 this.fooundefined)。

测试请求(想探测的字段): admin' && this.password!=',如果它的响应和 1 一样,说明 password 字段存在;如果和 2 一样,说明字段不存在。

做法:

  • 一个方法是将this.§password§!=' 中的字段名设为变量,选取包含常见字段名的字典,进行爆破看哪个存在。(依靠上面的差异)

  • 另一个方法是逐个提取字符,利用 $where 或运算符注入,把字段名本身猜出来。

    例如:this.keys()[0][0] == 'p'(探测第一个字段名的第一个字母是不是 'p')。

利用运算符注入提取数据
  • 在正常的 JSON 请求中,额外塞进一个数据库本不期待的运算符。

  • 发送 {"username":"wiener", "password":"peter", "$where":"1"}

    发送 {"username":"wiener", "password":"peter", "$where":"0"}

    如果上述两次结果不同,说明后端直接接收了传入的整个 JSON 对象 并传给了数据库,数据库执行了强行注入的 $where 脚本。

  • $where允许在执行数据库查询时执行JS代码

提取字段名称

可以使用该payload探测,"$where":"Object.keys(this)[0].match('^.{0}a.*')"

  • Object.keys(this)[0] :这是一个 JavaScript 函数,它会把当前查询对象里的所有"键"(字段名)变成一个数组。例如:["username", "password", "role"]。这里取数组里第一个字段名

  • .match('^.{0}a.\*'):利用正则表达式探测该字段名的第一个字符。

    • ^.{0}a 的意思是:从开头数 0 个字符,紧接着是字母 a

    • 如果想探测第二个字符,就改成 ^.{1}b

(对应的这个bp实验没做出来,利用{"password": {"$ne": "invalid"}},发现虽然没有登录,但响应也会比较特殊,可以利用盲注。之后添加了"$where"参数,发现其值为0时和为1时响应不同,说明该参数可利用。可以爆破字段名,"$where":"Object.keys(this)[0].match('^.{0}a.*')"表示第一个字段的第一个字符是否为a,可以得到四个参数(id,username,password,email),本题要修改密码才算成功,"$where":"this.email.match('^.{§0§}§a§.*')"拿到了carlos的email为carloscarlosmontoya,但是username参数和email参数都试了,在get请求里没反应,在post请求里让去查看邮件获得修改密码的链接。而且用爆破出来的carlos的密码登也登不进去,怀疑人生)

很多现代 NoSQL 环境会禁用 JavaScript 执行(即禁用 $where),但几乎没有数据库会禁用正则表达式

  • {"username":"admin", "password":{"$regex":"^a.*"}}

    • ^a 表示以 a 开头。

    • 如果登录成功(或响应不同),就知道第一位是 a

    • 以此类推试出完整的字符串

基于时序的注入
  • 发现页面无论怎么输入,返回的内容、长度、状态码都一模一样(即"真假逻辑"无法直接观察)

  • 时间盲注:让数据库在满足特定条件时,响应延迟一会,例如{"$where": "sleep(5000)"}

  • 死循环模拟法

    有些数据库环境为了安全,禁用了原生的 sleep() 函数。这时候,攻击者会写一个死循环,让 CPU 疯狂计算直到 5 秒钟结束。

    复制代码
    admin' + function(x){
        var waitTill = new Date(new Date().getTime() + 5000);  
        //获取当前时间,再加上 5000 毫秒(即 5 秒后的时间点),存为 waitTill。
        while((x.password[0]==="a") && waitTill > new Date()){};
        //前边是注入的判断,后边是判断是否没到5秒后。如果两个条件都满足,就执行空语句 {}(即什么都不做,原地转圈)
    }(this) + '  //this 代表当前查询到的那条数据库记录
  • 原生函数法

    复制代码
    admin' + function(x){
        if(x.password[0]==="a"){ sleep(5000) };
    }(this) + '

防止NoSQL注入

  • 对用户输入进行净化和验证

    • 白名单机制:比如用户名只允许字母和数字。如果用户输入里包含 $'{ 等特殊字符,直接拒绝请求,而不是尝试去修补它。

    • 确保预期的字符串确实是字符串,而不是一个包含运算符的对象。

  • 参数化查询 (最有效)

    • 错误做法(拼接): "{ username: '" + input + "' }"

    • 正确做法(参数化): 使用数据库驱动提供的 API,将数据作为参数传递。

      db.collection.find({ "username": input }),此时,即使 input{"$ne": ""},数据库也会把它当成一个普通的字符串去搜索,而不会把它当作一个逻辑运算符来执行。

  • 针对运算符注入的防御

    • 在处理 JSON 输入时,很多后端框架会自动把 {"$ne": ""} 转换成对象。
    • 在将数据传给数据库之前,检查 JSON 的 Key(键) 。如果发现 Key 是以 $ 开头的(这通常意味着它是 MongoDB 的运算符),直接过滤掉。
  • 架构层面的加固

    • 禁用危险功能: 如果应用不需要复杂的逻辑,直接在数据库配置里禁用 $where 运算符

    • 最小权限原则: 数据库账号不应该有权访问 Object.keys 或者执行系统命令。

相关推荐
zzh940771 小时前
MySQL中的通配符
数据库·mysql
gameboy0311 小时前
MySQL:基础操作(增删查改)
数据库·mysql·oracle
yoyo_zzm2 小时前
MySQL的索引
android·数据库·mysql
未来龙皇小蓝2 小时前
【MySQL-索引调优】06:最左匹配原则及优化
数据库·mysql·oracle·性能优化
一个有温度的技术博主2 小时前
Redis系列三:在linux上安装Redis
linux·数据库·redis
changhong19862 小时前
redis批量删除namespace下的数据
数据库·redis·缓存
IvorySQL2 小时前
PostgreSQL 技术日报 (3月18日)|从 MD5 到 SCRAM:PG 的安全转变
数据库·postgresql·开源
不吃香菜kkk、2 小时前
通过夜莺n9e监控Kubernetes集群
安全·云原生·容器·kubernetes
Mimo_YY2 小时前
SQL-忘记sa密码,如何安全的尝试旧密码,如何修改新密码
安全