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:
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
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
- 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
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:
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á