☕
Writeups
TryHackMeHackTheBoxReferralsDonateLinkedIn
  • Writeups
  • TryHackme
    • 2025
      • Security Footage
      • Ledger
      • Moebius
      • Mayhem
      • Robots
      • Billing
      • Crypto Failures
      • Rabbit Store
      • Decryptify
      • You Got Mail
      • Smol
      • Light
      • Lo-Fi
      • Silver Platter
    • 2024
      • Advent of Cyber '24 Side Quest
        • T1: Operation Tiny Frostbite
        • T2: Yin and Yang
        • T3: Escaping the Blizzard
        • T4: Krampus Festival
        • T5: An Avalanche of Web Apps
      • The Sticker Shop
      • Lookup
      • Mouse Trap
      • Hack Back
      • SeeTwo
      • Whiterose
      • Rabbit Hole
      • Mountaineer
      • Extracted
      • Backtrack
      • Brains
      • Pyrat
      • K2
        • Base Camp
        • Middle Camp
        • The Summit
      • The London Bridge
      • Cheese CTF
      • Breakme
      • CERTain Doom
      • TryPwnMe One
      • Hammer
      • U.A. High School
      • IronShade
      • Block
      • Injectics
      • DX2: Hell's Kitchen
      • New York Flankees
      • NanoCherryCTF
      • Publisher
      • W1seGuy
      • mKingdom
      • Airplane
      • Include
      • CyberLens
      • Profiles
      • Whats Your Name?
      • Capture Returns
      • TryHack3M
        • TryHack3M: Burg3r Bytes
        • TryHack3M: Bricks Heist
        • TryHack3M: Sch3Ma D3Mon
        • TryHack3M: Subscribe
      • Creative
      • Bypass
      • Clocky
      • El Bandito
      • Hack Smarter Security
      • Summit
      • Chrome
      • Exfilibur
      • Breaking RSA
      • Kitty
      • Reset
      • Umbrella
      • WhyHackMe
      • Dodge
    • 2023
      • Advent of Cyber '23 Side Quest
        • The Return of the Yeti
        • Snowy ARMageddon
        • Frosteau Busy with Vim
        • The Bandit Surfer
      • Stealth
      • AVenger
      • Dreaming
      • DockMagic
      • Hijack
      • Bandit
      • Compiled
      • Super Secret TIp
      • Athena
      • Mother's Secret
      • Expose
      • Lesson learned?
      • Grep
      • Crylo
      • Forgotten Implant
      • Red
    • Obscure
    • Capture
    • Prioritise
    • Weasel
    • Valley
    • Race Conditions
    • Intranet
    • Flip
    • Cat Pictures 2
    • Red Team Capstone Challenge
      • OSINT
      • Perimeter Breach
      • Initial Compromise of Active Directory
      • Full Compromise of CORP Domain
      • Full Compromise of Parent Domain
      • Full Compromise of BANK Domain
      • Compromise of SWIFT and Payment Transfer
  • HackTheBox
    • 2025
      • Certified
    • 2024
      • BoardLight
      • Crafty
      • Devvortex
      • Surveillance
      • Codify
      • Manager
      • Drive
      • Zipping
    • 2023
      • Topology
Powered by GitBook
On this page
  • PCAP Analysis
  • Research
  • Modifying havoc-pcap-parser.py And Decryption Of The Traffic
  • What is the SID of the user that the attacker is executing everything under?
  • What is the Link-local IPv6 Address of the server? Enter the answer exactly as you see it
  • The attacker printed a flag for us to see. What is that flag?
  • The attacker added a new account as a persistence mechanism. What is the username and password of that account? Format is username:password
  • The attacker found an important file on the server. What is the full path of that file?
  • What is the flag found inside the file from question 5?

Was this helpful?

  1. TryHackme
  2. 2025

Mayhem

Can you find the secrets inside the sea of mayhem? - by hadrian3689

PreviousMoebiusNextRobots

Last updated 23 days ago

Was this helpful?

The following post by 0xb0b is licensed under


In Mayhem we have been provided with a PCAP file. Our task is to analyze it, find the secrets and decode the traffic.

PCAP Analysis

At first glance, we have HTTP traffic. This originates from a Python web server. The files Install.ps1 and notepad.exe are being transmitted via port 1337.

