Persistent JXA

Leo Pitt
Posts By SpecterOps Team Members
9 min readAug 6, 2020

--

A poor man’s Powershell for macOS

Introduction

Over the last year or so, I have been diving into macOS tradecraft. Through my research, an area of particular interest is the use of JavaScript for Automation (JXA) on macOS. My goal was to fulfill an operational need to decrease dependency on Python and further my understanding of macOS tradecraft that I can hopefully build on. This post serves as an overview of the benefits of JXA for macOS tradecraft development, highlights a well-known persistence method, and a few lesser-known JXA execution methods.

A Brief History of JXA

MacOS Yosemite (10.10) introduced JXA. Its implementation allows users to control applications and the operating system using the JavaScript language. It can be invoked via osascript, a compiled script (.scpt), or a compiled Application (.app). Additionally, it can be leveraged in OSAKit from within other macho binaries without spawning the osascript binary.

Why JXA?

The preferred method for post-exploitation macOS tradecraft is scripting languages over binaries. They allow attackers to avoid leveraging unsigned code and avoid going through the code signing cert process with Apple. Compiled binaries are subject to more scrutiny due to the built-in protections on macOS (Gatekeeper, Notarization, and XProtect).

As mentioned previously, Python was the favorite for macOS post-exploitation tradecraft. Python was great for macOS tradecraft as it was easy to develop with and installed by default of macOS. However, the following note from the macOS Catalina (10.15) release indicates Apple is slowly migrating away from having Python and other scripting languages included by default.

Excerpt from macOS Catalina (10.15) Release Notes

Upon looking for future areas of tradecraft development, another option that arose was AppleScript. AppleScript has the same benefits of JXA but is quirky and, in my opinion, harder to develop with than JXA. It’s quirky because it attempts to use “natural language.” The best description I heard was it is trying to script through a series of conversations with Siri.

Based on those considerations, I turned towards JXA. JXA has similarities with early Powershell versions. It is not as robust as Powershell but allows for a convenient method to interact with macOS. JXA has a built-in Objective-C bridge that enables you to access the file system and Cocoa frameworks, such as Foundation and AppKit, which allows us to call Apple APIs without building binaries. Similar to Powershell commands, you can even obfuscate your commands using a simple JavaScript obfuscator to slow down defenders.

There are a few projects that leverage JXA for macOS tradecraft. HealthInspector and Orchard from its-a-feature leverage JXA for host situational awareness and Active Directory enumeration, respectively. Additionally, the Apfell JXA agent which facilitates Command and Control (C2).

Persistent JXA

To further develop my understanding of established macOS tradecraft, I developed the PersistentJXA project.

This section goes through a couple of usage examples as well as the related detections.

Bash Profiles for Persistence

Background

An older persistence method on macOS is the use of bash profiles. As a brief background, the ~/.bash_profile is a shell script that contains shell commands and is executed in the user’s context when a new shell opens. Attackers can abuse this by adding malicious commands to the profile to maintain persistence.

The benefit of this persistence mechanism is that if an organization allows end-users to modify bash profiles, mitigation, and even detection becomes difficult due to the number of false positives. In my testing, I have found it easier to detect the actions the profile performs than the modification of the bash profile itself.

The implementation within PersistentJXA checks to determine if osascript is running; if not, it executes our persistence action (assumed osascript) upon opening a new terminal window. Since macOS Catalina, zshell is the default shell. If the script detects the host as macOS Catalina, then ~/.zshenv is used instead.

Usage

The Apfell JXA agent can leverage all of the scripts within the PersistentJXA project. To gain access to the function, use the jsimport command.

Importing Bash Profile Persistence Script

After importing, we can use the jsimport_call command to run the imported function.

Calling Script and Setting Persistence Action

The PersistentJXA implementation of bash profile persistence creates two shell scripts in a created hidden directory. The apple.sh script provides the osascript monitoring action and the update.sh contains the user-specified persistence action.

Detection

Again the modification of the ~/.bash_profile can be tracked but is not scalable. However, other elements of the persistence method can be detected.

osquery does not capture process events by default. For my testing, I made modifications to the Palantir osquery configuration found here and here. I then forwarded the osquery logs to Splunk.

osquery captures the chmod actions on the apple.sh and update.sh scripts within the created hidden directory ./security.

Process Events to Make Scripts executable within osquery

Additionally, osquery captures those scripts’ execution events upon opening a new terminal window in which osascript is not already running.

Execution of apple.sh which contains the osascript monitoring function

You may have noticed that the separate actions have the same Process Identifier (PID). Additional details on macOS process execution and why different commands can have the same PID is discussed here.

Execution for update.sh which contains the persistence action

Lastly, osquery captures the launching of the persistence action (Apfell JXA payload). Flagging on the osascript invocation would be the most straightforward detection approach and would still occur if the persistence action was placed directly in the bash profile.

JXA command to pull and execute Apfell payload
Agent in Apfell running in the osascript PID 1200

