WSO2 RCE (CVE-2022-29464) exploit and writeup

渗透技巧 2年前 (2022) admin
1,442 0 0

CVE-2022-29464

WSO2 RCE (CVE-2022-29464) exploit and writeup.

Details

CVE-2022-29464 is critical vulnerability on WSO2 discovered by Orange Tsai. the vulnerability is an unauthenticated unrestricted arbitrary file upload which allows unauthenticated attackers to gain RCE on WSO2 servers via uploading malicious JSP files.

the vulerable upload route is /fileupload which is handled by FileUploadServlet servlet. and it is an unprotected route as we can see in the indentity.xml configuration file:

<Resource context="(.*)/fileupload(.*)" secured="false" http-method="all"/>

the FileUploadServlet and upon init() and through a series of method calls loads eventually from the carbon.xml configuration file multiple upload file formats/actions along with the object which hanldes every format.

    public void init(ServletConfig servletConfig) throws ServletException {
        this.servletConfig = servletConfig;
        try {
            fileUploadExecutorManager = new FileUploadExecutorManager(bundleContext, configContext, webContext);
            //Registering FileUploadExecutor Manager as an OSGi service
            bundleContext.registerService(FileUploadExecutorManager.class.getName(), fileUploadExecutorManager, null);
        } catch (CarbonException e) {
            log.error("Exception occurred while trying to initialize FileUploadServlet", e);
            throw new ServletException(e);
        }
    }

the FileUploadExecutorManager class constructor is as follows:

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

