Seguridad

Auditar permisos de tus scripts y binarios: find, SUID y sticky bit

La mayoría de las auditorías de permisos que hago terminan en la misma pregunta: ¿este binario con SUID estaba ahí desde la instalación o alguien lo dejó? Y la respuesta nunca se obtiene leyendo el bit, se obtiene con inventario: una lista completa de qué archivos tienen bits especiales, quién es su dueño, de qué paquete salieron y qué otro archivo con el mismo nombre aparece en el disco. Sin ese inventario, cualquier revisión de permisos es una opinión.

El problema es que find -perm tiene tres sintaxis que parecen intercambiables y no lo son. La forma exacta, sin guion ni barra, exige que todos los bits coincidan uno a uno, y en la práctica no encuentra nada. La forma con guion matchea “todos los bits indicados”, la de barra matchea “cualquiera de ellos”. Es un detalle de sintaxis, pero es el que separa una auditoría que dice “aquí están tus 21 binarios SUID” de una que devuelve una lista vacía y te deja pensando que estás limpio. Esta guía es el procedimiento completo: inventariar, verificar el origen, revisar las capacidades y endurecer.

TL;DR — Lo que vas a tener al final

  • Tiempo de setup: 10-15 minutos para el primer inventario completo con find y stat; menos de 30 segundos por host adicional cuando el comando ya vive en tu runbook.
  • Requisitos: GNU findutils 4.5.12 o superior (para que exista -perm /MODE), root para leer todo el árbol y consultar el gestor de paquetes, y las herramientas stat, findmnt y getcap disponibles.
  • Resultado: un inventario de binarios SUID y SGID con su modo octal, propietario y paquete de origen, más los directorios con bit pegajoso que sí tienen efecto real.
  • Troubleshooting: si find -perm -4000 no devuelve nada, casi siempre es que el árbol está montado con nosuid o que el binario vive en otro disco: revisa findmnt antes de asumir que el sistema está limpio.

Entiende los tres bits especiales que estás auditando

Linux tiene tres bits de permiso especiales, que se escriben en el primer dígito del modo octal: el bit 4 (SUID), el bit 2 (SGID) y el bit 1 (sticky o bit pegajoso). No son un extra decorativo: cada uno cambia quién puede hacer qué con un archivo, y por eso son el objetivo habitual de un atacante que ya logró escribir en el disco.

Bit Nombre Qué hace en un archivo ejecutable Qué hace en un directorio
4 SUID (set-user-ID) El proceso se ejecuta con el UID del dueño del archivo, no con el del que lo invoca No tiene efecto
2 SGID (set-group-ID) El proceso se ejecuta con el GID del dueño del archivo Los archivos nuevos heredan el grupo del directorio
1 Sticky (bit pegajoso) Sin efecto en Linux actual: es una costumbre de sistemas antiguos Impide que un usuario sin privilegios borre o renombre archivos que no son suyos

La fila del sticky merece una aclaración, porque es el bit que más se malinterpreta. En Linux actual el sticky bit solo restringe el borrado y el renombrado dentro de un directorio. No acelera la carga de binarios: eso era un comportamiento de la familia BSD y de sistemas Unix viejos, donde el texto del programa se mantenía en swap para cargarlo más rápido. Linux lo ignora en archivos regulares. Si estás leyendo documentación que promete “sticky bit acelera tus binarios”, estás leyendo documentación de otra época.

El SUID es el que concentra el riesgo. Un binario SUID root que el usuario puede ejecutar corre con privilegios de root, y por lo tanto cualquier fallo de escalación en ese binario es un escalamiento de privilegios local. El sudo de tu sistema es SUID root por diseño: sin ese bit no podría funcionar. La diferencia entre el sudo legítimo y uno pisado por un atacante es exactamente la que el inventario te deja ver.

Lee el modo octal antes de auditar nada

El primer paso es saber leer lo que vas a encontrar. El modo octal de un archivo se imprime como cuatro dígitos: el primero son los bits especiales, los otros tres son los permisos de usuario, grupo y otros. stat te da el número crudo y el nombre legible en una sola línea.

stat -c '%a %U %G %n' /usr/bin/sudo /usr/bin/passwd /usr/bin/mount /usr/bin/wall

La salida en una Debian 13 recién instalada es esta:

4755 root root /usr/bin/sudo
4755 root root /usr/bin/passwd
4755 root root /usr/bin/mount
755 root root /usr/bin/wall

