Antes de empezar

Reporté este bug dos veces con la intención de que se resolviera el problema y se protegiera la información de los ciudadanos. La primera vez que reporté el problema recibí confirmación de que sería atendido, pero ocho meses más tarde el problema no había sido resuelto.

Recientemente, reporté el bug por segunda vez e hice énfasis en el riesgo potencial de este tipo de vulnerabilidad y en mi intención de hacer público el problema siguiendo el modelo de “responsible disclosure”. Esta vez había gente diferente a cargo del otro lado y se atendió la situación con prontitud hasta corregir el problema.

A cambio de este reporte no se pidió recompensa, ni contratos para asesoría, ni contratos para programar, ni ninguna otra forma de compensación directa o indirecta. Tengo trabajo y estoy bastante ocupado con eso pero siento que es mi responsabilidad reportar este tipo de problema cuando lo encuentro para que se pueda resolver y evitar situaciones que lamentar.

En el pasado he reportado problemas como este a compañías pequeñas y grandes dentro y fuera de Puerto Rico. Esta es mi primera vez haciendo “responsible disclosure” con una entidad del Gobierno de Puerto Rico, lo que me ha tenido algo nervioso durante los últimos días.

Como todos sabemos, en nuestras islas cualquier cosa que hace o no hace el gobierno es una controversia y estoy seguro que algunos me criticarán por mi acercamiento y otros dirán lo bien que lo hice según les convenga para adelantar su punto de vista.

Me siento satisfecho y convencido de que hice lo correcto y logré conseguir el resultado deseado.

El bug

Según OWASP, el nombre técnico de este bug es un “insecure direct object reference“. Este tipo de bug permite a un usuario autorizado, cambiar el valor de un parámetro para tener acceso a un objeto para el cual no tiene autorización.

Trataré de explicarlo con un ejemplo simple.

Cuando usted usa Google Drive y está viendo un documento, el browser muestra arriba la dirección web de ese documento. Para propósitos de esta explicación digamos que la dirección es https://drive.google.com/documentos/101. La última parte de esa dirección (101) muestra el nombre o el valor que identifica a ese documento dentro de Google Drive.

Si usted modifica la dirección y reemplaza el 101 por 102 casi seguro verá un error en la pantalla. El error será algo como “no se encontró el documento” o “no tiene permiso para ver este documento”.

Lo que está pasando en este ejemplo es que Google Drive usa la última parte de la dirección para identificar cada documento que almacena para sus usuarios. El documento 101 te pertenece, así que lo puedes ver, pero el documento 102 probablemente sea de otro usuario así que no tienes permiso para verlo.

El API que usa el app de CESCO Digital para acceder la información de los ciudadanos tenía un bug que permitía ver la información de cualquier ciudadano con solo cambiar la parte final de la dirección, tal y como expliqué en el ejemplo anterior. Si su información personal era visible en la dirección que termina en 101 y usted cambia esa dirección para que termine en 102, usted vería la información de otro ciudadano.

Un atacante al darse cuenta de esta vulnerabilidad podría escribir un programa que de forma automática modifique la dirección empezando por el número 1, luego 2, 3, 4, 5 y así consecutivamente hasta llegar a ver y almacenar los datos de todos los usuarios.

Este ataque se podría completar en horas pero si se tratara de atacante un poco mas sofisticado, podría estirar el proceso a lo largo de multiples días o incluso semanas para asegurarse de no levantar alarmas o sospechas de los administradores del sistema.

Entre lo datos de los ciudadanos que quedaron expuestos están:

  • Nombre legal del ciudadano
  • Fecha de nacimiento
  • Dirección física
  • Últimos 4 dígitos del número de seguro social
  • Número de licencia de conducir
  • Fecha de expiración de licencia de conducir
  • Autos registrados con marca, modelo, color, tablilla y VIN

Un poco más técnico

Para detectar el problema lo único que había que hacer era usar un proxy e inspeccionar los requests que hace la aplicación al API. En mi caso uso Charles Proxy en iOS y mitmproxy en macOS.

En este vídeo se demuestra el bug antes de ser corregido

Lo primero que llama la atención es que desde el primer request al API se puede ver un valor en el header de “Authorization”. Esto es inesperado porque no he entrado ningún dato y el app ya está haciendo llamadas autenticadas al API. Aunque es poco común, no es necesariamente un problema.

GET /config HTTP/1.1
Host: cescodigital.dtop.gov.pr
Authorization: Basic dGVzdDoxMjM0NTppb3NDZXNjb0RpZ2l0YWw=
User-Agent: dtop_mobile/1 CFNetwork/976 Darwin/18.2.0
Connection: keep-alive
Accept: application/json
Accept-Language: en-us
Accept-Encoding: gzip, deflate, br

