Conectándonos al mundo (Parte 2)

11 de abril de 2018
La primera pregunta que nos surgiría sería: ¿Realizar un penetration test a nuestra API Rest es lo mismo que hacer un penetration test a nuestra aplicación web?
Cuando hablamos de un penetration test o prueba de seguridad a la aplicación web se entiende que este tipo de pruebas a nuestra API Rest estarán incluidas en el alcance de la misma. Pero, cuando nos adentramos en este mundo de la seguridad en las API nos damos cuenta de que la mayoría de las aplicaciones que suele utilizar un pentester dentro del penetration test, no cubre todo el espectro de las vulnerabilidades que podría tener un API Rest en si mismo. Es por ello, que, muy probablemente, en algunos puntos, tendrán que realizar pruebas manuales y con el tiempo, generarán sus propios scripts.
Si tratamos de identificar una metodología de testing, podríamos reutilizar cualquiera que deseemos. Pero, si estamos comenzando en este camino, también podemos definir un camino propio para realizar los test a las API Rest.
El primer paso dentro de nuestras pruebas de seguridad sería identificar la superficie de ataque, es decir, los endpoints del API Rest. Como vimos en nuestro post anterior, una de las formas de conseguir información es por medio de la documentación. Otra de las maneras es realizando alguna llamada a una de las funciones principales de la API para ver qué tipo de información devuelve. Cuando comenzamos a realizar diferentes request a posibles endpoints que vamos infiriendo, nos puede brindar información sobre cómo realizar el request correcto.
El siguiente request está realizado con la herramienta Postman, una de las más utilizadas para interactuar con las API e interpretar los response en diferentes formatos.
Captura de la herramienta PostMan
Podríamos incluso realizar un brute force por medio de un diccionario con lo que podrían ser los endpoint más comunes, como por ejemplo:
/user
/users/
/v1
/v2
/v1/users
auth
login
groups
Algo importante que quiero añadir en este punto: es bueno conocer la existencia de los frameworks. Muchos desarrollos de APIs hoy en día están basados en algún tipo de framework que permite la generación más rápida del componente de software.
El segundo paso será comprender cómo son los request y los response que se van generando, siempre y cuando utilicen JSON o XML, que suelen ser los más comunes. En este caso, podemos utilizar un proxy local como ZAP y Burp y de esta manera observar la información de respuesta en base a nuestro requerimiento.
Request analizada con Burp imagen
Request analizada con Burp
Response analizada  con Burp imagen
Response analizada con Burp
Es importante observar el formato de respuesta del JSON para comprender la estructura de la misma, o mejor dicho, cómo se están almacenando los datos. Esto también nos permitirá entender cómo se relacionan entre sí, y nos va a dar una idea muy clara de cómo podrían ser las sentencias de extracción de datos en el backend. Debemos de tener en cuenta que el backend podría ser una base de datos relacional, o una NOSQL, o cualquier tipo de estructuras de almacenamiento.
Podemos aprovechar también para verificar en esta instancia cuáles son los métodos permitidos, datos del servidor y los códigos de respuesta. Como sabemos, cada programador decide qué código de respuesta HTTP envía al browser, y si bien algunos códigos de respuesta como HTTP 200, 302, 404 suelen ser los más comunes, el programador puede decidir emplear estos mismos códigos para otro tipo de respuestas e incluso implementar códigos diferentes más detallados.
En la siguiente imagen vemos los códigos de respuesta utilizados en esta API Rest:
Códigos de respuesta comunes imagen
Códigos de respuesta comunes
  • Encabezamos HTTP anormales
  • URL con estructuras repetidas, parecidas. Esto nos podría dar la idea de que cierta parte del URL es un parámetro variable. Por ejemplo request como:
http://api.localhost.com/user/123ad/account.php
http://api.localhost.com/user/165ff/account.php
http://api.localhost.com/user/567mm/account.php
  • Verificar si los ítems a los cuales se realizan los request poseen extensiones. Supongamos que hemos identificado que los diferentes request al servidor son .php, .css, .png, etc, pero en ciertos momentos, algunos request no poseen extensiones. Esto nos podría también dar la idea de que este ítem en particular es un parámetro. Por ejemplo:
