
استغلال WSO2 RCE (CVE-2022-29464).
CVE-2022-29464 هو ثغرة حرجة في WSO2 اكتشفها Orange Tsai. الثغرة هي رفع ملفات عشوائي غير مقيد وغير مصادق عليه، مما يسمح للمهاجمين غير المصادق عليهم بالحصول على RCE على خوادم WSO2 عبر رفع ملفات JSP خبيثة.
المسار القابل للاستغلال هو /fileupload والذي تتم معالجته بواسطة FileUploadServlet وهو مسار غير محمي كما نرى في ملف الإعداد indentity.xml:```xml
يقوم `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>` التي تم إنشاؤها سابقًا ويعثر على الإجراء (المفتاح) الذي يساوي `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. أحد هذه المواقع هو `./repository/deployment/server/webapps`:

يحتوي هذا المجلد على عدة تطبيقات WAR منشورة وأيضًا ملفات WAR خام. أحد تطبيقات WAR هذه هو `authenticationendpoint` الذي يعالج المصادقة إلى WSO2 وموقعه هو `./repository/deployment/server/webapps/authenticationendpoint`:

# PoC


