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