Post

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 withdraw 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. With-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 CRLF 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. Exploit Pwned

This post is licensed under CC BY 4.0 by the author.