Linux y seguridad de sistemas · Lección 08

cron, systemd y mecanismos de persistencia

Una tarea programada es código que se ejecuta solo, una y otra vez, con la identidad de quien la programó. Esa propiedad la vuelve utilísima para operar un sistema y, a la vez, el lugar favorito para quedarse a vivir en él: si un atacante logra que algo suyo se ejecute en cada ciclo, ha convertido un acceso puntual en acceso permanente. Y no hace falta un exploit; basta un permiso de más sobre lo que la tarea ejecuta. Esta lección recorre cron, /etc/cron.d, las units y los timers de systemd, para reconocer dónde una tarea mal permisada se convierte en persistencia y ejecución con privilegios, y cómo asegurarla.

Al terminar sabrás

  • Leer una línea de crontab y decir qué se ejecuta, cuándo y con qué identidad.
  • Localizar dónde viven las tareas: crontab -l, /etc/crontab, /etc/cron.d, units y timers.
  • Explicar por qué un script escribible por otros, ejecutado por root, es ejecución como root.
  • Reconocer la persistencia: un archivo de tarea que se vuelve a ejecutar en cada ciclo.
  • Asegurar una tarea privilegiada: propiedad de root y sin escritura ajena, en el script, el directorio y la unit.
  • Confinar un servicio de systemd con directivas de hardening.

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.

El objetivo real

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

Vocabulario mínimo
  • 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 .service que describe cómo arrancar un proceso.
  • systemd timer: archivo .timer que dispara una unit según un calendario (el cron de 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.
El fallo no es la nota ni el conserje: es dónde está pegado el papel. Quien pueda reescribir lo que una tarea privilegiada ejecuta, ejecuta con sus privilegios, y en cada ciclo.

5Ejemplo técnico

Los sitios donde viven las tareas, y qué mirar en cada uno:

UbicaciónQué esQué revisar
/etc/cron.d/*Tareas del sistema con usuarioDueño root, no escribible; y el script que llaman, igual.
/etc/systemd/system/*.serviceUnits de servicioExecStart a un binario no escribible; la unit tampoco.
*.timerDisparador por calendarioQué 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

Un usuario sin privilegios reescribe un script world-writable que la tarea de cron de root ejecuta en cada ciclo, obteniendo ejecución como root de forma repetida becario sin privilegios backup.sh 0777 · escribible cron de root cada ciclo ejecuta como root persistencia reescribe lee
El becario no ataca a root: reescribe un archivo que root ya iba a ejecutar. Como la tarea corre en cada ciclo, la ejecución se repite sola: acceso puntual convertido en persistencia.
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

Los errores clásicos

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

Vista desde el otro lado

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

Qué hacer con esto mañana
  • 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=strict y ProtectHome=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

Estado: laboratorio local lab-linux-persist
Targetcontenedor persist-caja, sin puertos publicados
Autorizaciónsistema local creado por ti para entrenamiento
Este laboratorio es tuyo, está aislado y no tiene salida a internet. Todo lo que se practica aquí se practica aquí dentro. Ninguna de estas técnicas debe usarse contra un sistema que no sea tuyo o para el que no tengas autorización escrita.

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:

construir → atacar → observar → defender → verificar
cat /var/log/tarea.log [backup] 10:22:31 ejecutado por: root ls -l /opt/tareas/backup.sh -rwxrwxrwx 1 root root ... /opt/tareas/backup.sh # el script que root ejecuta es escribible por ti echo 'id -un > /tmp/quien_ejecuta' >> /opt/tareas/backup.sh # espera al siguiente ciclo (<=15s) y comprueba quien lo creo ls -l /tmp/quien_ejecuta; cat /tmp/quien_ejecuta -rw-r--r-- 1 root root ... /tmp/quien_ejecuta root # tu linea corrio como root, y se repetira cada ciclo

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.

Pregúntate quién ejecuta y quién puede modificar lo ejecutado.
Borrar y crear archivos es un permiso del directorio.
Un directorio escribible anula la protección del archivo que contiene.
Solución

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

El problema es que corre como root y sin ninguna restricción.
Busca User=, NoNewPrivileges=, ProtectSystem= y ReadWritePaths=.
ProtectSystem=strict lo hace todo de solo lectura salvo lo que declares.
Solución
[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.

Piensa en qué se ejecuta al iniciar sesión o al arrancar una shell.
Los archivos de perfil de shell y los servicios de usuario cuentan.
Mira ~/.bashrc, /etc/profile.d, units de usuario y @reboot en cron.
Solución

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.

El script se ejecuta como root cada 15 segundos.
Añade una línea al final de /opt/tareas/backup.sh.
Haz que escriba algo cuyo dueño delate quién lo ejecutó.
Solución
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.
La verificación es reintentar el ataque y comprobar que ya no puedes escribir.
Usa el código de salida: ... && echo FALLA || echo OK.
Solución

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.

Un usuario propio del backup, no root, con lo justo para su tarea.
Script y directorio propiedad de root o del usuario del backup, no escribibles por otros.
La credencial en un archivo 600 del usuario, y la unit con ProtectSystem=strict.
Solución

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

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

0 / 4

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.

Los archivos de tarea están en /etc/crontab y /etc/cron.d.
find ... -perm -0002 encuentra lo escribible por otros.
Hay que comprobar tanto el script como el directorio que lo contiene.
Solución
# 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

Puntos clave
  • 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.
  • systemd confina servicios con User=, 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.
¿Por qué las tareas programadas son el sitio favorito para persistir?
Porque existen para ejecutarse solas y de forma repetida; el código del atacante vuelve a arrancar en cada ciclo, sin que él actúe.
Un script de root es 644, pero su directorio es 777. ¿Hay riesgo?
Sí. En un directorio escribible cualquiera borra el script y pone otro; borrar es un permiso del directorio, no del archivo.
¿Qué tres piezas hay que asegurar en una tarea privilegiada?
El script, el directorio que lo contiene y el archivo de la tarea (cron.d o la unit). Los tres, root y sin escritura ajena.
¿Qué hace NoNewPrivileges=yes en una unit?
Impide que el proceso y sus hijos ganen privilegios, por ejemplo vía SUID. Contiene el servicio aunque se comprometa.

16Mis notas

Cierre de la lección

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.