Desde la semana pasada estoy como sysadmin freelance de una serie de webservers en una empresa de diseño que hostea los sitios webs de sus propios clientes.

El objetivo es actualizar, optimizar y asegurar esos pocos webservers (menos de 10) y justo cuando tomé el control de los mismos me estreno con un deface a TODOS los sitios de varios de ellos apareciendo una portada como la siguiente:

Por supuesto, se fue al garete todo el plan de upgrades que tenia pensado y tuve que ponerme a trabajar contrareloj para solventar este inconveniente y permitir que los sitios esten nuevamente online.
En un principio sólo restauré archivos borrados y databases y los sitios volvieron a estar online pero a las pocas horas nuevamente volvieron a comprometer los sitios asi que había una cuestión de fondo que resolver.

Para el ejemplo sólo usaré dominios bajo mi control para mantener en privado los reales sobre los que realizé el trabajo

En la GRAN mayoría de los casos cuando nos encontramos con sitios defaced, la causa es una combinación de frameworks sin actualizar y por consiguiente vulnerables (WordPress, Joomla, Drupal, etc) + algo típico en sysadmins sin mucha experiencia: servers mal configurados.

Este caso no fue la excepción por lo que nada podía hacer si la propia empresa no actualizaba esos WordPress & Joomla (la mayoría) que estaban siendo vulnerados a cada momento. Por lo que tuve que implementar una solución para al menos mitigar estas vulnerabilidades y en el caso de que comprometan un sitio, no puedan acceder a los demás.

Esto último lo explico de esta forma: Por defecto (incluso yo mismo he hecho esto en el pasado) la mayoría de los webservers con apache+php o nginx+php tienen definido en el php.ini una variable open_basedir que en el caso de los servidores Debian es /var/www esto hace que si logran comprometer un sitio y NO estan deshabilitadas las funciones exec,passthru,shell_exec,eval,system los defacers pueden ganar acceso a otros directorios donde se alojen otros sitios y poder comprometerlos sin necesidad de que esos otros sitios tengan vulnerabilidades php, sql injections, etc.
Se entiende entonces la importancia de definir correctamente variables y funciones en la configuración de PHP para aumentar las chances en la seguridad de un sitio.

Lo primero fue dar de baja Apache+PHP e instalar nginx+php-fpm, los servidores son Debian Squeeze por lo que tuve que instalar todos los paquetes desde los repositorios de dotdeb

Usuarios

Una vez instalado nginx+php-fpm quería ejecutar un pool por cada site y con usuarios (sin privilegios) y paths distintos, para lo cual tuve que crear usuarios nuevos iguales a nobody (por ejemplo) comenzando con el usuario y grupo.

root@lrserver01:~# groupadd --gid 65531 site1
root@lrserver01:~# useradd -M -d /nonexistent -c site1 -s /bin/sh -u 65531 -g 65531 site1

Nota importante, el usuario que acabamos de crear tiene el mismo perfil que nobody:nogroup

Permisos

Si el sitio se publica desde /var/www/site1.ngen.com.ar debemos cambiar los permisos y owner de todos los archivos y directorios.

root@lrserver01:~# find /var/www/site1.ngen.com.ar -type d -exec chmod 755 {} \; -exec chown site1. {} \;
root@lrserver01:~# find /var/www/site1.ngen.com.ar -type f -exec chmod 664 {} \; -exec chown site1. {} \;

Pools

Llegó el momento de configurar un pool, cuando se instala php-fpm por defecto se crea un pool “www” en /etc/php5/fpm/pool.d/www.conf, podemos mantenerlo, modificarlo o crear uno nuevo y tantos como necesitemos y/o soporte nuestro server.

Si decidimos crear un nuevo pool con hacer una copia de ese archivo (/etc/php5/fpm/pool.d/site1.conf) y editarlo es suficiente, los valores a editar van a ser:

listen = /var/run/php-fpm/site1.sock
listen.owner = site1
listen.group = site1
listen.mode = 0666
user = site1
group = site1
chroot =
chdir = /var/www/site1.ngen.com.ar

listen = /var/run/php-fpm/site1.sock es para diferenciar los distintos sockets de cada pool.

Una vez que tienen aceitado el procedimiento para crear distintos pooles separados con sus usuarios pueden ver los permisos de los directorios de cada sitio y los procesos (con su owner).

root@lrserver01:/etc/php5/fpm/pool.d# ls -lh /var/www/
total 16K
drwxr-xr-x 7 blog     blog     4.0K Nov 16 08:07 blog.ngen.com.ar
drwxr-xr-x 4 mavi     mavi     4.0K Nov  3 20:54 mavi.ngen.com.ar
drwxr-xr-x 3 www-data www-data 4.0K Dec  1  2012 ngen.com.ar
drwxr-xr-x 3 site1    site1    4.0K Nov 17 11:29 site1.ngen.com.ar

