Very High
CVE-2024-41874
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
CVE-2024-41874
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
ColdFusion versions 2023.9, 2021.15 and earlier are affected by a Deserialization of Untrusted Data vulnerability that could result in arbitrary code execution in the context of the current user. An attacker could exploit this vulnerability by providing crafted input to the application, which when deserialized, leads to execution of malicious code. Exploitation of this issue does not require user interaction.
Add Assessment
Ratings
-
Attacker ValueVery High
-
ExploitabilityMedium
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!
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
Vendors
- adobe
Products
- coldfusion 2021,
- coldfusion 2023
References
Additional Info
Technical Analysis
Report as Emergent Threat Response
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: