lunes, 24 de agosto de 2015

Jugando con un Web Service, WS-Security y OWASP ZAP scripting

Hace algunas semanas y durante el proceso de análisis de la seguridad de un Web Service solo quedaba probar si este era vulnerable a un ataque de inyección SQL y, como no podía ser de otra forma, lo mejor era "tirarle a dar" con sqlmap. El problema era que al ejecutarlo devolvía un error:

<?xml version='1.0' encoding='UTF-8'?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<soapenv:Fault xmlns:wsse="...">
<faultcode>wsse:InvalidSecurity</faultcode>
<faultstring>
Nonce value : jO3csXbBeVKftuqx17niRQ==, already seen before for user name : username.
Possibly this could be a replay attack.
</faultstring>
<detail />
</soapenv:Fault>
</soapenv:Body>
</soapenv:Envelope>
Lo que sigue es una descripción del proceso para llegar hasta el error y superar la restricción que lo provoca, empezando por la configuración de soapUI para generar peticiones válidas, pasando por el desarrollo de un script para conseguir reenviarlas mediante OWASP ZAP proxy y finalizando con la ejecución correcta de sqlmap consiguiendo explotar un SQL injection sobre un Web Service totalmente funcional, sencillo, pero funcional, y del que incluyo el código fuente.

Desplegando el Web Service

El servidor utilizado para correr el Web Service es una máquina virtual Debian 8 sobre VMware Workstation.

Lo primero, crear la base de datos de MySQL que utilizará el Web Service; para ello incluyo un volcado con los datos que he utilizado para las pruebas, library.sql, y que podéis descargar y recuperar ejecutando:
# wget https://raw.githubusercontent.com/neofito/webservice_testing/master/library.sql
# mysql -u root -p < library-ws.sql
Si no queréis modificar el código y recompilarlo, para que el Web Service funcione correctamente el usuario root de MySQL debe tener asignada la contraseña "root" (sin las comillas), así que aseguraos de que MySQL solo sea accesible desde localhost y que solo vosotros tenéis acceso al sistema.

Java debe estar instalado y la variable de entorno JAVA_HOME seteada; si necesitáis una ayuda con el proceso el siguiente enlace puede servir como ejemplo.

Como contenedor para correr el Web Service utilizo Apache Axis2, concretamente la versión 1.6.1, que es preciso descargar y extraer en el directorio /opt:
# cd /opt
# wget ftp://ftp.cixug.es/apache//axis/axis2/java/core/1.6.2/axis2-1.6.2-bin.zip
# unzip axis2-1.6.2-bin.zip
# rm axis2-1.6.2-bin.zip
Necesitaremos también el driver JDBC para las conexiones a la bases de datos de MySQL desde el Web Service:
# wget http://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.36.zip
# unzip mysql-connector-java-5.1.36.zip
# mv mysql-connector-java-5.1.36/mysql-connector-java-5.1.36-bin.jar axis2-1.6.1/lib/.
# rm -Rf mysql-connector-java-5.1.36/
Nos quedaría descargar rampart, el cual permite utilizar las extensiones de WS-Security desde los Web Services corriendo sobre Axis2:
# wget http://archive.apache.org/dist/axis/axis2/java/rampart/1.6.1/rampart-dist-1.6.1-bin.zip
# unzip rampart-dist-1.6.1-bin.zip
# mv rampart-1.6.1/lib/* axis2-1.6.1/lib/.
# mv rampart-1.6.1/modules/* axis2-1.6.1/repository/modules/
# rm -Rf rampart-1.6.1/
Y ya por último el Web Service de ejemplo que se ejecutará desde Axis2:
# wget https://github.com/neofito/webservice_testing/blob/master/library-ws.aar?raw=true
-O axis2-1.6.1/repository/services/library-ws.aar
# export AXIS2_HOME=/opt/axis2-1.6.1 && sh /opt/axis2-1.6.1/bin/axis2server.sh
Si todo va bien desde el navegador obtendremos:


Desde este momento, la dirección IP del servidor Axis2 será la 192.168.2.96 y todos los "ataques" se realizarán desde otra máquina en la misma subred.

