Phân tích và xây dựng POC khai thác CVE-2022-22980

PHÂN TÍCH VÀ XÂY DỰNG POC KHAI THÁC CVE-2022-22980

Bài viết này sẽ trình bày về cách thức khai thác lỗ hổng CVE-2022-22980 – Spring Data MongoDB Remote Code Execution (RCE)

Introduction:

CVE-2022-22980 là một lỗ hổng bảo mật của thư viện Spring Data MongoDB với version 3.4.0, 3.3.0 to 3.3.4, cho phép kẻ tấn công thực thi bất cứ câu lệnh nào trên server chỉ bằng việc truyền vào 1 đoạn mã độc ở user input để trigger thư viện thực thi nếu ứng dụng Spring Boot + MongoDB sử dụng JSON Based query methods với @Query, @Aggregation annotation có chứa parameter expression SpEL – Spring Expression Language tương tự như sau:

@Query("{ 'firstName' : ?#{?0} }")

Customer findByFirstName(String firstName);

Vậy tại sao ở đây lại bị lỗi, thì ở đây tôi có build 1 ví dụ sử dụng Spring Expression để chứng minh có thể chạy được command:

- Đầu tiên, chương trình này sử dụng thư viện Spring Expression ver 5.2.1

- Sau đó, chương trình nhận input từ người dùng rồi sử dụng class Expression để parse input đấy từ người dùng

- Cuối thì dùng toString() để in ép dạng chuỗi và in output ra màn hình

Ảnh 1: Spring Expression có thể chạy command (có thể gọi calc.exe)

- Ở đây, tôi truyền vào câu lệnh: T(java.lang.Runtime).getRuntime().exec("calc.exe")

- Nói sơ 1 chút về câu lệnh, về cơ bản thì class chính là 1 class đa hình, nếu ta truyền vào class này 1 cái gì đó, thì class này sẽ trở thành cái đó. Cụ thể ở đây ta truyền vào class java.lang.Runtime. Như vậy đã trở thành java.lang.Runtime. Từ đó gọi tới hàm getRuntime()exec()

- Như vậy, về cơ bản thì Spring Expression Language có thể chạy được command

Phân tích:

Sau nỗ lực tìm kiếm thông tin về CVE này thì tôi đọc được thông tin trên trang chủ của Spring đề cập về lỗ hổng này:


Ảnh 2: Nguồn: https://spring.io/security/cve-2022-22980

Như vậy, chúng ta thật sự xác định được các annotation là @Query @Aggregation sẽ chịu trách nhiệm cho lỗ hổng này.

Vậy trước khi bắt đầu phân tích, ta cần phải tìm hiểu cách hoạt động của Spring Boot Framework khi xử lý SpEL Query thông qua 2 annotation trên:

- Khai báo CustomerRepository:

Ảnh 3: Khai báo CustomerRepository

- Tiến hành gọi câu truy vấn:

Ảnh 4: Gọi câu truy vấn ?query= và đã hit được debug

- Step into:

Ảnh 5: Step into - invoke()

Sau khi debug và step into vài lần, có thể thấy Spring Boot Framework gọi qua rất nhiều class, tuy nhiên, ta có thể tóm gọn và hiểu đơn giản workflow của chúng như sau:

- Khi gọi đến phương thức findByFirstname() trong CustomerRepository, thì tất cả mọi lời gọi phương thức đều phải đi qua một handler duy nhất – đó chính là phương thức invoke()

- Bên trong phương thức invoke() này, ứng dụng sẽ tiến hành thực thi các bước tiền xử lý trước khi được đưa vào database để truy vấn dữ liệu. Ví dụ như hàm createQuery() trong class StringBasedMongoQuery().

Tuy nhiên, trước khi gọi được hàm này thì Spring Boot Framework cần binding parameter vào câu query trước thông qua hàm getBindingContext():

Ảnh 6: Bind Param bằng hàm getBindingContext()

Có thể biết được thông qua Step into để biết rằng ứng dụng sẽ gọi từ getBindingContext() -> captureExpressionDependencies() -> ParameterBindingJsonReader() dùng để parse JSON ra từ request -> bindableValueFor() để gán giá trị vào tham số sau khi parse. Tại đây có thể biết được sau khi gán mọi thứ xong, biến expression không hề phát hiện được input ta truyền vào là 1 đoạn mã chứ không phải 1 giá trị thông thường vì đoạn mã T(java.lang.Runtime).getRuntime().exec("calc.exe") vẫn được giữ nguyên:

Ảnh 7: Đoạn mã truyền vào vẫn được giữ nguyên

