
Exploit et writeup pour la RCE WSO2 (CVE-2022-29464).
Exploitation et article sur WSO2 RCE (CVE-2022-29464).
CVE-2022-29464 est une vulnérabilité critique sur WSO2 découverte par Orange Tsai. La vulnérabilité est un téléchargement arbitraire de fichier non authentifié, sans restriction, qui permet à des attaquants non authentifiés d'obtenir une exécution de code à distance (RCE) sur les serveurs WSO2 en téléchargeant des fichiers JSP malveillants.
La route de téléchargement vulnérable est /fileupload, gérée par le servlet FileUploadServlet. Et 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
Et également non protégée par la mesure de connexion par défaut, `handleSecurity()` est la fonction responsable de sécuriser les différentes routes servies par WSO2 et fournit un mécanisme pour effectuer des vérifications 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'autoriser 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()` retournant `CarbonUILoginUtil.RETURN_TRUE`, `handleSecurity()` retournera `true`, l'accès sera alors accordé à `/fileupload` sans authentification.
la 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éthodes charge finalement depuis le fichier de configuration `carbon.xml` plusieurs formats/actions de téléchargement de fichiers 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 constructeur de la classe FileUploadExecutorManager 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 dans le 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. Ceci sera utilisé plus tard pour choisir quelle classe utiliser pour gérer correctement un format/action donné.
Ensuite, 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 les objets 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 enregistrera dans ses propriétés.
après cela, l'objet carbonXmlExecHandler ainsi que d'autres objets seront ajoutés à execHandlerManager à l'aide de la méthode addExecHandler().```java
public void addExecHandler(FileUploadExecutionHandler handler) {
if (prevHandler != null) {
prevHandler.setNext(handler);
} else {
firstHandler = handler;
}
prevHandler = handler;
}
puis `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 la `HashMap` de `<Action, Class>` créée précédemment et trouve l'Action (clé) qui est égale à `actionString` ; si trouvée, la méthode `executeGeneric()` de l'objet associé à cette Action sera appelée.
pour information, la configuration par défaut comporte 7 actions qui sont :
* `keystore`, `certificate`, `*` géré 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éversement 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, lui, possède 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 le request object 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, s'assure que la requête POST contient au moins un fichier téléchargé et le valide par rapport à la taille maximale de fichier.
après le retour de `parseRequest()`, `executeGeneric()` appellera maintenant la méthode `execute()` qui est [overrode](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;
}
Voici où se trouve le bug : la méthode execute() est vulnérable à une vulnérabilité de traversée de chemin car elle fait confiance au nom de fichier donné par l'utilisateur dans la requête POST. Sans échappement de traversée de chemin, le fichier est en réalité enregistré dans le répertoire tmp :```
./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 endroit 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 vers 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 simplement continuer et utiliser `authenticationendpoint`.
# PoC
* Avec Burpsuite :



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