After the notepad.exe is transferred we have further HTTP traffic. This time only on port 80. Post requests are sent at regular intervals, the data content of these appears to be encrypted.

This seems to be caused from notepad.exe. We extract the HTTP objects in order to analyze them further.

Research

Decompiling the executable using ghidra did not bring any new findings. But we can also submit them to Virustotal for analysis.

As expected, this is flagged as malicious. It is also associated with Havoc, a C2 framework. This could be a beacon. A Havoc C2 beacon is a lightweight agent that communicates with a Havoc C2 server, allowing remote control of a compromised system. It typically operates covertly, sending periodic check-ins and receiving instructions.

Havoc:

How traffic from a Havoc beacon can be analyzed is described by Immersive Labs in a blog post.

From the blog post we can draw the necessary information on how to identify and decode the traffic as such.

The traffic is encrypted as assumed. Havoc uses AES in CTR mode for this purpose.

The key material (AES key + IV) is also transmitted. Since we have HTTP traffic in front of us, we probably have access to it.

In addition, a magic byte value is transmitted to identify the demon traffic

Upon checking the Havoc C2 GitHub repository, we identified the definition of the 0xDEADBEEF magic value, found in the Defines.h file.

We search for the hex value 0xDEADBEEF and actually find it.

In addition to these, we also find the agent id, the AES key and the IV as described in the blog post.

length field: 00000113   
magic byte header: deadbeef
agent id: b7d8
aes key: 946cf2f65ac2...
iv key: 8cd00c3e4392...

Using tshark we can extract all media.type post requests and decipher them individually using CyberChef.

tshark -r traffic.pcapng -Y 'http.request.method == "POST"' -T fields -e media.type 

However, we must remove the header information beforehand, or only take the data into account. (Marked in white in the image above). And we see that we can decipher the traffic:

The following packets after the initial one have a header without the AES key and IV. This header is only 40 bytes long (to be identified by the COMMAND_NOJOB). By removing this header first, the subsequent data can be decrypted with the same key and IV.

Fortunately, Immersive Labs also provides a script that allows you to analyze the Havoc PCAP traffic. This script is able to extract the AES key and IV, identify the commands and store the deciphered requests and responses.

We run the script without any special flags and see that it was able to extract the AES key and the IV and some commands. We do not see what exactly happens.

We can identify the following commands.

COMMAND_NOJOB
COMMAND_MEM_FILE
COMMAND_PROC

We look at the source and see that the response and request bodies are stored decrypted as .bin files at the storage path if the --save flag is used. Unfortunately it is a bit of a mess and not chronologically ordered. Nevertheless, sufficient to answer all questions.

python3 havoc-pcap-parser.py --pcap traffic.pcapng --save .

Example content:

Modifying havoc-pcap-parser.py And Decryption Of The Traffic

Tested with the following packages and tshark version:

pyshark 0.6

lxml 5.3.2

tshark 4.2.2 There may be an issue where different versions of pyshark / tshark parse the pcap differently, resulting in a different hex encoding in packet.http.file_data / empty file_data field which will not allow you to decrypt the data due to an exception thrown by tsharkbody_to_bytes.

We customize the script to decrypt even if the --save flag is not set and if something can be decrypted it is also printed to the console:

    # If we have keys lets decode the payload
    aes_keys = sessions.get(request_header['agent_id'], None)

    if not aes_keys:
        print(f"[!] No AES Keys for Agent with ID {request_header['agent_id']}")
        return

    # If save_path is set, make sure the directory exists
    if save_path and not os.path.exists(save_path):
        print(f"[!] Save path {save_path} does not exist, creating")
        os.makedirs(save_path)

    # Decrypt the Request Body
    if request_payload:
        print("  [+] Decrypting Request Body")
        decrypted_request = aes_decrypt_ctr(aes_keys['aes_key'], aes_keys['aes_iv'], request_payload)
    
        # Always print
        decoded = decrypted_request.decode('utf-8', errors='ignore').strip()
        print(f"\n\033[92m[Decrypted Request]\033[0m\n{decoded}\n\n")

        # Save only if save_path is set
        if save_path:
            save_file = f'{save_path}/{unique_id}-request-{request_header["agent_id"]}.bin'
            with open(save_file, 'wb') as output_file:
                output_file.write(decrypted_request)

    # Decrypt the Response Body
    if response_payload:
        print("  [+] Decrypting Response Body")
        
        decrypted_response = aes_decrypt_ctr(aes_keys['aes_key'], aes_keys['aes_iv'], response_payload)
    
        # Always print
        decoded = decrypted_response.decode('utf-8', errors='ignore').strip()
        print(f"\n\033[94m[Decrypted Response - {command}]\033[0m\n{decoded}\n\n")

        # Save only if save_path is set
        if save_path:
            save_file = f'{save_path}/{unique_id}-response-{request_header["agent_id"]}.bin'
            with open(save_file, 'wb') as output_file:
                output_file.write(decrypted_response)

