InsoBank
The web application in this challenge is a banking app that supports transferring deposits from one account to another. However, all the accounts involved in a transfer operation must belong to the same person; this means we cannot transfer money from our account to other people’s accounts.
Through the registration functionality, we can create new accounts. Every new account starts with a $10 initial deposit in the current account
, and has empty saving account
and checking account
.
To obtain the flag, we must have one account with more than $10 in deposits. However, transferring exactly $10 around is pointless. How do we unexpectedly increase the amount?
After a thorough code review, here are the vulnerabilities that we can exploit:
- Discrepancies in Data Precision
There are differences in the data precision of the amount
field between two batch_transactions
tables.
In the MySQL database, the data type of amount
is decimal(10,2)
, while in the PostgreSQL database, it is just decimal
. This causes a floating-point number like 0.014 to be stored in MySQL as 0.01, but as 0.014 in PostgreSQL.
- Logic Flaws
The transaction process allows a user to set up a request to transfer a certain amount of money from one account to another. If the account has enough funds, the transaction is marked as validated, and the amount being transferred is deducted.
Based on the batch_id
, the server-side periodically operates transfer requests in batches via a cron job named exec_transfers.py
. This job sums up the total number of transactions with the same batch_id
and adds that number to the recipient’s account.
The logic flaw arises here. The amount to deduct is based on decimal(10,2)
, while the amount to increase is based on decimal
. When two transactions with the same batch_id
are processed at once, an extra $0.01 gets smuggled into the recipient’s account.
For example, if two transactions are each transferring $0.014 from saving to checking account, the sender’s account gets deducted by $0.02, but the recipient’s account gets increased by $0.028, which rounds up to $0.03.
- Race condition
Now that we have a clear approach to siphoning money from thin air, there’s one thing I haven’t mentioned: the application actually doesn’t allow two transfers per recipient in a batch.
However, we can achieve this by exploiting a race condition, as there is no lock when looking up the table to check for redundant transfers.
Here, I use Turbo Intruder, a plugin of Burp Suite, to concurrently send two requests.
Things are going well!
And I’m definitely on my way to becoming a billionaire!
Flag
Update
01/23/2024: I got a message from @shokhie, who asked me about the details of the race condition attacks. I paste my reply here to help people who have the same question:
Our goal is to initiate two transactions with the same batchid
. Normally, the task is not feasible due to the backend logic of this part:
- Check if there are transactions already created in the
batch_transactions
table. - If the check gets passed, it will accept the transfer request and insert a new transaction record into the
batch_transactions
table. Otherwise, the request will be rejected immediately.
So, when we create the second transaction with the same batchid
, we would get rejected.
However, we can exploit it by concurrently sending two transfer requests with the same batchid
in a very short timeframe. This approach can bypass the check due to the absence of a lock, potentially allowing both transactions to proceed as the table may not yet reflect the new record.
Ideally, Implementing a lock before the check and releasing it after the insertion would safeguard the entire verification and transaction process against race condition attacks.