WSO2 RCE (CVE-2022-29464) exploit and writeup.
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 byorg.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor
jarZip
handled byorg.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor
dbs
handled byorg.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor
tools
handled byorg.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor
toolsAny
handled byorg.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:
the file can be found in:
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:
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
one of those applications is authenticationendpoint
(//host/authenticationendpoint
) which handles the authentication to WSO2 and its location is:
./repository/deployment/server/webapps/authenticationendpoint
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
.
- Using Burpsuite:
- Using exploiy.py:
Usage:
python3 exploit.py https://host:9443/ ArbitraryShellName.jsp