1. Prerequisites
- Ubuntu GNOME
- Spring Boot
- MongoDB
2. Overview
CVE-2022-22980 là một lỗ hổng bảo mật của thư viện spring-data-mongodb
có thể khiến attacker chạy bất kì câu lệnh nào trên máy chủ bằng việc truyền một đoạn mã độc vào user input để trigger thư viện thực thi nếu như ứng dụng Spring Boot
+ MongoDB
sử dụng JSON based query methods với @Query
, @Aggregation
annotaion có chứa parameter expression SpEL - Spring Expression Language tương tự như sau:
@Query("{ 'firstName' : ?#{?0} }") Customer findByFirstName(String firstName);
Đến đây bạn có thể thực hiện mở ứng dụng gnome-calculator
trên Ubuntu GNOME bằng cách thực thi câu lệnh truy vấn như thế này:
repository.findByFirstName("T(java.lang.Runtime).getRuntime().exec(\"gnome-calculator\")");
Do lổ hổng bảo mật này đã được report từ hơn 1 tháng trước (13/06/2022) nên nếu bạn sử dụng spring-data-mongodb
depedency mới release gần đây (v.3.3.5 hay v3.4.1 trở lên) thì sẽ không khai thác được lỗ hổng này.
3. Exploitation Explain
Trước khi đi vào chi tiết, các bạn có thể xem toàn bộ source code ở đây
Vì lỗ hổng này liên quan trực tiếp đến cách Spring Boot framework xử lý SpEL query parameter của MongoDB interface repository, nên để hiểu chúng ta cần phải biết quá trình một câu truy vấn được xử lý ra sao.
- Khai báo CustomerRepository
- Tiến hành gọi câu truy vấn
Bạn có thể thấy, CustomerRepository là một proxy class (JdkDynamicAopProxy), có nghĩa rằng tại runtime (dynamic), Spring Boot framework đã tiến hành proxy CustomerRepository bean và khi bạn bạn tiến hành gọi câu truy vấn thì bạn phải gọi qua proxy class trước.
Khi tiến hành debug, nhìn qua callstack bạn sẽ thấy khá loạn vì Spring Boot framework gọi qua nhiều proxy class khác nhau, nhưng thực sự để mà nói thì source code quá lớn và phức tạp để chúng ta có thể hiểu một cách tường tận một workflows hoàn chỉnh nhưng có thể hiểu nôm na như sau (theo cách hiểu của mình):
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, chúng ta sẽ thực thi các bước tiền xử lý trước khi tiến hành chọc thẳng vào database truy vấn dữ liệu, đơn cử như là StringBasedMongoQuery#createQuery()
. Tuy nhiên trước khi gọi #createQuery()
thì Spring Boot framework cần binding paramter vào câu query trước thông qua phương thức getBindingContext()
:
Tiếp theo là bước phân tích câu query và gán giá trị từ tham số.
Bạn có thể thấy, với parameter expression đầu vào là T(java.lang.Runtime).getRuntime().exec(\"gnome-calculator\")
thì sau khi được xử lý giá trị của biến expression
vẫn như thế, điều đó có nghĩa là phương thức ParameterBindingJsonReader#bindableValueFor()
không phát hiện ra được đây là một đoạn mã được truyền vào thay vì một giá trị thông thường.
Và lỗ hổng bảo mật xảy ra ở đây chính là khi gọi phương thức ParameterBindingJsonReader#evaluateExpression()
, expression
truyền vào được covert thành một UUNIXProcess
và tiến hành việc mở ứng dụng gnome-calculator
ngay sau đó.
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)
Vào một tuần sau khi lỗ hổng được report (20/06/2022), Spring Boot framework tung ra hai bản vá mới cho spring-data-mongodb
dependency là v3.3.6
và v3.4.2
.
Vậy chúng ta hãy cùng xem source code đã thay đổi như thế nào nhé.
Biến expression
không còn mang giá trị cũ nữa: #_QVar0
, qua đó thì lỗ hổng đã được vá.
Fixed commit:
4. Workaround
- Sử dụng parameter array syntax:
@Query("{ 'firstName' : ?#{?[0]} }") Customer findByFirstName(String firstName);
- Sử dụng custom repository.
5. References
- 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
- PoC CVE-2022-22980
- [CVE-2022-22980] Spring Data MongoDB SpEL Expression injection
- Code difference between vulnerable version and fixed version