We are handed a grabkit collection — a triage pull of the Chrome profile
belonging to NORTHVANE\p.raman. Somewhere in it is a database password that an operator
typed into an internal "add datasource" wizard and then, in a later save, took back out.
The current state of the profile no longer contains it. Our job is to prove it was there and recover it.
On paper this is "find a string in a browser profile." In practice the value is buried under two independent layers of compression and only survives as a superseded database record that no normal LevelDB client will ever hand you. The whole challenge is knowing that "deleted from the UI" is not the same as "gone from disk," and being willing to parse the on-disk format yourself.
A Chromium profile keeps per-site web-app state in Local Storage, which on disk is a
LevelDB database at Local Storage/leveldb/. That directory is not a single
file — it is a set of immutable sorted-string tables (*.ldb), an append-only write log
(*.log), a MANIFEST and a CURRENT pointer.
The wizard we care about autosaves its draft there:
| Field | Value |
|---|---|
| Store | Local Storage/leveldb |
| Origin | https://atlas.northvane.internal |
| Key | atlas.setup.autosave (Atlas “add datasource” draft) |
Local Storage keys on disk are framed as _<origin>\x00\x01<scriptkey>, and each
value carries a one-byte type prefix: 0x00 means the payload is UTF-16LE, 0x01
means Latin-1/UTF-8. Hold onto that 0x00 — it is the first clue that the value is not
plain text.
*.ldb) plus a write-ahead log (*.log).
Before anything else: the profile is stuffed with realistic-but-fake secrets, and some of them even rotate between snapshots to bait a diff. None are the flag.
CF26{…} format counts. A plausible-looking token is not a
flag. This field is an explicit trap for analysts (and AI agents) who report the first high-entropy
string they see.
The reflex move — grep -a CF26 / strings over the .ldb files
— comes back empty. Even a UTF-16 grep finds nothing. That is because the value is hidden twice over:
compressToUTF16 (15 bits per character, +32
offset, so every code unit lands in [0x20, 0x8020]). On screen these render as CJK
“mojibake,” which is exactly why grep and strings slide right past them.0x01 is Snappy-compressed, so even a raw byte-grep over the .ldb
never sees the (already mojibake) value. You have to parse the table and decompress each block first.LevelDB is log-structured: an update to a key does not overwrite the old bytes, it writes a new version with a higher sequence number. The old version lingers in its original sstable until a compaction happens to rewrite that table. The autosave draft went through several saves:
| Save | Location | Content of atlas.setup.autosave |
|---|---|---|
| step 1 | 000138.ldb seq 1217 | {…"host":"db01…","user":"svc_wh"} — no secret yet |
| step 3 | 000144.ldb seq 1570 | "dsn":"postgres://svc_wh:CF26{…}@db01…/analytics" — the flag |
| step 5 | 000145.log seq 1778 (live) | password stripped → "connectionRef":"nv-conn-7741" |
plyvel — or any LevelDB client — returns only the newest value per key,
i.e. the sanitized step-5 draft. The flagged step-3 version still exists physically inside
000144.ldb as an overwritten record. You must enumerate all sequence numbers, not
just whatever a client resolves.
Instead of using a LevelDB client, you must use a forensic LevelDB
raw-record reader — one that iterates every physical record across the
.ldb tables and the .log instead of resolving a key to its latest value. Two
standard ones are ccl_leveldb (from Alex Caithness's ccl_chromium_reader) and
Google's dfindexeddb; both Snappy-decompress the blocks for you and return superseded and
deleted versions:
import ccl_leveldb
db = ccl_leveldb.RawLevelDb("Local Storage/leveldb")
for rec in db.iterate_records_raw():
if b"atlas.setup.autosave" in rec.user_key:
print(rec.origin_file, rec.seq, rec.state, rec.value[:40])
# the SAME key comes back at seq 1217 (step 1), 1570 (step 3), 1778 (live) —
# every version, not just the one a merged read returns.
# sstable structure, bottom-up:
footer # fixed 48 bytes at EOF; points to the index block
→ index block # one entry per data block (last-key + BlockHandle)
→ data block # prefix-compressed key/value entries
# trailer after block_size bytes: 1 type byte (0=none, 1=snappy, 2=zstd) + 4 CRC
# type==1 -> cramjam.snappy.decompress_raw(block)
# each entry's internal key = user_key + 8-byte trailer
# trailer = (seq << 8) | type -> this is how you see EVERY version
Walk every data block, Snappy-decompressing the ones whose trailer byte is 1, then compare the same user-key across 000138.ldb, 000144.ldb and the .log. The diff between step 1 and step 3 is where the credential appears.
Something like...
import cramjam
def records(ldb): # yield (user_key, seq, value) from one .ldb
buf = open(ldb, "rb").read()
idx_off, idx_size = parse_footer(buf) # footer = last 48 bytes → index BlockHandle
for b_off, b_size in index_handles(buf, idx_off, idx_size):
block = buf[b_off : b_off + b_size]
if buf[b_off + b_size] == 1: # the 1 trailer byte after the block
block = cramjam.snappy.decompress_raw(block)
for ikey, val in entries(block): # prefix-compressed entries
yield ikey[:-8], int.from_bytes(ikey[-8:], "little") >> 8, val
# internal key = user_key + 8-byte trailer; trailer = (seq<<8)|type
target = b"_https://atlas.northvane.internal\x00\x01atlas.setup.autosave"
for f in ("000138.ldb", "000144.ldb"): # .log is the WAL (different framing; the readers handle it too)
for ukey, seq, val in records(f):
if ukey == target:
print(f, seq, val[:40]) # diff versions → 000144.ldb / seq 1570 carries the DSN
parse_footer / index_handles / entries are just the varint decoders for the footer, index block and data block.
Now unwrap Layer A. A value with the 0x00 (UTF-16) type prefix whose first code unit is
large (here 0x1BE1) is an lz-string compressToUTF16 blob. The quick path is
CyberChef's LZString Decompress operation — paste the value and it decodes. If you
script it in Python instead, two gotchas bite:
# lzstring==1.0.4 decompressFromUTF16() is BROKEN (it does char-32 on a str).
# Call the internal _decompress directly:
from lzstring import LZString
s = # the raw UCS-2 code units of the value (see below)
out = LZString()._decompress(len(s), 16384, lambda i: ord(s[i]) - 32)
# Rebuild the JS string as raw UCS-2 units so lone surrogates survive;
# NEVER value.decode('utf-16-le','replace') (it mangles them):
s = ''.join(chr(u) for (u,) in struct.iter_unpack('<H', value_bytes))
Decompress every candidate value, then grep CF26 over the decompressed output. The hit is in
atlas.setup.autosave, seq 1570, inside 000144.ldb.
The recovered step-3 draft holds the full connection string with the password still in it:
"dsn": "postgres://svc_wh:CF26{c0ldch41n_dr4y4g3_0v3r_th3_d0cks}@db01.northvane.internal:5432/analytics"
plyvel (or anything that
resolves a key) shows only the live, sanitized value. Reach for a forensic raw-record reader
(ccl_leveldb, dfindexeddb) that yields every version instead.CF26{…} format after decoding, not before.decompressFromUTF16() is broken in 1.0.4; use the
internal _decompress, and rebuild the string as raw UCS-2 units.(seq<<8)|type exposes
every historical version of a key. This is the general recipe for “it was deleted” browser-storage cases.
0x00/0x01 value
prefix and the compressToUTF16 signature (first unit > 0x1000) tell you exactly how to decode.Tools: ccl_leveldb / dfindexeddb (forensic raw-record readers — recover superseded versions, Snappy handled), plyvel (quick live-value / origin enumeration), cramjam.snappy + a small sstable walker (zero-dependency alternative), CyberChef LZString Decompress or lzstring via internal _decompress (compressToUTF16), grep/strings (triage — deliberately failed, which pointed to the decompression path).