HTBank (easy) challenge walk through
This application uses a python front-end (flask) and a PHP backend. The flag gets returned in the WithdrawController:
class WithdrawController extends Controller
{
public function __construct() {
parent::__construct();
}
public function index($router) {
$amount = $_POST['amount'];
$account = $_POST['account'];
if ($amount == 1337) {
$this->database->query('UPDATE flag set show_flag=1');
return $router->jsonify([
'message' => 'OK'
]);
}
return $router->jsonify([
'message' => 'We don\'t accept that amount'
]);
}
}
Evidently we need to withdraw an amount equal to 1337. So I took a look at the front-end code to look for any validation on the amount we withdraw and found this:
@api.route('/withdraw', methods=['POST'])
@isAuthenticated
def withdraw(decoded_token):
body = request.get_data()
amount = request.form.get('amount', '')
account = request.form.get('account', '')
if not amount or not account:
return response('All fields are required!'), 401
user = getUser(decoded_token.get('username'))
try:
if (int(user[0].get('balance')) < int(amount) or int(amount) < 0 ):
return response('Not enough credits!'), 400
res = requests.post(f"http://{current_app.config.get('PHP_HOST')}/api/withdraw",
headers={"content-type": request.headers.get("content-type")}, data=body)
jsonRes = res.json()
return response(jsonRes['message'])
except:
return response('Only accept number!'), 500
Here we see the amount to withdraw has to be greater than the account balance and greater than 0. Unfortunately the account balance is 0 and cannot be changed. Also, we cannot directly make a request to the php backend because it’s an internal-only service which users don’t have access to.
I took a look at the request:

Given the simplicity of this lab, the only possible vulnerability I can come up with is parameter pollution so I gave it a try:

And sure enough, that was it.

Explanation
The reason why I was pretty sure this was gonna work is because when you make a request to a flask API, even though flask parses all occurences of the parameter, the function request.form.get() only returns the first entry. Whereas PHP overwrites the earlier values when mapping parameters to $_POST meaning only the last value is persisted.
In conclusion this was a business logic vulnerability caused by HTTP parameter pollution as a result of inconsistent request parsing between 2 components. This can be avoided by only using data that has been explicitly validated and using only that value for further work.