En resumen: el usuario del servidor web con el que se ejecuta PHP debería, según el principio de mínimo privilegio (Least Privilege), leer solo lo que el sitio necesita y escribir solo donde este guarda datos. No tiene shell de inicio de sesión, ni derechos sudo, ni cronjobs propios, ni claves SSH, y su cuenta de base de datos solo puede leer y modificar datos. Opciones de systemd como ProtectSystem y PrivateTmp acotan además PHP-FPM.
La pregunta tras la intrusión: ¿qué puede hacer este proceso?
PHP2Shell trata de la ejecución remota de código malicioso sin autenticación. Si tiene éxito, el código no se ejecuta en el vacío, sino como un usuario muy concreto: el usuario del proceso PHP. Todo lo que ese usuario puede hacer, lo puede hacer también el atacante desde ese momento.
En muchos servidores eso es sorprendentemente mucho. El usuario es propietario de todo el código del programa, tiene una shell, puede crear cronjobs propios y la cuenta de base de datos de la aplicación tiene todos los derechos sobre todas las bases de datos. Para un atacante es la situación de partida ideal para instalarse de forma permanente.
Cómo asignar correctamente archivos y carpetas lo describe la guía Permisos de archivos en el servidor web. Este artículo se centra en la propia cuenta: inicio de sesión, derechos especiales, tareas programadas, base de datos y los límites que traza systemd.
Inventario: ¿quién es el usuario del servidor web?
Primero hay que aclarar qué usuarios intervienen. Con PHP-FPM, cada pool define con user y group bajo qué usuario se ejecutan sus workers. Con mod_php, PHP se ejecuta como el usuario de Apache. Cómo interactúan el maestro y los workers en PHP-FPM lo explica PHP-FPM explicado: gestor de procesos, pools y workers. Los comandos siguientes solo leen:
# ¿Bajo qué usuarios se ejecutan el servidor web y PHP?
ps -eo user,group,cmd | grep -E "[p]hp-fpm|[n]ginx|[a]pache2|[h]ttpd" | sort | uniq -c
# Datos de la cuenta: directorio home y shell (adapte el usuario)
getent passwd www-data
# ¿Tiene el usuario derechos sudo?
sudo -l -U www-data
# ¿Cronjobs propios?
sudo crontab -l -u www-data
# Claves SSH en el directorio home (en Debian/Ubuntu es /var/www)
sudo ls -la /var/www/.ssh 2>/dev/null
Anote los resultados de cada usuario de pool. Si todos los sitios se ejecutan con el mismo usuario, ese es el primer punto de la lista; cómo separarlos lo describe Aislar los pools de PHP-FPM por sitio web.
Permisos del usuario del servidor web: lo que la cuenta no necesita
La tabla siguiente muestra lo que un usuario de servidor web normalmente no necesita y cómo retirárselo.
| Permiso | Por qué es peligroso | Recomendación |
|---|---|---|
| Shell de inicio de sesión | Posibilidad de conectarse y usarla de forma interactiva | Shell /usr/sbin/nologin |
| Entradas sudo | Camino a derechos de root | Ninguna entrada, ni siquiera para comandos sueltos |
| Crontab propia | Puerta trasera permanente que sobrevive a cada reinicio | Ninguna; limitar mediante /etc/cron.allow |
| Claves SSH | Acceso desde el exterior | Sin authorized_keys |
| Propiedad del código del programa | El código puede modificarse | El propietario es un usuario de despliegue |
| Escritura fuera de las carpetas de datos | Depósito de código malicioso | Solo uploads, caché y temporales |
| Derechos completos de base de datos | Bases de datos de otras aplicaciones, acceso a archivos a través de la base de datos | Solo la base de datos propia, solo derechos de datos |
La shell se establece con usermod -s /usr/sbin/nologin www-data. Para cron rige lo siguiente: si existe /etc/cron.allow, solo los usuarios allí indicados pueden crear cronjobs propios. Cronjobs recién creados o claves SSH desconocidas son una señal típica de que alguien se ha afianzado. Una señal de alarma especialmente clara la describe el artículo Señal de alarma: el servidor web inicia una shell.
La base de datos: limitar los derechos a lo necesario
La cuenta de base de datos de un sitio web es, de hecho, un segundo usuario del servidor web. Sus credenciales están en el archivo de configuración de la aplicación y son legibles para el código inyectado. Por eso solo debería poder hacer lo que la aplicación necesita en funcionamiento.
-- Ejemplo MySQL/MariaDB: una cuenta propia por sitio web, solo derechos de datos
CREATE USER 'seite1'@'localhost' IDENTIFIED BY 'hier-ein-langes-Zufallspasswort';
GRANT SELECT, INSERT, UPDATE, DELETE ON seite1.* TO 'seite1'@'localhost';
-- Comprobar los derechos
SHOW GRANTS FOR 'seite1'@'localhost';
Una cuenta de aplicación no necesita derechos como FILE, SUPER o GRANT OPTION, ni acceso a *.*. Muchos CMS necesitan además CREATE, ALTER, INDEX y DROP para las actualizaciones. Es más limpio tener una cuenta separada para actualizaciones y migraciones, que solo se use durante el mantenimiento. Compruebe los requisitos en la documentación del fabricante.
Acotar además PHP-FPM con systemd
systemd puede restringir la visión que un servicio tiene del sistema. Para PHP-FPM resultan especialmente adecuadas algunas opciones de la documentación de systemd. También actúan sobre el proceso maestro, que arranca como root.
# sudo systemctl edit php8.3-fpm
[Service]
# /usr, /boot y /etc en solo lectura
ProtectSystem=full
# /home, /root y /run/user invisibles
ProtectHome=true
# /tmp y /var/tmp propios solo para este servicio
PrivateTmp=true
# sin ampliación de privilegios mediante setuid o capabilities de archivo
NoNewPrivileges=true
Pruebe estas opciones antes del uso en producción. Si los sitios están bajo /home, ProtectHome=true impide el acceso. NoNewPrivileges=true puede perturbar programas que PHP invoca y que cambian sus derechos de grupo al arrancar, por ejemplo algunos programas de correo locales para la función mail() de PHP. Por eso, proceda por pasos:
- Añadir una opción, reiniciar el servicio y comprobar
systemctl status php8.3-fpm. - Probar las funciones básicas del sitio: inicio de sesión, formularios, envío de correo, subidas, cronjobs de la aplicación.
- Revisar el log de errores de PHP-FPM y de la aplicación.
- Solo entonces añadir la siguiente opción.
Por último, el socket: según el manual de PHP, recibe por defecto los permisos 0660 para el usuario y el grupo del pool. Fije listen.owner y listen.group de modo que solo el servidor web pueda conectarse, no cualquier usuario local.
Los permisos mínimos son una actitud, no un proyecto puntual. Repita el inventario de este artículo tras cada reforma y cada nuevo sitio web, porque los servicios nuevos suelen traer permisos predeterminados generosos.
Conclusión: el atacante hereda lo que usted permite
El mínimo privilegio suena a teoría, pero es una cuestión muy práctica: ¿qué puede hacer alguien que ejecuta código como usuario del servidor web? En un servidor con shell, entrada sudo, crontab propia y una cuenta de base de datos con todos los derechos, la respuesta es: casi todo. En un servidor acotado, la cosa se queda más bien en un incidente en un único sitio web, que se puede detectar y limpiar.
El cambio no requiere software nuevo, solo un inventario cuidadoso y pruebas por pasos. Empiece por los puntos que apenas pueden romper nada, es decir, shell, sudo y claves SSH, y avance después hacia los derechos de base de datos y las opciones de systemd.
¿Cómo ayuda PHP SOS en concreto?
- Revisar las cuentas: registramos, para cada pool y usuario del servidor web, la shell, las entradas sudo, los cronjobs, las claves SSH y los derechos de base de datos.
- Reducir los permisos: retiramos lo que no se necesita y separamos las cuentas de despliegue, de ejecución y de mantenimiento.
- Acotar los servicios: aplicamos las opciones de protección de systemd por pasos y probamos el sitio web después de cada paso.
- Mantenerlo en el tiempo: en el marco del refuerzo del servidor documentamos el concepto de permisos para futuros cambios.
Preguntas frecuentes
¿Con qué usuario se ejecuta PHP en mi servidor?
Depende del modo de funcionamiento. Con mod_php, PHP se ejecuta como el usuario de Apache, por ejemplo www-data o apache. Con PHP-FPM, cada pool define su usuario con user y group; el proceso maestro suele ejecutarse como root y los workers, como el usuario del pool. El comando ps lo muestra enseguida.
¿Necesita el usuario del servidor web una shell de inicio de sesión?
No. Ni Nginx, ni Apache, ni PHP-FPM necesitan un inicio de sesión interactivo para este usuario. Una shell como /usr/sbin/nologin impide que alguien se conecte con esta cuenta. Los despliegues se realizan con un usuario propio.
¿Por qué no dar derechos sudo a www-data, ni siquiera para un único comando?
Porque el código inyectado hereda exactamente esos derechos. Incluso un comando aparentemente inofensivo, si se autoriza mal, puede conducir a derechos de root. Las tareas que requieren privilegios elevados pertenecen a servicios separados o a procesos fuera del servidor web.
¿Ayuda esto frente a PHP2Shell?
No cierra la vulnerabilidad; para eso hacen falta la actualización de PHP y el reinicio de todos los workers. Pero los permisos mínimos limitan lo que un atacante puede hacer tras una explotación exitosa: modificar archivos, afianzarse o alcanzar otros sitios web.