Linux y seguridad de sistemas · Lección 07

Procesos, /proc, PATH y variables de entorno

Un proceso no es una caja cerrada. Mientras corre, el kernel publica todo sobre él en /proc: sus argumentos, sus variables de entorno, sus archivos abiertos, su mapa de memoria. Y su ejecución no es tan fija como parece: dos cosas que el proceso hereda —el PATH y el entorno— deciden qué binario se ejecuta cuando pide un comando por su nombre. Quien controla esas dos cosas controla lo que corre el proceso, sin tocar su código. Esta lección enseña qué filtra /proc, cómo se secuestra la ejecución vía PATH y entorno, y cómo cerrar ambos con rutas absolutas, un PATH mínimo y disciplina de entorno.

Al terminar sabrás

  • Leer /proc/<pid> y decir qué expone: cmdline, environ, maps, fd, exe.
  • Explicar por qué pasar un secreto por variable de entorno lo deja legible en /proc.
  • Describir el orden de resolución del PATH y por qué un directorio escribible en él es una vía de ejecución.
  • Reconocer el secuestro por entorno: LD_PRELOAD y LD_LIBRARY_PATH sustituyendo código.
  • Endurecer un servicio con rutas absolutas, un PATH fijo y mínimo, y un entorno limpio.
  • Aplicar hidepid y separación de identidad para que un proceso no filtre a otros.

1Por qué importa

Cuando piensas en atacar un programa, imaginas un fallo en su código. Pero mucha de la escalada real en Linux no toca el código: toca lo que el programa hereda del entorno en que arranca. Un script de despliegue que llama a tar por su nombre confía en que el tar que se ejecute sea el del sistema. Esa confianza vive enteramente en el PATH, una variable heredada. Si alguien puede colocar un tar propio en un directorio que aparece antes en ese PATH, el script ejecutará su código, con la identidad del script, y nadie habrá explotado nada.

El otro lado de la moneda es lo que el proceso revela. El kernel mantiene un sistema de archivos virtual, /proc, con una carpeta por proceso. Ahí está su línea de comandos completa, su entorno completo y la lista de sus descriptores de archivo. Si el proceso recibió una contraseña como argumento o como variable de entorno, esa contraseña está en /proc, en texto plano, mientras el proceso viva. Entender qué filtra /proc es entender por qué "pasar el secreto por variable de entorno" no es tan privado como parece.

El objetivo real

No estás aprendiendo trucos de /proc. Estás aprendiendo que un proceso tiene dos superficies que casi nadie mira: lo que hereda (y que decide qué ejecuta) y lo que publica sobre sí mismo. Cerrar las dos es endurecer el proceso antes de que exista un atacante.

2Conceptos

Vocabulario mínimo
  • Proceso / PID: una ejecución en curso y su número identificador.
  • /proc: sistema de archivos virtual del kernel; una carpeta por proceso.
  • /proc/<pid>/environ: las variables de entorno con las que arrancó el proceso.
  • /proc/<pid>/cmdline: su línea de comandos exacta, argumentos incluidos.
  • /proc/<pid>/fd: los descriptores de archivo abiertos (qué archivos y sockets usa).
  • PATH: lista ordenada de directorios donde la shell busca un comando por nombre.
  • Variable de entorno: un par nombre=valor que un proceso hereda de su padre.
  • LD_PRELOAD: variable que fuerza a cargar una biblioteca propia antes que las del sistema.

3Explicación profunda

/proc no está en el disco: lo genera el kernel al vuelo. Cada proceso vivo tiene un directorio /proc/<pid> con archivos que son en realidad ventanas al estado del proceso. cmdline te da los argumentos con los que se lanzó; environ, su entorno; maps, las regiones de memoria y las bibliotecas cargadas; fd/, enlaces a cada archivo o socket abierto; exe, un enlace al binario en ejecución. Nada de esto es un fallo: es introspección legítima, la base de herramientas como ps, lsof o top. El problema de seguridad aparece por quién puede leerlo.

