A macOS workstation belonging to a corporate employee began exhibiting unusual behavior following the installation of what appeared to be a legitimate utility. Shortly after, the system showed signs of elevated privileges being abused, persistence techniques being deployed to survive reboots, and outbound connections to an unauthorized remote host. Investigators must analyze the available forensic evidence to uncover how the attacker gained access, maintained control, and interacted with the compromised system.
The user installed a trojanised utility, which gave the attacker a foothold on a macOS 13.0 laptop. A public MacDirtyCow-style exploit overwrote the PAM configuration for su so any caller became root. With root, the attacker created a new local account and switched on Remote Login. After the user rebooted the next morning, the attacker connected over SSH, dropped a backdoor binary and persisted it through a typo-squatted LaunchAgent. The user later noticed the account and deleted it, but the backdoor and its plist were still recoverable from the archived home folder and the filesystem events.
ireks-Mac-Triage/ → browse with ls and read plists with plistlib. Contains /private/etc, the user’s home, Safari data and Full_file_listing.txtcom.apple.LaunchServices.QuarantineEventsV2 → sqlite3logarchive → unifiedlog_iterator (Mandiant macos-UnifiedLogs, built from source because no unified log parser was installed). Output is a CSV of about 1.4 GB, filtered with grepzcat and strings7z x, no mount neededTimestamp note: All answers are UTC in
YYYY-MM-DD hh:mm:ss. Unified log lines carry sub-second precision, so truncate. Several time questions do not say which log line they mean, so expect to pick between start and completion lines.
The triage collection carries a copy of System/Library/CoreServices/SystemVersion.plist. ProductVersion gives the OS release. The version matters later: it places the host inside the range vulnerable to the kernel bug used for privilege escalation.
cat ireks-Mac_20250308_160413/ireks-Mac-Triage/System/Library/CoreServices/SystemVersion.plist
Start with the user’s browser artifacts. Safari’s Downloads.plist and the QuarantineEventsV2 SQLite database both record what was downloaded and from where. The database shows an installer-style download that matches the “legitimate utility” in the scenario, a .app bundle. The name follows the format mask exactly.
sqlite3 -readonly com.apple.LaunchServices.QuarantineEventsV2 'select datetime(LSQuarantineTimeStamp+978307200,"unixepoch"), LSQuarantineDataURLString, LSQuarantineOriginURLString from LSQuarantineEvent'
python3 -I -c "import plistlib,sys,pprint; pprint.pprint(plistlib.load(open(sys.argv[1],'rb')))" Downloads.plist
Downloads.plist stores a start time for each entry (the added-date field), which is when the user first initiated the download. Convert the Cocoa epoch to UTC and cross-check it against the quarantine event for the same file.
python3 -I -c "import plistlib,sys,pprint; pprint.pprint(plistlib.load(open(sys.argv[1],'rb'))['DownloadHistory'])" Downloads.plist
Safari and quarantine data only cover the download. For the most recent interaction, switch to the unified log. No unified log parser ships in the toolbox, so unifiedlog_iterator from Mandiant’s macos-UnifiedLogs was built from source and run against the collected logarchive, producing a CSV. Searching it for the app name and taking the last launchd line that spawns the process gives the final time the user launched it.
unifiedlog_iterator -m log-archive -i <logarchive> -o work/ulogs.csv -f csv
grep 'Successfully spawned Scientific-Calculator' work/ulogs.csv | tail -n 1
Gotcha: The quarantine and Safari timestamps are the download, not the use. The question asks for the last interaction, so keep the last launch line rather than the first.
The shell history of the irek account shows how the payload files reached disk. Files fetched with curl do not get the com.apple.quarantine attribute, so Gatekeeper never checks them, which is the bypass the question describes. This answer was reasoned from the history, the four-character format mask and the quarantine behaviour of curl, not read from a single log line that says “Gatekeeper bypass”.
cat ireks-Mac_20250308_160413/ireks-Mac-Triage/Users/irek/.zsh_history
Users/irek/Desktop holds a compiled binary named expooo next to the exploit source. The path was derived from the format mask (/*****/****/*******/******) together with the file on the Desktop, so it is reasoned from artifacts rather than taken from an execution log. The same session left an overwrite_file.bin in the Trash, which is the payload the exploit writes over a protected file (see Q8).
ls -laR ireks-Mac_20250308_160413/ireks-Mac-Triage/Users/irek/Desktop
The exploit source in the user’s directory is switch_race.c, a race condition that overwrites read-only files, the pattern of the public MacDirtyCow proof of concept. Q1 puts the host on macOS 13.0, which predates the fix. The CVE ID was reasoned from the public PoC and the OS version, then accepted.
head -n 40 switch_race.c
Tip: The mask
***-****-*****matches a 2022 CVE; MacDirtyCow is the public name of the bug.
The exploit overwrote /etc/pam.d/su (collected as private/etc/pam.d/su) with the content of overwrite_file.bin from the Trash. Diffing the two shows one changed module on the first auth line: the stock pam_rootok.so became pam_permit.so. With sufficient pam_permit.so, su accepts any caller without a password, which hands over a root shell.
diff ireks-Mac_20250308_160413/ireks-Mac-Triage/private/etc/pam.d/su ireks-Mac_20250308_160413/ireks-Mac-Triage/Users/irek/.Trash/overwrite_file.bin
With root access, the attacker created a local account named loki. In the unified log, filter on sysadminctl and dscl. Several lines sit within a few seconds of each other, so the choice of line matters. The accepted time is the sysadminctl line that logs the home directory creation for /Users/loki. This was reasoned: the question does not say which log line is meant, and the earlier lines in the burst were not accepted.
grep -E 'sysadminctl|dscl' work/ulogs.csv | grep -i loki
Gotcha: The account creation produces several log lines within about four seconds, and the home directory line is the one HTB accepts. Earlier lines in the burst are rejected.
Remote Login is enabled with launchctl. The log shows sudo launchctl load -w on the ssh.plist launch daemon, followed by launchd reporting Enabling service com.openssh.sshd. That launchd line carries the answer timestamp.
grep -E 'launchctl|com.openssh.sshd' work/ulogs.csv | head
fsevents records show a file created at Users/loki/Desktop/b4ckd00r. The loki home directory was later archived into Users/Deleted-Users/loki.dmg when the account was deleted, so 7z can extract it statically (no mount). b4ckd00r and .local/bin/sysetmd share the same SHA-256, so the Desktop drop is the persistent binary.
7z x Users/Deleted-Users/loki.dmg -owork/lokidmg
sha256sum work/lokidmg/Desktop/b4ckd00r work/lokidmg/.local/bin/sysetmd
The same dmg contains Library/LaunchAgents/com.appule.sysetmd.plist, and fsevents independently logs its creation under Users/loki/Library/LaunchAgents. The label com.appule is a typo-squat of com.apple, and sysetmd imitates systemd.
find work/lokidmg -path '*LaunchAgents*' -name '*.plist'
Read the plist from Q14. ProgramArguments points at the binary and passes d as its argument. The plist also sets RunAtLoad, KeepAlive on network state and a 5 second StartInterval.
cat work/lokidmg/Library/LaunchAgents/com.appule.sysetmd.plist
A property list in a LaunchAgents folder that runs a binary at login is Create or Modify System Process: Launch Agent. The ID comes from MITRE ATT&CK, not from a tool, so there is no command for this one.
Hash the binary and look it up. The SHA-256 of sysetmd is 6b379289033c4a17a0233e874003a843cd3c812403378af68ad4c16fe0d9b9c4. VirusTotal’s threat_label is trojan.demsty/bckdr, so the family is the second token of the first label.
vt 6b379289033c4a17a0233e874003a843cd3c812403378af68ad4c16fe0d9b9c4
Tip: Pick the family name, not the category (
trojan) or thebckdrbehaviour tag.
The user shut down the Mac the same evening and powered it on the next morning. Each boot has its own UUID in the unified log. Group the CSV rows by boot UUID and take the minimum timestamp of the boot that follows the attack session. That earliest log entry is the power-on.
grep -m1 'BEC8F435' work/ulogs.csv
Remote Login was enabled, so the attacker connected as soon as the machine came back. Look for sshd processes after the boot from Q11. An sshd process for the loki session logs sandbox_prepare_user_home_mount, which marks a successful login. That time is the answer. It is reasoned from the log (the question does not name a specific line), and the earlier, nearby timestamps were not accepted.
grep sshd work/ulogs.csv | grep -i 'loki\|sandbox_prepare_user_home_mount'
Deleting the account in System Settings (Users & Groups) required an admin authentication at 07:58:49, and the loki home was then archived to loki.dmg after the volume mounted at 07:58:56. The accepted time is one second before that mount. It was reasoned from that sequence of events, because no log line states the start of the deletion directly.
grep '2025-03-08T07:58:' work/ulogs.csv | grep -E 'authd|opendirectoryd|loki'
Gotcha: The mount time (07:58:56) was not accepted. The deletion starts before the archive is written.
| Phase | Action |
|---|---|
| Initial Access | User downloads a trojanised calculator app and launches it |
| Defense Evasion | Payloads fetched with curl, so Gatekeeper never inspects them |
| Privilege Escalation | expooo abuses the MacDirtyCow race to overwrite /etc/pam.d/su with pam_permit |
| Persistence | loki account created with root rights |
| Persistence | Remote Login enabled through launchctl |
| Command and Control | User shuts down; next morning the machine boots and the attacker logs in over SSH |
| Persistence | b4ckd00r placed at ~/.local/bin/sysetmd and started by a LaunchAgent |
| Response | User deletes the loki account; its home is archived to loki.dmg |
| Timestamp (UTC) | Event |
|---|---|
| 2025-03-07 21:11:56 | User starts downloading the malicious app |
| 2025-03-07 21:14:37 | Last launch of the app |
| 2025-03-07 21:28:01 | loki account created (home directory) |
| 2025-03-07 21:36:14 | SSH enabled with launchctl |
| 2025-03-08 07:51:38 | Laptop powered on |
| 2025-03-08 07:54:48 | Attacker’s SSH session as loki |
| 2025-03-08 07:58:55 | User deletes the loki account |
| Type | Value |
|---|---|
| App | Scientific-Calculator.app |
| Exploit binary | /Users/irek/Desktop/expooo |
| CVE | CVE-2022-46689 |
| Modified file | /etc/pam.d/su (pam_permit.so) |
| Account | loki |
| Backdoor | b4ckd00r, /Users/loki/.local/bin/sysetmd |
| SHA-256 | 6b379289033c4a17a0233e874003a843cd3c812403378af68ad4c16fe0d9b9c4 |
| Persistence | com.appule.sysetmd.plist in ~/Library/LaunchAgents |
| Technique | ID | Description |
|---|---|---|
| User Execution: Malicious File | T1204.002 | User ran the trojanised app |
| Subvert Trust Controls: Gatekeeper Bypass | T1553.001 | curl downloads carry no quarantine flag |
| Exploitation for Privilege Escalation | T1068 | MacDirtyCow (CVE-2022-46689) |
| Modify Authentication Process: Pluggable Authentication Modules | T1556.003 | pam_rootok replaced by pam_permit in su |
| Create Account: Local Account | T1136.001 | loki |
| Remote Services: SSH | T1021.004 | Remote Login enabled and used |
| Create or Modify System Process: Launch Agent | T1543.001 | com.appule.sysetmd.plist |
| Masquerading: Match Legitimate Name or Location | T1036.005 | com.appule and sysetmd imitate Apple and systemd |
Unsigned utilities should not run on a corporate Mac. The foothold was a user-run app. Application allow-listing or MDM-managed installs would have stopped the chain before it started.
Patch kernel bugs promptly. The host was still on 13.0 when MacDirtyCow’s PoC was public. Moving to a fixed release would have blocked the PAM overwrite.
Watch /etc/pam.d. A single changed module turned su into a no-password root shell. File integrity monitoring on PAM files catches this immediately.
Alert on new admin accounts and Remote Login being enabled. Both happen through ordinary tools (sysadminctl, launchctl) and are logged in the unified log, but nothing flagged them.
Look for lookalike LaunchAgent labels. com.appule and sysetmd read as Apple and systemd at a glance. Alert on LaunchAgents whose label is not a known vendor and whose target lives in a user-writable path.
The attacker went from a user-run app to root, SSH and a login-persistent backdoor in about a day, using a public exploit and built-in macOS tooling. The user’s own deletion of the account removed the access, but the archived home folder still held the evidence.