El 4 inicial de los tres primeros significa SUID. sudo, passwd y mount lo necesitan para funcionar: mount no es una operación que un usuario sin privilegios pueda hacer, así que el bit le permite ejecutarse como root. wall no lo necesita y por eso no lo tiene. Ese es el criterio para leer un hallazgo: la pregunta no es “tiene SUID”, es “está para lo que dice tener SUID”.

Un detalle que confunde cuando recién empiezas: el bit sticky en un archivo regular no desaparece nunca por si solo. El modo 1777 en un archivo es un residuo de cuando ese archivo era un directorio, y Linux lo conserva sin que signifique nada. Por eso el inventario debe filtrar por tipo de archivo, no solo por modo.

Encuentra los binarios SUID con la sintaxis correcta de find

Este es el punto donde la mayoría de las auditorías fallan, y no por el SUID sino por el predicado. find -perm acepta tres formas y cada una pregunta algo distinto:

Forma Pregunta que hace Resultado real en /usr/bin
-perm -4000 Están TODOS los bits indicados activos 12 archivos
-perm /4000 Esta CUALQUIERA de los bits indicados activo 12 archivos
-perm 4000 El modo coincide EXACTAMENTE, bit por bit 0 archivos

La tercera forma es la trampa. Sin el guion ni la barra, find exige que el modo completo del archivo sea exactamente 4000: eso significa SUID activado y los nueve permisos de lectura, escritura y ejecución apagados. ningún binario real del sistema se ve así, así que la consulta devuelve una lista vacia y tu auditoría concluye, falsamente, que no hay nada que revisar.

La diferencia entre el guion y la barra se ve cuando pides más de un bit a la vez. Este test con tres archivos, modos 4755, 2755 y 6755, lo deja claro:

mkdir -p /tmp/audit-demo && cd /tmp/audit-demo
touch m1 m2 m3
chmod 4755 m1
chmod 2755 m2
chmod 6755 m3
find . -maxdepth 1 -type f -perm -6000
find . -maxdepth 1 -type f -perm /6000
find . -maxdepth 1 -type f -perm 6000

La primera busqueda, con guion, devuelve solo m3: es el único que tiene SUID y SGID activos al mismo tiempo. La segunda, con barra, devuelve los tres, porque en cualquiera de los tres hay al menos uno de los dos bits. La tercera no devuelve nada, otra vez por la exigencia de coincidencia exacta. Para un inventario de SUID puro, -perm -4000 es la forma correcta; para “cualquier binario con un bit especial de ejecución privilegiada”, -perm /6000.

Y un aviso sobre la sintaxis que vas a encontrar en tutoriales viejos: -perm +4000 ya no existe. Era la forma antigua de “tiene este bit”, se deprecó en 2005 por problemas de compatibilidad y se elimino por completo en findutils 4.5.12, en 2013. Tu find actual simplemente no la reconoce.

Ejecuta el inventario completo sobre el sistema real

Un inventario de SUID útil tiene que cubrir el árbol de archivos completo, deduplicar rutas que apuntan al mismo inodo y mostrar el modo, el dueño y la ruta en una sola línea. El filtro -xdev evita meterse en otros sistemas de archivos montados, y -printf con %n es la forma de obtener el nombre real de cada ruta.

find / -xdev -type f -perm -4000 -printf '%m %u %g %p\n' 2>/dev/null | sort -k4

En una Debian 13 de escritorio ese comando devuelve 21 archivos. Los esperados son los de siempre:

4755 root root /usr/bin/su
4755 root root /usr/bin/gpasswd
4755 root root /usr/bin/sudo
4755 root root /usr/bin/fusermount3
4755 root root /usr/bin/chfn
4755 root root /usr/bin/chsh
4755 root root /usr/bin/ntfs-3g
4755 root root /usr/bin/pkexec
4755 root root /usr/bin/umount
4755 root root /usr/bin/mount
4755 root root /usr/bin/passwd
4755 root root /usr/bin/newgrp
4755 root root /usr/lib/openssh/ssh-keysign
4754 root messagebus /usr/lib/dbus-1.0/dbus-daemon-launch-helper
6755 root root /usr/lib/xorg/Xorg.wrap
4755 root root /usr/lib/polkit-1/polkit-agent-helper-1
4755 root root /usr/sbin/userhelper
4755 root root /usr/sbin/mount.nfs
4755 root root /usr/sbin/mount.cifs
4755 root root /usr/sbin/exim4

Cuatro detalles de esa lista merecen atención. El 4754 de dbus-daemon-launch-helper tiene el bit SUID pero no permiso de ejecución para el grupo, lo cual es intencional. El 6755 de Xorg.wrap combina SUID y SGID porque necesita ejecutar como root y mantener el grupo de la sesión grafica. ssh-keysign y fusermount3 son SUID porque dan acceso a cosas que de otro modo estarían restringidas a root. Y exim4 aparece porque el gestor de correo necesita esa elevacion, no porque sea un problema.

