HTBank (easy) challenge walk through

#Parameter pollution #Lab 2026/01/30 12:00:00

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.