1Por qué importa
Cuando alguien consigue ejecutar como root, su siguiente problema es que ese acceso es
frágil: un reinicio, una sesión cerrada, un proceso que muere, y lo pierde. La solución que
busca es persistencia: un sitio desde el que su código vuelva a arrancar solo. Las
tareas programadas son el sitio ideal, porque su razón de existir es precisamente ejecutarse
solas y de forma repetida. Una línea en /etc/cron.d o una unit de
systemd le devuelve el acceso en cada ciclo, sin que tenga que volver a entrar.
Pero el problema empieza mucho antes del atacante, en el despliegue. Una tarea de root que ejecuta un script escribible por un usuario cualquiera es una escalada de privilegios esperando a ocurrir: ese usuario reescribe el script y, en el siguiente ciclo, root ejecuta lo que él quiera. Lo mismo con el directorio que contiene el script, o con el propio archivo de la tarea. La lección de diseño es que todo lo que una tarea privilegiada ejecuta forma parte de su frontera de confianza, y esa frontera se define con permisos, en el momento de configurar, no cuando ya hay un incidente.
No estás memorizando la sintaxis de cron. Estás aprendiendo a mirar cualquier
tarea programada y responder dos preguntas: ¿quién la ejecuta, y quién puede modificar lo
que ejecuta? Cuando esas dos identidades no coinciden, tienes una vía de escalada.
2Conceptos
- cron: demonio que ejecuta comandos según un calendario.
- crontab: la tabla de tareas; hay una por usuario y varias del sistema.
- /etc/cron.d: fragmentos de crontab del sistema, cada uno con su usuario de ejecución.
- systemd unit: archivo
.serviceque describe cómo arrancar un proceso. - systemd timer: archivo
.timerque dispara una unit según un calendario (elcronde systemd). - ExecStart: la línea de la unit que dice qué binario o script se ejecuta.
- persistencia: mecanismo por el que el código de un atacante vuelve a ejecutarse solo.
- hardening de unit: directivas que confinan un servicio (
ProtectSystem,NoNewPrivileges…).
3Explicación profunda
cron ejecuta comandos a horas fijas. Cada usuario tiene su crontab
(crontab -l), y el sistema tiene los suyos: /etc/crontab y los
fragmentos de /etc/cron.d. La diferencia clave de las tareas del sistema es que
declaran con qué usuario se ejecutan. Una línea en /etc/cron.d/backup
como 0 * * * * root /opt/tareas/backup.sh dice: cada hora, como root, ejecuta ese
script. Todo el peso de seguridad recae en ese script y en dónde vive: si
/opt/tareas/backup.sh es escribible por un usuario cualquiera, ese usuario
controla lo que root ejecutará. No importa que el archivo de cron.d sea perfecto;
lo que se ejecuta es el script.
Los permisos que hay que mirar son tres, y la gente suele mirar solo el primero. Uno, el
script: no debe ser escribible por otros (nada de 666 ni 777).
Dos, el directorio que lo contiene: si el directorio es escribible, da igual el permiso
del archivo, porque un usuario puede borrarlo y poner otro en su lugar —borrar es un permiso
del directorio, como viste en la lección de permisos—. Tres, el archivo de la tarea
(/etc/cron.d/… o la unit): si es escribible, se cambia el comando entero. Los tres
tienen que ser propiedad de root y cerrados a la escritura ajena.
systemd reemplaza a cron en la mayoría de sistemas modernos y traslada
el mismo problema a otros archivos. Una unit .service tiene una línea
ExecStart= con el binario o script a ejecutar, y un .timer la dispara
según OnCalendar=. Las mismas preguntas: ¿es la unit escribible por un usuario sin
privilegios? ¿lo es el binario al que apunta ExecStart? ¿lo es su directorio? Un
ExecStart=/opt/app/run.sh donde run.sh es escribible por otros es
exactamente el mismo fallo que en cron. Y como systemd arranca las
units al inicio y las reinicia, un servicio comprometido es una persistencia especialmente
cómoda para un atacante.
La otra cara de systemd es que trae herramientas de confinamiento que
cron no tenía. Una unit puede declarar User= para no correr como root,
NoNewPrivileges=yes para que el proceso no pueda ganar privilegios,
ProtectSystem=strict para montar el sistema de solo lectura,
ProtectHome=yes, PrivateTmp=yes y muchas más. Bien usadas, convierten
un servicio en algo que, aunque se comprometa, casi no puede tocar nada. Esa es la forma
moderna de aplicar mínimo privilegio a una tarea: no solo permisos de archivo correctos, sino
un servicio confinado por el propio systemd.
4Ejemplo sencillo
# Una nota pegada en la nevera que alguien cumple cada mañana
En la nevera hay una nota (la tarea): "cada manana, el conserje
(root) hace lo que diga este papel".
Si el papel esta en un cajon cerrado que solo el conserje abre
(script de root, no escribible por otros), bien: hace lo previsto.
Si el papel esta pegado en un tablon publico (script world-writable),
cualquiera tacha la instruccion y escribe otra. Manana el conserje,
con sus llaves, cumplira la instruccion NUEVA sin dudar.
Y como lo hace CADA manana, quien cambio el papel no necesita volver:
la nota se ejecuta sola, para siempre. Eso es persistencia.
5Ejemplo técnico
Los sitios donde viven las tareas, y qué mirar en cada uno:
| Ubicación | Qué es | Qué revisar |
|---|---|---|
/etc/cron.d/* | Tareas del sistema con usuario | Dueño root, no escribible; y el script que llaman, igual. |
/etc/systemd/system/*.service | Units de servicio | ExecStart a un binario no escribible; la unit tampoco. |
*.timer | Disparador por calendario | Qué unit dispara y con qué usuario corre esa unit. |
El inventario que hay que saber correr. Lista las tareas y, sobre todo, comprueba los permisos de lo que ejecutan:
# tareas de cron del sistema y sus scripts cat /etc/crontab /etc/cron.d/* 2>/dev/null # timers activos de systemd y la unit que disparan systemctl list-timers --all # lo que de verdad importa: ¿algo que ejecuta root es escribible por otros? find /etc/cron.d /opt /usr/local -perm -0002 -type f 2>/dev/null
6Diagrama
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
U[becario · sin privilegios] -->|reescribe| S[backup.sh · 0777]
C[cron de root · cada ciclo] -->|lee| S
C -->|ejecuta como root| P{{persistencia}}
7Código
Asegurar una tarea es dejar sus tres piezas en manos de root y cerradas a la escritura ajena,
y confinar el servicio si es systemd. Primero, los permisos:
# el script, el directorio y el archivo de la tarea: root y sin escritura ajena
chown root:root /opt/tareas /opt/tareas/backup.sh /etc/cron.d/backup
chmod 0755 /opt/tareas
chmod 0755 /opt/tareas/backup.sh
chmod 0644 /etc/cron.d/backup
Y, en systemd, una unit confinada. Estas directivas hacen que aunque el servicio
se comprometa, apenas pueda tocar el sistema:
# /etc/systemd/system/backup.service [Service] ExecStart=/opt/tareas/backup.sh User=servicio NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes ReadWritePaths=/var/backups
NoNewPrivileges=yes impide que el proceso gane privilegios (por SUID, por
ejemplo); ProtectSystem=strict monta casi todo de solo lectura y solo deja
escribir en ReadWritePaths. Es mínimo privilegio aplicado a una tarea: corre como
un usuario acotado y solo puede tocar lo que necesita.
8Qué puede salir mal
Script de tarea escribible por otros. Un chmod 777 "para que el equipo
pueda editar el backup" sobre un script que ejecuta root convierte a cualquiera en root en
el siguiente ciclo.
Directorio de la tarea abierto. Aunque el script sea 644, si su
directorio es escribible, se borra y se sustituye. El permiso del archivo no protege.
La tarea o la unit escribibles. Un /etc/cron.d/backup o un
.service que un usuario puede editar deja cambiar el comando entero, no solo su
contenido.
Servicio de root sin confinar. Correr como root "porque es más fácil" y sin ninguna directiva de hardening convierte cualquier fallo del servicio en control total de la máquina.
9Cómo lo abusaría un atacante
Alguien con ejecución como un usuario cualquiera busca dos cosas a la vez: subir de
privilegios y quedarse. Las tareas programadas dan las dos. Enumera crontab -l,
/etc/crontab, /etc/cron.d y systemctl list-timers, y
para cada tarea que corre como root revisa los permisos de lo que ejecuta. En cuanto
encuentra un script o un directorio escribible, ya no necesita un exploit: reescribe el
script y espera al siguiente ciclo. Root ejecutará su código.
Y si no encuentra un fallo ya presente, lo crea: con privilegios suficientes, deja
su propia línea en /etc/cron.d o su propia unit con un .timer. A
partir de ahí su acceso sobrevive a reinicios y a que le maten el proceso, porque el sistema
lo vuelve a lanzar solo. Es persistencia limpia: parece una tarea más.
Para ti como defensor, la lección es doble. Primero, audita los permisos de todo lo que
ejecutan las tareas privilegiadas, no solo el archivo de la tarea. Segundo, vigila la
aparición de tareas nuevas: un cron.d o una unit que nadie recuerda
haber creado es una de las señales más claras de persistencia.
10Cómo defenderlo
- Todo lo que ejecuta una tarea privilegiada —script, directorio y archivo de la tarea— debe ser propiedad de root y no escribible por otros.
- Comprueba el directorio, no solo el script: un directorio abierto anula el permiso del archivo.
- Confina los servicios con
User=,NoNewPrivileges=yes,ProtectSystem=strictyProtectHome=yes. - Da a cada servicio el usuario mínimo que necesita; nunca root por comodidad.
- Inventaría las tareas y alerta sobre las nuevas: una tarea que aparece sola es señal de persistencia.
- Convierte la auditoría de permisos de tareas en un check repetible tras cada despliegue.
11Laboratorio
La caja monta una tarea de root que ejecuta /opt/tareas/backup.sh cada
15 segundos y escribe en /var/log/tarea.log. El script y su directorio
están deliberadamente en 0777. Levanta el escenario y observa la tarea:
cd cursos/seguridad/labs/lab-linux-persist
docker compose up -d
docker compose exec caja su - becario
Recorre el ciclo del curso sobre una tarea mal permisada real:
Ahora defiende: desde otra terminal, como root
(docker compose exec caja bash), devuelve la propiedad y cierra la
escritura con chown -R root:root /opt/tareas,
chmod 0755 /opt/tareas y chmod 0755 /opt/tareas/backup.sh.
Como becario, intenta volver a modificar el script: debe fallar. Cuando termines:
docker compose down -v
12Ejercicios
Lee tres tareas y di el riesgo
Para cada caso di si hay riesgo y por qué: (1) /etc/cron.d/backup ejecuta como
root /opt/tareas/backup.sh, que es 644 root:root; (2) el mismo,
pero el script es 777; (3) el script es 644 pero
/opt/tareas es 777.
(1) Sin riesgo: root ejecuta un script que solo root puede modificar. (2) Riesgo alto:
cualquiera reescribe el script y root lo ejecuta; escalada y persistencia. (3) Riesgo
alto igual: aunque el script sea 644, en un directorio 777
cualquiera lo borra y pone otro; borrar es del directorio.
Clave: solo (1) es seguro. En (2) el problema es el script; en (3), el directorio. Hay que cerrar los dos, no solo el archivo.
Confina un servicio de systemd
Tienes una unit backup.service que corre como root y solo tiene
ExecStart=/opt/tareas/backup.sh. Reescríbela para que corra con un usuario
propio y quede confinada: que no pueda ganar privilegios, que vea el sistema de solo
lectura y que solo pueda escribir donde el backup lo necesita.
User=, NoNewPrivileges=, ProtectSystem= y ReadWritePaths=.ProtectSystem=strict lo hace todo de solo lectura salvo lo que declares.[Service] ExecStart=/opt/tareas/backup.sh User=servicio NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes ReadWritePaths=/var/backups
Clave: usuario propio, sin nuevos privilegios, sistema de solo lectura y un único directorio escribible. El servicio hace su trabajo sin poder tocar nada más.
Otras vías de persistencia
cron y systemd no son los únicos sitios desde los que algo se
ejecuta solo. Investiga y nombra al menos tres mecanismos más de arranque automático en
Linux que un atacante podría usar para persistir, y di cómo se auditaría cada uno.
~/.bashrc, /etc/profile.d, units de usuario y @reboot en cron.
Algunos ejemplos: (1) archivos de perfil de shell (~/.bashrc,
~/.profile, /etc/profile.d/*), que se ejecutan al abrir una
sesión; se auditan revisando su contenido y sus permisos. (2) Units de usuario de
systemd (~/.config/systemd/user/) con lingering
activado, que arrancan sin login; systemctl --user list-units. (3) La
entrada @reboot de cron, que ejecuta al arrancar. También .desktop
de autostart en escritorios. La idea común: cualquier lugar donde el sistema ejecute
algo solo es un candidato a persistencia, y se audita por contenido y permisos.
Clave: perfiles de shell, units de usuario con lingering y @reboot son tres vías. Todas se auditan igual: ¿qué ejecutan y quién puede modificarlo?
Red Team · conviértela en persistencia
Dentro del lab-linux-persist, y solo dentro de él: como becario,
aprovecha el script world-writable para que la tarea de root deje una marca a tu favor en
cada ciclo. Documenta el comando exacto, muestra que la marca la creó root y explica por
qué esto es persistencia y no una acción de una sola vez.
/opt/tareas/backup.sh.echo 'id -un > /tmp/quien_ejecuta' >> /opt/tareas/backup.sh # espera <=15s ls -l /tmp/quien_ejecuta # dueño: root cat /tmp/quien_ejecuta # root
Clave: la línea corre como root y se repite en cada ciclo; el archivo es de root. Es persistencia porque no necesitas volver a actuar: la tarea reejecuta tu código sola. La defensa es cerrar la escritura del script y su directorio.
Blue Team · asegura la tarea y verifica
Corrige el fallo del laboratorio: devuelve el script y su directorio a root y cierra la
escritura ajena. Después demuestra, con comandos, que becario ya no puede
modificar el script y que la tarea sigue funcionando con su contenido legítimo. Escríbelo
como un check que devuelva éxito o fallo.
chown a root y chmod 0755 en script y directorio.... && echo FALLA || echo OK.Como root: chown -R root:root /opt/tareas, chmod 0755 /opt/tareas /opt/tareas/backup.sh. Verificación como becario:
# debe fallar ahora: si escribe, la defensa no sirvio echo malicia >> /opt/tareas/backup.sh 2>/dev/null \ && echo "FALLA: aun escribe" || echo "OK: sin escritura"
Clave: devolver la propiedad a root y cerrar la escritura del script y del directorio; verificar que el ataque de antes ahora da "Permission denied". La tarea sigue corriendo, pero ya nadie puede secuestrarla.
Architecture Challenge · un backup nocturno seguro
Diseña una tarea programada que haga un backup nocturno de una base de datos y lo suba a un almacenamiento. Define: qué la dispara (cron o timer), con qué usuario corre, qué permisos llevan el script y su directorio, cómo se protege la credencial del almacenamiento y qué directivas de confinamiento usarías para que un fallo del script no comprometa la máquina.
600 del usuario, y la unit con ProtectSystem=strict.
Una propuesta razonable: un .timer con OnCalendar=*-*-* 03:00:00
dispara un backup.service que corre con el usuario backup. El
script y su directorio son propiedad de root (o de backup) con permisos
755, no escribibles por nadie más. La credencial del almacenamiento vive en
un archivo 600 propiedad de backup, que el script lee, no en la
línea de comandos. La unit lleva NoNewPrivileges=yes,
ProtectSystem=strict, ProtectHome=yes y
ReadWritePaths=/var/backups. Así, aunque el script falle, el proceso apenas
puede tocar el sistema y no puede escalar.
Clave: no hay una única respuesta. Se evalúa que corra con usuario propio, que script y directorio no sean escribibles por otros, que la credencial no quede expuesta y que la unit esté confinada.
13Preguntas de comprensión
Un script de cron escribible por un usuario sin privilegios y ejecutado por root, ¿qué permite?
Lo que se ejecuta es el script. Si un usuario puede reescribirlo, controla lo que root ejecuta en el siguiente ciclo: ejecución como root y persistencia.
¿Qué permisos debe tener un script ejecutado por una tarea de root?
Lo que ejecuta una tarea privilegiada debe ser propiedad de root y cerrado a la escritura ajena, igual que su directorio y el archivo de la tarea.
En una unit de systemd, ¿qué hay que vigilar para evitar persistencia?
Si el binario de ExecStart o la propia unit son escribibles por otros, se controla lo que systemd ejecuta, con la persistencia que da el arranque automático.
¿Cómo se confina un servicio de systemd más allá de los permisos de archivo?
Las directivas de hardening confinan el servicio: usuario propio, sin nuevos privilegios y con el sistema de solo lectura salvo lo justo. Un fallo queda contenido.
14Reto adicional
Escribe un detector de tareas inseguras
Escribe un pequeño script que, en un Linux tuyo, recorra las tareas de cron del sistema y los scripts que ejecutan, y liste las que sean escribibles por usuarios sin privilegios (el script o su directorio). Explica qué comprueba y por qué esa comprobación basta para detectar el fallo de la lección.
/etc/crontab y /etc/cron.d.find ... -perm -0002 encuentra lo escribible por otros.# archivos de tarea escribibles por otros find /etc/crontab /etc/cron.d -perm -0002 -type f 2>/dev/null # scripts y directorios bajo rutas de servicio, escribibles por otros find /opt /usr/local/bin -perm -0002 \( -type f -o -type d \) 2>/dev/null
Clave: se busca lo escribible por "otros" (-perm -0002) entre las tareas y lo que ejecutan, incluyendo directorios. Eso cubre las tres piezas: script, directorio y archivo de tarea.
15Resumen
- Una tarea programada ejecuta código solo y repetido, con la identidad de quien la programó: es el sitio ideal para persistir.
- El riesgo aparece cuando quien ejecuta la tarea y quien puede modificar lo ejecutado son identidades distintas.
- Hay que asegurar las tres piezas: el script, su directorio y el archivo de la tarea (cron.d o unit).
- Un directorio escribible anula el permiso del script que contiene; hay que mirar los dos.
systemdconfina servicios conUser=,NoNewPrivileges=,ProtectSystem=y compañía.- Una tarea que aparece sola es una de las señales más claras de persistencia: inventaría y alerta sobre las nuevas.
644, pero su directorio es 777. ¿Hay riesgo?NoNewPrivileges=yes en una unit?16Mis notas
Estas notas se guardan solo en este navegador. No se envían a ningún servidor. Expórtalas desde el panel de progreso si cambias de máquina.
17Recursos
Los comentarios se cargan solo si los pides, desde GitHub Discussions. No se carga nada de terceros mientras no pulses.