
Exploit e writeup di WSO2 RCE (CVE-2022-29464).
CVE-2022-29464 è una vulnerabilità critica su WSO2 scoperta da Orange Tsai. La vulnerabilità è un upload arbitrario di file non autenticato e senza restrizioni che consente ad attaccanti non autenticati di ottenere RCE sui server WSO2 caricando file JSP dannosi.
Il percorso di upload vulnerabile è /fileupload, gestito dal servlet FileUploadServlet. Ed è un percorso non protetto da IAM, come si può vedere nel file di configurazione indentity.xml:```xml
E inoltre non protetta dalla misura di login predefinita, `handleSecurity()` è la funzione responsabile della protezione delle diverse route servite da WSO2 e fornisce un meccanismo per eseguire controlli di sicurezza sulle richieste HTTP ricevute; `handleSecurity()` chiamerà `CarbonUILoginUtil.handleLoginPageRequest()` e in base al suo valore di ritorno verrà deciso se consentire o negare l'accesso all'URI richiesto:```java
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() restituisce CarbonUILoginUtil.RETURN_TRUE quando la rotta è /fileupload:```java
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;
}
con `CarbonUILoginUtil.handleLoginPageRequest()` che restituisce `CarbonUILoginUtil.RETURN_TRUE`, `handleSecurity()` restituirà `true`, quindi l'accesso a `/fileupload` sarà concesso senza autenticazione.
il servlet `FileUploadServlet` e, su [`init()`](https://github.com/wso2/carbon-kernel/blob/d47232dfb2b26c0ef18a74e2ef4aa503caa59697/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/FileUploadServlet.java#L71) e attraverso una serie di chiamate di metodo, carica infine dal file di configurazione `carbon.xml` più formati/azioni di upload dei file insieme all'oggetto che gestisce ogni formato.```java
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);
}
}
il costruttore della classe FileUploadExecutorManager è il seguente:```java
public FileUploadExecutorManager(BundleContext bundleContext,
ConfigurationContext configCtx,
String webContext) throws CarbonException {
this.bundleContext = bundleContext;
this.configContext = configCtx;
this.webContext = webContext;
this.loadExecutorMap();
}
il costruttore chiama il metodo privato [`loadExecutorMap()`](https://github.com/wso2/carbon-kernel/blob/d47232dfb2b26c0ef18a74e2ef4aa503caa59697/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/FileUploadExecutorManager.java#L131) che è il punto in cui viene eseguito il caricamento della configurazione:```java
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]
le configurazioni dei formati di caricamento dei file si trovano nel namespace FileUploadConfig nel file di configurazione XML, questa è la configurazione predefinita:```xml
100
<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>
il metodo `loadExecutorMap()` crea e riempie una `HashMap` di `<Action, Class>` con le Action e le Class estratte dal file di configurazione. che verrà poi utilizzata per scegliere quale classe usare per gestire correttamente un dato formato/azione.
Successivamente, quando la rotta `/fileupload` riceve una richiesta POST, viene chiamato il metodo [`doPost()`](https://github.com/wso2/carbon-kernel/blob/d47232dfb2b26c0ef18a74e2ef4aa503caa59697/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/FileUploadServlet.java#L53) del servlet. il metodo si limita a inoltrare gli oggetti request e response al metodo `execute()` di `fileUploadExecutorManager`, che è stato inizializzato in `init()`.```java
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);
}
}
il metodo execute(), divide l'URL della richiesta subito dopo la stringa fileupload/, il che significa che estrae tutto ciò che viene dopo /fileupload/ nell'URL della richiesta e lo assegna a actionString.```java
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;
}
l'`actionString` viene passato al costruttore della classe `CarbonXmlFileUploadExecHandler` insieme a `request` e `response`:```java
private CarbonXmlFileUploadExecHandler(HttpServletRequest request,
HttpServletResponse response,
String actionString) {
this.request = request;
this.response = response;
this.actionString = actionString;
}
il costruttore li salverà nelle sue proprietà.
dopo di che l'oggetto carbonXmlExecHandler insieme ad altri oggetti verrà aggiunto a execHandlerManager utilizzando il metodo addExecHandler().```java
public void addExecHandler(FileUploadExecutionHandler handler) {
if (prevHandler != null) {
prevHandler.setNext(handler);
} else {
firstHandler = handler;
}
prevHandler = handler;
}
quindi `execHandlerManager.startExec()` viene chiamato:```java
public void startExec() throws IOException {
firstHandler.execute();
}
startExec() chiama execute() del primo oggetto aggiunto, che è CarbonXmlFileUploadExecHandler:```java
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()`](https://github.com/wso2/carbon-kernel/blob/d47232dfb2b26c0ef18a74e2ef4aa503caa59697/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/FileUploadExecutorManager.java#L430) scorre la `HashMap` di `<Action, Class>` creata in precedenza e trova l'Action (chiave) uguale a `actionString`; se trovata, viene chiamato il metodo `executeGeneric()` dell'oggetto associato a quell'Action.
Per rivedere la configurazione predefinita ci sono 7 azioni, che sono:
* `keystore`, `certificate`, `*` gestiti da `org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor`
* `jarZip` gestito da `org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor`
* `dbs` gestito da `org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor`
* `tools` gestito da `org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor`
* `toolsAny` gestito da `org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor`
Ciascuno di questi oggetti gestisce l'upload in modo diverso; alcuni accettano estensioni specifiche.
Il primo che ho trovato vulnerabile alla scrittura arbitraria di file è stato `toolsAny` ([`ToolsAnyFileUploadExecutor`](https://github.com/wso2/carbon-kernel/blob/4.4.x/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/ToolsAnyFileUploadExecutor.java)).
`ToolsAnyFileUploadExecutor` non ha un metodo `executeGeneric()`, ma estende [`AbstractFileUploadExecutor`](https://github.com/wso2/carbon-kernel/blob/d47232dfb2b26c0ef18a74e2ef4aa503caa59697/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/AbstractFileUploadExecutor.java#L61), che dispone di un metodo [`executeGeneric()`](https://github.com/wso2/carbon-kernel/blob/d47232dfb2b26c0ef18a74e2ef4aa503caa59697/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/AbstractFileUploadExecutor.java#L97):```java
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() chiama prima parseRequest() con l'oggetto request come parametro:```java
protected void parseRequest(HttpServletRequest request) throws FileUploadFailedException,
FileSizeLimitExceededException {
fileItemsMap.set(new HashMap<String, ArrayList>());
formFieldsMap.set(new HashMap<String, ArrayList>());
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);
}
}
}
}
}
prima assicura che la richiesta POST sia una richiesta POST multipart, quindi estrae i file caricati, assicura che la richiesta POST contenga almeno un file caricato e lo valida rispetto alla dimensione massima del file.
dopo il ritorno da `parseRequest()`, `executeGeneric()` chiamerà ora il metodo `execute()` che è [sovrascritto](https://github.com/wso2/carbon-kernel/blob/d47232dfb2b26c0ef18a74e2ef4aa503caa59697/core/org.wso2.carbon.ui/src/main/java/org/wso2/carbon/ui/transports/fileupload/ToolsAnyFileUploadExecutor.java#L36) da `ToolsAnyFileUploadExecutor`:```java
@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;
}
È qui che si trova il bug: il metodo execute() è vulnerabile a una vulnerabilità di path traversal, poiché si fida del nome file fornito dall'utente nella richiesta POST. Senza che il path traversal fuoriesca dalla directory tmp, il file viene effettivamente salvato in:```
./tmp/work/extra/$uuid/$filename
con `uuid` restituito nella risposta:

il file può essere trovato in:

Ora dobbiamo solo uscire dalla directory `tmp` e aggiungere la nostra shell JSP in qualche posizione servita dal WSO2.
troviamo la directory `appBase` di tomcat:

questa directory è la posizione delle applicazioni deployate su tomcat, contiene diverse applicazioni WAR già deployate e anche i loro file WAR grezzi:```
./repository/deployment/server/webapps

una di quelle applicazioni è authenticationendpoint (//host/authenticationendpoint) che gestisce l'autenticazione a WSO2 e la sua posizione è:```
./repository/deployment/server/webapps/authenticationendpoint

**NOTA:** possiamo anche usare la vulnerabilità per creare una nostra nuova directory (context path) nella directory `appBase` e verrà distribuita automaticamente, ma io ne prenderò semplicemente una e userò `authenticationendpoint`.
# PoC
* Usando Burpsuite:



* Usando exploiy.py:
> Uso:
> ```
> python3 exploit.py https://host:9443/ ArbitraryShellName.jsp
> ```
