whip1ash

从一篇文章再探Java反序列化 - 从0开始学习Java反序列化 (2)

2018-11-06

从一篇文章再探Java反序列化(Binary) - 从0开始学习Java反序列化 (2)

0x00 Intro

上一次走马观花的复现了CVE-2017-3506 & CVE-2017-10271,也算是摸到了Java反序列化的大门,这次恰好有看到了一篇Java反序列的文章 - Java Deserialization — From Discovery to Reverse Shell on Limited Environments。并不能说是一篇好文章,基本可以说是题文不符了,但是其中还是涵盖了一些可以深入学习的方向。最近有深入学习Java的打算,正好就借此为题。

0x01 从文章中寻找学习的方向

在文章中我看到了以下三个点:

既然是从WebGoat8开始,那就下载下来看看~ (github),直接java -jar webgoat-server-8.0.0.VERSION.jar --server.port=8080就可以跑起来。对照文章所讲的反序列化的练习,可以找到这个路径。
/WebGoat-8.0.0.M21/webgoat-lessons/insecure-deserialization/src/main/java/org/owasp/webgoat/plugin/InsecureDeserializationTask.java
代码很简单,接收数据解base64,然后直接进行readObject。

既然源码这么简单,直接readObject()。回想起来以前看过反序列化最简单的Demo,好像跟这个差不多。那我们为什么不尝试构造一下poc呢,而不是用ysoserial自动化构造。
get!第一个目标是不用文章的方法尝试构造poc。

0x02 尝试构造POC

于是本人天真的构造了如下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
import java.io.*;

public class Exploit {
public static void main(String[] argv) throws Exception{
poc e = new poc();

FileOutputStream fos=new FileOutputStream("ser");
ObjectOutputStream objOut = new ObjectOutputStream(fos);
objOut.writeObject(e);
objOut.close();

}
}

class poc implements Serializable{

//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
}

用UltraEdit打开文件,看到了如下数据:

就这么短一点!这说明序列化并不能把poc类给压缩到这一堆二进制数据里,那么疑问就出来了,那么ysoserial是如何将我们要执行的命令转化生一堆二进制的数据的呢?序列化数据的每一字节的含义又是什么呢?能不能通过构造二进制序列从而把我们的poc类给包含进去,然后让服务器生成一个poc的实例并执行呢? 经过很漫长的搜索,决定从二进制序列的每一位含义入手。

0x03 为什么说 AC ED 00 05 开头就是序列化数据?

看看序列化的概念

序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

所以序列化并不仅仅局限于各个语言的序列化,比如Java/PHP/Python…..在现代业务场景中,为了解耦,序列化有可能是跨语言甚至跨平台的。为了实现这样的功能,不同的序列化协议就应运而生了。其实序列化协议简单的分为可读和不可读两种,可读的包括我们熟悉的Json,XML,不可读的就是单纯的二进制序列。所以在这里我们使用的是原生的Java序列化协议。在原生的Java序列化协议中 AC ED 是STREAM_MAGIC,两个字节声明使用了序列化协议,00 05 是STREAM_VERSION,声明了序列化版本。这些常量在java的源码中是有定义的(/jdk1.8.0_131.jdk/Contents/Home/src.zip!/java/io/ObjectStreamConstants.java)。详细可以查看下面这篇文章。
The Java serialization algorithm revealed
有一个读取序列化数据各个字段的工具
https://github.com/NickstaDB/SerializationDumper

常见序列化协议的特征码目前还没有找到太多的资料,这里留个坑,见到了再更。

通过了解序列化协议,就可以知道我们无法反序列化一个不存在的类,只能通过构造二进制序列来一步步的调用已经存在的类,从而找到一个可以利用的点来执行我们的代码,比如能够反射调用任意函数的点,也就是构造POP链。

面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。

如何构造POP链的构造我们通过Apache Commons Collections 这个漏洞去研究,在这里暂且不提。

Java反射机制在POP链的构造中经常用到,所以在这里学习一下。

0x04 Java反射

先正常的写两个小Demo。

调用Runtime

1
2
3
4
5
public class runtime {
public static void main(String[] argv) throws Exception{
Runtime.getRuntime().exec("open /Applications/Calculator.app");
}
}

写文件

1
2
3
4
5
6
7
8
import java.io.FileOutputStream;

public class writefile {
public static void main(String[] argv) throws Exception {
FileOutputStream fos = new FileOutputStream("test.txt");
fos.write("asdfasdf".getBytes());
}
}

因为Runtime需要使用getRuntime方法,稍微复杂一点,我们先用反射调用写文件的这个demo。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.FileOutputStream;
import java.lang.reflect.*;

public class reflact_writefile {

public static void main(String[] argv) throws Exception{
Class clazz = FileOutputStream.class;
//getConstructor()中带参数类型
Constructor con = clazz.getConstructor(String.class);
FileOutputStream fos = (FileOutputStream) con.newInstance("b.txt");
fos.write("qwert".getBytes());
}
}

这是反射就常用的方法,先获取字节码的class对象,然后获取构造函数,然后进行实例化,就可以进行调用了。这是最简单的反射,如果我们把条件限制为只能使用getMethod和invoke以及getClass方法,可以如下写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.FileOutputStream;
import java.lang.reflect.*;

public class reflact_writefile {

public static void main(String[] argv) throws Exception{

Object obj = FileOutputStream.class;
Class cls = obj.getClass();
//获取构造方法,传入参数类型并调用
Method method = cls.getMethod("getConstructor",Class[].class);
//invoke在调用函数的时候,invoke的第二个参数是Object数组类型,构造函数的参数又是Class数组类型,所以我们在这里需要用匿名类包装一下。
obj = method.invoke(obj, new Object[]{new Class[]{String.class}});
//此时的obj已调用过构造函数,所以需要重新获取,这一步实例化对象
cls = obj.getClass();
method = cls.getMethod("newInstance",Object[].class);
obj = method.invoke(obj,new Object[]{new Object[]{"c.txt"}});
//调用write方法
cls = obj.getClass();
method = cls.getMethod("write",byte[].class);
obj = method.invoke(obj,"asdf".getBytes());

}
}

这里理解一下也不算太难,我们看一下如何通过反射调用Runtime。因为Runtime是一个单例模式,没有办法铜鼓构造函数来实例化,所以只能通过getRuntime来获取实例。

1
2
3
4
5
6
7
8
9
10
11
import java.lang.reflect.Method;

public class reflact_runtime {
public static void main(String[] argv) throws Exception{
Class clz = Class.forName("java.lang.Runtime");
Method gr = clz.getMethod("getRuntime");
Method exec = clz.getMethod("exec", String.class);

exec.invoke(gr.invoke(null,new Object[]{}),"open /applications/Calculator.app");
}
}

不是太难,仔细看看都可以理解。

0x05 自说自话

这些底层的知识,非常的杂乱,此篇文章不能说是一个杂乱知识的总结,只能算是一个学习历程的一个记录。前后大概花了一周的时间(这一周里杂事也非常多,一直在被打断),查阅了非常多的资料,问了很多研发的朋友,(感谢大家对我的帮助),自己已经对Java底层的一些知识有了一定的掌握,但是仍然无法系统的把这些东西总结出来。心中大概已经有一个认识,还需要时间去消化吸收。
接下来就要开始调一些漏洞了,同时要学一下ysoserial是如何自动化构造POP链的。
希望离职后的时间不要荒废,今天只要比昨天强一点点就好了。
终南山下,求仙问道。

扫描二维码,分享此文章