El punto no es que ninguno sea un problema: es que ahora sabes que ninguno lo es, porque lo comparaste contra una lista conocida. Un SUID con nombre que no reconoces es la señal de alarma real, y sin inventario jamas lo vas a notar.

Un falso positivo que verás en servidores: en distribuciones con /lib y /usr/lib fusionados en un solo directorio, find reporta el mismo binario por dos rutas. En el servidor que aloja este blog el inventario da dos líneas, /lib/polkit-1/polkit-agent-helper-1 y /usr/lib/polkit-1/polkit-agent-helper-1, y el inodo es el mismo. No son dos binarios: es el mismo counted dos veces. Para confirmarlo, compara el número de inodo.

stat -c '%i %n' /lib/polkit-1/polkit-agent-helper-1 /usr/lib/polkit-1/polkit-agent-helper-1

Las dos líneas devuelven el mismo número de inodo, 122065449 en ese servidor. Es una sola ruta contada desde dos prefijos.

Audita los directorios con SGID y bit pegajoso

Los bits especiales se comportan de forma distinta en directorios, y ahí es donde el sticky bit sí tiene un efecto real y verificable. El SGID en un directorio hace que todo lo que se cree dentro herede el grupo del directorio en vez del grupo de quien lo crea, que es como se montan los volúmenes de datos compartidos en un equipo con varios usuarios. El sticky bit impide que un usuario borre o renombre archivos que pertenecen a otro.

find / -xdev -type d -perm -2000 -printf '%m %u %g %p\n' 2>/dev/null | sort -k4

En mi Debian 13 el resultado son 7 directorios y todos son esperados:

2750 root dip /etc/chatscripts
2750 root dip /etc/ppp/peers
3775 root lpadmin /usr/share/ppd/custom
2750 Debian-exim adm /var/log/exim4
2755 root systemd-journal /var/log/journal
2755 root systemd-journal /var/log/journal/b2a814c9e0404de3bf36d47abb55fb30
2775 root mail /var/mail

Los dos del grupo systemd-journal son el mecanismo que evita que los logs del sistema puedan ser modificados por un usuario sin privilegios, y los de /var/mail y /var/log/exim4 garantizan que el buzon y el log de correo pertenezcan al grupo correcto. El 2775 de /var/mail es el caso interesante: el bit sticky se combina con el SGID para que el buzon pertenezca al grupo mail pero el usuario de correo no pueda borrar los mensajes de otro. Si aparece un directorio SGID con nombre de usuario que no reconoces, o en una ruta de datos, revisalo.

El sticky bit se audita igual, y aquí el resultado es una lista corta y con significado claro: son los pocos directorios donde el mundo puede escribir y el bit pegajoso protege lo que hay dentro.

find / -xdev -type d -perm -1000 -printf '%m %u %g %p\n' 2>/dev/null | sort -k4
1777 root root /tmp
3775 root lpadmin /usr/share/ppd/custom
1730 root crontab /var/spool/cron/crontabs
1777 root root /var/tmp

/tmp y /var/tmp con modo 1777 son la configuración correcta y esperada: el primer dígito es el sticky, y sin el podrías borrar archivos de otros usuarios. /var/spool/cron/crontabs con 1730 es igual de correcto: el directorio pertenece al grupo crontab, los usuarios pueden escribir en el, y el bit pegajoso impide que borres el crontab de otro usuario. Si /tmp aparece como 777 sin el 1 inicial, no es un problema de permisos sino de seguridad: cualquiera puede borrar los archivos temporales de cualquiera.

Verifica el origen de cada binario SUID

El inventario te dice que hay 21 binarios con SUID. Todavía no te dice si son los originales. Ese es el paso que separa una lista de un diagnóstico, y la herramienta depende del gestor de paquetes de tu distribución.

En Debian y Ubuntu, dpkg --search devuelve el paquete al que pertenece una ruta concreta:

dpkg --search /usr/bin/sudo
sudo: /usr/bin/sudo

Y dpkg --verify comprueba que los archivos instalados coincidan con los datos del paquete. Ojo con un detalle que muerde: dpkg --verify espera el nombre del paquete, no la ruta del archivo. Si le pasas una ruta, falla con “illegal package name in specifier”:

dpkg --verify sudo
dpkg --audit

