
استغلال لـ CVE-2022-29464، وهي ثغرة رفع ملف تعسفي دون مصادقة في خوادم WSO2، مما يتيح تنفيذ تعليمات برمجية عن بُعد عبر رفع ملف JSP خبيث.
استغلال وشرح لـ 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` بدون مصادقة.
servlet `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>` الذي تم إنشاؤه سابقًا وتجد الإجراء (المفتاح) المساوي لـ `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 يحتوي على ملف مرفوع واحد على الأقل ويتحقق من صحته مقابل الحد الأقصى لحجم الملف.
بعد العودة من `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
> ```