Here is the customized version of the script:

havoc-pcap-modified.py
# Copyright (C) 2024 Kev Breen, Immersive Labs
# https://github.com/Immersive-Labs-Sec/HavocC2-Forensics
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import argparse
import struct
import binascii
from binascii import unhexlify

from uuid import uuid4


try:
    import pyshark
except ImportError:
    print("[-] Pyshark not installed, please install with 'pip install pyshark'")
    exit(0)

try:
    from Crypto.Cipher import AES
    from Crypto.Util import Counter
except ImportError:
    print("[-] PyCryptodome not installed, please install with 'pip install pycryptodome'")
    exit(0)



demon_constants = {
    1: "GET_JOB",
    10: 'COMMAND_NOJOB',
    11: 'SLEEP',
    12: 'COMMAND_PROC_LIST',
    15: 'COMMAND_FS',
    20: 'COMMAND_INLINEEXECUTE',
    21: 'COMMAND_JOB',
    22: 'COMMAND_INJECT_DLL',
    24: 'COMMAND_INJECT_SHELLCODE',
    26: 'COMMAND_SPAWNDLL',
    27: 'COMMAND_PROC_PPIDSPOOF',
    40: 'COMMAND_TOKEN',
    99: 'DEMON_INIT',
    100: 'COMMAND_CHECKIN',
    2100: 'COMMAND_NET',
    2500: 'COMMAND_CONFIG',
    2510: 'COMMAND_SCREENSHOT',
    2520: 'COMMAND_PIVOT',
    2530: 'COMMAND_TRANSFER',
    2540: 'COMMAND_SOCKET',
    2550: 'COMMAND_KERBEROS',
    2560: 'COMMAND_MEM_FILE', # Beacon Object File
    4112: 'COMMAND_PROC', # Shell Command
    4113: 'COMMMAND_PS_IMPORT',
    8193: 'COMMAND_ASSEMBLY_INLINE_EXECUTE',
    8195: 'COMMAND_ASSEMBLY_LIST_VERSIONS',
}


# Used to store the AES Keys for each session
sessions = {}


def tsharkbody_to_bytes(hex_string):
    """
    Converts a TShark hex formated string to a byte string.
    
    :param hex_string: The hex string from TShark.
    :return: The byte string.
    """
    # its concatonated strings
    hex_string = hex_string.replace(':', '')
    #unhex it
    hex_bytes = unhexlify(hex_string)
    return hex_bytes



def aes_decrypt_ctr(aes_key, aes_iv, encrypted_payload):
    """
    Decrypts an AES-encrypted payload in CTR mode.

    :param aes_key: The AES key as a byte string.
    :param aes_iv: The AES IV (Initialization Vector) for the counter, as a byte string.
    :param encrypted_payload: The encrypted payload as a byte string.
    :return: The decrypted plaintext as a byte string.
    """
    # Initialize the counter for CTR mode
    ctr = Counter.new(128, initial_value=int.from_bytes(aes_iv, byteorder='big'))

    # Create the cipher in CTR mode
    cipher = AES.new(aes_key, AES.MODE_CTR, counter=ctr)

    # Decrypt the payload
    decrypted_payload = cipher.decrypt(encrypted_payload)

    return decrypted_payload



