nines-nine/GadgetExplorer
GitHub: nines-nine/GadgetExplorer
针对 .NET 托管应用的反序列化 gadget 链发现工具,通过扫描程序集并构建可达性图来定位从反序列化入口到危险 sink 方法的潜在利用路径。
Stars: 14 | Forks: 1
```
██████╗ █████╗ ██████╗ ██████╗ ███████╗ ████████╗
██╔════╝ ██╔══██╗ ██╔══██╗ ██╔════╝ ██╔════╝ ╚══██╔══╝
▓▓║ ▓▓▓╗ ▓▓▓▓▓▓▓║ ▓▓║ ▓▓║ ▓▓║ ▓▓▓╗ ▓▓▓▓▓╗ ▓▓║
▒▒║ ▒▒║ ▒▒╔══▒▒║ ▒▒║ ▒▒║ ▒▒║ ▒▒║ ▒▒╔══╝ ▒▒║
╚░░░░░░╔╝ ░░║ ░░║ ░░░░░░╔╝ ╚░░░░░░╔╝ ░░░░░░░╗ ░░║
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝
███████╗ ██╗ ██╗ ██████╗ ██╗ ██████╗ ██████╗ ███████╗ ██████╗
██╔════╝ ╚██╗██╔╝ ██╔══██╗ ██║ ██╔═══██╗ ██╔══██╗ ██╔════╝ ██╔══██╗
▓▓▓▓▓╗ ╚▓▓▓╔╝ ▓▓▓▓▓▓╔╝ ▓▓║ ▓▓║ ▓▓║ ▓▓▓▓▓▓╔╝ ▓▓▓▓▓╗ ▓▓▓▓▓▓╔╝
▒▒╔══╝ ▒▒╔▒▒╗ ▒▒╔═══╝ ▒▒║ ▒▒║ ▒▒║ ▒▒╔══▒▒╗ ▒▒╔══╝ ▒▒╔══▒▒╗
░░░░░░░╗ ░░╔╝ ░░╗ ░░║ ░░░░░░░╗ ╚░░░░░░╔╝ ░░║ ░░║ ░░░░░░░╗ ░░║ ░░║
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝
```
# .NET 应用程序反序列化 Gadget 链发现
## 概述
GadgetExplorer 是一款 .NET 命令行工具,用于在托管应用程序中寻找潜在的可以利用的反序列化 gadget 链。它扫描一个或多个程序集,结合分派和回调的启发式方法构建可达性图,并在反序列化入口点能够触达您关注的目标方法时发出报告。
该工具面向从事 .NET 反序列化研究、gadget 挖掘、漏洞利用开发以及托管应用程序漏洞安全研究的人员。当您已经明确关注的序列化器行为和目标方法类别,并希望以更快的速度得到解答时,此工具尤为实用:“这个反序列化入口点是否可能导向一条可利用的目标方法 gadget 链?”
开箱即用,内置的目标方法集包含了数百个目标,涵盖以下常见类别:
- 文件写入及文件系统影响:`System.IO.File.WriteAllText`、`System.IO.File.WriteAllBytes`、`System.IO.FileStream.Write`、`System.IO.Compression.ZipFile.ExtractToDirectory`
- 命令或脚本执行:`System.Diagnostics.Process.Start`、`System.Management.Automation.PowerShell.Invoke`
- SSRF 及出站网络访问:`System.Net.WebRequest.Create`、`System.Net.WebRequest.GetResponse`、`System.Net.Http.HttpClient.GetAsync`、`System.Net.Http.HttpClient.SendAsync`、`System.Net.WebClient.DownloadString`
- 与 XXE 相关的 XML 加载和转换路径:`System.Xml.XmlDocument.LoadXml`
- 反射与动态调用:`System.Reflection.MethodBase.Invoke`、`System.Reflection.MethodInfo.Invoke`、`System.Type.InvokeMember`、`System.Activator.CreateInstance`
- 链式反序列化:`System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize`、`System.Runtime.Serialization.Formatters.Soap.SoapFormatter.Deserialize`、`System.Runtime.Serialization.NetDataContractSerializer.Deserialize`
- 程序集加载与动态代码加载:`System.Reflection.Assembly.Load`、`System.Reflection.Assembly.LoadFrom`、`System.Reflection.Assembly.UnsafeLoadFrom`
## 安装
从本仓库的 GitHub Releases 页面下载最新版本的发布压缩包,将其解压,并保持内部所有文件放在一起。
发布文件夹有意采用基于文件的管理方式:
- `GadgetExplorer.exe`:扫描器可执行文件
- `sinks\*.sinks.json`:默认内置的目标方法包,按宽泛的漏洞类别进行分组
- `ignore-sinks\*.ignore-sinks.json`:默认内置的忽略目标方法包
- `serializer-profiles\*.profile.json`:内置的序列化器配置文件
请在该目录下运行工具,或在调用 `GadgetExplorer.exe` 时确保这些附属文件夹与其保持在同一目录下。
## 从源码构建
还原依赖项:
```
dotnet restore
```
构建发布版本的二进制文件:
```
dotnet build .\GadgetExplorer.sln -c Release
```
构建输出将位于 `artifacts\bin\...` 目录下。
## 用法
```
Usage:
GadgetExplorer [options]
Input:
Assembly file or directory tree to scan.
Directories are searched recursively for managed assemblies and runtimeconfig files.
Example: GadgetExplorer "C:\Target\App" -p JsonDotNet
Note: At least one assembly or directory is required.
Profile:
-p, --profile [BinaryFormatter | JsonDotNet | JsonDotNetGetters | MessagePackTypeless | PublicTwoStringConstructor | XmlSerializer]
Use a shipped serializer profile.
This controls which deserialization trigger policy and activation policies are modeled.
Required unless --profile-file is used.
Example: -p JsonDotNet
-pf, --profile-file
Use a custom serializer profile JSON file.
This replaces built-in profile selection for the scan.
Required unless --profile is used.
Example: -pf .\Profiles\Custom.profile.json
Sink Configuration:
-is, --sinks
Load a custom sink JSON file or a directory of *.sinks.json files.
This changes which methods count as reportable sinks.
Default: use the shipped `sinks` directory beside the executable.
Example: -is .\CustomSinks.json
-ig, --ignore-sinks
Load a custom ignore-sink JSON file or a directory of *.ignore-sinks.json files.
This suppresses configured sink patterns and can reduce noise.
Default: use the shipped `ignore-sinks` directory beside the executable.
Example: -ig .\CustomIgnoreSinks.json
Scan Behavior:
-ie, --interface-expansion [off | strict | broad]
Control dynamic dispatch handling during graph construction.
off: only follow interface calls when concrete receiver identity is already known.
strict: allow strong receiver evidence, but stop when evidence runs out.
broad: opt into heuristic fallback across compatible implementations.
Default: strict.
Example: -ie broad
-s, --sort [shortest-path | per-sink-shortest-path | type-name]
Control finding order in the final report.
shortest-path: shortest paths first globally.
per-sink-shortest-path: group by sink, then shortest paths first within each sink.
type-name: stable type-centric ordering by root class identity.
Default: shortest-path.
Example: -s per-sink-shortest-path
-mpl, --max-path-length
Limit the maximum graph path length from trigger to sink.
Lower values reduce noise and runtime but hide longer gadget chains.
Default: unbounded.
Example: -mpl 8
-arm, --assembly-resolution-mode [restricted | inference-no-fallback | inference-with-fallback]
Control how assembly resolution expands beyond the supplied input roots.
restricted: only resolve assemblies inside the supplied directory tree or beside supplied assembly files.
inference-no-fallback: infer the target runtime from runtimeconfig files, but stay inside the inputs if inference fails.
inference-with-fallback: infer the target runtime first, then fall back to the host runtime if inference fails.
Default: inference-no-fallback.
Output:
-o, --output
Write the final report to a file.
Progress still goes to the console; only the report is redirected.
The output path does not choose the format; use --output-format for that.
Default: write the report to stdout.
Example: -o .\Scan.txt
-of, --output-format [text | json]
Control the final report serialization format.
text: the existing human-readable report.
json: a structured machine-friendly document with recon metadata and flat ordered findings.
Default: text.
Example: -of json
```
结构化的 JSON 输出专为下游工具设计。它保持了与文本报告相同的发现结果渲染顺序,但将扫描标头和每个发现作为具有特定类型的字段输出,从而更易于进行过滤、排序和分组。
### 内置配置文件
内置的配置文件如下:
- `JsonDotNet`:模拟 Json.NET `TypeNameHandling != None` 场景,包含构造函数选择、公共属性 setter、选定 `JsonProperty`/`DataMember` 的非公共 setter、包括 `OnError` 在内的反序列化回调属性、终结器启用,以及在类型解析到达时处理公共和非公共的根类型。
- `JsonDotNetGetters`:针对相同 `TypeNameHandling != None` 场景的、范围更窄的面向 Json.NET 的配置。它禁用了构造函数、setter、回调及终结器的触发面,并专注于公共属性 getter,同时保持与 `JsonDotNet` 相同的根类型可见性覆盖范围。
- `BinaryFormatter`:模拟 `BinaryFormatter` 风格的行为,包含 `[Serializable]` 根类型、未初始化对象创建、跨越各种构造函数可见性的精确签名的 `ISerializable` 序列化构造函数、反序列化回调属性、`IDeserializationCallback`、`IObjectReference` 以及终结器。
- `MessagePackTypeless`:通过 `MessagePackSerializer.Typeless` / `TypelessContractlessStandardResolver` 模拟不安全的 MessagePack-CSharp 无类型反序列化,包括带注解和不带注解的无契约根类型、具有 `SerializationConstructor` 优先级和最佳匹配选择的构造函数触发器、通过允许私有路径可达的公共及非公共属性 setter 触发器、`IMessagePackSerializationCallbackReceiver.OnAfterDeserialize()` 以及终结器。它故意未加入仅字段触发器建模或更广泛的自定义格式化器执行逻辑。
- `XmlSerializer`:模拟公共可见的根类型、无参构造函数激活、公共属性 setter、`IXmlSerializable.ReadXml(XmlReader)`、终结器启用、针对接口和 `System.Type` 成员形状的保守型普通成员兼容性过滤,并且没有格式化器风格的回调。
- `PublicTwoStringConstructor`:一种用于通过公共可见类型访问对象的研究型窄配置文件,该类型具有一个接收恰好两个 `System.String` 参数的公共构造函数,同时启用了终结器并禁用了 setter/回调行为。
### Sink 文件
- `sinks\*.sinks.json`:定义哪些方法可作为值得关注并报告的目标方法。
- `ignore-sinks\*.ignore-sinks.json`:定义应被忽略或被视为切片边界的 sink 模式。
这两种文件类型均使用带有顶层 `sinks` 数组的 JSON 文档。`--sinks` 和 `--ignore-sinks` 选项接受单个 JSON 文件或单个目录作为输入。当提供目录时,GadgetExplorer 仅读取顶层目录,加载匹配的文件,使用序号比较按文件名对它们进行排序,并确定性地拼接它们的 `sinks` 数组。
包含目标方法的文件既可以是宽泛的,也可以是针对特定签名的。更丰富的 `parameters` 形式允许每个参数独立选择是否启用常量参数抑制:
```
{
"sinks": [
{
"declaringType": "System.Xml.XmlDocument",
"methodName": "Load",
"parameters": [
{
"typeName": "System.String",
"ignoreSinkIfConstant": true
}
]
}
]
}
```
`ignoreSinkIfConstant` 仅适用于 include-sink 定义。它的含义是“如果 GadgetExplorer 断定传递给此参数的值在调用点是一个常量,则不报告此 sink。” 这有助于减少来自 API(例如当 XML 路径是硬编码的本地配置文件时的 `XmlDocument.Load(string)`)的噪音。这种判定基于启发式方法而非绝对完美,因此常量检测应被视为一种降噪辅助手段,而不能作为路径绝对安全的证明。
ignore-sink 文件不使用 `ignoreSinkIfConstant`。它们仅用于描述应被忽略或用作切片边界的 sink 模式。
## 示例
使用内置的 Json.NET 配置扫描单个程序集:
```
.\GadgetExplorer.exe .\App.dll -p JsonDotNet -o .\Scan-JsonDotNet.txt
```
默认使用内置的 sink 目录:
```
.\GadgetExplorer.exe "C:\Target\App" -p JsonDotNet -o .\Scan-DefaultSinks.txt
```
使用内置的 BinaryFormatter 配置扫描目录树,最大路径长度设为 12:
```
.\GadgetExplorer.exe "C:\Target\App" -p BinaryFormatter -mpl 12 -o .\Scan-BinaryFormatter.txt
```
使用内置的 XmlSerializer 配置扫描目录树:
```
.\GadgetExplorer.exe "C:\Target\App" -p XmlSerializer -o .\Scan-XmlSerializer.txt
```
使用内置的 MessagePack Typeless 配置扫描目录树,最大路径长度设为 8:
```
.\GadgetExplorer.exe "C:\Target\App" -p MessagePackTypeless -mpl 8 -o .\Scan-MessagePackTypeless.txt
```
使用内置的 Json.NET 配置、自定义 sink 文件、自定义 ignore-sink 文件并进行 `type-name` 排序:
```
.\GadgetExplorer.exe "C:\Target\App" -p JsonDotNet -is .\CustomIncludeSinks.json -ig .\CustomIgnoreSinks.json -s type-name -o .\Scan-CustomSinks-TypeName.txt
```
结合自定义 sink 目录使用内置的 Json.NET 配置:
```
.\GadgetExplorer.exe "C:\Target\App" -p JsonDotNet -is .\CustomSinks -o .\Scan-CustomSinkDirectory.txt
```
将结构化 JSON 报告写入磁盘以便进行下游处理:
```
.\GadgetExplorer.exe "C:\Target\App" -p JsonDotNet -of json -o .\Scan-JsonDotNet.json
```
从 runtimeconfig 文件推断目标运行时,但如果推断失败,则仍限制在提供的输入范围内:
```
.\GadgetExplorer.exe "C:\Target\App" -p JsonDotNet -arm inference-no-fallback -o .\Scan-InferenceNoFallback.txt
```
首先推断目标运行时,如果推断失败则允许回退到主机运行时:
```
.\GadgetExplorer.exe "C:\Target\App" -p JsonDotNet -arm inference-with-fallback -o .\Scan-InferenceWithFallback.txt
```
使用内置配置文件,并显式指定接口处理方式、排序规则和最大路径长度:
```
.\GadgetExplorer.exe .\App.dll -p JsonDotNet -arm restricted -ie broad -s shortest-path -mpl 8 -o .\Scan-Restricted-Broad.txt
```
使用自定义序列化器配置文件、自定义 sink 文件、自定义 ignore-sink 文件,按每个 sink 的最短路径排序,并将路径长度限制为 12:
```
.\GadgetExplorer.exe "C:\Target\App" -pf .\Profiles\Custom.profile.json -is .\CustomIncludeSinks.json -ig .\CustomIgnoreSinks.json -s per-sink-shortest-path -mpl 12 -o .\Scan-Custom.txt
```
## 发现结果示例
### 任意方法调用
```
System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
Assembly: C:\Example\AppA\PresentationFramework.dll (AssemblyVersion=4.0.0.0, FileVersion=4.800.122.15205, Origin=input-root)
System.Windows.Data.ObjectDataProvider::set_MethodName(System.String)
-> [DirectCall] System.Windows.Data.DataSourceProvider::Refresh()
-> [VirtualDispatch] System.Windows.Data.ObjectDataProvider::BeginQuery()
-> [DirectCall] System.Windows.Data.ObjectDataProvider::QueryWorker(System.Object)
-> [DirectCall] System.Windows.Data.ObjectDataProvider::InvokeMethodOnInstance(System.Exception&)
-> [DirectCall] System.Type::InvokeMember(System.String, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object, System.Object[], System.Globalization.CultureInfo)
```
### 任意 Getter
```
System.Windows.Forms.BindingSource, System.Windows.Forms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Assembly: C:\Example\AppB\System.Windows.Forms.dll (AssemblyVersion=9.0.0.0, FileVersion=9.0.1326.6403, Origin=input-root)
System.Windows.Forms.BindingSource::set_DataMember(System.String)
-> [DirectCall] System.Windows.Forms.BindingSource::ResetList()
-> [DirectCall] System.Windows.Forms.ListBindingHelper::GetList(System.Object, System.String)
-> [VirtualDispatch] System.ComponentModel.ReflectPropertyDescriptor::GetValue(System.Object)
-> [DirectCall] System.Reflection.MethodBase::Invoke(System.Object, System.Object[])
```
### BinaryFormatter 桥接
```
System.Security.Principal.GenericIdentity, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Assembly: C:\Example\AppC\Managed\mscorlib.dll (AssemblyVersion=4.0.0.0, FileVersion=4.6.57.0, Origin=input-root)
Declared On: System.Security.Claims.ClaimsIdentity
Note: Inherited deserialization callback declared on System.Security.Claims.ClaimsIdentity.
System.Security.Claims.ClaimsIdentity::OnDeserializedMethod(System.Runtime.Serialization.StreamingContext)
-> [DirectCall] System.Security.Claims.ClaimsIdentity::DeserializeClaims(System.String)
-> [DirectCall] System.Runtime.Serialization.Formatters.Binary.BinaryFormatter::Deserialize(System.IO.Stream, System.Runtime.Remoting.Messaging.HeaderHandler, System.Boolean)
```
### SSRF
```
System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
Assembly: C:\Example\AppC\Managed\System.Drawing.dll (AssemblyVersion=4.0.0.0, FileVersion=4.6.57.0, Origin=input-root)
Declared On: System.Drawing.Image
Note: Inherited finalizer declared on System.Drawing.Image.
System.Drawing.Image::Finalize()
-> [VirtualDispatch] System.Drawing.Image::Dispose(System.Boolean)
-> [DirectCall] System.IO.Stream::Dispose()
-> [VirtualDispatch] System.IO.Stream::Close()
-> [VirtualDispatch] System.Net.WebClient+WebClientWriteStream::Dispose(System.Boolean)
-> [VirtualDispatch] System.Net.WebClient::GetWebResponse(System.Net.WebRequest)
-> [VirtualDispatch] System.Net.HttpWebRequest::GetResponse()
```
### 任意程序集加载
```
System.CodeDom.Compiler.CompilerResults, System.CodeDom, Version=9.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Assembly: C:\Example\AppD\System.CodeDom.dll (AssemblyVersion=9.0.0.0, FileVersion=9.0.325.11113, Origin=input-root)
System.CodeDom.Compiler.CompilerResults::get_CompiledAssembly()
-> [DirectCall] System.Reflection.Assembly::LoadFile(System.String)
```
## 局限性
GadgetExplorer 不是一款通用的污点分析引擎,也不是一键式的漏洞可利用性判定神谕。它是一款辅助研究人员的工具,旨在筛选出候选的反序列化 gadget 链以供进一步的深入手动分析。
当前的实现限制包括:
- 它不会跨 gadget 链跟踪对象状态。在实践中,这意味着在跨对象填充步骤时缺乏通用的字段/属性污点传播能力。
- 它不执行完整的符号执行或路径敏感的分支分析。
- 除了已加载代码中存在的图边缘外,它不对任意反射语义进行建模。
- 它不对任意事件语义进行建模。它仅处理常见的观察到的“订阅并触发”模式,而非所有可能的事件用法。
- 接口分派虽然经过了仔细建模,但依然基于启发式方法。缺失的元数据、外观/引用不匹配或接收方信息不完整仍可能导致欠近似或过近似。
- 受限模式的结果取决于加载的内容。如果省略了某些程序集,图可能会遗漏某些路径。如果允许运行时推断和回退,图可能会变得更具干扰性(噪音更多)。
- `--assembly-resolution-mode restricted` 改善了隔离性和可重复性,但它也可能隐藏那些依赖于所提供目录之外的运行时/框架程序集的真实路径。
- 默认的 `inference-no-fallback` 模式通常能提供更广泛的感知框架的覆盖范围,同时仍能在推断失败时避免回退到主机运行时。
- `--interface-expansion off` 仅保留由具体接收方身份支持的接口调用,因此子类型约束和探索性回退路径将会消失。
- `--interface-expansion broad` 启用了探索性回退启发式方法,这可能会暴露出额外的路径,但其噪音故意设定得比 `strict` 模式更大。
## 许可证
```
GPLv3
```
## 版权
```
Copyright (C) 2026 Dane Evans
```
标签:CISA项目, GadgetChain, SAST, 反序列化漏洞, 可达性分析, 多人体追踪, 安全扫描, 应用程序安全测试, 恶意代码分析, 攻击链生成, 文档结构分析, 时序注入, 盲注攻击, 配置文件