前言
@frohoff在2015年初发现commons-collections
的反序列化利用链并发布了ysoserial
工具[1]。9个月后,@breenmachine对众多知名Java中间件的利用文章[2]使Java反序列化漏洞变得广为人知,Weblogic中首当其冲的就是大家多少都有点耳熟的T3协议反序列化。本篇从CVE-2015-4852入手了解T3协议的构造,作为后续T3反序列化漏洞学习和利用的基础。
环境搭建与补丁定位
Weblogic官网提供无补丁的初始版本下载[3],为了方便后续调试分析可以先把几个大版本的安装包(Generic)、以及某些有较大安全特性变化的JDK准备好。
官方只为付费帐户提供安全补丁的下载权限,但普通账户在高级搜索页面[4]列出对应版本的补丁号与发布时间,结合官方安全公告页的CVE发布时间[5],基本可以让CVE对应上Patch。
有了Patch号可以通过CSDN+某宝积分或者某鱼卖家等等方式下到补丁,Patch详情页里可以看到哈希,有官方原版zip洁癖的同学也可以对一下。
环境搭建时,用WeblogicEnvironment[6]构建不同JDK与Weblogic版本的docker镜像会比较方便,后续也能用容器区分不同的补丁版本。运行前需要参考issues/8
改一下Dockerfile,另外就是根据个人需要做适当调整了,比如Weblogic开启的远程调试端口默认为8453(对应IDEA默认的5005)、调整JDK版本等等。
CVE-2015-4852
《攻击JavaRMI概述》[7]中说过JavaRMI机制基于JRMP协议通信,Weblogic有一个与JRMP同类的应用层协议就是T3。T3协议用在Weblogic的这类商用产品当中,协议实现并不开源且没有文档,小编主要靠连蒙带猜和逆向去管中窥豹。
/u01/app/oracle/Domains/ExampleSilentWTDomain/bin/stopWebLogic.sh
用到了T3协议去停掉Weblogic,在执行前挂上tcpdump -i any -w t3-stop.pcap
抓一下数据包。
Java原生序列化流通过十六进制为ac ed 00 05
的字节作为开头的标识(黑话管它叫做魔术字节),ysoserial
也正是通过原生序列化生成的payload,自然能简单粗暴地想到是不是直接替换就行了,那么要替换掉哪些部分呢?能看出先进行了第一次通信,请求和响应都是可读的ASCII字符,后续的的请求中似乎也没有用到响应的内容。
把第二次请求字节流扒下来反序列化并处理异常后可以发现存在五个Object,因为WebServer每个请求通常会起一个新的线程来处理,不用担心像内核Pwn一样没处理好异常整个系统蹦掉,那么从第一处魔术字节开始替换为payload,后续部分抛掉不管必然会报错,但问题不大,将原本第二次请求的A + Serial + B + Serial + ...
改成A + Payload
。
构造如下脚本用作发送payload的客户端,它好比航天发射场组装火箭后打出去,payload就是火箭里用于开展科学实验的载荷。
那么问题来了,首先是为什么要先经过第一次通信交互(t3Bootstrap)。我们大家都知道在黑盒打不通的时候通常都是打不通的,对此小编也感到很惊讶,将payload硬莽过去只会得不到响应。这就如同需要用火箭将实验载荷送到预定轨道一样。
第二个问题是payload前加上的那些字节数据是干嘛的,即火箭为什么要设计构建成这样?自然是为了让载荷顺利进入预定轨道。相信有的同学想喷我了,这里看似相互解释了实则什么都没解释,如果前面还能从没响应的现象下手企图蒙混过关,但这里不啃代码是圆不过去了。
我知道你很急,但你先别急。我们先来看看稍微简单一点的第三个问题,即payload用什么?要使用ysoserial
生成的CC链就需要看目标环境有没有相应依赖,快速但不准确的方法是直接找有没有Jar包。
# find /u01/ -name "*commons*collections*.jar"
/u01/app/oracle/middleware/modules/com.bea.core.apache.commons.collections_3.2.0.jar
-
为了更精确定位则可以采用META-INF等信息判断版本、运行时相应上下文能否调到关键类等等方法。
如此我们便能利用这个祖师爷漏洞,对着周朝出土的Weblogic一顿RCE了:
java -jar ysoserial.jar CommonsCollections6 "touch /tmp/pwned" > /tmp/poc.ser
python3 t3client.py 127.0.0.1 7001 /tmp/poc.ser
# ls -al /tmp/ | grep 'pwned'
-rw-r----- 1 root root 0 Feb 17 17:18 pwned
逆向T3协议头
猜测关键词暴力搜索后,找到第一次交互时响应数据的来源。下断点得到调用栈:
connectReplyOK:160, Login (weblogic.socket)
readBootstrapMessage:189, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:323, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:394, BaseAbstractMuxableSocket (weblogic.socket)
dispatch:185, MuxableSocketDiscriminator (weblogic.socket)
readReadySocketOnce:960, SocketMuxer (weblogic.socket)
readReadySocket:897, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)
MuxableSocketT3#dispatch
方法根据bootstrapped
来判断连接是否经readBootstrapMessage
方法初始化过,解析提取的就是上文首次通信时发给服务端的数据。(这是10.3.6.0的位置,12.2.1.3在com.oracle.weblogic.rjvm.jar
中weblogic/rjvm/t3/MuxableSocketT3#readIncomingConnectionBootstrapMessage
)。
MuxableSocketDiscriminator#dispatch
方法根据协议名将请求分配给HTTP、T3等不同处理类,以此实现端口复用。
再将断点下在ObjectInputStream#readObject
并得到第二次请求时的调用栈:
readObject0:1327, ObjectInputStream (java.io)
readObject:349, ObjectInputStream (java.io)
readObject:67, InboundMsgAbbrev (weblogic.rjvm)
read:39, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:215, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:394, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:960, SocketMuxer (weblogic.socket)
readReadySocket:897, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)
数据由MsgAbbrevInputStream#init
调用各个方法解析。先看super.init(chunk, 4)
,最终前4字节被skip掉:
这4个字节其实就是EXP中的chunkLength
,表示包括自己在内的这段Header
的字节长度,可以偷懒直接赋0。
继续看到readHeader
方法中存在大量标识数据:
cmd
字节应该是指代通信类型,可以从weblogic/rjvm/JVMMessage
类中的变量名看出:
static final byte CMD_UNDEFINED = 0;
static final byte CMD_IDENTIFY_REQUEST = 1;
static final byte CMD_IDENTIFY_RESPONSE = 2;
static final byte CMD_REQUEST_CLOSE = 11;
static final byte CMD_IDENTIFY_REQUEST_CSHARP = 12;
static final byte CMD_IDENTIFY_RESPONSE_CSHARP = 13;
static final byte CMD_NO_ROUTE_IDENTIFY_REQUEST = 9;
static final byte CMD_TRANSLATED_IDENTIFY_RESPONSE = 10;
static final byte CMD_PEER_GONE = 3;
static final byte CMD_ONE_WAY = 4;
static final byte CMD_REQUEST = 5;
static final byte CMD_RESPONSE = 6;
static final byte CMD_ERROR_RESPONSE = 7;
static final byte CMD_INTERNAL = 8;
没猜到QOS
字节的含义,类初始化时被赋为十进制101
,所以EXP同样用了这个值。
flags
字节从后面getFlag
方法可以看出用来从二进制位控制hasJVMIDs
、hasTX
、hasTrace
(类比Linux的rwx权限与777)。
responseId
字节用于标识通信顺序、invokeableId
字节用于标识被调用的方法,目前用不到置为初始值-1。
abbrevOffset
字节顾名思义是abbrev的偏移长度,表示Header
结尾处 相距 后面字节流MsgAbbrevs
部分的距离,在init
方法中会被skip掉,EXP中直接赋0表示没有额外的数据需要跳过。
读完Header
部分继续进入到readMsgAbbrevs
方法中,会调用InboundMsgAbbrev#read
方法:
length
就是EXP中的countLength
,可以看到这个值对应for循环的次数。
length2
就是EXP中的capacityLength
,bubblingAbbrever.getCapacity()
拿到的就是第一次请求中的AS
,要设置得比它大才会进入readObeject
分支。
最后进入重写的有参readObject
中,msgAbbrevInputStream.read()
读到的字节是十进制0时进入无参readObject
。但ServerChannelInputStream
没有重写无参readObject
,所以最终进到父类ObjectInputStream
的readObject
衔接上CC链。