They tried to scam me. This is what happened to their database.

hacking
Two years ago, fed up of receiving scam messages on my phone, I decided to fight back. My plan was to saturate the databases of some phishing websites that ask for credit card numbers, in hope that legit information sent by people who got scammed would forever be lost in an ocean of garbage data. I'm going to show you how I developed a tool to do exactly that, using some knowledge exposed in a previous article.

Disclaimer: I will argue that flooding such fraudulent websites falls in the category of ethical white hat hacking, as the intention is to protect people from being stolen money, but I’m no lawyer and don’t perceive the legal ramifications of this activity. So I won’t advise you to reproduce what I’m presenting here for educational purpose only, and by no means do I condone using these techniques on legitimate websites. That being said, let’s drown the phish!

Target acquired

So I received a strange text message with a spoofed sender ID to make it look like it comes from the EDF company. EDF is a French multinational electric utility company of which I’m not a customer. Then just by reading the sender ID I already know that it is a scam.

Reconstitution of the message I received. If you’re not familiar with badly spelled French, it reads ‘[REMINDER] Due to an error on our part, you can ask for a 49.59€ ($56.86) refund, please visit aquitement-fr.com’. The URL is misspelled too.

But I’m really bored and I have some autism to weaponize, so I go to this URL using the Tor network, with the devtools panel opened. I’m now contemplating a hastily made phishing webpage with an offending pure white background, some more alternative spellings, an EDF logo to make it look official and the coveted URL to the “refund” form: aquitement-fr.com/fr/remboursement.php.

Of course, the form requires you to provide your credit card details. I read the source code and can’t find the form tag. They tried to hide the form action URL by injecting the tag dynamically like so:

<script language='Javascript'>
    document.write(unescape('[long encoded string]'))
</script>

In the devtools console, I simply execute:

console.log(unescape('[long encoded string]'));

and I get:

<form action="jkljkl654.php" method="POST">

So that’s the file responsible for collecting your credit card information, and pushing it to a database and / or sending it by email to the scammer. I feed the form with fake information, and use an online generator to produce a random credit card number that passes a mod 10 check, as they do verify that your credit card number is valid. I click on the send button and I’m redirected to a page that confirms that they received my information, and that I’m going to be contacted by email soon (surejan.gif). From the devtools in the Network tab, I can filter the requests by method:POST to have a look at the HTTP header of interest. At this point, I can associate field names to each piece of data that I fed to the form, and I know the URL and content of the webpage I’m redirected to if everything went alright.

So I write a little Python script in order to check that I can send data to this form programatically:

from urllib.parse import urlencode
from urllib.request import Request, urlopen

url = 'https://aquitement-fr.com/fr/jkljkl654.php'
data = { 'emaile': 'jean.peeters@gmail.com',
         'username': 'Jean Peeters',
         'day': '07',
         'month': '12',
         'year': '1956',
         # ...
         'cc': '5131201122876857',
         'expm': '06',
         'expy': '2025',
         'cvc': '038'
}

response = Request(url, urlencode(data).encode())
json = urlopen(response).read().decode()
print(json)

I’m skipping a few lines of code that set an HTTP header for this request where I fake a user-agent and a few other fields to pose as a browser. I’m also using a proxy thanks to a ProxyHandler object from urllib. I don’t recommend you ever do these kinds of manipulations without hiding your IP address. You can also use urllib over Tor thanks to pysocks if you like, but know that some scammers will filter out Tor endpoint IPs (as the list is public). I execute this script, and I can read the HTML code of the confirmation page. Omae Wa Mou Shindeiru.

Let’s be nasty

At this point I know that I can feed them garbage data from a script, so I’m now working on a fake identity generator in order to flood their database with procedural nonsense, that I deem credible enough. I’m quite meticulous with my procedural generation: I don’t want the scammer to be able to distinguish my crap data from real information that was sent by people who actually got scammed. The goal is to make their whole database useless, in hope that the scammees will dodge the bullet.

Generating fake personal information

I’m downloading lists of French first names, surnames, pseudonyms, email providers, user agents and addresses that I reformat and export using various scripts. I load the lists globally, and write an Identity object that will contain a fake identity:

class Identity:
	def __init__(self):
		global names
		global surnames
		global pseudonyms
		global emaildoms
		global addresses
		global cities
		global postals
		global banks
		self.name     = random.choice(names).title()
		self.surname  = random.choice(surnames).title()
		self.pseudo   = random.choice(pseudonyms).lower()
		self.username = self.name + ' ' + self.surname

		self.address  = random.choice(addresses).title()
		rnd_city_idx  = random.randint(0,len(cities)-1);
		self.city     = cities[rnd_city_idx].title()
		self.postal   = postals[rnd_city_idx]

		self.day      = '{:02d}'.format(random.randint(1,29))
		self.month    = '{:02d}'.format(random.randint(1,12))
		self.year     = str(random.randint(1940,1996))

		self.generate_email()
		self.generate_phone()

		if(random.randint(1,10)>5):
			self.card = str(card_generator.generate_card("mastercard"))
		elif(random.randint(1,10)>5):
			self.card = str(card_generator.generate_card("americanexpress"))
		else:
			self.card = str(card_generator.generate_card("visa"))

		self.expm = "{:02d}".format(random.randint(1,12))
		self.expy = str(random.randint(2019,2025))
		self.cvv  = "{:03d}".format(random.randint(0,999))
		self.bank = random.choice(banks)
		self.generate_card_holder()

		# Internet identity
		global useragents
		self.useragent = random.choice(useragents)