Web Services: un poco de teoría

Si atendemos a la definición de la Wikipedia:

"Un servicio web (en inglés, Web Service o Web services) es una tecnología que utiliza un conjunto de protocolos y estándares que sirven para intercambiar datos entre aplicaciones. Distintas aplicaciones de software desarrolladas en lenguajes de programación diferentes, y ejecutadas sobre cualquier plataforma, pueden utilizar los servicios web para intercambiar datos en redes de ordenadores como Internet. La interoperabilidad se consigue mediante la adopción de estándares abiertos."

Un Web Service es por tanto una API expuesta al exterior y con la que es posible interactuar mediante el protocolo HTTP, de forma que dos sistemas que utilizan tecnologías que pueden ser diametralmente opuestas puedan interactuar entre sí; una parte servidor publicaría y ofrecería el servicio mientras que una parte cliente (p.e. otra aplicación) consumiría y utilizaría los datos devueltos.

Actualmente, y hasta donde yo conozco, pueden distinguirse distintos tipos de Web Services:
  • Servicios RESTful, que basan su funcionamiento en los verbos proporcionados por HTTP (GET, POST, PUT, DELETE, etc) y la utilización de URIs como entidades. Normalmente utilizan JSON para la representación de los datos.
  • Basados en SOAP; son los que trataremos aquí.
  • Basados en XML-RPC y que explicados a muy grosso modo son servicios RPC cuya comunicación se encapsula en mensajes XML.
La clave de SOAP esta en el uso de documentos XML encapsulados en peticiones HTTP POST y que permiten el intercambio de datos. El protocolo SOAP, del cual existen actualmente dos versiones (1.1 y 1.2), define el formato de los documentos XML, dividiéndolos en una cabecera y un cuerpo dentro de un contenedor, siguiendo unas reglas específicas:


La definición del tipo de documento XML como SOAP (envelope) es obligatoria; la cabecera es opcional y permite añadir extensiones al protocolo mientras que el cuerpo del mensaje transporta lo que se conoce como payload y que se correspondería con los datos de la solicitud o los obtenidos como respuesta.

Para poder utilizar un Web Service el cliente debe conocer la interfaz pública, de igual forma que para poder utilizar cualquiera de las funciones de una API es necesario conocer el tipo y numero de los parámetros a fin de ejecutar correctamente la llamada. De esto se encarga WSDL.

WSDL es un formato especial de documento XML que describe la interfaz pública del Web Service, esto es, los métodos y parámetros necesarios para efectuar correctamente las llamadas. Por normal general cada Web Service publicado tiene asociado un WSDL que suele ser accesible públicamente. En nuestro caso la url asociada es:
http://192.168.2.96:8080/axis2/services/LibraryManagementService?wsdl

Generando peticiones con SoapUI

Vamos a utilizar SoapUI para que nos abstraiga del proceso de generación de peticiones SOAP válidas. soapUI es una herramienta open source, aunque también hay una versión comercial, y que permite testear el funcionamiento de Web Services haciendo las veces de cliente.

Suponiendo que ya lo hemos descargado e instalado, una vez iniciado y desde el menú File, New SOAP Project:


Cuando pulsemos el botón OK la aplicación se descargará y analizará el WSDL correspondiente a nuestro Web Service, identificando de forma automática los métodos disponibles y generando plantillas para las peticiones en las dos versiones del protocolo SOAP (1.1 y 1.2):


Si, por ejemplo, desplegamos LibraryManagementServiceSoap11Binding, getBookInfo y hacemos doble click sobre Request 1 se nos abrirá una nueva ventana con el formato del mensaje SOAP que permite hacer uso del método. Vamos a completar el campo correspondiente al teórico isbn del libro a consultar:


y ejecutaremos la consulta pulsando el botón de color verde situado justo encima de la ventana con la petición, de forma que, en la ventana adyacente, aparecerá el siguiente resultado:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<soapenv:Fault xmlns:wsse="...">
<faultcode>wsse:InvalidSecurity</faultcode>
<faultstring>Missing wsse:Security header in request</faultstring>
<detail/>
</soapenv:Fault>
</soapenv:Body>
</soapenv:Envelope>
Parece que hemos olvidado algún campo, concretamente algo relacionado con WS-Security.