Por defecto puedes leer el /proc de tus propios procesos, y algunos campos de los ajenos. En un sistema donde varios usuarios comparten máquina, o donde una aplicación comprometida corre junto a otras, esto importa: si un proceso recibió un secreto por línea de comandos (mysql -pMiClave) o por entorno, ese secreto es legible para quien pueda mirar su /proc. Por eso pasar contraseñas como argumento está prohibido en cualquier guía seria, y por eso pasar secretos por variable de entorno, aunque es mejor que el argumento, tampoco es privado frente a quien comparte la máquina con los privilegios adecuados.

La segunda mitad de la lección es la herencia que decide qué se ejecuta. Cuando escribes tar sin ruta, la shell recorre los directorios del PATH en orden y ejecuta el primer tar que encuentra. El orden es todo. Si el PATH es /opt/util:/usr/bin y /opt/util es escribible por un usuario cualquiera, ese usuario planta un /opt/util/tar con su código y lo ejecutará cualquier proceso que llame a tar por su nombre y tenga ese PATH. Es la razón por la que un directorio world-writable dentro del PATH es tan grave: convierte "escribir un archivo" en "ejecutar código en otro proceso". El clásico peligroso es el . (directorio actual) en el PATH: hace que cualquier carpeta en la que entres pueda contener un binario que suplante un comando común.

El entorno también secuestra la ejecución sin tocar el PATH. El enlazador dinámico lee variables como LD_PRELOAD y LD_LIBRARY_PATH. Con LD_PRELOAD apuntando a una biblioteca propia, esa biblioteca se carga antes que las del sistema y puede sustituir funciones estándar: el programa cree que llama a getuid() del sistema y llama a la tuya. El kernel protege el caso más grave —ignora LD_PRELOAD para binarios SUID— pero en procesos normales que heredan un entorno controlado por el atacante, es una vía directa a ejecutar su código dentro del proceso. La conclusión de diseño es dura y simple: el entorno de un proceso privilegiado es parte de su superficie de ataque, y hay que fijarlo, no heredarlo.

4Ejemplo sencillo

# Un cocinero que pide "el cuchillo" sin mirar de cuál se fía

El cocinero (proceso) grita "traedme el cuchillo" (tar, cp, id...).
Un ayudante recorre los cajones EN ORDEN (el PATH) y le trae el
primero que encuentre con ese nombre.

Si el primer cajon es de acceso publico (directorio world-writable
en el PATH), cualquiera deja ahi un "cuchillo" propio. El cocinero
lo usara sin darse cuenta, creyendo que es el de siempre.

# Y una libreta que el cocinero deja abierta

El cocinero anota en una libreta (environ) las claves de la caja
fuerte que le dieron al empezar. Cualquiera que pueda asomarse a su
puesto (/proc) lee esas claves mientras el cocinero trabaja.
Dos descuidos distintos: fiarse del primer "cuchillo" del cajón (PATH) y dejar las claves anotadas a la vista (environ). Ninguno es un fallo del cocinero; los dos son del sitio en que trabaja.

5Ejemplo técnico

Mira lo que /proc filtra de un proceso y cómo se resuelve un comando por el PATH:

RutaQué contieneRiesgo
/proc/<pid>/cmdlineArgumentos exactos del procesoUn secreto pasado como argumento queda a la vista.
/proc/<pid>/environVariables de entorno de arranqueTokens y claves puestos en el entorno son legibles.
/proc/<pid>/fd/Descriptores abiertosRevela qué archivos y sockets toca el proceso.

Los comandos que lo hacen concreto. El primero enseña el entorno del propio proceso; el segundo demuestra cómo el orden del PATH decide qué binario gana:

# el entorno con el que arranco este proceso (separado por NUL)
cat /proc/self/environ | tr '\0' '\n'

# qué 'tar' se ejecutaria: el primero del PATH, en orden
echo "$PATH"
which -a tar        # lista cada tar del PATH, por orden de prioridad

# un PATH con un directorio escribible delante es una via de ejecucion
# /opt/util:/usr/bin  -> un /opt/util/tar propio gana a /usr/bin/tar

6Diagrama

Un proceso privilegiado llama a un comando por su nombre; la resolución del PATH encuentra primero un binario plantado en un directorio escribible y ejecuta ese en lugar del legítimo proceso root llama a: backup busca en el PATH en orden, el primero /opt/util/backup plantado · world-writable /usr/bin/backup el legítimo · nunca se llega gana se ignora
La resolución del PATH se detiene en el primer resultado. Un directorio escribible situado antes que /usr/bin deja al proceso ejecutar código ajeno creyendo que llama al binario de siempre.
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
    P[proceso root · llama a: backup] -->|busca en el PATH| R{primer match}
    R -->|gana| M[/opt/util/backup · world-writable/]
    R -.se ignora.-> L[/usr/bin/backup · legítimo/]

7Código

La defensa se escribe como configuración, no como intención. Un servicio se lanza con un PATH fijo y mínimo, llama a sus binarios por ruta absoluta y arranca con el entorno limpio de variables peligrosas:

#!/usr/bin/env bash
# arranque endurecido de un servicio
set -euo pipefail

# 1) PATH fijo, minimo y sin directorios escribibles ni '.'
export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'

# 2) limpiar variables que secuestran la carga de bibliotecas
unset LD_PRELOAD LD_LIBRARY_PATH LD_AUDIT

# 3) llamar a los binarios por ruta absoluta, no por nombre
/usr/bin/tar -czf /var/backups/datos.tgz /srv/datos

En systemd lo mismo se declara en la unit: Environment=PATH=... fija el PATH, y las directivas de aislamiento (que ves en la lección de systemd) evitan que el servicio herede un entorno del que no te fías. La regla práctica: un proceso privilegiado nunca debe usar el PATH ni el entorno que le llegue "por defecto"; los fija él.

8Qué puede salir mal

Los errores clásicos

Secretos por línea de comandos o entorno en máquina compartida. app --password=... o un token en una variable quedan legibles en /proc para quien pueda mirar. En hosts compartidos, eso es filtrar el secreto.

Un directorio escribible en el PATH. Basta que un proceso privilegiado tenga /opt/util (0777) antes que /usr/bin para que un usuario cualquiera plante un binario y lo ejecute con esos privilegios.

El . en el PATH. Poner el directorio actual en el PATH convierte cada carpeta que visitas en una trampa: un ls o un make falsos dejados ahí se ejecutan en tu lugar.

Llamar a binarios por nombre en scripts privilegiados. Un script de root que hace tar, cp o find sin ruta absoluta depende del PATH heredado, y ese PATH puede no ser el que crees.

9Cómo lo abusaría un atacante

Vista desde el otro lado

Quien acaba de conseguir ejecución como un usuario cualquiera enumera primero. Mira /proc: recorre los procesos, lee el environ de los que puede y busca tokens, contraseñas de base de datos, claves de API que alguien pasó por entorno. Mira cmdline por si un cron lanzó algo con un secreto en los argumentos. Nada de esto rompe nada: el kernel se lo está enseñando.

Después busca el secuestro de ejecución. Enumera el PATH de los servicios y los directorios escribibles que aparecen en él. Si encuentra uno, planta un binario con el nombre de algo que un proceso privilegiado llama —tar, backup, logrotate— y espera a que ese proceso lo ejecute con su identidad. Si controla el entorno de un proceso que va a lanzar otro, prueba LD_PRELOAD para inyectar su biblioteca.

Lo relevante para ti como defensor es el patrón: el atacante no explota el programa, explota lo que el programa hereda y lo que el programa filtra. Son huecos de configuración, invisibles en el día a día, que llevaban ahí desde el despliegue.

10Cómo defenderlo

