A targeted attack on Wika’s macOS device has left traces hidden deep within system logs and artifacts. As a DFIR analyst, your mission is to reconstruct the attack timeline and uncover key evidence using macOS forensic tools.
A fake photo application was dropped into the user’s Downloads folder and moved to /Applications, then launched with the user’s help, sidestepping Gatekeeper’s quarantine check. It opened a reverse shell back to an attacker host on the local network, where the attacker first confirmed their identity and then pulled a zip archive over HTTP. The archive held exploit source that was compiled on the Mac and used against a vulnerable diagnostics binary, which mounted a crafted disk image and produced a root shell. The attacker then deleted their working directory with sudo and removed two of the user’s files. The user noticed something was off and searched Safari for how to tell whether they had been hacked. The unified logs carried nearly every step; they had to be parsed offline because the live-response set holds them only as a raw logarchive.
BasicInfo/, Full_file_listing.txt, per-user files and a unified-log logarchive → browse with cat, grep and sqlite3logarchive → parsed offline with unifiedlog_iterator into a CSV, then filtered with Python.bash_sessions/ → read with catHistory.db and QuarantineEventsV2 → sqlite3RecentApplications.sfl2 → a bookmark blob, decoded with PythonTimestamp note: The host is on GMT+2 and the logs carry a Pacific label, so read the unified-log times as UTC from the parsed CSV. The UTC values are what the answers use.
BasicInfo/ in the collection holds the output of sw_vers. The product version line gives the answer directly.
cat BasicInfo/sw_vers.txt
Search the full file listing for the app bundle and cross-check system.log: launchd logs a one-shot job for funphotos when it runs. The bundle sits in /Applications, while a copy also exists under the user’s Downloads.
grep -i funphotos Full_file_listing.txt
Gotcha: The Downloads copy and the Trash entry are the same download in earlier states. The question wants the path the app executed from.
The listing has no inode column, so the number comes from other artifacts. RecentApplications.sfl2 stores a bookmark for Users/wika/Downloads/funphotos.app, and the bookmark carries the catalog node ID (CNID), which on HFS+ is the inode number. A short Python script decodes the plist and bookmark and extracts it. The same node ID can be cross-checked in the fsevents records.
python3 -I decode_sfl2.py RecentApplications.sfl2
This one was reasoned rather than read from an artifact. The app arrived as funphotos.gz, and the gzip archive was left in the Trash. A quarantine-flag bypass tied to extracting a gzip-delivered app on macOS of this era points to CVE-2022-22616 (Gatekeeper bypass through a crafted gzip). Nothing in the collection names the CVE, so the match rests on the delivery format, and HTB accepted it.
sqlite3 QuarantineEventsV2 'select * from LSQuarantineEvent'
Gotcha: Other well-known Gatekeeper bypass CVEs (for example CVE-2021-30657 and CVE-2021-1810) fit a “Gatekeeper bypass” label but not a gzip-delivered app.
The unified logs only exist as a logarchive, so parse it offline into a CSV first. Around the launch of the app the log shows AppTranslocation activity and a bash process spawned from the app, which is the first sign of the shell. Pivot on process name bash after the app launch and take the first entry.
unifiedlog_iterator --mode log-archive --format csv --input system_logs.logarchive --output work/ul/logs.csv
python3 -I filter_ul.py work/ul/logs.csv --from "2025-02-03 21:09" --to "2025-02-03 21:20"
The shell history under .bash_sessions records the attacker’s commands in order, and the unified log shows whoami running at 21:10:32, about 40 seconds after the shell appeared. The two sources agree on the first command.
cat .bash_sessions/*
The same shell history holds a curl -O against an HTTP server on 192.168.1.2:8000, the attacker’s staging host. The saved file is the archive.
grep -h curl .bash_sessions/*
Unzipping priv.zip yields source that has to be built. In the parsed unified log, filter to the window after the download and look for the make process; its first appearance is the compile.
python3 -I filter_ul.py work/ul/logs.csv --process make --from "2025-02-03 21:09" --to "2025-02-03 21:20"
After the compile, the exploit runs and the log shows the privileged activity (hdiutil, kernel mount messages and the test binary) in the minute that follows. The root shell is the last step of that chain. The exact second was reasoned from that sequence rather than read from a single “root” event, and HTB accepted it.
python3 -I filter_ul.py work/ul/logs.csv --process "test,hdiutil,kernel,sudo" --from "2025-02-03 21:14" --to "2025-02-03 21:16"
Gotcha: The activity spans several seconds. Seconds just before the accepted one belong to the exploit still running, not to the shell, and one second off is not accepted.
The kernel log records the HFS mount of the volume the exploit created. The exploit binary confirms it: running strings on its uuidtext entry shows the same volume name passed to hdiutil.
python3 -I filter_ul.py work/ul/logs.csv --process kernel --grep hfs
strings -a uuidtext/10/12D53A...
Tip: The name contains a backtick, so copy it exactly as shown in the kernel mount line.
The strings from the same exploit binary reference tmdiagnose, Apple’s diagnostics tool, whose log-collection command is open to command injection through the mounted volume name. That is the “rootpipe reborn” bug published at codecolor.ist and Exploit-DB 47070, assigned CVE-2019-8513. The fit with the 10.13.6 host from Q1 holds, since the bug is fixed in later releases.
strings -a uuidtext/10/12D53A...
The exploit injects commands into tmdiagnose so that a trusted binary runs the attacker’s payload. That is the shape of Indirect Command Execution, T1202. This was reasoned from the mechanism and not read from an artifact.
Gotcha: “Injection” invites process-injection or command-interpreter IDs. The abuse here is a trusted utility executing attacker input, which maps to Indirect Command Execution.
sudo logs every elevated command line to the unified log. After the root shell, the only cleanup command is a recursive delete of the working directory the exploit lived in.
python3 -I filter_ul.py work/ul/logs.csv --process sudo --from "2025-02-03 21:15" --to "2025-02-03 21:20"
The timestamp of the sudo entry from Q13 is the start of the cleanup.
The shell history also holds rm commands against the user’s documents. Two files are removed, and the answer lists them in alphabetical order, case-insensitive.
grep -h 'rm ' .bash_sessions/*
Safari’s History.db stores search-engine visits. A Google results URL in the history_visits window after the compromise carries the user’s query in its q= parameter.
sqlite3 History.db "select url, title from history_items where url like '%google.com/search%'"
| Phase | Action |
|---|---|
| Execution | User runs funphotos.app from /Applications, bypassing Gatekeeper |
| Command and Control | App spawns a reverse shell back to the attacker |
| Discovery | Attacker runs whoami as the first command |
| Command and Control | priv.zip pulled from 192.168.1.2:8000 with curl |
| Privilege Escalation | Exploit source compiled with make |
| Privilege Escalation | tmdiagnose command injection mounts a crafted volume and yields a root shell |
| Defense Evasion | sudo rm -r priv removes the working directory |
| Impact | Two user files deleted |
| Timestamp (UTC) | Event |
|---|---|
| 2025-02-03 21:09:51 | Reverse shell activity begins |
| 2025-02-03 21:10:32 | First attacker command, whoami |
| 2025-02-03 21:13:50 | Exploit compiled |
| 2025-02-03 21:15:07 | Root shell obtained |
| 2025-02-03 21:15:49 | sudo rm -r priv starts the cleanup |
| Type | Value |
|---|---|
| Application | /Applications/funphotos.app |
| Archive | priv.zip |
| Staging host | hxxp[://]192[.]168[.]1[.]2:8000/priv.zip |
| Volume name | disk`t*:1` |
| Deleted files | mytarget.txt, Targets.rtf |
| Technique | ID | Description |
|---|---|---|
| User Execution: Malicious File | T1204.002 | User opens the trojanised photo app |
| Subvert Trust Controls: Gatekeeper Bypass | T1553.001 | App delivered as a gzip to dodge quarantine checks |
| Command and Scripting Interpreter: Unix Shell | T1059.004 | Reverse shell and interactive commands |
| Ingress Tool Transfer | T1105 | priv.zip fetched with curl |
| Exploitation for Privilege Escalation | T1068 | tmdiagnose flaw used to reach root |
| Indirect Command Execution | T1202 | Injection into a trusted diagnostics binary |
| Indicator Removal: File Deletion | T1070.004 | sudo rm -r priv |
| Data Destruction | T1485 | The user’s target files removed |
Gatekeeper decisions need to be watched, not trusted. A gzip-delivered app launched from /Applications with a reverse shell as its only payload went unchallenged. Alert on a freshly downloaded app spawning bash and on AppTranslocation followed by shell activity.
Unpatched 10.13.6 handed over root. The host ran an old release where the tmdiagnose flaw was still open. Keeping macOS current closes this class of local escalation outright.
Compiling on a workstation is a signal. make running on a user’s Mac minutes after a curl download is rare outside of developers. Flag compiler activity that follows an HTTP download from a LAN address.
Unified logs hold the story, but only if collected in a parseable form. Command lines from sudo and process starts were enough to rebuild the whole timeline, while a raw logarchive needed offline parsing. Ship them to a central store so they survive cleanup.
The attacker went from a single user-assisted app launch to root and then to destroying the user’s files, using one known local flaw and an HTTP file server. Nothing was hidden from the unified logs, which is why the timeline could be rebuilt to the second.