Sublime Plugin for Persistence

Background

Another persistence method is abusing sublime plugins. This method discovered by Chris Ross (xorrior) exploits the ability of end-users creating plugins for Sublime.

The Sublime Text Editor provides the ability to create plugins that Sublime loads upon application execution. Adversaries can take advantage of this mechanism to plant malicious plugins to gain persistence. The abuse of Sublime Text Editor plugins requires a plugin at ~/Library/Application\ Support\ Text\ <2 or 3>\Packages. These plugins are typically python scripts. If a malicious plugin is placed in the Packages directory and is formatted to load our malicious dynamic library (dylib), then Sublime will execute our code upon initialization by the user under the context of Sublime plugin_host.

Usage

Importing Sublime Plugin Persistence Script

This persistence method requires uploading/creating a dylib on the target. After importing, we can use the jsimport_call command to run the imported function, specifying the targeted dylib location.

Calling Script and Setting Persistence Action

The PersistentJXA implementation creates a fake plugin called PrettyText to load the dylib.

Confirmation of Successful Sublime Text Plugin Persistence Execution

Detection

From Truetree, we can follow the process execution from Finder to Sublime Text and ultimately to the Sublime Text plugin_host, which is PID 661.

TrueTree Excerpt of Sublime Plugin Execution

Additionally, we can monitor the creation of Sublime plugins within the packages folder, but your mileage will vary depending on use in the environment. Also, network connections under the plugin host process is a nontypical occurrence.

Venator Excerpt showing network connection for PID 661
Agent in Apfell running in the plugin_host PID 661

Sublime Application Script for Persistence

Background

Another fun method is using the Sublime application script for persistence. This method, discovered by theevilbit, allows us to simply execute our JXA payload on Sublime startup without requiring a dylib on the target. Through the modification of the application script located at /Applications/Sublime\ Text.app/Contents/MacOS/sublime.py, we can have our payload executed when the target user starts Sublime.

Usage

Importing Sublime Plugin Persistence Script

Next, call the function and specify our persistence action.

Calling Script and Setting Persistence Action
Confirmation of Successful Sublime Text Plugin Persistence Execution

Detection

This method follows a similar process tree as the plugin method except osascript is a child of Sublime Text. We can see the process execution from Finder to Sublime Text to osascript, PID 2212.

TrueTree excerpt showing execution of our payload

Furthermore, osquery captures the launching of our payload.

Process Events to Execute from osquery in Splunk
Agent in Apfell running in the osascript PID 2212

If enabled, another artifact would be the file modification of the sublime.py script, which is not typically modified unless the entire Sublime package is under a change.

Automator Workflows for Persistence

Through my research, I found that Automator can execute JXA as well. Automator allows for the completion of tasks through workflow files. The actions can interact with a variety of apps and parts of macOS. Perhaps unknown to some, there is a command-line tool for Automator. It is vastly reduced compared to its GUI counterpart but allows for the execution of workflow files.

We can create a workflow in the Automator GUI that will run our JXA payload and leverage the Automator command-line tool to execute. This method is merely replacing osascript command-line usage with Automator for a command-line detection perspective. Although simple, it is a useful option to bypass any of the alerts only looking at osascript.

Within the PersistentJXA project is a workflow template, which is a modified version of the one created through the Automator GUI.

To create through the workflow through Automator, first, we select the workflow template.

Workflow option within GUI for Automator

Next, we select the Run JavaScript action and paste our JXA payload.

Inserting Apfell payload into Automator

After creation, we can take out the created document.wflow file under <FileName>.workflow/Contents/, upload to the target, and invoke using the command line Automator binary.

Agent in Apfell running in com.automator.runner.xpc PID 2569

Detection

Command-line detection for Automator binary is a simple alert as its usage is probably uncommon in environments.

osquery excerpt capturing Automator execution

Another indicator with this method is due to the long-running processes resulting in the “cog” indicator. As noted in the post regarding folder actions, longer running processes result in an icon in the top menu bar indicating that something is processing, and if you click on it, you can see the workflow name.

Spinning cog wheel highlighting long-running process

As far as the process tree, the execution of Automator shows a child of the terminal app, but the com.automator.runner.xpc PID 2569 in which our payload is executing under is not a child of Automator PID 2658 yet appears to be a separate child of the terminal app. However, killing the Automator process kills the runner, which is what our payload is running in.

TrueTree excerpt detailing the execution of the Automator workflow

Conclusion

The purpose of this post was to display the benefits of JXA while highlighting some lesser-known persistence methods on macOS. By no means are the implementations within the PersistentJXA project perfect. I’m still new to macOS tradecraft and JXA development. If you have any thoughts on improvements/ additions, I am open to pull requests 😃. Regardless, I hope this post serves as a starting point for those interested in reviewing some macOS persistence methods and provides a starting point on some indicators to aid detections of malicious behavior.

--

--