Qué hacer con esto mañana
  • Nunca pases secretos por argumento. Prefiere archivos con permisos 600 o un gestor de secretos a las variables de entorno en hosts compartidos.
  • Fija un PATH mínimo y absoluto en cada servicio. Sin ., sin directorios escribibles por usuarios.
  • En scripts privilegiados, llama a los binarios por ruta absoluta: /usr/bin/tar, no tar.
  • Limpia el entorno peligroso al arrancar: unset LD_PRELOAD LD_LIBRARY_PATH LD_AUDIT.
  • Monta /proc con hidepid=2 para que un usuario no vea el /proc de procesos ajenos.
  • Da a cada servicio su propio usuario: así su /proc no es legible por los demás y su entorno no se comparte.

11Laboratorio

Estado: laboratorio local lab-linux-privesc
Targetcontenedor privesc-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.

Reutilizamos la caja de F03. Trae un directorio world-writable, /opt/util, perfecto para ver el secuestro por PATH. Entra como el usuario sin privilegios:

cd cursos/seguridad/labs/lab-linux-privesc docker compose up -d docker compose exec caja su - becario

Observa qué filtra /proc y cómo el orden del PATH cambia qué se ejecuta:

construir → atacar → observar → defender → verificar
cat /proc/self/environ | tr '\0' '\n' # el entorno del proceso, en texto plano printf '#!/bin/bash\necho SECUESTRADO como $(id -un)\n' > /opt/util/id chmod +x /opt/util/id PATH="/opt/util:$PATH" id SECUESTRADO como becario # el 'id' plantado gano al de /usr/bin: el PATH decide

Ahora defiende: desde otra terminal, como root (docker compose exec caja bash), cierra el directorio con chmod 755 /opt/util y borra el binario plantado. Como becario, intenta de nuevo escribir en /opt/util: debe fallar. Cuando termines:

docker compose down -v

12Ejercicios

Qué filtra cada archivo de /proc

Di qué expone cada uno y por qué puede importar para la seguridad: (1) /proc/<pid>/cmdline; (2) /proc/<pid>/environ; (3) /proc/<pid>/fd/; (4) /proc/<pid>/exe.

Todos son ventanas al estado del proceso que mantiene el kernel.
Piensa qué pasa si el proceso recibió un secreto por argumento o por entorno.
cmdline son argumentos; environ, variables; fd, archivos abiertos.
Solución

(1) cmdline: los argumentos exactos; un --password= queda a la vista. (2) environ: las variables de entorno; tokens y claves puestos ahí son legibles. (3) fd/: los descriptores abiertos; revela qué archivos y sockets usa el proceso. (4) exe: un enlace al binario en ejecución; dice qué se está corriendo de verdad.

Clave: cmdline y environ son los peligrosos para secretos; fd y exe revelan qué toca y qué ejecuta el proceso.

Endurece el PATH de un arranque

Tienes un script de servicio que empieza sin fijar el PATH y llama a tar y gzip por su nombre. Reescríbelo para que sea seguro: fija un PATH mínimo, limpia el entorno peligroso y usa rutas absolutas. Justifica cada línea.

El problema es que hereda PATH y entorno de quien lo lanza.
Necesitas export PATH=..., unset LD_* y rutas absolutas.
El PATH mínimo típico no incluye . ni ningún directorio escribible.
Solución
set -euo pipefail
export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
unset LD_PRELOAD LD_LIBRARY_PATH LD_AUDIT
/usr/bin/tar -czf /var/backups/d.tgz /srv/datos

Clave: fijar PATH mínimo, quitar LD_* y llamar por ruta absoluta. Las tres cosas cortan la herencia de la que dependía la ejecución.

Por qué el SUID ignora LD_PRELOAD

LD_PRELOAD permite cargar una biblioteca propia antes que las del sistema. Investiga por qué el enlazador dinámico ignora LD_PRELOAD (y otras variables similares) cuando el binario es SUID o SGID, y qué pasaría si no lo hiciera.