def parse_header(header_bytes):
    """
    Parses a 20-byte header into an object.

    :param header_bytes: A 20-byte header.
    :return: A dictionary representing the parsed header.
    """
    if len(header_bytes) != 20:
        raise ValueError("Header must be exactly 20 bytes long")

    # Unpack the header
    payload_size, magic_bytes, agent_id, command_id, mem_id = struct.unpack('>I4s4sI4s', header_bytes)

    # Convert bytes to appropriate representations
    magic_bytes_str = binascii.hexlify(magic_bytes).decode('ascii')
    agent_id_str = binascii.hexlify(agent_id).decode('ascii')
    mem_id_str = binascii.hexlify(mem_id).decode('ascii')
    command_name = demon_constants.get(command_id, f'Unknown Command ID: {command_id}')

    return {
        'payload_size': payload_size,
        'magic_bytes': magic_bytes_str,
        'agent_id': agent_id_str,
        'command_id': command_name,
        'mem_id': mem_id_str
    }


def parse_request(http_pair, magic_bytes, save_path):
    request = http_pair['request']
    response = http_pair['response']#

    unique_id = uuid4()

    print("[+] Parsing Request")


    try:
        request_body = tsharkbody_to_bytes(request.get('file_data', ''))
        header_bytes = request_body[:20]
        request_payload = request_body[20:]
        request_header = parse_header(header_bytes)
    except Exception as e:
        print(f"[!] Error parsing request body: {e}")
        return


    # If there is no magic this is not Havoc
    if request_header.get("magic_bytes", '') != magic_bytes:
        return


    if request_header['command_id'] == 'DEMON_INIT':
        print("[+] Found Havoc C2")
        print(f"  [-] Agent ID: {request_header['agent_id']}")
        print(f"  [-] Magic Bytes: {request_header['magic_bytes']}")
        print(f"  [-] C2 Address: {request.get('uri')}")

        aes_key = request_body[20:52]
        aes_iv = request_body[52:68]

        print(f"  [+] Found AES Key")
        print(f"    [-] Key: {binascii.hexlify(aes_key).decode('ascii')}")
        print(f"    [-] IV: {binascii.hexlify(aes_iv).decode('ascii')}")

        if request_header['agent_id'] not in sessions:
            sessions[request_header['agent_id']] = {
                "aes_key": aes_key,
                "aes_iv": aes_iv
            }
        
        # We dont want to process the rest of the request
        response_payload = None

    elif request_header['command_id'] == 'GET_JOB':
        print("  [+] Job Request from Server to Agent")
         # if the pcap did not contain an init or we have manually passed keys add the found keys message
        
        # Grab the response header to get the incoming request. 

        try:
            response_body = tsharkbody_to_bytes(response.get('file_data', ''))

        except Exception as e:
            print(f"[!] Error parsing request body: {e}")
            return


        header_bytes = response_body[:12]
        response_payload = response_body[12:]
        command_id = struct.unpack('<H', header_bytes[:2])[0]

        command = demon_constants.get(command_id, f'Unknown Command ID: {command_id}')

        print(f"    [-] C2 Address: {request.get('uri')}")
        print(f"    [-] Comamnd: {command}")

    else:
        print(f"  [+] Unknown Command: {request_header['command_id']}")
        response_payload = None

    # If we have keys lets decode the payload
    aes_keys = sessions.get(request_header['agent_id'], None)

    if not aes_keys:
        print(f"[!] No AES Keys for Agent with ID {request_header['agent_id']}")
        return

    # If save_path is set, make sure the directory exists
    if save_path and not os.path.exists(save_path):
        print(f"[!] Save path {save_path} does not exist, creating")
        os.makedirs(save_path)

    # Decrypt the Request Body
    if request_payload:
        print("  [+] Decrypting Request Body")
        decrypted_request = aes_decrypt_ctr(aes_keys['aes_key'], aes_keys['aes_iv'], request_payload)
    
        # Always print
        decoded = decrypted_request.decode('utf-8', errors='ignore').strip()
        print(f"\n\033[92m[Decrypted Request]\033[0m\n{decoded}\n\n")

        # Save only if save_path is set
        if save_path:
            save_file = f'{save_path}/{unique_id}-request-{request_header["agent_id"]}.bin'
            with open(save_file, 'wb') as output_file:
                output_file.write(decrypted_request)

    # Decrypt the Response Body
    if response_payload:
        print("  [+] Decrypting Response Body")
        
        decrypted_response = aes_decrypt_ctr(aes_keys['aes_key'], aes_keys['aes_iv'], response_payload)
    
        # Always print
        decoded = decrypted_response.decode('utf-8', errors='ignore').strip()
        print(f"\n\033[94m[Decrypted Response - {command}]\033[0m\n{decoded}\n\n")

        # Save only if save_path is set
        if save_path:
            save_file = f'{save_path}/{unique_id}-response-{request_header["agent_id"]}.bin'
            with open(save_file, 'wb') as output_file:
                output_file.write(decrypted_response)