¿Qué es WS-Security?

Son una serie de extensiones para el protocolo SOAP que permiten agregar autenticación mediante el uso de tokens, integridad y no repudio mediante la firma de los mensajes y confidencialidad mediante cifrado. La forma de utilizar estas extensiones es mediante la definición de políticas en formato WS-SecurityPolicy (etiquestas wsp:Policy) en la cabecera del mensaje XML correspondiente al protocolo SOAP.

Detengámonos a analizar manualmente el WSDL correspondiente al web service, concretamente el contenido delimitado por la etiqueta <wsp:Policy ...>:
<wsp:Policy wsu:Id="UTOverTransport">
<wsp:ExactlyOne>
<wsp:All>
<sp:TransportBinding>
<wsp:Policy>
<sp:TransportToken>
<wsp:Policy/>
</sp:TransportToken>
<sp:AlgorithmSuite>
<wsp:Policy>
<sp:Basic128/>
</wsp:Policy>
</sp:AlgorithmSuite>
<sp:Layout>
<wsp:Policy>
<sp:Lax/>
</wsp:Policy>
</sp:Layout>
<sp:IncludeTimestamp/>
</wsp:Policy>
</sp:TransportBinding>
<sp:SignedSupportingTokens>
<wsp:Policy>
<sp:UsernameToken sp:IncludeToken="..."/>
</wsp:Policy>
</sp:SignedSupportingTokens>
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
La etiqueta <sp:SignedSupportingTokens> permite especificar los parámetros de autenticación para comunicarse con el Web Service y, en este caso concreto, nos obliga a incluir necesariamente en cada petición un token de usuario (un par de valores usuario/contraseña), para poder interactuar correctamente.

Si disponemos de estos valores o debemos obtenerlos por nuestra cuenta no es el objeto del presente artículo; supondremos para este caso concreto que si los conocíamos previamente así que modificaremos la configuración de SoapUI para incluirlos en cada petición.

Dentro de soapUI los parámetros pueden indicarse de forma global para todas las consultas a los métodos incluidos en el proyecto o de forma individual para cada request. Lo haremos así, teniendo la request correspondiente al método getBookInfo seleccionada, en la parte inferior de la ventana de soapUI, donde dice Request Properties agregaremos tanto en el campo Username como en Password el valor neofito y en WSS-Password Type seleccionaremos la opción PasswordText del desplegable:


El significado para cada una de las opciones del desplegable:
  • PasswordText, la opción seleccionada para nuestro ejemplo: implica que la contraseña se incluye en cada una de las peticiones y como tal viajará en texto claro. Suele configurarse así para simplificar la programación y configuración del Web Service derivando la complejidad de añadir una capa de cifrado a las comunicaciones mediante el uso de HTTPs en la configuración del servidor; en nuestro ejemplo, y por simplificar al máximo, este no es el caso, pero sería lo recomendable.

  • PasswordDigest: en este caso la contraseña no es tal, sino que se correspondería con un resumen codificado en base64 y calculado utilizando la contraseña original y otros parámetros incluidos en la petición. De esta forma el servidor sería capaz de corroborar que la contraseña es la correcta pero cualquier otro elemento que interceptase una petición no podría derivarla a partir de los datos incluidos en el mensaje SOAP.
Si ahora ejecutamos nuevamente la request vemos que obtenemos un mensaje de error completamente distinto:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<soapenv:Fault xmlns:wsse="...">
<faultcode>wsse:InvalidSecurity</faultcode>
<faultstring>Missing Timestamp</faultstring>
<detail/>
</soapenv:Fault>
</soapenv:Body>
</soapenv:Envelope>
¿Qué ha ocurrido esta vez? Si analizamos nuevamente el WSDL correspondiente a nuestro servicio localizaremos una etiqueta relacionada directamente con el error anterior:
<wsp:Policy wsu:Id="UTOverTransport">
<wsp:ExactlyOne>
<wsp:All>
<sp:TransportBinding>
<wsp:Policy>
...
<sp:IncludeTimestamp/>
</wsp:Policy>
</sp:TransportBinding>
...
</wsp:All>
</wsp:ExactlyOne>
</wsp:Policy>
Incluyendo un valor de timestamp en la cabecera de la petición limitamos la posibilidad de reenviar posteriormente el paquete para completar un ataque dado que lo más probable es que la validez del mismo haya caducado.