La salida de dpkg --verify sudo lista solo los archivos de configuración modificados, que es normal en cualquier sistema vivo, y dpkg --audit no devuelve nada cuando la base de datos de paquetes está consistente. Ninguno de los dos comandos es un escáner de SUID: dpkg --verify valida los archivos que el paquete declaró, y nada más. La combinación que sí sirve es: find te da la lista de SUID, dpkg --search te dice de que paquete salió cada uno, y dpkg --verify te dice si ese paquete está intacto.

En Fedora, RHEL y openSUSE el equivalente es rpm: rpm -qf responde a que paquete pertenece un archivo y rpm -V verifica los archivos instalados contra los metadatos guardados en la base de datos de rpm. según la documentación de rpm, la verificación compara tamaño, digest, permisos, tipo, propietario y grupo de cada archivo, y por defecto solo muestra los que tienen discrepancias.

rpm -qf /usr/bin/sudo
rpm -V sudo

Revisa las capacidades de Linux antes de preocuparte por SUID

Linux tiene un mecanismo más moderno que SUID para conceder privilegios puntuales: las capacidades del sistema de archivos, que viajan en el binario y no en los permisos del inodo. getcap las lista, y la forma de leerlas en todo el árbol es con -r.

getcap -r /usr 2>/dev/null | head -5
/usr/bin/mtr-packet cap_net_raw=ep
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper cap_net_bind_service,cap_net_admin,cap_sys_nice=ep
/usr/lib/nagios/plugins/check_icmp cap_net_raw=ep
/usr/lib/nagios/plugins/check_dhcp cap_net_bind_service,cap_net_raw=ep
/usr/sbin/etherwake cap_net_raw=ep

El significado de cap_net_raw=ep es e (effective, la capacidad está activa) y p (permitted, está en el conjunto permitido pero se puede descartar). La capacidad cap_net_raw permite abrir sockets raw, que es lo que necesita ping para craftar paquetes ICMP sin privileges. Esta es la razón por la que mtr y check_icmp pueden correr como usuarios sin privilegios en vez de como SUID root: el privilegio es más pequeño y más específico.

La combinación de ambos mecanismos en un solo inventario es lo que da una visión completa:

find / -xdev -type f -perm -4000 -printf '%m %u %g %p\n' 2>/dev/null | sort -k4
getcap -r /usr /opt /bin /sbin 2>/dev/null | sort

Un binario sin SUID y con capacidades es software moderno bien configurado. Un binario con SUID y sin capacidades es software clasico que todavía depende del bit. Y un binario con SUID que también debería tener solo capacidades es un candidato a revisar.

Endurece el sistema con nosuid, noexec y fs.suid_dumpable

Hay una segunda barrera que no depende de que tu inventario este completo: los bits de montaje. Un sistema de archivos montado con nosuid ignora por completo el bit SUID y el SGID, sin importar que permisos tenga el archivo. Con noexec, directamente no se puede ejecutar nada de ese árbol. El servidor que aloja este blog ya trae nosuid en /tmp, y por eso un binario SUID depositado allí no sería escalable.

findmnt -no TARGET,OPTIONS /tmp
findmnt -no TARGET,OPTIONS | grep -E 'nosuid|noexec'
/tmp rw,nosuid,nodev,size=8134252k,nr_inodes=1048576,inode64

La segunda orden te da el panorama de todos los puntos de montaje con esas protecciones. Conviene revisarlo porque el caso interesante es el contrario: un árbol de datos o de cache montado sin nosuid donde un atacante sí podría escribir y ejecutar.

El otro control es fs.suid_dumpable, que decide si los procesos con privilegios degradados pueden generar un core dump. Con el valor por defecto, 0, un proceso que cambió de nivel de privilegio no se vuelca. Ese es el valor que encuentras en mi máquina y en el servidor:

sysctl fs.suid_dumpable
cat /proc/sys/fs/suid_dumpable

La documentación del kernel describe tres modos. En 0, el comportamiento tradicional, cualquier proceso que haya cambiado de nivel de privilegio o que sea solo ejecutable no se vuelca. En 1, el modo de depuracion, todos los procesos vuelcan core y el dump pertenece al usuario actual, sin aplicar ninguna protección: la propia documentación lo marca como inseguro, porque permite a un usuario regular examinar la memoria de procesos privilegiados. En 2, suidsafe, se vuelcan igual, pero solo si core_pattern apunta a un manejador o a una ruta completa.

El punto de esta configuración es que un core dump de un proceso SUID contiene las claves de memoria del proceso, y eso es un volcado de credenciales esperando a que alguien lo lea. Si ves fs.suid_dumpable = 1 en un servidor de produccion, devuélvelo a 0.