def read_pcap_and_get_http_pairs(pcap_file, magic_bytes, save_path):
    capture = pyshark.FileCapture(pcap_file, display_filter='http')
    http_pairs = {}
    current_stream = None
    request_data = None

    print("[+] Parsing Packets")

    for packet in capture:
        try:
            # Check if we are still in the same TCP stream
            if current_stream != packet.tcp.stream:
                # Reset for a new stream
                current_stream = packet.tcp.stream
                request_data = None

            if 'HTTP' in packet:
                if hasattr(packet.http, 'request_method'):
                    # This is a request
                    request_data = {
                        'method': packet.http.request_method,
                        'uri': packet.http.request_full_uri,
                        'headers': packet.http.get_field_value('request_line'),
                        'file_data': packet.http.file_data if hasattr(packet.http, 'file_data') else None
                    }
                elif hasattr(packet.http, 'response_code') and request_data:
                    # This is a response paired with the previous request
                    response_data = {
                        'code': packet.http.response_code,
                        'phrase': packet.http.response_phrase,
                        'headers': packet.http.get_field_value('response_line'),
                        'file_data': packet.http.file_data if hasattr(packet.http, 'file_data') else None
                    }
                    # Pair them together in a dictionary
                    http_pairs[f"{current_stream}_{packet.http.request_in}"] = {
                        'request': request_data,
                        'response': response_data
                    }

                    parse_request(http_pairs[f"{current_stream}_{packet.http.request_in}"], magic_bytes, save_path)

                    #print(http_pairs[f"{current_stream}_{packet.http.request_in}"])

                    request_data = None  # Reset request data after pairing
        except AttributeError as e:
            # Ignore packets that don't have the necessary HTTP fields
            pass



if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Extract Havoc Traffic from a PCAP')

    parser.add_argument(
        '--pcap',
        help='Path to pcap file',
        required=True)
    

    parser.add_argument(
        "--aes-key", 
        help="AES key", 
        required=False)
    
    parser.add_argument(
        "--aes-iv", 
        help="AES initialization vector", 
        required=False)
    
    parser.add_argument(
        "--agent-id", 
        help="Agent ID", 
        required=False)

    parser.add_argument(
        '--save',
        help='Save decrypted payloads to file',
        default=False,
        required=False)

    parser.add_argument(
        '--magic',
        help='Set the magic bytes marker for the Havoc C2 traffic',
        default='deadbeef',
        required=False)


    # Parse the arguments
    args = parser.parse_args()

    # Custom check for the optional values
    if any([args.aes_key, args.aes_iv, args.agent_id]) and not all([args.aes_key, args.aes_iv, args.agent_id]):
        parser.error("[!] If you provide one of 'aes-key', 'aes-iv', or 'agent-id', you must provide all three.")
    
    if args.agent_id and args.aes_key and args.aes_iv:
        sessions[args.agent_id] = {
            "aes_key": unhexlify(args.aes_key),
            "aes_iv": unhexlify(args.aes_iv)
        }
        print(f"[+] Added session keys for Agent ID {args.agent_id}")

    #find_havoc_packets(packets, args.save)


    # Usage example
    http_pairs = read_pcap_and_get_http_pairs(args.pcap, args.magic, args.save)

After running the script we can answer the question with context to the commands issued:

What is the SID of the user that the attacker is executing everything under?

What is the Link-local IPv6 Address of the server? Enter the answer exactly as you see it

The attacker printed a flag for us to see. What is that flag?

The attacker added a new account as a persistence mechanism. What is the username and password of that account? Format is username:password

The attacker found an important file on the server. What is the full path of that file?

What is the flag found inside the file from question 5?

CC BY 4.0
MayhemTryHackMe
VirusTotalVirusTotal
GitHub - HavocFramework/Havoc: The Havoc Framework.GitHub
Havoc C2 Framework – A Defensive Operator’s Guide
HavocC2-Forensics/PacketCapture/havoc-pcap-parser.py at main · Immersive-Labs-Sec/HavocC2-ForensicsGitHub
Logo
Logo
Logo
Logo
We can identify the following commands.
Logo