Un binario SUID corre con la identidad de su dueño, no la de quien lo lanza.
Busca "secure-execution mode" y AT_SECURE en la documentación de ld.so.
Si se respetara, cargarías tu código dentro de un proceso que corre como root.
Solución

Un binario SUID hereda el entorno de quien lo lanza, que es un usuario sin privilegios, pero corre con la identidad del dueño (a menudo root). Si respetara LD_PRELOAD, ese usuario podría cargar su biblioteca dentro de un proceso root y sustituir funciones: ejecución como root inmediata. Por eso el enlazador entra en "secure-execution mode" (marcado con AT_SECURE) cuando detecta el bit SUID/SGID e ignora LD_PRELOAD, LD_LIBRARY_PATH y compañía.

Clave: el entorno lo controla quien lanza, no el dueño del binario; respetar LD_PRELOAD en SUID sería entregar el proceso privilegiado. El modo seguro del enlazador lo evita.

Red Team · secuestra un comando por el PATH

Dentro del lab-linux-privesc, y solo dentro de él: como becario, planta en /opt/util un binario llamado id que imprima un mensaje propio, y consigue que se ejecute en lugar del id real anteponiendo /opt/util al PATH. Explica qué identidad usó y por qué ganó tu binario.

La resolución del PATH se queda con el primer resultado.
Crea un script ejecutable en /opt/util/id y ajusta el PATH.
PATH="/opt/util:$PATH" id hace que se busque primero en /opt/util.
Solución
printf '#!/bin/bash\necho SECUESTRADO como $(id -un)\n' > /opt/util/id
chmod +x /opt/util/id
PATH="/opt/util:$PATH" id     # ejecuta tu 'id', no /usr/bin/id

Clave: el binario plantado corre como becario (tu identidad); ganó porque /opt/util iba antes en el PATH. Si el proceso víctima fuera root, tu código correría como root. La defensa es que /opt/util no sea escribible ni esté en el PATH.

Blue Team · cierra el PATH y verifica

Corrige el fallo del laboratorio: quita la escritura de /opt/util y elimina el binario plantado. Después demuestra, con comandos, que becario ya no puede escribir ahí y que el id que se ejecuta vuelve a ser el del sistema. Escribe la comprobación como un check que devuelva éxito o fallo.

chmod 755 /opt/util como root cierra la escritura ajena.
La verificación es reintentar el secuestro y comprobar que ya no puedes plantar el binario.
Usa el código de salida: ... && echo FALLA || echo OK.
Solución

Como root: chmod 755 /opt/util y rm -f /opt/util/id. Verificación como becario:

# debe fallar ahora: si puede escribir, la defensa no sirvio
touch /opt/util/prueba 2>/dev/null \
  && echo "FALLA: aun escribe" || echo "OK: sin escritura"
command -v id      # debe apuntar a /usr/bin/id

Clave: cerrar la escritura del directorio y confirmar que ya no se puede plantar nada; el comando por nombre vuelve a resolver al binario del sistema.

Architecture Challenge · secretos y entorno de un servicio

Diseña cómo un servicio recibe su token de base de datos sin que quede legible en /proc para otros procesos de la máquina, y cómo fijas su PATH y su entorno para que no herede nada del atacante. Define dónde vive el secreto, con qué permisos, y qué directivas usarías en systemd o en el compose.

Un secreto por argumento o entorno es legible en máquina compartida.
Un archivo 600 del usuario del servicio, o un gestor de secretos, no aparece en /proc de otros.
hidepid=2, usuario propio y Environment=PATH=... cierran la herencia.
Solución

Una propuesta razonable: el token vive en un archivo config.env propiedad del usuario del servicio con permisos 600, y el proceso lo lee al arrancar en vez de recibirlo por línea de comandos (que quedaría en cmdline). El servicio corre con su propio usuario sin login, con /proc montado hidepid=2 para que otros usuarios no vean su environ. En la unit se fija Environment=PATH=/usr/local/bin:/usr/bin:/bin y se limpia el entorno heredado. Mejor aún: sacar el secreto a un gestor y que el proceso lo pida por identidad, sin que toque nunca el disco ni el entorno.