Al inspeccionar el valor que se está enviando me doy cuenta que esta usando “basic access authentication” y se que en esos casos se envía el valor codificado en base 64. Copié el valor y lo descodifiqué para darme cuenta que definitivamente era un valor “hard coded” dentro del app. El valor descodificado era “test:12345:iosCescoDigital”.

$ echo "dGVzdDoxMjM0NTppb3NDZXNjb0RpZ2l0YWw="|base64 -D
test:12345:iosCescoDigital

Al darme cuenta de esto pensé que quizás una vez se completara el cuestionario que hace el app para autorizarme se crearía algún tipo de sesión que me identificaría en los otros requests del app.

Entré mi número de licencia de conducir, últimos 4 dígitos del seguro social y mi fecha de nacimiento. El API validó mis datos y respondió con alguna información personal y un “uid” que me identifica dentro del sistema.

{
  "uid": "1234567",
  "name": "DEL PUEBLO, JUAN",
  "entityType": "ssid"
}

Inmediatamente el app cambia de pantalla y muestra mis datos personales. Cuando miro el request que hace el app puedo ver que la dirección que usa contiene el mismo “uid” del request anterior y cuando me fijo en los headers me doy cuenta que no hay ninguna información de sesión o algo parecido para identificarme. Lo único que puedo ver es el mismo authorization header “hard coded” del principio.

GET /entity/1234567 HTTP/1.1
Host: cescodigital.dtop.gov.pr
Authorization: Basic dGVzdDoxMjM0NTppb3NDZXNjb0RpZ2l0YWw=
User-Agent: dtop_mobile/1 CFNetwork/976 Darwin/18.2.0
Connection: keep-alive
Accept: application/json
Accept-Language: en-us
Accept-Encoding: gzip, deflate, br
{
  "principal": {
    "uid": "1234567",
    "name": "DEL PUEBLO, JUAN",
    "email": null,
    "entityType": "ssid"
  },
  "license": {
    "category": "3",
    "expirationDate": 946699200000,
    "dateOfBirth": 946699200000,
    "address": {
      "line1": "123 MAIN STREET",
      "line2": "",
      "city": "SAN JUAN",
      "state": "PR",
      "zipCode": "00901"
    },
    "unpaidFines": 0,
    "alert": {
      "type": "Warning",
      "text": "RenewLicense"
    },
    "id": XXXXXX
  },
  "vehicles": [
    {
      "vinNumber": "XXXXXXXXXXXXXXXXX",
      "registrationNumber": "XXXXXXXX",
      "year": XXXX,
      "make": "XXXXXX",
      "model": "XXXXXX",
      "color": "XXXXX",
      "plate": "XXXXXX",
      "tag": {
        "number": "XXXXXXXX",
        "expirationDate": XXXXXXXXXXX,
        "id": XXXXXXXX
      },
      "unpaidFines": X,
      "alert": null,
      "canRenewTag": false,
      "id": XXXXXXXX
    }
  ]
}

En este punto ya estaba casi seguro del problema por lo que hice “log out” y le dije a mi esposa que hiciera “login” para confirmar el problema. Siguió el mismo proceso que yo y tan pronto entró pude ver que sus datos personales venían de una dirección diferente pero estaba usando la misma información de autenticación que yo. Esto confirmó el bug.

Primer intento

Tan pronto confirmé el bug traté de reportarlo y logré conseguir una confirmación de que fue recibido y que se atendería a la mayor brevedad posible. Dejé el tema ahí y lo di por resuelto. Me olvidé de este asunto.

Hace más o menos una semana tuve que entrar al app para buscar la licencia nueva de mi auto. Me tocaba renovar el marbete. Cuando entro al app veo que todo se ve igual y recordé el bug, así que me dio curiosidad mirar a ver si resolvieron el problema que reporté.

Activé Charles Proxy para inspeccionar la comunicación del app con el API. Para mi sorpresa habían pasado ocho meses y no se había resuelto el problema. La información de todos los ciudadanos seguía expuesta y disponible para el que la quisiera obtener.

Segundo intento

Esta vez decidí tomar un acercamiento un poco más fuerte y proveer los incentivos necesarios para que las personas a cargo se vieran motivadas a resolver el problema con la prisa que requería.

Cuando reporté el problema esta vez expliqué nuevamente los riesgos y mi intención de hacer “responsible disclosure”. En otras palabras, que era importante ponernos de acuerdo en un “timeline” en el que las personas a cargo resolverían el problema y que en un tiempo específico yo publicaría este blog post que estas leyendo, explicando el problema en detalle.

Esto significa que si no lograran resolver el problema en un tiempo previamente acordado, la única opción que tendrían sería apagar el servicio por completo para evitar que atacantes que lean sobre la vulnerabilidad la exploten.