Nuevamente desde la ventana con las Request Properties agregaremos un valor de 60 para el campo WSS TimeToLive:


Pero antes de ejecutar la petición vamos a modificar la configuración de SoapUI de forma que pongamos en el medio de la comunicación a OWASP zaproxy, que ejecutaremos previamente y que, si no se ha modificado ex-profeso la configuración y no hay ningún otro servicio ocupando el puerto, escuchará por defecto en localhost:8080.

En SoapUI desde el menú File, Preferences, opción Proxy Settings configuraremos los valores adecuados tras lo que finalizaremos pulsando el botón OK:


Ahora sí, ejecutaremos la petición y recibiremos un mensaje confirmando que se ha obtenido la información correspondiente al libro con el isbn indicado:
<soapenv:Envelope xmlns:soapenv="...">
<soapenv:Header>
<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="...">
<wsu:Timestamp wsu:Id="Timestamp-1" xmlns:wsu="...">
<wsu:Created>2015-08-24T18:09:37.437Z</wsu:Created>
<wsu:Expires>2015-08-24T18:14:37.437Z</wsu:Expires>
</wsu:Timestamp>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<ns:getBookInfoResponse xmlns:ns="http://neofito/library/book/types">
<ns:return xsi:type="ns:Book" xmlns:xsi="...">
<ns:autor>Albert Einstein</ns:autor>
<ns:isbn>0001</ns:isbn>
<ns:titulo>La teoria de la relatividad</ns:titulo>
</ns:return>
</ns:getBookInfoResponse>
</soapenv:Body>
</soapenv:Envelope>
Y si consultamos la interfaz de zaproxy veremos el contenido completo tanto de la petición enviada como de la respuesta recibida:


Reenviando peticiones mediante OWASP zaproxy

Hasta el momento nos hemos servido de soapUI para abstraernos de conocer en detalle el protocolo SOAP y las extensiones WS-Security a la hora de generar peticiones válidas para interactuar con un método determinado de un Web Service, pero a partir de ahora no lo utilizaremos más y nos centraremos en OWASP zaproxy.

Teniendo seleccionada la petición válida generada mediante soapUI y respondida correctamente por el servidor haremos clic con el botón derecho del ratón sobre cualquiera de los cuadros de texto correspondiente a la misma (cabecera o cuerpo) y seleccionaremos la opción Reenviar:


Si ahora directamente pulsamos el botón SEND lo más probable es que obtengamos un error como respuesta debido a que el time-to-live de la petición ha caducado:
HTTP/1.1 500 The message has expired (WSSecurityEngine: Invalid timestamp
The security semantics of the message have expired)
Date: Mon, 24 Aug 2015 18:23:10 GMT
Server: Simple-Server/1.1
Content-Type: text/xml; charset=UTF-8


<?xml version='1.0' encoding='UTF-8'?>
<soapenv:Envelope xmlns:soapenv="...">
<soapenv:Body>
<soapenv:Fault xmlns:axis2ns1="...">
<faultcode>
axis2ns1:MessageExpired
</faultcode>
<faultstring>
The message has expired (WSSecurityEngine: Invalid timestamp
The security semantics of the message have expired)
</faultstring>
<detail />
</soapenv:Fault>
</soapenv:Body>
</soapenv:Envelope>
Para evadir esta protección frente a ataques de reenvío de solicitudes nos serviremos de la posibilidad de ejecutar secuencias de comandos (a.k.a. scripts) para alterar cada uno de los paquetes que pasan a través de ZAP.

IMPORTANTE: para ejecutar el script de ejemplo es necesario haber instalado previamente desde el Marketplace de OWASP zaproxy la extensión que permite utilizar Python como motor de scripting para ZAP; menú Ayuda, Comprobando actualizaciones, pestaña Marketplace, botón Instalar selección:


