unnamed sandbox escape (CVE-2023-32364) - a macOS sandbox escape by mounting
Posted on 2023-09-26 in blog
This post is a writeup of CVE-2023-32364, a macOS application sandbox escape bug I found. It was supposed to be unveiled in my upcoming talk:
"Unexpected, Unreasonable, Unfixable: Filesystem Attacks on macOS" at OBTS v6,
but I needed to cut some bugs out. This is one of them.
macOS Sandboxing basics
If you are not a macOS researcher by trade, let me quickly explain the Sandbox
. The application sandbox is a
protection mechanism implemented on macOS/iOS to confine applications. It leverages SIP
(System Integrity Protection),
a mechanism in the kernel that's similar to AppArmor. To configure it, an application's signature can
contain information whether to confine this application or not:
$ codesign -d --entitlements - ~/myapp/build/Release/sandbox_helper.app/Contents/MacOS/sandbox_helper
Executable=/Users/gergelykalman/myapp/build/Release/sandbox_helper.app/Contents/MacOS/sandbox_helper
[Dict]
[Key] com.apple.security.app-sandbox
[Value]
[Bool] true
[Key] com.apple.security.files.user-selected.read-only
[Value]
[Bool] true
...
If the application does not have the com.apple.security.app-sandbox
field present or the app is not signed:
it inherits the parent's sandbox. There is however a major caveat:
On macOS an application can be spawned using the open
terminal command. This instructs launchd
(basically init)
to start it.
If an application is started by launchd
and it doesn't have the com.apple.security.app-sandbox
value set,
it will run unsandboxed!
Background
You are probably aware of the fact that on macOS the "quarantine" functionality is enforced with the help of
an extended attribute (xattr): com.apple.quarantine
.
What you might not know is that xattr
s are pretty flimsy:
- Some filesystems simply don't support them at all (like FAT)
- symbolic links can't have them
This is already fishy as it's usually not a great idea to have security functionality rely on flimsy features, but it gets worse.
On macOS an app (like Terminal.app) is not actually a file but a directory trying (badly) to pretend to be a file. This begs the question:
How does the quarantine flag really work?
To find this out, I created a trivial app outside the sandbox using osacompile:
$ osacompile -o output.app -e 'do shell script "whoami > /tmp/x"'
$ find output.app/
output.app/
output.app//Contents
output.app//Contents/_CodeSignature
output.app//Contents/_CodeSignature/CodeResources
output.app//Contents/MacOS
output.app//Contents/MacOS/applet
output.app//Contents/Resources
output.app//Contents/Resources/applet.rsrc
output.app//Contents/Resources/Scripts
output.app//Contents/Resources/Scripts/main.scpt
output.app//Contents/Resources/applet.icns
output.app//Contents/Info.plist
output.app//Contents/PkgInfo
$ open output.app
$ cat /tmp/x
gergelykalman
This just runs whoami
and dumps the output to /tmp/x
.
We used osacompile
to generate a valid unsigned
.app
, but this could be any valid .app
as long as it has
sandboxing off or is not signed.
This app works okay when ran from the sandbox, since no file in it has the quarantine flag. However, if I re-create it from a sandboxed app, all the files/dirs will be marked as quarantined.
Since everything else is identical, let's start there: By randomly removing the flags until stuff works I quickly realized that macOS only enforced the quarantine flag on the top level directory and the executable
To put it another way: if I managed to remove the flag from output.app
and output.app/Contents/MacOS/applet
I could
break out of the sandbox :)
The exploit
Looking for a way to remove the flag I came to the conclusion that there are few ways which I could use to do so. So, it might actually be easier to not have it placed in the first place.
If we want to prevent the flag from being placed we can:
- use filesystems that don't support it
- use symlinks
Now, I'm fairly sure that there are about a million other ways, but since I'm allowed to mount filesystems as a user on macOS (an extremely powerful weapon for an attacker), I felt confident that I can leverage this.
After a couple tries it turned out that from the app sandbox I can mount stuff, but to mount anything useful (like a disk image), I need to talk to diskarbitrationd (among others). Since these entitlements are pretty suspicious I'd rather not rely on them.
From the really basic things I could mount, one stood out above the rest: devfs
devfs
is basically a highly restricted tmpfs
in macOS, that barely allows an attacker to do anything useful.
Crucially, it does not support xattr
s, but it allows me to create directories and symbolic links. Well, as
luck would have it, this is precisely what we need.
With our newfound capabilities we now have a plan:
- create the top level directory without the quarantine flag
- create the executable without the quarantine flag
2 is easy, so let's do that first:
I can simply symlink to /bin/bash
. Since I can specify ENV vars like BASH_ENV
via Info.plist
I can make bash execute commands on my behalf. Done.
1 is trickier:
I need a directory that has the following name: X.Y
The X
doesn't matter, but the Y
does. When reporting this, Y
could have been pretty much anything as long as it is "app" or not another specially
handled extension like "plist", "png", etc... Now it seems that Y
is enforced to be "app" only, but that does not
change much.
To create such a directory, we can simply mount devfs (with noowners), give us permissions using chmod and create a directory on the volume. From this point onward we can symlink any other important files/directories to a location we wish.
We can use the terminal command open
to have it start the app using launchd
, and bash
will execute
outside the sandbox with the environment variables we specified :)
Demo and code
The full exploit code is on my github: https://github.com/gergelykalman/CVE-2023-32364-macos-app-sandbox-escape
The fix
Apple fixed this bug by preventing sandboxed applications from creating directories on devfs.
Apple apparently now also forces the app to have the .app
extension, which is a minor added hurdle.
The main fix seems like a band-aid: If an attacker can create a directory without the quarantine flag, they'll be able to escape the sandbox still.
I have tested this on Sonoma (14.0-23A344), that came out today.
Root causes
- Unprivileged user mounting is an incredibly powerful attack tool
- Relying on optional filesystem features for security is a timebomb
- Mixing of unrelated identities (file / directory) in the context of apps makes defending difficult
Conclusion
Since the fix is band-aid and the core issues would be extremely costly to fix, my prediction is that we will see many more exploits like this.
A clear path forward for example seems to be race conditions: there's nothing that prevents an attacker from altering an .app structure while it's being inspected for the flags...
Timeline
- 2022.12.15: Report sent to Apple
- 2023.01.04: Requested clarification
- 2023.01.18: Request for update
- 2023.01.18: Apple: Still working on it
- ... lots of ping/pong later
- 2023.06.07: Bug is fixed in the current beta release
- 2023.07.24: Bounty awarded
While the timeline could be shorter, my contact at Apple responded within days to my pings. Usually with the message that they're still working on it, but I'd much rather have this than ghosting.
Thanks Apple :)