HTBank
HTBank Challenge
A notorious bank has recently opened its doors, claiming to house the wealth of the entire world. Can you reclaim what is rightfully yours? Here’s a wizard’s tip: the bank only permits withdrawals of 1337 units of currency at a time. Are you up for the challenge?
The web page simulate some crypto bank where you can witdraw money , but we don’t have any credits
The other thing that caught my atention was the Username: user message, though could be some type of second order vulnerability.
Code Review
Looking at the source code we found Two apps, flask_frontend and php_backend, Interesting.
flask_frontend
On the template/home.html folder we say this code where we observe a hint on how obtain the flag.
1
2
3
4
5
6
7
8
9
<div class="nes-container with-title is-centered" style="border-color: #eae1e1;">
<p class="title" style="color: #000;">Account Information</p>
<p>Username: </p>
<p>Balance: HTB_Credits</p>
<p>Wallet Address: 0x</p>
</div>
DJango uses a Model-View-Template, asume is the same for Flask. Here We find the Template that will print the Flag if is present, What templates show are control by the View wich use the Model (connection to the Database) to fetch the values in order to update the Templates. With this we know that looking on the Model we hunt for bad SQL queries unsanitized. Looking at the View /blueprints/routes.py we audit the logic of the application We have 5 endpoints
- /home -> requiere Auth
- /logout
- login
- register
- withdraw -> requiere Auth From the /home endpoint, the most important uses de Model function getFlag() to extract our flag form the Database and show us only if his attribute show_flag is equal to 1
1 2 3 4 5
user = getUser(decoded_token.get('username')) flag = getFlag() if(flag[0].get('show_flag') == 1): return render_template('home.html', user=user[0], flag=flag[0].get('flag'))
The /withdraw enpoint take from the body of the request the value of amount and account, check if they are not empty and extract the username form the JWT Token. Then pass to a try-catch where first check the balance in the account, looking at the entrypoint.sh file we notice that by defualt is set to 0. Then make a requeste to the Backend PHP server to /api/withdraw with the body and the “content-type” header that we send to the flask_frontend wich make me think on some kind of request splitting via CRLF injetion %0a%0d in this header.
1 2 3 4 5 6 7 8 9 10 11 12
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
php_backend
This backend only expose one endpoint /api/withdraw I we can confirm some kind of SSRF, Request Splitting, etc to inject a request directly to the backend we can bypass the balance check.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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'
]);
}
Local Testing
We consider that our injection point had to be the Content-Type header in the request, lest view how manage a simple request.
Lets see what happen if we inject a CRLF payload on the header, also i change the Content-tpye format to make more simple to understand
But with we didn’t bypass the balance check, even worst we are a step back in the check for the params. This is not the way
Back to Source Code Review
All this challenge its about the way Flask and PHP handle Parameter Parsing , ref , knowing this its just very simple.
Proof of Concept
Sending a Parameter polution that exploit the diferences between PHP and Flask.