So that’s a lot of random element picking in various lists, and a few calls to some helper functions I’m going to discuss next.

Note: During the development of my tool I needed to go fast and didn’t wanted to code a card number generator myself. So I simply adapted this credit card generator module by Bhuwan Garbuja to work with Python 3. But it’s frankly not that hard to implement from scratch, and for the sake of this article I came up with some generic code that you can extend to support other credit card issuers. I’m giving an in-depth explanation on how to do that in this short article.

Here, I’m generating Mastercard, American Express and Visa numbers. The expiration date and the card security code can be purely random numbers within the appropriate ranges, as long as the format allows leading zeros. The card holder information can vary, starting either by the first name or the surname, with several possibilities for the capitalization:

	def generate_card_holder(self):
		if(random.randint(1,10)>7):
			holder_surname = self.surname
		else:
			holder_surname = remove_accents(self.surname).upper()

		if(random.randint(1,10)>5):
			self.holder = self.name + ' ' + holder_surname
		else:
			self.holder = holder_surname + ' ' + self.name

I’m careful enough to generate phone numbers with area codes that match the corresponding geographic locations:

	def generate_phone(self):
		global prefixes
		key = self.postal[0:2]
		if key in prefixes and random.randint(1,10)>4:
			# Generate area code according to postal code
			digits = str(random_with_N_digits(6))
			prefix = prefixes[key]
		else:
			# Cellphone
			digits = str(random_with_N_digits(8))
			prefix = '06'
			if(random.randint(1,10)>6):
				prefix = '07'

		self.phone = prefix + digits

The prefixes table associates postal codes to phone area codes (which in France consist in a 4 digits prefix). I also generate cell phone numbers. The email addresses are formed by combining a name, surname, possibly a pseudonym and random numbers. I’m skipping this bit, you get the picture. Have a look at the full implementation (link below) if you really want to see how stupidly overengineered the stuff is. So now, I can generate fake identities on the fly:

[Elisa Martinez]
birth: 12/03/1977
mail: elisa-martinez770@aim.com
address: 11 Grand Place, 20219 Vivario
phone: 0693204109
cc: 5395699917847999 exp: 06/2020 cvv: 326 holder: Elisa MARTINEZ
bank: LCL
user-agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.207.0 Safari/532.0

Writing a flooder

All that’s left to do is to write a flooder. Basically, I’m putting the above request code in an infinite loop, but the HTTP POST data is generated by a new instance of the Identity class at each iteration. The user-agent field of the request header is also chosen at random. Most importantly, I found a way to automate the gathering of hundreds of free proxy addresses. I’m writing a ProxyLocator class that uses multiple web scraper objects targetting various proxy lists on the web. When the flooder is launched, a list of proxies is assembled by the ProxyLocator, and ordered by latency. Then each request uses a random proxy, which reduces the scammer’s ability to filter data entries by IP, if such data is collected by the form PHP script:

import random
from urllib.request import Request, urlopen, ProxyHandler, build_opener, install_opener

    # In the while loop
    # ...
    identity = generator.Identity()
    # Generate form data from Identity object
    data = forge_data(identity)
    headers['user-agent'] = identity.useragent
    
    proxy = random.choice(proxy_list)
    req = Request(url, data=urlencode(data).encode(), headers=headers)
    proxy_support = ProxyHandler({'https': proxy})
    opener = build_opener(proxy_support)
    install_opener(opener)

    try:
        response = urlopen(req, timeout=.500).read().decode()
        # Analyze response
        # ...
    except:
        # ...

I soon added support for multi-threading in order to send requests more rapidly. My first run was distributed on two computers. I think I sent them 100k+ fakes over the course of 48h period, using successive versions of the tools, after which the site was brought down.

Epilogue

A few days later, I received another scam SMS with a shady link. I made my tool more data-oriented so that I could write a scammer profile quickly and launch a flood attack within a few minutes, and in two weeks I had attacked three more scam websites like this. Each of them went down during the attack, so even though those kinds of websites are short-lived by nature, I guess I had something to do with it. I spotted two more scam websites sharing odd similarities with the first one in their codebase (same weird spellings on data fields, same comments…), but this time, my requests were dropped if sent them too quickly. I assumed they learned from past experience and implemented some basic anti-flood mechanism. So I developped a stealth mode that sends requests from time to time with irregular delays, which was quite successful. I had to solve a few more issues along the way. Some websites use session cookies, for instance. Also, some proxies may deny me access at some point and I must remove them from the list…

At the end, my codebase became a mess because I had so many edge cases to handle, I lost interest in the project and progressed to the next obsession. At that time, I didn’t know about headless browsers, but I had a few projects with Puppeteer and particularly Selenium since then. I think that’s what I’m going to use in a potential next version. I came across a few websites that resisted my attack because my simple program was quite easy to spot as a bot, but I don’t think they test for headless browsers (yet).




The comment section requires the Utterances cookie in order to work properly. If you want to see people's comments or post a comment yourself, please enable the Utterances cookie here.