Skip to content

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

Notifications You must be signed in to change notification settings

hack-Red-moon/CVE-2022-29464

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 

Repository files navigation

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 unprotected route by IAM as we can see in the indentity.xml configuration file:

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

And also unprotected by the default login measure, handleSecurity() is the function responsible for securing the different routes served by WSO2 and provides a mechanism for performing security checks on the received HTTP requests, handleSecurity() will call CarbonUILoginUtil.handleLoginPageRequest() and based on its return value it will be decided to allow or deny access to the requested URI:

    public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
	[snipped]
        if ((val = CarbonUILoginUtil.handleLoginPageRequest(requestedURI, request, response,
                authenticated, context, indexPageURL)) != CarbonUILoginUtil.CONTINUE) {
            if (val == CarbonUILoginUtil.RETURN_TRUE) {
                return true;
            } else {
                return false;
            }
        }
	[snipped]
    }

CarbonUILoginUtil.handleLoginPageRequest() returns CarbonUILoginUtil.RETURN_TRUE when the route is /fileupload:

    protected static int handleLoginPageRequest(String requestedURI, HttpServletRequest request,
            HttpServletResponse response, boolean authenticated, String context, String indexPageURL)
            throws IOException {
        boolean isTryIt = requestedURI.indexOf("admin/jsp/WSRequestXSSproxy_ajaxprocessor.jsp") > -1;
        boolean isFileDownload = requestedURI.endsWith("/filedownload");
        if ((requestedURI.indexOf("login.jsp") > -1
                || requestedURI.indexOf("login_ajaxprocessor.jsp") > -1
                || requestedURI.indexOf("admin/layout/template.jsp") > -1
                || isFileDownload
                || requestedURI.endsWith("/fileupload")
                || requestedURI.indexOf("/fileupload/") > -1
                || requestedURI.indexOf("login_action.jsp") > -1
                || isTryIt
                || requestedURI.indexOf("tryit/JAXRSRequestXSSproxy_ajaxprocessor.jsp") > -1)
                && !requestedURI.contains(";")) {

            if ((requestedURI.indexOf("login.jsp") > -1
                    || requestedURI.indexOf("login_ajaxprocessor.jsp") > -1 || requestedURI
                    .indexOf("login_action.jsp") > -1) && authenticated) {
                [snipped]
            } else if ((isTryIt || isFileDownload) && !authenticated) {
                [snipped]
            } else if (requestedURI.indexOf("login_action.jsp") > -1 && !authenticated) {
                [snipped]
            } else {
				if (log.isDebugEnabled()) {
					log.debug("Skipping security checks for " + requestedURI);
				}
                return RETURN_TRUE;
            }
        }

        return CONTINUE;
    }

with CarbonUILoginUtil.handleLoginPageRequest() returning CarbonUILoginUtil.RETURN_TRUE, handleSecurity() will return true, the access will be then granted to /fileupload without authentication.

the FileUploadServlet servlet 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:

  • keystore, certificate, * 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:

image

the file can be found in:

image

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:

image

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

image

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

./repository/deployment/server/webapps/authenticationendpoint

image

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:

image

image

image

  • Using exploiy.py:

Usage:

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

poc

About

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

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python 100.0%