
WSO2 RCE(CVE-2022-29464) 익스플로잇 및 분석 문서.
CVE-2022-29464 WSO2의 심각한 취약점입니다. 이 취약점은 인증되지 않은, 제약 없는 임의 파일 업로드로, 인증되지 않은 사용자가 WSO2 서버에 악성 JSP 파일을 업로드하여 원격 코드 실행(RCE)을 달성할 수 있게 합니다.
취약한 업로드 경로는 /fileupload이며, FileUploadServlet 서블릿이 처리합니다. 그리고 indentity.xml 구성 파일에서 볼 수 있듯이 이 경로는 IAM으로 보호되지 않는 경로입니다:```xml
The function 'handleSecurity()' is in charge of safeguarding the various routes provided by WSO2 and offers a mechanism for performing security checks on the received HTTP requests. 'handleSecurity()' will call 'CarbonUILoginUtil.handleLoginPageRequest(),' and based on its return value, it will be decided whether to grant or deny access to the requested 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()` 메서드는 config 파일에서 추출한 Action과 Class를 사용해 `<Action, Class>`의 `HashMap`을 생성하고 채웁니다. 이 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에서 /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()은 추가된 첫 번째 객체인 execute()를 호출합니다. 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)는 앞서 생성된 `<Action, Class>`의 `HashMap`을 순회하며 `actionString`과 동일한 Action(키)을 찾습니다. 찾으면 해당 Action과 연결된 객체의 `executeGeneric()` 메서드가 호출됩니다.
기본 구성을 살펴보면 7개의 Action이 있으며 다음과 같습니다:
* `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 요청이 multipart POST 요청인지 확인한 다음 업로드된 파일을 추출하고, POST 요청에 업로드된 파일이 하나 이상 포함되어 있는지 확인한 후 최대 파일 크기와 대조하여 검증합니다.
`parseRequest()`에서 반환된 후, `executeGeneric()`은 이제 `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)된 `execute()` 메서드를 호출합니다:```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 요청에서 사용자가 제공한 파일명을 신뢰하므로 경로 탐색(path traversal) 취약점에 노출되어 있습니다. 경로 탐색이 tmp 디렉터리를 벗어나지 않으면 파일은 실제로 다음 위치에 저장됩니다:```
./tmp/work/extra/$uuid/$filename
with `uuid` being returned in the response:

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

이제 `tmp` 디렉터리에서 벗어나 JSP 셸을 WSO2가 서비스하는 일부 위치에 추가하기만 하면 됩니다.
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
> ```