root@lrserver01:/etc/php5/fpm/pool.d# ps -eaf |grep '[p]hp-fpm'
root      7766     1  0 11:53 ?        00:00:00 php-fpm: master process (/etc/php5/fpm/php-fpm.conf)
mavi      7767  7766  0 11:53 ?        00:00:00 php-fpm: pool mavi
mavi      7768  7766  0 11:53 ?        00:00:00 php-fpm: pool mavi
site1     7769  7766  0 11:53 ?        00:00:00 php-fpm: pool site1
site1     7771  7766  0 11:53 ?        00:00:00 php-fpm: pool site1
www-data  7772  7766  0 11:53 ?        00:00:00 php-fpm: pool www-data
www-data  7773  7766  0 11:53 ?        00:00:00 php-fpm: pool www-data
blog      7777  7766  0 11:53 ?        00:00:01 php-fpm: pool blog
blog      7812  7766  0 11:53 ?        00:00:01 php-fpm: pool blog
blog      7814  7766  0 11:53 ?        00:00:01 php-fpm: pool blog

php.ini

Una de las ventajas que nos da utilizar php-fpm es que podemos ser lo más restrictivos posibles en la configuración usada en /etc/php5/fpm/php.ini y a medida que necesitemos determinada configuración mas permisiva podemos habilitarla en forma individual en cada pool.

Por lo que en este caso reemplazo la configuración x default de estos valores.

disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,exec,passthru,shell_exec,eval,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
allow_url_fopen = Off
allow_url_include = Off
expose_php = Off
log_error = Off
file_uploads = Off
sql.safe_mode = On
magic_quotes_gpc = Off
max_execution_time =  30
max_input_time = 30
memory_limit = 40M
cgi.force_redirect = On

pool.d/blog.conf

Esta es la configuración que estoy utilizando en este blog a modo de ejemplo, donde muestro qué valores utilizo y cómo aplicar cambios para sobreescribir el php.ini el cual tiene una configuración MUY restrictiva.

[blog]
user = blog
group = blog
listen = /var/run/php-fpm/blog.sock
listen.owner = blog
listen.group = blog
listen.mode = 0666
chroot =
chdir = /var/www/blog.ngen.com.ar
php_admin_value[file_uploads] = on
php_admin_value[memory_limit] = 64M
php_flag[display_errors] = off
php_admin_value[sql.safe_mode] = off
php_admin_value[memory_limit] = 64M
php_admin_value[open_basedir] = /var/www/blog.ngen.com.ar
php_admin_value[cgi.force_redirect] = on
php_admin_value[allow_url_fopen] = on
php_admin_value[allow_url_include] = off
php_admin_value[magic_quotes_gpc] = off
php_admin_value[upload_tmp_dir] = /tmp
php_admin_value[disable_functions] = exec,passthru,shell_exec,eval,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority

Es importante definir en los pools valores de php distintos a los que hay en /etc/php5/fpm/php.ini sino muchas aplicaciones no funcionarán correctamente, tal es el caso del valor “sql.safe_mode” con WordPress que debe estar en off para que funcione.

Tambien es MUY importante el valor “open_basedir” porque en combinación con el “chroot” del propio php-fpm aísla el acceso de la aplicación php a solamente el path que definamos, en el ejemplo /var/www/blog.ngen.com.ar y no se permite que los scripts php puedan escalar directorios.

Con estos simples pasos pude aislar exitosamente los distintos sitios de los servers, en todos los casos excepto en 1 pude evitar que los sitios se vuelvan a ser comprometidos, ademas tuve que probar y flexibilizar bastante los valores definidos en “disabled_functions” para que otros sitios funcionaran correctamente, eso depende de cada caso en particular.
Por lo menos en el caso del único sitio que quedó vulnerable (Joomla 1.1) y volvía a ser comprometido una y otra vez no afectó el normal funcionamiento de los demás, y en cuestión de dias fue actualizado y no presentó mas problemas.

Hay formas de aplicar el mismo tipo de seguridad con Apache+php-fpm pero prácticamente no utilizo Apache asi que no sé cómo hacerlo.

Algo que también se puede implementar para incrementar la seguridad de nginx y de las aplicaciones es utilizar naxsi un WAF (Web Application Firewall) similar a mod-security para Apache, algo que estoy implementando en este blog y testeando antes de ponerlo en producción en otros servers.

http://php.net/manual/en/install.fpm.configuration.php

http://www.cyberciti.biz/tips/php-security-best-practices-tutorial.html

http://myjeeva.com/php-fpm-configuration-101.html

https://docs.newrelic.com/docs/php/per-directory-settings