Insomni'hack CTF 2024-Writeup

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.

Untitled

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.

Untitled

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?

Untitled

After a thorough code review, here are the vulnerabilities that we can exploit:

  1. 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.

Untitled

  1. 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.

Untitled

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.

Untitled

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.

  1. 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.

Untitled

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.

Untitled

Here, I use Turbo Intruder, a plugin of Burp Suite, to concurrently send two requests.

Untitled

Things are going well!

Untitled

And I’m definitely on my way to becoming a billionaire!

Untitled

Flag

Untitled

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:

image-20240123100929747

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:

  1. Check if there are transactions already created in the batch_transactions table.
  2. 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.

img

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.

img