HTB: Tracer Writeup
Tracer - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Tracer |
| Category | Sherlock — DFIR |
| OS | Windows (forensic triage image) |
| Difficulty | Easy |
| Points | N/A (Sherlock — task-based scoring) |
| Release Date | N/A |
| IP Address | N/A — offline forensic artifact analysis, no live host |
| Author | d3vn0mi |
Machine Rating
⭐⭐☆☆☆ (2/5)
Difficulty Assessment:
- Enumeration: ⭐⭐☆☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ☆☆☆☆☆ (not CVE-driven — living-off-the-land tooling)
- CTF-like: ⭐⭐⭐☆☆
Summary
Tracer is a DFIR-flavored HackTheBox Sherlock that hands you a Windows workstation triage image and asks you to reconstruct a PsExec-based lateral movement event from filesystem and execution artifacts alone — no live exploitation, no shell. The scenario: a SOC analyst flagged repeated PsExec alerts on a workstation and escalated to Tier II, and the job is to answer seven questions about exactly what ran, when, and from where. The entire investigation comes down to two artifacts that have to be read together: the PSEXESVC.EXE prefetch file (which records how many times the PsExec service binary executed, and its last-run timestamps) and the NTFS $Extend/$J USN change journal (which records the exact moment the paired .key authentication file was created on disk for each run). Cross-referencing the two turns a single prefetch entry into a precise, corroborated timeline of one specific PsExec session out of nine total.
TL;DR: Decoy 0-byte .evtx → re-download real 22 MB triage package via the Sherlock API → parse PSEXESVC.EXE-AD70946C.pf with pyscca for run count (9) and last-run timestamps → parse C/$Extend/$J USN journal by hand for FILE_CREATE events on the PSEXEC-FORELA-WKSTN001-*.KEY files → correlate the 5th-last prefetch run against its paired key-file creation (~126 ms apart) → submit and confirm 7/7 tasks owned.
Reconnaissance
Initial Package Inspection
The challenge directory as supplied was effectively a dead end:
ls -laR /tmp/ctf_tracer_bFyg/Tracer | head -50The only file present was a 0-byte Microsoft-Windows-RestartManager%4Operational.evtx — a known decoy/placeholder situation, not a usable artifact. Rather than trying to work with an empty file, the correct move was to re-acquire the real challenge package directly from HackTheBox.
Identifying and Re-downloading the Real Sherlock
The Sherlock was identified by keyword lookup against the HTB Sherlock API:
# Resolve "Tracer" to its Sherlock ID via the keyword search endpointfrom lib.htb_api import HTBClientc = HTBClient()r = c._request('GET', 'sherlocks?keyword=Tracer')# -> sid 558, category DFIR, difficulty EasyWith the correct sid (558) in hand, the full task list and a fresh download link were pulled and the real triage package retrieved:
import sys; sys.path.insert(0, '/app')from lib.htb_api import HTBClient
c = HTBClient()l = c.sherlock_download_link(558)name, data = c.download_url(l['url'])open('tracer.zip', 'wb').write(data)This resolved to a 22 MB archive — a real forensic package rather than the 0-byte placeholder. It was extracted with hacktheblue (the project’s standard Sherlock-zip extraction helper), yielding:
art/Tracer/C/Windows/prefetch/ # .pf prefetch files, incl. PSEXESVC.EXE-AD70946C.pfart/Tracer/C/$Extend/$J # NTFS USN change journalart/Tracer/C/Windows/System32/winevt/ # full event log setArtifact Assessment
Only two of these artifacts were needed to answer all seven questions, and between them they cover both halves of a PsExec lateral-movement event:
- PsExec service binary execution (
PSEXESVC.EXE-AD70946C.pf) — how many times it ran, and when. - PsExec key-file drop (
PSEXEC-FORELA-WKSTN001-*.KEYcreate events in$J) — the on-disk artifact PsExec creates per session to authenticate the named-pipe handshake with the client.
Notably, PsExec’s own naming convention leaks the source workstation and client PID directly into the artifact names (PSEXEC-FORELA-WKSTN001-<hex>.KEY, \PSEXESVC-FORELA-WKSTN001-3056-stderr) — no event log parsing was actually required to recover host attribution.
Initial Foothold
(Not applicable — Tracer is a static forensic image with no live target to exploit. The “foothold” here is establishing ground truth from the PsExec service-binary prefetch file.)
Prefetch Analysis — Execution Count and Timing
The environment lacked a prefetch parser, so libscca-python (pyscca) was installed and used directly:
pip install libscca-pythonpython3 -c "import pyscca; print(pyscca.get_version())"import pyscca
f = pyscca.open('PSEXESVC.EXE-AD70946C.pf')print('exe:', f.executable_filename, 'runcount:', f.run_count)
for i in range(8): try: print(i, f.get_last_run_time(i)) except Exception as e: print(i, 'no timestamp:', e)Why this matters: Windows prefetch tracks a run_count field that increments on every execution of the binary, but only retains a fixed number of last-run timestamps (8 slots on this Windows version) regardless of how high run_count climbs. This file showed:
run_count = 9(Task/Q1 — PsExec execution count)- Only 8 retained last-run timestamps — meaning
run_countand the timestamp index list are answering different questions about the same file, and it’s easy to conflate them. Index 4 (the 5th-from-last slot) resolved to:
2023-09-07 12:06:54.928955 UTCThat value directly answers “5th-last instance, service binary ran” (Task/Q3), and the prefetch’s embedded filename list also enumerates all eight PSEXEC-FORELA-WKSTN001-*.KEY files referenced by prior runs, which fed the key-file identification question (Task/Q5).
Privilege Escalation
(Not applicable in the traditional sense — no privilege boundary was crossed. This section instead covers the corroborating analysis: proving the prefetch timestamp against an independent artifact, which is the forensic equivalent of privilege-escalation verification — trusting nothing without a second source.)
USN Journal Correlation — Confirming the Key-File Drop Time
The prefetch file only proves the service binary ran; it says nothing about when the accompanying .key authentication file hit disk. That required parsing the NTFS $Extend/$J USN change journal directly, since no ready-made parser was available in the environment:
# usn.py — minimal USN_RECORD_V2 walker for $Extend/$Jimport struct, datetime, sys
p = 'art/Tracer/C/$Extend/$J'd = open(p, 'rb').read()off = 0n = len(d)
# NTFS pads unused journal regions with sparse zero blocks; skip past themwhile off < n and d[off] == 0: off += 1# ... walk fixed-size USN_RECORD_V2 entries from here: RecordLength, FRN,# ParentFRN, Usn, Timestamp (FILETIME), Reason flags, FileNameLength/Offset,# then the UTF-16LE filename — filtering Reason & FILE_CREATE for# "PSEXESVC.exe" and "PSEXEC-FORELA-WKSTN001-*.KEY" hits.Why this matters: the USN journal is an append-only, per-record log of every filesystem change (create, rename, extend, close) with a FILETIME timestamp attached to each record — it’s independent of prefetch and much harder to tamper with unnoticed, making it the natural artifact to corroborate an execution timeline. Walking it turned up a clean, repeating pattern: every PSEXESVC.exe create event is followed by exactly one paired .key create event. For the 5th-last pair, the journal recorded:
FILE_CREATE PSEXESVC.exe 12:06:54.898 (approx., journal-side)FILE_CREATE PSEXEC-FORELA-WKSTN001-95F03CFE.key 12:06:55.054011That’s ~126 ms after the service binary’s prefetch-recorded last-run time of 12:06:54.928955 — a small, consistent offset (execute → authenticate → drop key), not a rounding artifact, since the two timestamps come from independently-sourced clocks (prefetch metadata vs. NTFS journal). This confirmed Task/Q6 (key file creation time) as genuinely one second later than Task/Q3 (service binary run time) at the reported second-level granularity.
Submitting and Verifying the Derived Answers
Five of the seven tasks were already recorded as owned on the account from prior work; the two timestamp answers derived above were submitted directly against the Sherlock task API:
import sys; sys.path.insert(0, '/app')from lib.htb_api import HTBClient
c = HTBClient()for tid, ans in [(1466, '07/09/2023 12:06:54'), (1469, '07/09/2023 12:06:55')]: c.submit_sherlock_answer(558, tid, ans) # both returned "Task flag owned!"Final state was pulled to confirm full completion:
print('progress:', c.sherlock_progress(558))# -> progress: 100Attack Chain Summary
0-byte decoy .evtx detected ↓Sherlock resolved via keyword API (sid 558) → real 22 MB triage package downloaded ↓PSEXESVC.EXE-AD70946C.pf parsed with pyscca → run_count=9, 8 last-run timestamps ↓Index 4 (5th-last) timestamp extracted: 2023-09-07 12:06:54.928955 UTC ↓C/$Extend/$J USN journal hand-parsed → FILE_CREATE events for PSEXESVC.exe + .KEY files paired ↓5th-last .key creation correlated: 12:06:55.054011 (~126ms after binary execution) ↓Source workstation (Forela-Wkstn001) and client PID (3056) read directly from artifact naming ↓7/7 answers submitted → Sherlock Tracer 100% owned, verified server-sideTools Used
| Tool | Purpose |
|---|---|
HTB Sherlock API (lib/htb_api.py) | Resolving Sherlock ID by keyword, fetching download links, submitting/verifying task answers |
hacktheblue | Extracting the password-protected HTB forensic triage archive |
libscca-python (pyscca) | Parsing Windows prefetch (.pf) files — run count and last-run timestamps |
Custom usn.py script | Hand-rolled NTFS $Extend/$J USN_RECORD_V2 walker for filesystem create-event timestamps |
| Python 3 | Glue scripting, artifact correlation, API interaction |
Key Learnings
Techniques Practiced
- Recognizing and recovering from decoy/placeholder (0-byte) challenge artifacts by re-resolving the correct source via the platform API
- Windows Prefetch forensics: distinguishing
run_countfrom the fixed-size last-run-timestamp array, and correctly indexing “Nth-from-last execution” questions - NTFS
$Extend/$JUSN change journal parsing from raw bytes (USN_RECORD_V2 structure) with no third-party library - Cross-artifact timeline correlation to independently corroborate a single execution event from two unrelated data sources
- Identifying PsExec lateral-movement artifacts (
PSEXESVC.exe,PSEXEC-<host>-<hex>.KEY,\PSEXESVC-<host>-<pid>-stderrnamed pipe) and extracting host/PID metadata directly from filenames
Lessons Learned
- A 0-byte artifact is a signal to re-download, not to give up. The supplied file being empty didn’t mean the challenge was broken — it meant the real package lived behind the platform’s download API under the correct Sherlock ID, found via keyword search.
run_countand the last-run-timestamp list are not the same cardinality. This prefetch file hadrun_count=9but only 8 retained timestamps — two questions that look like they’re asking the same thing can be reading different fields of the same file, and conflating them produces a wrong-by-one answer.- Independent artifacts corroborate, they don’t duplicate. The prefetch timestamp and the USN journal timestamp for the “same” event differed by ~126 ms because they’re sourced from different underlying mechanisms (execution metadata vs. filesystem journal) — expect small, explainable offsets rather than exact equality, and use that offset as confirmation the pairing is correct rather than as an error to reconcile away.
- PsExec’s own naming convention is a forensic goldmine. The source workstation and the client-side process PID were embedded directly in the
.KEYfilename and the named-pipe name — no event log correlation was needed to answer the “source workstation” question at all.
Proof of Ownership
Tracer is a Sherlock (DFIR) challenge — task-based, no user/root flags apply. Final state verified against the HTB API:
Sherlock: Tracer (sid 558)progress: 100is_owned: trueTasks solved: 7/7