Clave: no hay una única respuesta. Se evalúa que el secreto no quede en cmdline/environ visibles, que el PATH sea fijo y mínimo, y que el servicio no herede entorno del atacante.

13Preguntas de comprensión

Comprobación

¿Qué expone /proc/<pid>/environ?

environ guarda el entorno de arranque. Un token o una clave puestos ahí son legibles para quien pueda mirar ese /proc.

¿Por qué es peligroso un directorio escribible situado antes en el PATH?

La resolución del PATH se queda con el primer resultado. Un directorio escribible por delante deja plantar un binario que gana al legítimo.

¿Qué permite LD_PRELOAD?

LD_PRELOAD fuerza a cargar tu biblioteca antes que las estándar, de modo que el programa llama a tus versiones de las funciones. Por eso el enlazador lo ignora en binarios SUID.

¿Cómo se endurece la resolución de comandos de un servicio?

Un PATH fijo y mínimo, sin directorios escribibles ni ., más llamadas por ruta absoluta, cortan el secuestro por herencia.

0 / 4

14Reto adicional

Encuentra un secreto en /proc y ciérralo

En un Linux tuyo (o en el contenedor del laboratorio), lanza un proceso que reciba un valor "secreto" por variable de entorno y otro que lo reciba por argumento. Léelos desde /proc. Luego explica qué método filtró el secreto, a quién, y cuál sería la forma correcta de pasárselo al proceso.

environ y cmdline son los dos sitios donde buscar.
Lanza algo como SECRETO=abc sleep 300 & y mira su /proc/<pid>/environ.
La forma correcta es un archivo 600 o un gestor de secretos, no el argumento.
Solución

SECRETO=abc sleep 300 & deja el valor en /proc/<pid>/environ; sleep 300 --clave=abc lo deja en cmdline. Ambos son legibles para quien pueda mirar ese /proc (el propio usuario siempre; otros según hidepid y privilegios). La forma correcta es que el proceso lea el secreto de un archivo 600 de su usuario o lo pida a un gestor: así no aparece ni en cmdline ni en environ.

Clave: argumento y entorno filtran el secreto vía /proc; un archivo 600 o un gestor de secretos no. "Pasarlo por entorno" es mejor que por argumento, pero no es privado en máquina compartida.

15Resumen

Puntos clave
  • /proc/<pid> publica cmdline, environ, fd y maps: un secreto por argumento o entorno queda legible ahí.
  • La resolución del PATH se queda con el primer resultado; un directorio escribible por delante es una vía de ejecución.
  • El . en el PATH convierte cada carpeta que visitas en una posible trampa.
  • LD_PRELOAD y LD_LIBRARY_PATH secuestran la carga de bibliotecas; el enlazador los ignora en SUID por eso mismo.
  • La defensa es fijar un PATH mínimo, usar rutas absolutas y limpiar el entorno peligroso al arrancar.
  • hidepid=2 y un usuario propio por servicio evitan que un proceso filtre a otros a través de /proc.
¿Dónde queda un secreto que pasas a un proceso por variable de entorno?
En /proc/<pid>/environ, en texto plano, legible mientras el proceso viva. En máquina compartida, no es privado.
Tu PATH es /opt/util:/usr/bin y /opt/util es 0777. ¿Qué riesgo hay?
Cualquiera planta /opt/util/<comando> y se ejecuta en lugar del de /usr/bin, con la identidad del proceso que lo llame.
¿Por qué un script privilegiado debe llamar a /usr/bin/tar y no a tar?
Porque tar por nombre depende del PATH heredado; la ruta absoluta no se puede secuestrar.
¿Qué hace hidepid=2 al montar /proc?
Oculta a cada usuario el /proc de los procesos que no son suyos, cerrando la fuga de environ y cmdline ajenos.

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.