http://api.localhost.com/price/ListItems
Todo este tipo de actividades nos ayudarán a comprender la aplicación y poder pasar a la etapa de fuzzing y ser más efectivo. Para realizar el fuzzing y automatizar el proceso de identificación de vulnerabilidades, podemos utilizar herramientas open source como fuzzapi.
Pantalla de FuzziApi para lanzar un scan
Pantalla de FuzziApi para lanzar un scan
Informe de resultados de FuzzApi
Informe de resultados de FuzzApi
Hasta aquí, en nuestro test trabajamos con identificación y reconocimiento, y con técnicas de fuzzing, pero hay más cosas que analizar, y como sabemos, las API Rest deberán ir validando las comunicaciones para aceptar solamente aquellas que sean autorizadas. En el post anterior, mencionamos que uno de los mecanismos más utilizados es el basic authentication. En este nuestro browser envía las credenciales en el formato: user::password y todo ello lo codifica en base64. Como os imagináis, es muy simple capturar este paquete realizando un sniffing, decodificar los datos y acceder al usuario y contraseña en texto plano. Pero, también hay otras pruebas a los controles de acceso que podemos realizar, como por ejemplo, utilizando la técnica de password guessing, creando nuestro propio script, utilizando el NSE script de NMap http-brute o por medio de alguno de los proxy locales mencionados.
También mencionamos antes la autenticación basada en los tokens, como JWT, que de igual manera que en el caso anterior, si capturáramos el tráfico entre el cliente y el servidor y obtenemos un JWToken, nos encontraríamos con que el token está compuesto por 3 partes separadas por un punto (.) Las dos primeras están codificadas en base64, fácil de decodificar, y la tercera es una firma sobre las dos partes anteriores para verificar que ningún dato ha sido modificado. La firma que realiza el servidor contiene un valor secreto. En el caso de que consiguiéramos este valor, podríamos crear cualquier token, ya que bastaría con generar los dos primeros JSON y calcular un hash utilizando el valor secreto que averiguamos. Existen también otros mecanismos para saltar del algoritmo de firma, como por ejemplo, generando el primer JSON, pero con el campo del algoritmo en NONE. El último JSON de la firma lo vamos a dejar vacío ( {"alg":"none"} ), esto automáticamente hará que el JWT sea creado sin firma ni cifrado, tal como se especifica en la RFC 7519, de forma insegura. De esta manera, la aplicación va a decodificar el primer JSON (recuerden que es base64) y va ver que el algoritmo es NONE. A partir de allí nos va a permitir enviar cualquier dato, que será tomado como válido.
Supongamos que vamos a enviar la siguiente data:
{
''sub'': ''1234567890'',
''name'': ''Fabian Chiera'',
''iat'': 1516239022
}
Etnonces el header del JWT debe contener el tipo de algoritmo a utilizar:
{
''alq'': ''HS256''
''typ'': ''JWT''
}
Utilizaremos la key: apisecure
Por lo cual el JWT correspondiente será el siguiente:
JWT imagen
Cuando mi JWT posea el header alg:none, el tercer string correspondiente a la firma no será incluido ya que no se verificará la firma, y quedará de la siguiente manera:
Vean que los tres strings están separados por un punto (.)
Muchas librerías utilizadas dentro de los frameworks validan automáticamente un token que tiene en el header el valor “ none”.
Para realizar diferentes pruebas sobre los JWT pueden utilizar el sitio jwt.io
Supongamos ahora un caso, en donde la aplicación está utilizando firmas RSA, pero en lugar de recibir eso, recibe un token firmado con HMAC. ¿Qué sucederá? ¿La llave pública será entonces la llave privada del HMAC? Así es, porque en el framework de JWT se ejecutará algo como:
sign(tokenPayload, 'HS256', serverRSAPublicKey)
La diferencia es que utilizamos como “ secret key” la llave pública del servidor, la cual es pública.
Con este tipo de pruebas ofensivas podremos ver si podemos generar un token que pase perfecto el proceso de verificación. Existen muchas pruebas más que se pueden hacer, tal como lo mencionaba nuestro compañero Pablo González cuando hablaba de SAPPO.
Conclusiones
Como vemos, las API tienen varios aspectos a tener en cuenta. Si deseamos verificar la seguridad de nuestras interfaces, se debe realizar un proceso exhaustivo y debe ser considerado dentro de nuestras pruebas de seguridad de las aplicaciones como un apartado más. Siempre se debe de hacer dentro de un proceso ordenado y metodológico, de esta forma, no nos olvidaremos ningún punto a verificar.
La utilización de herramientas automatizadas nos brindará rapidez, tal como el caso que comenté de fuzzAPI, pero igualmente debéis de saber que se van a requerir pruebas manuales en las que es prioridad absoluta comprender cómo funciona el API.
Y recuerda, las API Rest no son las únicas formas de interfaces, hay diferentes tipos de API.
Para cualquier duda o comentario que tengas o que quieras hacer a nuestros expertos, accede a nuestra comunidad y dejad vuestras preguntas.
Fabián Chiera
Chief Security Ambassador en Panamá