Here are my initial attempts at trying to detect Log4j exploit attempts that may make it past our WAF/service provider protections. While our WAF stopped requests from Trend Micro's Log4j Tester, obfuscated requests made it through. At time of testing, Azure wasn't blocking requests. I had to be a little careful with the script as Windows kept instantly quarantining the CFM files and prevented ColdFusion from executing the template.
2021-12-29: Updated rules based on Google Cloud article to additionally block rmi
, ldaps
& dns
(in addition to stripping whitespace.)
Sample CFML code available at https://gist.github.com/JamoCA/6a8c612645b1b7c47eba8e317ad51d23
<!--- getRequestAsText() and containsLog4jExploit() ColdFusion UDF proof-of-concept | |
2021-12-21 | |
by James Moberg https://www.sunstarmedia.com/ | |
https://gist.github.com/JamoCA/6a8c612645b1b7c47eba8e317ad51d23 | |
Tested on CF2016+ and Lucee (using TryCF.com). | |
---> | |
<cfscript> | |
string function getRequestAsText() output=false hint="I return HTTP header, url and form data as text" { | |
var response = getHttpRequestData(); | |
var responseText = []; | |
var temp = {"lf":chr(10)}; | |
for (temp.header in response.headers){ | |
arrayAppend(responseText, "#temp.header#: #response.headers[temp.header]#"); | |
} | |
responseText = arrayToList(responseText, temp.lf); | |
responseText = responseText & temp.lf & "ip: " & cgi.remote_addr; | |
for (temp.field in URL){ | |
responseText = responseText & temp.lf & "url-#temp.field#: " & URL[temp.field]; | |
} | |
for (temp.field in FORM){ | |
if (temp.field neq "FIELDNAMES") responseText = responseText & temp.lf & "form-#temp.field#: " & FORM[temp.field]; | |
} | |
return trim(responseText); | |
} | |
boolean function containsLog4jExploit(string inputString="") output=false hint="I sanitize string and perform regex to identify exploit" { | |
local.pattern = "(?i)j1n2d3i5\:(ldap|rmi|ldaps|dns)"; | |
local.text = canonicalize(arguments.inputString, false, false); | |
local.text = local.text.replaceAll("\s", ""); | |
local.text = local.text.replaceAll("\$\{\:{1,2}\-([a-zA-Z]+)?\}", "$1"); | |
local.text = local.text.replaceAll("\$\{\:\-\:\}", "\:"); | |
local.text = local.text.replaceAll("\$\{(upper|lower)\:(.+?)\}", "$2"); | |
local.text = local.text.replaceAll("(?i)\$\{(env|sys)\:.*?\:\-(.+?)\}", "$2"); | |
local.text = local.text.replaceAll("(?i)\$\{date\:\'(.+?)\'\}", "$1"); | |
local.text = local.text.replaceAll("(?i)\$\{[a-zA-z_0-9]+\:[a-zA-z_0-9]+\:\-(.+?)\}", "$1"); | |
return javacast("boolean", reFindNoCase(local.pattern.replaceAll("\d",""), local.text)); | |
} | |
// Unit test samples are from https://github.com/Puliczek/CVE-2021-44228-PoC-log4j-bypass-words | |
// and https://log4j-tester.trendmicro.com/ | |
tests = [ | |
"1. System variables": "${${env:EN_AME:-j}ndi${env:EN_AME:-:}${env:EN_AME:-l}dap${env:EN_AME:-:}//127.0.0.1/z}" | |
,"2. Lower lookup": "${${lower:j}n" & "di:${lower:l}${lower:d}a${lower:p}://127.0.0.1/z}" | |
,"2. Upper lookup": "${${upper:j}n" & "di:${upper:l}${upper:d}a${upper:p}://127.0.0.1/z}" | |
,"3. '::-' notation": "${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://127.0.0.1/z}" | |
,"4. Invalid Unicode characters with upper": "${jnd${upper:ı}:ldap://127.0.0.1/z}" | |
,"5. System properties": "${jnd${sys:SYS_NAME:-i}:ldap:/127.0.0.1/z}" | |
,"6. ':' notation": "${j${${:-l}${:-o}${:-w}${:-e}${:-r}:n}di:ldap://127.0.0.1/z}" | |
,"7. Date": "${${date:'j'}${date:'n'}${date:'d'}${date:'i'}:${date:'l'}${date:'d'}${date:'a'}${date:'p'}://127.0.0.1/z}" | |
,"8. HTML URL Encoding": "%24%7Bjn" & "di%3A" & "ldap://127.0.0.1/z%7D" | |
,"9. Non-existent lookup": "${${what:ever:-j}${some:thing:-n}${other:thing:-d}${and:last:-i}:ldap://127.0.0.1/z}" | |
,"11. Unicode Characters": "${\u006a\u006e\u0064\u0069:ldap://127.0.0.1/z}" | |
,"12. Trick with ## (works on log4j 2.15)": "$" & "{" & "jndi" & ":ldap://127.0.0.1##baddomain.com/z}" | |
,"13. DoS attack": "${${::-${::-$${::-j}}}}" // not currently supported. may be too difficult to sanitize/detect. | |
,"TrendMicro-system environment": "${${env:3910238:-j}ndi${env:3910238:-:}${env:3910238:-l}dap${env:3910238:-:}//127.0.0.1/z}" | |
,"TrendMicro-system properties": "${${sys:3910238:-j}ndi${sys:3910238:-:}${sys:3910238:-l}dap${sys:3910238:-:}//127.0.0.1/z}" | |
,"TrendMicro-lower special": "${j${${:-l}${:-o}${:-w}${:-e}${:-r}:n}${:-d}${:-i}${:-:}${:-l}${:-d}${:-a}${:-p}${:-:}${:-/}${:-/}127.0.0.1/z}" | |
,"Current Header/URL/Form": getRequestAsText() | |
]; | |
</cfscript> | |
<h2>Log4j Detection Unit Tests</h2> | |
<cfset cr = 0> | |
<cfoutput> | |
<cfloop collection="#tests#" item="test"> | |
<cfset cr += 1> | |
<cfset isBad = containsLog4jExploit(tests[test])> | |
<fieldset><legend<cfif isBad> style="color:red;"</cfif>>[#cr#/#structCount(tests)#] #test# = <b>#isBad#</b></legend> | |
<pre>#encodeForHTML(tests[test])#</pre> | |
</fieldset> | |
</cfloop> | |
</cfoutput> |
Top comments (0)