
استغلال WSO2 RCE (CVE-2022-29464) وشرح تفصيلي.
استغلال ثغرة WSO2 RCE (CVE-2022-29464) وتقرير تفصيلي.
CVE-2022-29464 هي ثغرة حرجة في WSO2 اكتشفها Orange Tsai. الثغرة عبارة عن رفع ملفات تعسفي غير مقيد وغير مصادق عليه، مما يسمح للمهاجمين غير المصادق عليهم بالحصول على RCE على خوادم WSO2 عبر رفع ملفات JSP خبيثة.
مسار الرفع المعرض للثغرة هو /fileupload والذي تتم معالجته بواسطة سيرفلت FileUploadServlet. وهو مسار غير محمي بواسطة IAM كما نرى في ملف الإعداد indentity.xml:```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() يُرجع CarbonUILoginUtil.RETURN_TRUE عندما يكون المسار /fileupload:```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]
تقع إعدادات تنسيقات رفع الملفات ضمن مساحة الاسم FileUploadConfig في ملف إعداد XML، وهذا هو الإعداد الافتراضي:```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()` بإنشاء وملء `HashMap` من `<Action, Class>` بالإجراءات والفئات المستخرجة من ملف الإعداد. والتي ستُستخدم لاحقًا لتحديد الفئة المناسبة للتعامل بشكل صحيح مع تنسيق/إجراء معين.
لاحقًا، عندما يستقبل المسار `/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) الخاصة بـ servlet. وتقوم الطريقة فقط بإعادة توجيه كائن الطلب والاستجابة إلى طريقة `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/، مما يعني أنها تستخرج أي شيء يلي /fileupload/ في عنوان URL للطلب وتُسنده إلى 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() يستدعي 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) يتنقل عبر `HashMap` الخاص بـ `<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`](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()` التي تم [تجاوزها](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) بواسطة `ToolsAnyFileUploadExecutor`:```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` وإضافة قشرة JSP الخاصة بنا إلى موقع ما يتم تقديمه بواسطة WSO2.
لنجد دليل `appBase` الخاص بـ tomcat:

هذا الدليل هو موقع التطبيقات المنشورة على 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
> ```