Lỗ hổng thực sự xảy ra khi sau đó đoạn mã độc của chúng ta được truyền vào hàm evaluateExpression() và thực thi command được truyền vào là mở calc.exe lên (chúng ta đã xác định ngay từ đầu là Spring Expression có thể thực thi command)


Ảnh 8: Đoạn mã được truyền thẳng vào hàm evaluateExpression()

POC:


Ảnh 9: PoC - Gọi được calculator

Debug CallStack:

bindableValueFor:389, ParameterBindingJsonReader (org.springframework.data.mongodb.util.json)

readBsonType:304, ParameterBindingJsonReader (org.springframework.data.mongodb.util.json)

decode:227, ParameterBindingDocumentCodec (org.springframework.data.mongodb.util.json)

decode:180, ParameterBindingDocumentCodec (org.springframework.data.mongodb.util.json)

createQuery:121, StringBasedMongoQuery (org.springframework.data.mongodb.repository.query)

doExecute:122, AbstractMongoQuery (org.springframework.data.mongodb.repository.query)

execute:107, AbstractMongoQuery (org.springframework.data.mongodb.repository.query)

invoke:-1, 1601756706 (org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryQueryMethodInvoker$$Lambda$580)

doInvoke:137, RepositoryMethodInvoker (org.springframework.data.repository.core.support)

invoke:121, RepositoryMethodInvoker (org.springframework.data.repository.core.support)

doInvoke:159, QueryExecutorMethodInterceptor (org.springframework.data.repository.core.support)

invoke:138, QueryExecutorMethodInterceptor (org.springframework.data.repository.core.support)

proceed:186, ReflectiveMethodInvocation (org.springframework.aop.framework)

invoke:80, DefaultMethodInvokingMethodInterceptor (org.springframework.data.projection)

proceed:186, ReflectiveMethodInvocation (org.springframework.aop.framework)

invoke:97, ExposeInvocationInterceptor (org.springframework.aop.interceptor)

proceed:186, ReflectiveMethodInvocation (org.springframework.aop.framework)

invoke:215, JdkDynamicAopProxy (org.springframework.aop.framework)

findByFirstName:-1, $Proxy44 (com.sun.proxy)

invoke0:-1, NativeMethodAccessorImpl (sun.reflect)

invoke:62, NativeMethodAccessorImpl (sun.reflect)

invoke:43, DelegatingMethodAccessorImpl (sun.reflect)

invoke:498, Method (java.lang.reflect)

invokeJoinpointUsingReflection:344, AopUtils (org.springframework.aop.support)

invokeJoinpoint:198, ReflectiveMethodInvocation (org.springframework.aop.framework)

proceed:163, ReflectiveMethodInvocation (org.springframework.aop.framework)

invoke:137, PersistenceExceptionTranslationInterceptor (org.springframework.dao.support)

proceed:186, ReflectiveMethodInvocation (org.springframework.aop.framework)

invoke:215, JdkDynamicAopProxy (org.springframework.aop.framework)

findByFirstName:-1, $Proxy44 (com.sun.proxy)

run:65, AccessingDataMongodbApplication (com.logbasex.accessingdatamongodb)

callRunner:777, SpringApplication (org.springframework.boot)

callRunners:761, SpringApplication (org.springframework.boot)

run:310, SpringApplication (org.springframework.boot)

run:1312, SpringApplication (org.springframework.boot)

run:1301, SpringApplication (org.springframework.boot)

main:34, AccessingDataMongodbApplication (com.logbasex.accessingdatamongodb)

Những công cụ được sử dụng:

- IDE: IntelliJ Community Edition

- IDE: Visual Studio Code for testing SpEL Expression

- Spring Data MongoDB 3.3.4

- MongoDB version 4.2.21

- Brave

- Docker

REFERENCES:

- Spring Data MongoDB SpEL Expression Injection Vulnerability (CVE-2022-22980) - https://spring.io/blog/2022/06/20/spring-data-mongodb-spel-expression-injection-vulnerability-cve-2022-22980

- CVE-2022-22980: Spring Data MongoDB SpEL Expression injection vulnerability through annotated repository query methods - https://spring.io/security/cve-2022-22980

- [CVE-2022-22980] Spring Data MongoDB SpEL Expression Injection - https://github.com/murataydemir/CVE-2022-22980

- PoC CVE-2022-22980 - https://github.com/trganda/CVE-2022-22980

- Code được fix như thế nào sau khi ra bản patch - https://github.com/spring-projects/spring-data-mongodb/commit/30a417d810c43dd097e117beb24ca49074c5b04d?diff=split