A bot probing Lucee sites for web shells (`?cmd=` base64 "COLD-SHELL") - what it looks like and how to check you're clean

Heads up in case you see the same thing in your logs. One morning my Lucee site (on Linux) had about a hundred new missinginclude errors, all overnight, all from one IP, each for a random template name:

/assets/nxzqlls4r.cfm?cmd=ZWNobyBDT0xELVNIRUxMLTNqdnVyNGF1Z2Q%3D
Error Type      missinginclude
User Agent      ... Chrome/126.0 ...

The cmd value is base64 for echo COLD-SHELL-3jvur4augd. The bot isn’t trying to break in; it’s knocking where web shells are usually left and checking whether one answers with its marker. A missinginclude on every request means nothing ran, which is what you want to see.

Quick checks I ran to make sure the server was clean:

# any template that can run programs? (most apps have none)
grep -rlE --include=*.cfm --include=*.cfc "cfexecute|getRuntime\(\)|ProcessBuilder" /path/to/your/site

# recently changed templates outside your own folders
find /path/to/your/site \( -name "*.cfm" -o -name "*.cfc" \) -mtime -14

Then I blocked the address in the firewall (iptables + netfilter-persistent save), since a ban inside the application never sees these requests: the template doesn’t exist.

I wrote up the details, including the Windows/PowerShell versions of the checks, here: A Bot Was Looking for a Back Door on My ColdFusion Server

A question for the people running Lucee behind Apache or Nginx: do you stop requests for .cfm files that don’t exist at the web server, before they reach Lucee? If so, how? I’d be curious what’s worked for you.

Thanks for the heads-up and the write-up, Gregory. The checks you listed are a good way to confirm a server is clean.

A missinginclude on every request means Lucee looked for the template, didn’t find it, and threw. No code ran. That matches the probe: the bot only checks whether a web shell answers with its marker.

To stop these before they reach Lucee, check that the file exists at the web server. I tried both snippets in front of a local Lucee.

Nginx: add try_files to the location that proxies CFML, before proxy_pass:

location ~ \.cfm$ {
    try_files $uri =404;
    proxy_pass http://127.0.0.1:8888;
}

Apache (mod_proxy): add this above your ProxyPass lines:

RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f
RewriteRule \.cfm$ - [R=404,L]

In a vhost or server config, use %{DOCUMENT_ROOT}%{REQUEST_URI} rather than %{REQUEST_FILENAME}. At that stage REQUEST_FILENAME isn’t mapped to a file yet, so it also returns 404 for files that do exist.

Caveats:

  • The web server has to see the same webroot as Lucee. This won’t work if Nginx or Apache runs without the files, for example in a separate container.
  • Lucee never sees these requests, so onMissingTemplate won’t run for them. Apps that serve .cfm URLs with no file behind them (front controllers, REST-style routes) will break. Paths like /index.cfm/foo/bar still work, because they don’t end in .cfm.
  • To exempt a path, give it its own location above the check in Nginx: location ~ ^/api/.*\.cfm$ { proxy_pass http://127.0.0.1:8888; }. In Apache, add RewriteCond %{REQUEST_URI} !^/api/ above the other RewriteCond.
  • If you also serve .cfc or .cfml, extend the pattern to \.(cfm|cfml|cfc)$.

If you’d rather handle it in the app, onMissingTemplate in Application.cfc can return a plain 404. It’s cheap, and visitors no longer get Lucee’s detailed error page:

function onMissingTemplate( targetPage ) {
	header statuscode=404 statustext="Not Found";
	content type="text/plain";
	echo( "Not found" );
	return true;
}

Without the header line the response is a 200. Returning false brings back the normal missing-template error.

A few more cheap hardening steps:

  • Don’t expose /lucee/admin publicly. Restrict it by IP, or set LUCEE_ADMIN_ENABLED=false if you don’t need it on that server.
  • Keep Tomcat’s HTTP, AJP and shutdown ports private, so only the web server can reach Lucee.
  • Keep Lucee, Tomcat and the JVM updated.
  • Your find ... -mtime check for unexpected new or changed .cfm/.cfc files is worth repeating regularly.

I’m putting together a short recipe for the Lucee docs that covers all of this. I’ll post the link here once it’s up.

The recipe is up now: Hardening against probing for missing templates Hardening against probing for missing templates :: Lucee Documentation