How I discovered the underground world of credit card network exploitation
A couple of weeks back at work, we were alerted that suddenly our card decline rates are much higher than usual.
A quick glance at our Stripe dashboard revealed a lot of failed charges by users with a very auto-generated-sounding names and rather odd email domains1. We quickly concluded that we were hit with a classic card testing attack. We enabled Stripe Radar, tossed implementing captcha in our checkout into our backlog, and moved on.
In the meantime, I noticed a couple of tweets describing similar issues from Pieter Levels and Danny Postma. I shared them with my team, we looked for infamous Jake Smith in our Stripe account (he wasn’t there), and moved on.
Had to refund and cancel 240 customers called Jake Smith from Philippines with $7,000 in card testing payments— @levelsio (@levelsio) June 5, 2023
No idea why Stripe doesn’t catch these
They even passed my Cloudflare CAPTCHA?!
Had to block 🇵🇭 PH payments now sorry https://t.co/zK05Q1kvEP
Seems like everyone I know on Twitter is getting attacked by "jack smith" with fraudulent charges.— Danny Postma (@dannypostmaa) June 5, 2023
Hope @stripe comes with a solution FAST, because this is going to kill some businesses.
Fast forward couple weeks, we get high decline ratio alert again. This time I started implementing Stripe Radar rules ad-hoc (such as blocking transactions after specific number of failures within given timeframe). Looking more closely at the traffic however, it turned out that attackers tested up to four cards a minute. Most of the time the traffic was much less intense and much less consistent.
After reading some materials from Stripe on card testing, I realised that our implementation should be pretty well guarded against this kind of attack. We require users to log in before we allow them to open the checkout, and use Payment Element with some of the signals. According to the page linked above, our protection against card testing should be close to ‘excellent’.
After some more digging and consulting my colleagues, we arrived at a conclusion that the traffic we were experiencing was most likely manual, or at most very lightly automated.
My belief was being reinforced as I noticed that almost all of the cards that attackers used:
- Were all issued by the same bank
- Had the same funding source2: prepaid
- Came from the same country: USA
How was it possible that attackers had a list of cards with such similar parameters? I always imagined that compromised credit cards would would be much more diverse. Were they actually leaked from the bank itself?
One of my sub-hypotheses was that the goal of the attackers is to pay for VEED account with a stolen card and resell it for profit. I started pulling this thread and I found a Telegram channel that recommended VEED as a great tool for adding subtitles to your videos.
I also found in that channel messages with credit cards’ BINs3, CVC, expiration dates and links to tools that generate valid credit card numbers based on these inputs.
It turns out that there’s whole underground(-ish, all of of was publicly available on the internet to me)4 world of people who share credit card parameters with biggest likelihood creating a card that’s accepted on a specific website (usually some SaaS).
We were most likely a victim of such manual attack, initiated from some private Discord server or Telegram channel. I never managed to find a specific source, but I strongly suspect it given that all of the cards had the same parameters that were all determined by a BIN. In the public channels I managed to find they quite often sent nagging messages about going private in the couple following days. I suspect most of this activity is not accessible to me.
On top of that, there’s a plethora of online tools that will take a list of autogenerated cards and run it through any Stripe Checkout session automatically. This was a bit surprising as I expected Stripe Checkout to be the least prone to any sort of automation, since Stripe owns this integration end-to-end.
The painful aftermath of this attack for us was that some of the attackers succeeded and managed to pay for our product. At that stage we still weren’t sure of the scale of it though, so it was time to crunch some data.
We queried our data storage for customers with more than 5 failed charges since May 1st. Then I got ChatGPT to quickly create a Python script which found these customers’ email domains. Once we had a list of domains, we queried our database to get a list of all successful charges made by customers with emails in these domains.
With a list of charges I had another script written by ChatGPT to find which charges were being already disputed. We learnt that 15% of the successful fraudulent charges resulted in chargebacks. We decided to accept all of the disputes and swallow Stripe’s £20 fee for each. It’s a cost of running an online business, it seems. I created a restricted key in Stripe with lowest possible permissions, and prompted ChatGPT to create a script to accept the chargebacks.
The next Python script to come out of ChatGPT fetched all of the charges from the list, refunded all the non-disputed charges, and cancelled any active subscriptions for customers who created these charges. For these charges, we were at loss on Stripe’s and network fees both for the successful charge and – potentially – for the refund. It’s much less than £20 for a chargeback, but still a loss.
The final Python script I used wasn’t strictly necessary, but I wanted to ensure that all of the charges in the list are either disputed and accepted, or refunded. Additionally, I wanted to make sure that none of these customers had any active subscriptions anymore. I got ChatGPT to spit it out for me one more time, changed my Stripe restricted key to not have write access anymore, and ran the script. I was able to confirm the problem was solved (or at least part of the problem that we were able to uncover)!
As a sidenote: ChatGPT advised a lot of caution when running its scripts, suggested to test it well on a small sample, etc. Well appreciated!
On American banks
The world of online payments is well-known to be unfair to businesses. You are unlikely to win a dispute, need to maintain dispute activity below 0.75%, and pay £20 for every dispute, no matter if the business looses or wins.
At the same time, banks (usually American ones) will happily accept transactions that have:
- Incorrect full name
- Invalid CVV / CVC
- Wrong expiration date
- Only partial billing address provided, with incorrect ZIP code
All of the above is still not enough to trigger a 3D secure authorisation. I’m not entirely sure why businesses should be held accountable for charges in which literally only the card number is correct.
I suspect that possibility of the above checks is somewhat limited in prepaid cards, but I’m sure that there’s room for improvement in this area.
How can we prevent this from happening again?
As I mentioned before, at the beginning of the discovery process our gut reaction was enabling Stripe Radar. It’s a machine-learning-based solution that’s supposed to score every payment and block is automatically is some of the metrics do not add up.
As we learnt later on, it wasn’t very helpful with our case. The risk score for most of these transactions was between 0 and 5 (low risk). At the same time, I noticed a couple of legitimate customers being blocked after they failed to solve the 3D secure challenge twice. This is very anecdotal, but left me a little concerned about leaving the fate of the customer in hands of machine learning.
Thankfully, Stripe Radar also has the ability to write custom rules for when to request 3D secure challenge, send payment to manual review, or block it completely. It looks a little like pseudocode, but can be used to express surprisingly complex logic to narrow down malicious payment attempts.
This was the best weapon available to us. I put reasonable limits on the number of possible failed payment attempts within an hour, day, and week for a customer. Here’s a couple of rules from aforementioned Danny Postma that you can use as inspiration.
Here's my custom Stripe Radar block list for anyone interested. pic.twitter.com/jPm1BAA5yK— Danny Postma (@dannypostmaa) May 2, 2023
The cost of this fraudulent activity (starting with payment processor fees, chargeback penalties, engineering cost or even the risk of getting deplatformed) is being paid by businesses around the world. Stripe charging businesses £20 chargeback fee is another example of unfair treatment. All of these costs are eventually offloaded onto customers as higher prices.
The biggest learning to me is that the payment network that we all rely on a daily basis is quite exploitable. The ultimate decision whether the card can be charged is at issuing bank’s discretion.
Until the banks are willing to take more responsibility for their authorisations, this will keep on going. I’m not sure if there’s any incentive for them to fight it (are they at risk of getting deplatformed by the payment processors too?), but without their involvement best we can do is to keep a close eye on our charge failure rates, add captchas, and share our favourite Stripe Radar rules on Twitter.
Nothing against self-hosting email, but the sad reality is that email address domains these days are either a couple of the most popular email providers, or company domains. ↩
Funding source says if the card is a debit, credit, or prepaid card. ↩
Bank Identification Number, the first six up to eight digits of any credit card. ↩
I think the usual way of operating is starting out as a public channel, and then going private once they get the audience. ↩