WSO2 RCE (CVE-2022-29464) 漏洞利用和编写。
CVE-2022-29464是Orange Tsai发现的 WSO2 上的严重漏洞。该漏洞是一种未经身份验证的无限制任意文件上传,允许未经身份验证的攻击者通过上传恶意 JSP 文件在 WSO2 服务器上获得 RCE。
易受攻击的上传路由是/fileupload
由FileUploadServlet
servlet 处理的。indentity.xml
正如我们在配置文件中看到的,它是不受 IAM 保护的路由 :
<Resource context="(.*)/fileupload(.*)" secured="false" http-method="all"/>
同样不受默认登录措施保护的handleSecurity()
是负责保护 WSO2 服务的不同路由的功能,并提供一种对收到的 HTTP 请求执行安全检查的机制,handleSecurity()
将调用CarbonUILoginUtil.handleLoginPageRequest()
并根据其返回值决定允许或拒绝访问请求的 URI:
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()
当CarbonUILoginUtil.RETURN_TRUE
路线为/fileupload
:
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
servlet 并通过init()
一系列方法调用最终从carbon.xml
配置文件加载多个上传文件格式/操作以及处理每种格式的对象。
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
构造函数如下:
public FileUploadExecutorManager(BundleContext bundleContext,
ConfigurationContext configCtx,
String webContext) throws CarbonException {
this.bundleContext = bundleContext;
this.configContext = configCtx;
this.webContext = webContext;
this.loadExecutorMap();
}
构造函数调用loadExecutorMap()
配置加载完成的私有方法:
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]
文件上传格式配置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>
该loadExecutorMap()
方法创建并填充从配置文件中提取的操作和类HashMap
。<Action, Class>
稍后将用于选择使用哪个类来正确处理给定的格式/动作。
稍后当/fileupload
路由收到 POST 请求doPost()
时,将调用 servlet 的方法。该方法只是将请求和响应对象转发到初始化的execute()
方法fileUploadExecutorManager
init()
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/
,这意味着它提取/fileupload/
请求 URL 中 之后的任何内容并将其分配给actionString
.
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;
}
与and一起actionString
传递给CarbonXmlFileUploadExecHandler
类构造函数:request
response
private CarbonXmlFileUploadExecHandler(HttpServletRequest request,
HttpServletResponse response,
String actionString) {
this.request = request;
this.response = response;
this.actionString = actionString;
}
构造函数会将它们保存到其属性中。
之后该carbonXmlExecHandler
对象与其他对象将被添加到execHandlerManager
usingaddExecHandler()
方法中。
public void addExecHandler(FileUploadExecutionHandler handler) {
if (prevHandler != null) {
prevHandler.setNext(handler);
} else {
firstHandler = handler;
}
prevHandler = handler;
}
然后execHandlerManager.startExec()被称为:
public void startExec() throws IOException {
firstHandler.execute();
}
startExec()添加的第一个对象的调用execute()是CarbonXmlFileUploadExecHandler:
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()
循环通过之前创建的HashMap
of<Action, Class>
并找到等于 的 Action(键)actionString
,如果找到executeGeneric()
,将调用与该 Action 关联的对象的方法。
修改默认配置有 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
)。 ToolsAnyFileUploadExecutor
没有executeGeneric()
方法,但它扩展AbstractFileUploadExecutor
了它确实有一个executeGeneric()
方法:
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()
以请求对象作为参数调用:
protected void parseRequest(HttpServletRequest request) throws FileUploadFailedException,
FileSizeLimitExceededException {
fileItemsMap.set(new HashMap<String, ArrayList<FileItemData>>());
formFieldsMap.set(new HashMap<String, ArrayList<String>>());
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
:
@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
目录并将我们的 JSP shell 添加到 WSO2 服务的某个位置。
让我们找到tomcatappBase
目录:
此目录是部署在 tomcat 上的应用程序的位置,它包含多个已部署的 WAR 应用程序以及它们的原始 WAR 文件:
./repository/deployment/server/webapps
其中一个应用程序是authenticationendpoint
( //host/authenticationendpoint
),它处理对 WSO2 的身份验证,它的位置是:
./repository/deployment/server/webapps/authenticationendpoint
注意:我们也可以利用该漏洞在目录中创建自己的新目录(上下文路径),appBase
它会自动部署,但我只携带一个并使用authenticationendpoint
.
使用 Burpsuite:
使用exploiy.py:
用法:
python3 exploit.py https://host:9443/ ArbitraryShellName.jsp
原文地址:https://github.com/hakivvi/CVE-2022-29464
点击阅读原文——跳转