
Exploit pour CVE-2022-29464, un téléversement de fichier arbitraire non authentifié dans les serveurs WSO2 permettant l'exécution de code à distance via des téléversements JSP malveillants.
Exploitation et analyse de la RCE WSO2 (CVE-2022-29464).
CVE-2022-29464 est une grave vulnérabilité dans WSO2. Cette faille est un téléchargement arbitraire de fichiers non autorisé et sans contrainte, permettant à des utilisateurs non authentifiés de télécharger des fichiers JSP malveillants sur les serveurs WSO2 et d'obtenir une exécution de code à distance (RCE).
La route de téléchargement vulnérable est /fileupload, gérée par le servlet FileUploadServlet. Il s'agit d'une route non protégée par IAM, comme on peut le voir dans le fichier de configuration indentity.xml :```xml
La fonction `handleSecurity()` est en charge de la protection des différentes routes fournies par WSO2 et offre un mécanisme pour effectuer des contrôles de sécurité sur les requêtes HTTP reçues. `handleSecurity()` appellera `CarbonUILoginUtil.handleLoginPageRequest()`, et en fonction de sa valeur de retour, il sera décidé d'accorder ou de refuser l'accès à l'URI demandée :```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() retourne CarbonUILoginUtil.RETURN_TRUE lorsque la route est /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;
}
avec `CarbonUILoginUtil.handleLoginPageRequest()` renvoyant `CarbonUILoginUtil.RETURN_TRUE`, `handleSecurity()` renverra `true`, l'accès sera alors accordé à `/fileupload` sans authentification.
le servlet `FileUploadServlet` et lors de [`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) et à travers une série d'appels de méthode charge finalement depuis le fichier de configuration `carbon.xml` plusieurs formats/actions de fichiers téléchargés ainsi que l'objet qui gère chaque format.```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);
}
}
le FileUploadExecutorManager constructeur de la classe est le suivant :```java
public FileUploadExecutorManager(BundleContext bundleContext,
ConfigurationContext configCtx,
String webContext) throws CarbonException {
this.bundleContext = bundleContext;
this.configContext = configCtx;
this.webContext = webContext;
this.loadExecutorMap();
}
le constructeur appelle la méthode privée [`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) qui est l'endroit où le chargement de la configuration est effectué :```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]
les configurations des formats de téléchargement de fichiers se trouvent dans l'espace de noms FileUploadConfig du fichier de configuration XML, voici la configuration par défaut:```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>
la méthode `loadExecutorMap()` crée et remplit un `HashMap` de `<Action, Class>` avec les Actions et les Classes extraites du fichier de configuration. Cela sera utilisé plus tard pour choisir la classe à utiliser pour traiter correctement un format/action donné(e).
Plus tard, lorsque la route `/fileupload` reçoit une requête POST, la méthode [`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) du servlet sera appelée. La méthode transmet simplement l'objet requête et réponse à la méthode `execute()` de `fileUploadExecutorManager` qui a été initialisé dans `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);
}
}
la méthode execute() divise l'URL de la requête juste après la chaîne fileupload/, ce qui signifie qu'elle extrait tout ce qui se trouve après /fileupload/ dans l'URL de la requête et l'assigne à 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;
}
le `actionString` est passé au constructeur de la classe `CarbonXmlFileUploadExecHandler` avec `request` et `response` :```java
private CarbonXmlFileUploadExecHandler(HttpServletRequest request,
HttpServletResponse response,
String actionString) {
this.request = request;
this.response = response;
this.actionString = actionString;
}
le constructeur les sauvegardera dans ses propriétés.
après cela, l'objet carbonXmlExecHandler ainsi que d'autres objets seront ajoutés à execHandlerManager en utilisant la méthode addExecHandler().```java
public void addExecHandler(FileUploadExecutionHandler handler) {
if (prevHandler != null) {
prevHandler.setNext(handler);
} else {
firstHandler = handler;
}
prevHandler = handler;
}
ensuite, `execHandlerManager.startExec()` est appelé :```java
public void startExec() throws IOException {
firstHandler.execute();
}
startExec() appelle execute() du premier objet ajouté qui est 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) parcourt le `HashMap` de `<Action, Class>` créé précédemment et trouve l'Action (clé) qui est égale à `actionString`, si elle est trouvée, la méthode `executeGeneric()` de l'objet associé à cette Action sera appelée.
pour réviser, la configuration par défaut a 7 actions qui sont :
* `keystore`, `certificate`, `*` gérés par `org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor`
* `jarZip` géré par `org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor`
* `dbs` géré par `org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor`
* `tools` géré par `org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor`
* `toolsAny` géré par `org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor`
chacun de ces objets gère le téléchargement différemment, certains acceptent des extensions spécifiques.
le premier que j'ai trouvé vulnérable à l'écriture arbitraire de fichiers était `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` n'a pas de méthode `executeGeneric()` mais il étend [`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) qui a une méthode [`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() appelle d'abord parseRequest() avec l'objet request comme paramètre:```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);
}
}
}
}
}
il s'assure d'abord que la requête POST est une requête POST multipart, puis extrait les fichiers téléchargés, vérifie que la requête POST contient au moins un fichier téléchargé et le valide par rapport à la taille maximale du fichier.
après être revenu de `parseRequest()`, `executeGeneric()` appellera maintenant la méthode `execute()` qui est [remplacée](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) par `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;
}
C'est ici que se trouve le bug, la méthode execute() est vulnérable à une vulnérabilité de path traversal car elle fait confiance au nom de fichier fourni par l'utilisateur dans la requête POST. Sans l'échappement de path traversal pour sortir du répertoire tmp, le fichier est en fait sauvegardé dans :```
./tmp/work/extra/$uuid/$filename
avec `uuid` renvoyé dans la réponse :

le fichier se trouve dans :

Maintenant, il suffit de sortir du répertoire `tmp` et d'ajouter notre shell JSP à un emplacement servi par le WSO2.
trouvons le répertoire `appBase` de tomcat :

ce répertoire est l'emplacement des applications déployées sur tomcat, il contient plusieurs applications WAR déjà déployées ainsi que leurs fichiers WAR bruts :```
./repository/deployment/server/webapps

l'une de ces applications est authenticationendpoint (//host/authenticationendpoint) qui gère l'authentification à WSO2 et son emplacement est :```
./repository/deployment/server/webapps/authenticationendpoint

**NOTE :** nous pouvons également utiliser la vulnérabilité pour créer notre propre nouveau répertoire (chemin de contexte) dans le répertoire `appBase` et il sera automatiquement déployé, mais je vais juste en prendre un et utiliser `authenticationendpoint`.
# PoC
* Utilisation de Burpsuite :



* Utilisation de exploiy.py :
> Usage:
> ```
> python3 exploit.py https://host:9443/ ArbitraryShellName.jsp
> ```