the constructor calls the private method loadExecutorMap() which is where the configuration loading is done:

    private void loadExecutorMap() throws CarbonException {
        [snipped]
                try {
            documentElement = XMLUtils.toOM(serverConfiguration.getDocumentElement());
        } catch (Exception e) {
            String msg = "Unable to read Server Configuration.";
            log.error(msg);
            throw new CarbonException(msg, e);
        }
        [snipped]
        OMElement fileUploadConfigElement =
                documentElement.getFirstChildWithName(
                        new QName(ServerConstants.CARBON_SERVER_XML_NAMESPACE, "FileUploadConfig"));
        for (Iterator iterator = fileUploadConfigElement.getChildElements(); iterator.hasNext();) {
            OMElement mapppingElement = (OMElement) iterator.next();
            if (mapppingElement.getLocalName().equalsIgnoreCase("Mapping")) {
                OMElement actionsElement =
                        mapppingElement.getFirstChildWithName(
                                new QName(ServerConstants.CARBON_SERVER_XML_NAMESPACE, "Actions"));
                String confPath = System.getProperty(CarbonBaseConstants.CARBON_CONFIG_DIR_PATH);
        [snipped]

the file upload formats configurations is within the FileUploadConfig namespace in the XML configuration file, this is the default configuration:

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

the loadExecutorMap() method creates and fills a HashMap of <Action, Class> with the Actions and the Classes extracted from the config file. which will be later used to choose which class to use to handle properly a given format/action.

Later on when the /fileupload route recieves a POST request the doPost() method of the servlet will be called. the method just forwards the request and response object to execute() method of fileUploadExecutorManager which was intitialized on init()

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response) throws ServletException, IOException {

        try {
            fileUploadExecutorManager.execute(request, response);
        } catch (Exception e) {
            String msg = "File upload failed ";
            log.error(msg, e);
            throw new ServletException(e);
        }
    }

the execute() method, splits the request url just after the fileupload/ string, which means it extacts whatever is after the /fileupload/ in the request URL and it assignes is it to actionString.

    public boolean execute(HttpServletRequest request,
                           HttpServletResponse response) throws IOException {

        HttpSession session = request.getSession();
        String cookie = (String) session.getAttribute(ServerConstants.ADMIN_SERVICE_COOKIE);
        request.setAttribute(CarbonConstants.ADMIN_SERVICE_COOKIE, cookie);
        request.setAttribute(CarbonConstants.WEB_CONTEXT, webContext);
        request.setAttribute(CarbonConstants.SERVER_URL,
                             CarbonUIUtil.getServerURL(request.getSession().getServletContext(),
                                                       request.getSession()));


        String requestURI = request.getRequestURI();

        //TODO - fileupload is hardcoded
        int indexToSplit = requestURI.indexOf("fileupload/") + "fileupload/".length();
        String actionString = requestURI.substring(indexToSplit);

        // Register execution handlers
        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;
    }

the actionString is passed to CarbonXmlFileUploadExecHandler class constructor along with request and response:

        private CarbonXmlFileUploadExecHandler(HttpServletRequest request,
                                               HttpServletResponse response,
                                               String actionString) {
            this.request = request;
            this.response = response;
            this.actionString = actionString;
        }

the constructor will save them to its properties.

after that carbonXmlExecHandler object along with other objects will be added to execHandlerManager using addExecHandler() method.

        public void addExecHandler(FileUploadExecutionHandler handler) {
            if (prevHandler != null) {
                prevHandler.setNext(handler);
            } else {
                firstHandler = handler;
            }
            prevHandler = handler;
        }

then execHandlerManager.startExec() is called:

        public void startExec() throws IOException {
            firstHandler.execute();
        }

startExec() calls execute() of the first object added which is CarbonXmlFileUploadExecHandler:

        public void execute() throws IOException {
            boolean foundExecutor = false;
            for (String key : executorMap.keySet()) {
                if (key.equals(actionString)) {
                    AbstractFileUploadExecutor obj = executorMap.get(key);
                    foundExecutor = true;
                    obj.executeGeneric(request, response, configContext);
                    break;
                }
            }
            if (!foundExecutor) {
                next();
            }
        }

execute() loops trough the HashMap of <Action, Class> created earlier and finds the Action (key) which is equal to actionString, if found the executeGeneric() method of the object associated with that Action will be called.

to revise the default configuration has 7 actions which are:

  • keystorecertificate* handled by org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor
  • jarZip handled by org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor
  • dbs handled by org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor
  • tools handled by org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor
  • toolsAny handled by org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor

each of these objects does handle the upload differently some of them accepts specific extensions.

the first one i found vulnerable to arbitraty file write was toolsAny (ToolsAnyFileUploadExecutor). ToolsAnyFileUploadExecutor does not have a executeGeneric() method but it extends AbstractFileUploadExecutor which does have a executeGeneric() method:

    boolean executeGeneric(HttpServletRequest request,
                           HttpServletResponse response,
                           ConfigurationContext configurationContext) throws IOException {//,
        //    CarbonException {
        this.configurationContext = configurationContext;
        try {
            parseRequest(request);
            return execute(request, response);
        } catch (FileUploadFailedException e) {
            sendErrorRedirect(request, response, e);
        } catch (FileSizeLimitExceededException e) {
            sendErrorRedirect(request, response, e);
        } catch (CarbonException e) {
            sendErrorRedirect(request, response, e);
        }
        return false;
    }

executeGeneric() calls first parseRequest() with the request object as a parameter:

    protected void parseRequest(HttpServletRequest request) throws FileUploadFailedException,
                                                                 FileSizeLimitExceededException {
        fileItemsMap.set(new HashMap<String, ArrayList<FileItemData>>());
        formFieldsMap.set(new HashMap<String, ArrayList<String>>());

        ServletRequestContext servletRequestContext = new ServletRequestContext(request);
        boolean isMultipart = ServletFileUpload.isMultipartContent(servletRequestContext);
        Long totalFileSize = 0L;

        if (isMultipart) {

            List items;
            try {
                items = parseRequest(servletRequestContext);
            } catch (FileUploadException e) {
                String msg = "File upload failed";
                log.error(msg, e);
                throw new FileUploadFailedException(msg, e);
            }
            boolean multiItems = false;
            if (items.size() > 1) {
                multiItems = true;
            }

            // Add the uploaded items to the corresponding maps.
            for (Iterator iter = items.iterator(); iter.hasNext();) {
                FileItem item = (FileItem) iter.next();
                String fieldName = item.getFieldName().trim();
                if (item.isFormField()) {
                    if (formFieldsMap.get().get(fieldName) == null) {
                        formFieldsMap.get().put(fieldName, new ArrayList<String>());
                    }
                    try {
                        formFieldsMap.get().get(fieldName).add(new String(item.get(), "UTF-8"));
                    } catch (UnsupportedEncodingException ignore) {
                    }
                } else {
                    String fileName = item.getName();
                    if ((fileName == null || fileName.length() == 0) && multiItems) {
                        continue;
                    }
                    if (fileItemsMap.get().get(fieldName) == null) {
                        fileItemsMap.get().put(fieldName, new ArrayList<FileItemData>());
                    }
                    totalFileSize += item.getSize();
                    if (totalFileSize < totalFileUploadSizeLimit) {
                        fileItemsMap.get().get(fieldName).add(new FileItemData(item));
                    } else {
                        throw new FileSizeLimitExceededException(getFileSizeLimit() / 1024 / 1024);
                    }
                }
            }
        }
    }

it first assures that the POST request is a multipart POST request, and then extarcts the uploaded files, assures that the POST request contains at least on uploaded file and validates it against the maximum file size.

after returning from parseRequest()executeGeneric() will call now the execute() method which is overrode by ToolsAnyFileUploadExecutor:

	@Override
	public boolean execute(HttpServletRequest request,
			HttpServletResponse response) throws CarbonException, IOException {
		PrintWriter out = response.getWriter();
        try {
        	Map fileResourceMap =
                (Map) configurationContext
                        .getProperty(ServerConstants.FILE_RESOURCE_MAP);
        	if (fileResourceMap == null) {
        		fileResourceMap = new TreeBidiMap();
        		configurationContext.setProperty(ServerConstants.FILE_RESOURCE_MAP,
                                             fileResourceMap);
        	}
            List<FileItemData> fileItems = getAllFileItems();
            //String filePaths = "";

            for (FileItemData fileItem : fileItems) {
                String uuid = String.valueOf(
                        System.currentTimeMillis() + Math.random());
                String serviceUploadDir =
                        configurationContext
                                .getProperty(ServerConstants.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());
                try (FileOutputStream fileOutStream = new FileOutputStream(uploadedFile)) {
                    fileItem.getDataHandler().writeTo(fileOutStream);
                    fileOutStream.flush();
                }
                response.setContentType("text/plain; charset=utf-8");
                //filePaths = filePaths + uploadedFile.getAbsolutePath() + ",";
                fileResourceMap.put(uuid, uploadedFile.getAbsolutePath());
                out.write(uuid);
            }
            //filePaths = filePaths.substring(0, filePaths.length() - 1);
            //out.write(filePaths);
            out.flush();
        } catch (Exception e) {
            log.error("File upload FAILED", e);
            out.write("<script type=\"text/javascript\">" +
                    "top.wso2.wsf.Util.alertWarning('File upload FAILED. File may be non-existent or invalid.');" +
                    "</script>");
        } finally {
            out.close();
        }
        return true;
	}

Here is where the bug lies, execute() method is vulnerable to a path traversal vulenerabulity as it trusts the filename given by the user in the POST request. without the path traversal escaping the tmp dir the file is actually saved to:

./tmp/work/extra/$uuid/$filename

with uuid being returned in the response:

WSO2 RCE (CVE-2022-29464) exploit and writeup

the file can be found in:

WSO2 RCE (CVE-2022-29464) exploit and writeup

Now we just need to escape the tmp directory and add our JSP shell to some location being served by the WSO2.

lets find the tomcat appBase directory:

WSO2 RCE (CVE-2022-29464) exploit and writeup

this directory is the location of the applications that are deployed on tomcat, it contains multiple already deployed WAR applications and also thier raw WAR files:

./repository/deployment/server/webapps

WSO2 RCE (CVE-2022-29464) exploit and writeup

one of those applications is authenticationendpoint (//host/authenticationendpoint) which handles the authentication to WSO2 and its location is:

./repository/deployment/server/webapps/authenticationendpoint

WSO2 RCE (CVE-2022-29464) exploit and writeup

NOTE: we can also use the vulnerability to create our own fresh directory (context path) in the appBase directory and it will be auto deployed, but i will just carry one and use authenticationendpoint.

PoC

  • Using Burpsuite:

WSO2 RCE (CVE-2022-29464) exploit and writeup

WSO2 RCE (CVE-2022-29464) exploit and writeup

WSO2 RCE (CVE-2022-29464) exploit and writeup

  • Using exploiy.py:

Usage:

python3 exploit.py https://host:9443/ ArbitraryShellName.jsp

WSO2 RCE (CVE-2022-29464) exploit and writeup

 

 

版权声明:admin 发表于 2022年4月22日 下午12:18。
转载请注明:WSO2 RCE (CVE-2022-29464) exploit and writeup | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...