
WSO2 RCE (CVE-2022-29464) exploit and writeup.
WSO2 RCE (CVE-2022-29464) 익스플로잇 및 분석글.
CVE-2022-29464는 Orange Tsai가 발견한 WSO2의 심각한 취약점입니다. 이 취약점은 인증되지 않은 제한 없는 임의 파일 업로드로, 인증되지 않은 공격자가 악성 JSP 파일을 업로드하여 WSO2 서버에서 RCE를 얻을 수 있게 합니다.
취약한 업로드 경로는 FileUploadServlet 서블릿이 처리하는 /fileupload입니다. 그리고 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();
}
생성자는 구성 로딩이 수행되는 [`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()` 메서드는 설정 파일에서 추출한 액션과 클래스로 `<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) 메서드가 호출됩니다. 이 메서드는 요청 및 응답 객체를 `init()`에서 초기화된 `fileUploadExecutorManager`의 `execute()` 메서드로 전달합니다.```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을 분할합니다. 즉, 요청 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`이(가) `CarbonXmlFileUploadExecHandler` 클래스 생성자에 `request` 및 `response`와 함께 전달됩니다:```java
private CarbonXmlFileUploadExecHandler(HttpServletRequest request,
HttpServletResponse response,
String actionString) {
this.request = request;
this.response = response;
this.actionString = actionString;
}
생성자는 그것들을 자신의 속성에 저장합니다.
그 후에 carbonXmlExecHandler 객체는 다른 객체들과 함께 execHandlerManager에 addExecHandler() 메서드를 사용하여 추가됩니다.```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) 생성된 `<Action, Class>`의 `HashMap`을 순회하며 `actionString`과 동일한 Action(키)을 찾고, 찾으면 해당 Action과 연결된 객체의 `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 요청에 최소 하나의 업로드된 파일이 포함되어 있는지 확인하며 최대 파일 크기와 비교하여 유효성을 검사합니다.
`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/work/extra/$uuid/$filename
`uuid`가 응답에 포함되어 반환됩니다:

파일은 다음 위치에서 찾을 수 있습니다:

이제 `tmp` 디렉토리를 벗어나 WSO2가 서비스하는 위치에 JSP 셸을 추가하기만 하면 됩니다.
tomcat `appBase` 디렉토리를 찾아봅시다:

이 디렉토리는 tomcat에 배포된 애플리케이션들의 위치이며, 이미 배포된 여러 WAR 애플리케이션과 자신의 원시 WAR 파일들을 포함하고 있습니다.```
./repository/deployment/server/webapps

해당 애플리케이션 중 하나는 authenticationendpoint (//host/authenticationendpoint)이며, 이는 WSO2에 대한 인증을 처리하며 그 위치는 다음과 같습니다:```
./repository/deployment/server/webapps/authenticationendpoint

**참고:** 이 취약점을 사용하여 `appBase` 디렉터리에 새로운 디렉터리(컨텍스트 경로)를 생성할 수도 있으며, 이 디렉터리는 자동 배포됩니다. 하지만 여기서는 `authenticationendpoint`를 그대로 사용하겠습니다.
# PoC
* Burpsuite 사용:



* exploiy.py 사용:
> 사용법:
> ```
> python3 exploit.py https://host:9443/ ArbitraryShellName.jsp
> ```
