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 xattrs 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 xattrs, 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:

  1. create the top level directory without the quarantine flag
  2. 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

Demo video

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

  1. Unprivileged user mounting is an incredibly powerful attack tool
  2. Relying on optional filesystem features for security is a timebomb
  3. 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 :)