
CVE-2022-29464のエクスプロイト。WSO2サーバーにおける未認証の任意ファイルアップロードで、悪意のあるJSPアップロードによりリモートコード実行を可能にします。
WSO2 RCE(CVE-2022-29464)のエクスプロイトとWriteup。
CVE-2022-29464 はWSO2の深刻な脆弱性です。 この脆弱性は、認証不要かつ制限のない任意のファイルアップロードであり、認証されていないユーザーが悪意のあるJSPファイルをWSO2サーバーにアップロードし、リモートコード実行(RCE)を達成することを可能にします。
脆弱なアップロードルートは /fileupload であり、これは FileUploadServlet サーブレットによって処理されます。そして、indentity.xml 設定ファイルで確認できるように、このルートはIAMによって保護されていません。```xml
関数 'handleSecurity()' は、WSO2が提供する様々なルートを保護する役割を担い、受信したHTTPリクエストに対してセキュリティチェックを実行する仕組みを提供します。'handleSecurity()' は 'CarbonUILoginUtil.handleLoginPageRequest()' を呼び出し、その戻り値に基づいて、要求されたURIへのアクセスを許可するか拒否するかが決定されます。```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() は、ルートが /fileupload のときに CarbonUILoginUtil.RETURN_TRUE を返します:```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;
}
`CarbonUILoginUtil.handleLoginPageRequest()` が `CarbonUILoginUtil.RETURN_TRUE` を返すことで、`handleSecurity()` は `true` を返し、`/fileupload` へのアクセスが認証なしで許可されます。
`FileUploadServlet` サーブレットは、[`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) を経由し、一連のメソッド呼び出しを通じて、最終的に `carbon.xml` 設定ファイルから複数のアップロードファイル形式/アクションと、各形式を処理するオブジェクトを読み込みます。```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);
}
}
FileUploadExecutorManager クラスのコンストラクタは次の通りです:```java
public FileUploadExecutorManager(BundleContext bundleContext,
ConfigurationContext configCtx,
String webContext) throws CarbonException {
this.bundleContext = bundleContext;
this.configContext = configCtx;
this.webContext = webContext;
this.loadExecutorMap();
}
コンストラクタはprivateメソッド[`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)を呼び出し、これは設定の読み込みが行われる場所です:```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]
ファイルアップロードフォーマット設定は、XML設定ファイル内のFileUploadConfig名前空間にあります。これがデフォルト設定です:```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>
`loadExecutorMap()`メソッドは、設定ファイルから抽出されたActionsとClassesを使用して、`<Action, Class>`の`HashMap`を作成し、それを埋めます。これは後で、特定のフォーマット/アクションを適切に処理するためにどのクラスを使用するかを選択するために使用されます。
その後、`/fileupload`ルートがPOSTリクエストを受信すると、サーブレットの[`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)メソッドが呼び出されます。このメソッドは、リクエストとレスポンスオブジェクトを`fileUploadExecutorManager`の`execute()`メソッドに転送するだけです。`fileUploadExecutorManager`は`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);
}
}
execute()メソッドは、リクエストURLをfileupload/文字列の直後で分割します。つまり、リクエストURL内の/fileupload/の後にあるものを抽出し、それを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;
}
`actionString`は、`request`および`response`とともに、`CarbonXmlFileUploadExecHandler`クラスのコンストラクタに渡されます。```java
private CarbonXmlFileUploadExecHandler(HttpServletRequest request,
HttpServletResponse response,
String actionString) {
this.request = request;
this.response = response;
this.actionString = actionString;
}
コンストラクタはそれらを自身のプロパティに保存する。
その後、carbonXmlExecHandler オブジェクトは他のオブジェクトとともに、addExecHandler() メソッドを使って execHandlerManager に追加される。```java
public void addExecHandler(FileUploadExecutionHandler handler) {
if (prevHandler != null) {
prevHandler.setNext(handler);
} else {
firstHandler = handler;
}
prevHandler = handler;
}
次に `execHandlerManager.startExec()` が呼び出されます:```java
public void startExec() throws IOException {
firstHandler.execute();
}
startExec() は、最初に追加されたオブジェクトである CarbonXmlFileUploadExecHandler の execute() を呼び出します:```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)は、先に作成された`HashMap<Action, Class>`をループし、`actionString`と等しいアクション(キー)を見つけ、見つかった場合、そのアクションに関連付けられたオブジェクトの`executeGeneric()`メソッドが呼び出されます。
デフォルト設定には以下の7つのアクションがあります:
* `keystore`、`certificate`、`*` は `org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor` によって処理されます
* `jarZip` は `org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor` によって処理されます
* `dbs` は `org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor` によって処理されます
* `tools` は `org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor` によって処理されます
* `toolsAny` は `org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor` によって処理されます
これらの各オブジェクトはアップロードを異なる方法で処理し、一部は特定の拡張子を受け入れます。
任意のファイル書き込みに対して脆弱であると最初に発見したのは `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` には `executeGeneric()` メソッドはありませんが、 [`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) を継承しており、そちらには [`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()は最初にparseRequest()をリクエストオブジェクトをパラメータとして呼び出します:```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);
}
}
}
}
}
最初に、POSTリクエストがマルチパートPOSTリクエストであることを確認し、アップロードされたファイルを抽出して、POSTリクエストに少なくとも1つのアップロードファイルが含まれていることを確認し、最大ファイルサイズに対して検証します。
`parseRequest()`から戻った後、`executeGeneric()`は`execute()`メソッドを呼び出します。このメソッドは`ToolsAnyFileUploadExecutor`によって[オーバーライド](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)されています。```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;
}
ここがバグの箇所です。execute() メソッドは、POSTリクエストでユーザーから与えられたファイル名を信頼するため、パストラバーサルの脆弱性があります。パストラバーサルエスケープを行わずにtmpディレクトリからファイルが実際に保存される場所:```
./tmp/work/extra/$uuid/$filename
レスポンスで`uuid`が返される場合:

ファイルは以下にあります:

次に、`tmp`ディレクトリから脱出し、WSO2によって提供される何らかの場所にJSPシェルを追加する必要があります。
tomcatの`appBase`ディレクトリを見つけましょう:

このディレクトリは、tomcatにデプロイされたアプリケーションの場所です。複数の既にデプロイされたWARアプリケーションと、それらの生のWARファイルが含まれています。```
./repository/deployment/server/webapps

これらのアプリケーションの1つが authenticationendpoint (//host/authenticationendpoint) であり、WSO2への認証を処理します。その場所は次のとおりです。```
./repository/deployment/server/webapps/authenticationendpoint

**注記:** この脆弱性を利用して、`appBase` ディレクトリ内に独自の新しいディレクトリ(コンテキストパス)を作成することも可能で、自動的にデプロイされます。ただし、ここでは `authenticationendpoint` を使用して説明を進めます。
# PoC
* Burpsuiteを使用:



* exploiy.pyを使用:
> 使用方法:
> ```
> python3 exploit.py https://host:9443/ ArbitraryShellName.jsp
> ```