Desde el menú Ver, Show Tab seleccionaremos la opción Scripts tab. Haremos clic con el botón derecho del ratón sobre el elemento proxy, New script, e introduciremos los siguientes datos:


En la parte superior de la ventana nos aparecerá una plantilla de script a modo de ejemplo que sustituiremos por el contenido disponible aquí, chNonce.py.

Una vez incluido el código desde la ventana de la derecha, haremos clic con el botón derecho del ratón sobre el nombre asociado al script, Enable Script(s):


No podemos utilizar la funcionalidad de reenvio de zaproxy para confirmar el funcionamiento correcto, ya que el script no actuará sobre dichas peticiones. En su lugar lanzaremos sqlmap y si todo funciona correctamente conseguiremos explotar un SQL injection presente en todos los métodos del Web Service utilizado para el ejemplo.

Guardaremos el contenido de la petición válida enviada mediante soapUI y capturada por ZAP en un fichero de nombre, request.txt, dentro del directorio donde tengamos sqlmap y, a continuación, lanzaremos el ataque ejecutando el siguiente comando:
C:\sqlmap> python sqlmap.py --proxy=http://localhost:8080 -r request.txt -p typ:isbn
--current-user --threads=10 --answers="process=Y,proceed=C,skip=Y,provided=Y,keep=N"
El comando anterior hace que todas las peticiones lanzadas por sqlmap pasen por zaproxy (--proxy), utiliza el contenido del fichero request.txt como plantilla para el contenido de las peticiones (-r request.txt), centra el ataque sobre el valor del parámetro typ:isbn (-p typ:isbn), trata de obtener el usuario utilizado para ejecutar las consultas (--current-user), lanza diez threads simultáneos para acelerar la ejecución (--threads=10) y automáticamente responde a las preguntas planteadas por sqlmap con los valores indicados (--answers="process=Y,proceed=C,skip=Y,provided=Y,keep=N").

Si el script funciona correctamente sqlmap reenviará en varias ocasiones la petición inicial, 58 veces para ser exactos en el entorno utilizado para las pruebas, y será capaz de determinar la versión de MySQL así como que el usuario utilizado para acceder a la base de datos por la aplicación es root@localhost:


Analizando el script

El motor de scripting de ZAP nos permite acceder al contenido de las peticiones enviadas a través del proxy en la función:
proxyRequest(msg)
al igual que nos permite acceder al contenido de las respuestas recibidas a traves del proxy en la funcion:
proxyResponse(msg)
El parámetro msg se correspondería, en ambos casos, con un objeto que contiene todos los datos que componen el paquete HTTP.

Lo primero que hace el script es obtener el verbo/método utilizado para enviar el paquete, actuando únicamente si este se corresponde con POST:
if msg.getRequestHeader().getMethod() == "POST":
A continuación, y utilizando expresiones regulares, modificamos "al vuelo" los parámetros que dependen del timestamp en que se envió la petición así como el Nonce. En la especificación de WS-Security el Nonce asociado a un tipo de de autenticación mediante UsernameToken establece que se trata de un valor aleatorio, y por lo tanto difícilmente repetible, y codificado en base64; el proceso de generación del valor del Nonce se lleva a cabo dentro de la función:
generateNonce()
De este modo, y por cada petición enviada a través del proxy, zap ejecuta el script que se encarga de actualizar los valores del timestamp para el momento del envío (wsu:Created), el timestamp de validez de la petición que será igual al valor del timestamp del envío más el time-to-live (wsu:Expires) y el valor del nonce generado aleatoriamente y único por cada una de las peticiones (wsse:Nonce).

And that's all!

Referencias

Web Services Testing with soapUI
by Charitha Kankanamge
Published by Packt Publishing Ltd.
ISBN 978-1-84951-566-5

Securing Web Services with WS-Security
by Jothy Rosemberg, David L. Remy
Sams Publishing
ISBN 0-672-32651-5

1 comentario:

Ruben dijo...

Gran articulo felicidad yo estuve presente en el momento de toda esta historia y estoy orgulloso