Quita el bit que no debería estar

Si el inventario encontró un SUID que no debería estar, la corrección es una sola llamada y es reversible. chmod con la forma simbolica quita el bit exacto sin tocar el resto de permisos.

chmod u-s /ruta/al/binario
chmod g-s /ruta/al/directorio
chmod o-t /ruta/al/directorio

Para el sticky bit, chmod o-t lo retira de un directorio. Y hay un comportamiento de chmod que conviene saber: un modo de tres dígitos limpia los bits especiales. Si aplicas chmod 755 a un archivo que era 4755, queda en 0755, con SUID eliminado. Lo mismo con chmod 777 sobre un directorio que era 1777: queda en 0777 y pierde el sticky. Eso es práctico para limpiar, pero es una trampa si estabas contando con que chmod 755 conservaría el bit.

después de cambiar permisos, confirma que el cambio quedó aplicado y que el archivo sigue siendo lo que era:

stat -c '%a %U %G %n' /ruta/al/binario
dpkg --verify NOMBRE_PAQUETE

El 755 del primer comando es la señal de que el bit SUID ya no está. Si te queda 4755, el cambio no se aplicó: revisa que el archivo no esté en un montaje con nosuid o que el directorio padre no te impida cambiarlo.

Preguntas frecuentes

¿Por qué find -perm 4000 no me devuelve nada si el sistema tiene binarios SUID?

Porque sin guion ni barra find exige coincidencia exacta del modo completo: el archivo debe tener SUID activo y los nueve permisos de lectura, escritura y ejecución apagados. Ningún binario real tiene ese modo. La forma correcta de buscar SUID es -perm -4000 (todos los bits indicados activos) o -perm /4000 (cualquiera de ellos).

¿Cuál es la diferencia entre find -perm -4000 y find -perm /4000?

Solo se nota cuando pides más de un bit a la vez. Con un solo bit, -4000 y /4000 devuelven lo mismo. Con dos bits, por ejemplo -6000 exige que SUID y SGID estén ambos activos, mientras que /6000 acepta cualquiera de los dos. En un inventario, usa -4000 para SUID puro y /6000 para “cualquier privilegio de ejecución”.

¿El sticky bit acelera la carga de los binarios?

No en Linux actual. Ese comportamiento pertenecía a sistemas Unix antiguos y a la familia BSD, donde el texto del programa se conservaba en swap para cargarlo más rápido. En Linux el bit sticky solo tiene efecto en directorios, donde impide que un usuario sin privilegios borre o renombre archivos ajenos. En un archivo regular el bit no hace nada y puede quedar ahí como residuo.

¿dpkg --verify sirve para encontrar binarios SUID sospechosos?

No, y esa confusión es habitual. dpkg --verify valida los archivos que un paquete concreto declara, no rastrea el sistema en busca de binarios privilegiados. Además exige el nombre del paquete, no la ruta del archivo. El flujo correcto es: find arma la lista de SUID, dpkg --search identifica el paquete de cada uno y dpkg --verify confirma que ese paquete está intacto.

¿Un SUID root desconocido es una señal de que estoy comprometido?

Es una señal de que debes investigarlo, no un veredicto. En una distribución normal hay entre 15 y 25 binarios SUID, casi todos conocidos: sudo, passwd, mount, umount, pkexec y los auxiliares de polkit. Un SUID cuyo nombre no reconoces, o uno que está en un directorio de usuario o temporal, sí es anómalo. Verifica su paquete de origen, compara su hash con el del paquete oficial y revisa cuándo se modificó el archivo antes de escalar el hallazgo.

Lee también

Lo que queda en tu runbook

El inventario de permisos es barato y se puede automatizar: son tres comandos y un minuto de CPU. Lo que cuesta es la interpretacion, y por eso el orden importa. Primero inventarias con la sintaxis correcta, porque una consulta mal escrita devuelve una lista vacia y produce una conclusión falsa. después verificas el origen de cada binario con el gestor de paquetes, y solo entonces puedes decir si un SUID es legítimo o no.

Y recuerda lo que ese número significa. En una máquina con 21 binarios SUID, ninguno es un problema; lo problemático es no saber que son 21 y no 22. La diferencia entre un sistema auditado y uno ignorado no está en los permisos: está en que alguien sabe exactamente cual es la lista y puede detectar cuando cambia.

Alfonso Moreno | Sysadmin Senior

Soy Alfonso, sysadmin senior con RHCE y más de 10 años administrando sistemas Linux: infraestructura, redes, virtualización y self-hosting.