En ese momento no sabía cómo se tomaría la idea y si se volvería una situación incómoda pero para mi sorpresa se me escuchó y se le prestó la atención que pienso era necesaria. Durante el proceso hubo comunicación y cuando se me pidió, saqué tiempo y compartí todo lo que sabía sobre el problema con los involucrados del otro lado.

Para mantenerme honesto, tan pronto reporté el bug a las personas encargadas, también alerté de forma confidencial a miembros de la prensa bajo la condición de que no se publicara nada hasta por lo menos una semana después del reporte original.

Una semana me pareció un tiempo justo y suficiente para hacer los ajustes necesarios o apagar el servicio. Recordemos que el problema fue reportado ocho meses antes.

Tres días más tarde un equipo de desarrolladores resolvió el problema y al día siguiente y por petición de ese equipo, pude confirmar que el problema estaba resuelto.

La solución

Los desarrolladores implementaron una solución bastante ingeniosa que permite proteger los datos de los ciudadanos de este ataque sin la necesidad de actualizar el app.

Ahora, luego del cambio, cuando un usuario completa el proceso de login, el app recibe una respuesta idéntica a la anterior pero en el campo de “uid” el valor es ahora un hash SHA256 en vez de un número.

{
  "uid": "afa27b44d43b02a9fea41d13cedc2e4016cfc...",
  "name": "Juan del Pueblo",
  "entityType": "ssid"
}

En vez de tener direcciones como /entity/1234567 ahora tienen direcciones como esta /entity/afa27b44d43b02a9fea41d13cedc2e4…

No he visto el código pero de mi conversación con los developers y las pruebas que pude hacer me atrevo a adivinar lo siguiente:

  • El hash se calcula usando algún valor secreto, la fecha y/o data random.
  • El hash expira luego de un tiempo, forzando al usuario a entrar nuevamente su información de login para continuar usando el app.
  • El tiempo de validez es aproximadamente 30 minutos así que no debe tener impacto en la experiencia de los usuarios al usar el app.
  • Posiblemente están persistiendo el hash en la base de datos para poder expirarlo.
  • Es posible que también estén persistiendo la relación entre el hash generado y el ID interno del usuario.

En resumen, es suficientemente difícil adivinar un hash que sea válido para que valga la pena intentarlo.

Esta solución está cumpliendo el rol de una sesión que confirma que el usuario completó el proceso de login (autenticación) y que tiene permiso para ver ciertos datos (autorización) en todos los requests al API.

Timeline

2018-06-26Se reportó el problema por primera vez.
2018-06-26Recibí confirmación de que el reporte fue recibido y que se canalizaría.
2019-02-04Se reportó el problema por segunda vez.
2019-02-04Recibí confirmación de que el reporte fue recibido y que se trabajaría de inmediato.
2019-02-04Se compartió el bug confidencialmente con miembros de la prensa.
2019-02-07Se le explicó el problema al equipo de desarrollo, confirmaron el bug y dijeron que lo resolverían ese mismo día.
2019-02-08Confirmé que se resolvió el problema.
2019-02-12Se publicó este blog post.

Hace falta un proceso

Todo software tiene bugs. Esto siempre ha sido cierto y siempre será cierto. Algunos de estos bugs tienen implicaciones de seguridad y deben ser atendidos de una forma diferente. Nuestro gobierno es guardian de muchos datos sensitivos y deberían prepararse mejor para atender situaciones en las que estos datos estén en riesgo.

Hay muchas formas de hacer esto, pero hay que empezar por algún lado. Mi recomendación es publicar un proceso para reportar bugs de seguridad en el que los que reportan se sientan a salvo. Este documento debe contener nombres e información de contacto de las personas que atenderán los casos que se reporten. Además debe especificar términos de tiempo y los pasos que se seguirán para manejar la situación.

Luego de tener esto, las personas encargadas deben respetar ese proceso y colaborar con las personas que hagan los reportes. Tal y como pasó en mi segundo intento.

Como punto de partida pueden copiar el proceso que usamos en Gasolina Móvil que aunque no es perfecto, funciona cuando el volumen de reportes es bajo. Si lo usan recuerden cambiar los nombres e información de contacto. 😉

Una vez tengan algo simple funcionando, se podría pensar en implementar un programa de “bug bounties” aunque no se pague dinero por los reportes. La recompensa podría ser algo tan simple como tener una página web que contenga una lista de las personas que más bugs han reportado. Como los “leaderboards” que tienen algunos juegos de video.

Estoy seguro que hacer algo en esta dirección motivaría a personas con la capacidad de buscar, documentar y reportar responsablemente este tipo de problemas, logrando mejorar la seguridad y funcionamiento de los sistemas del gobierno.

Ver discusión en Twitter