Vaxicode Vérif...ie pas vraiment!

vaxicode quebec android ios
Zuyoutoki

La fameuse application Vaxicode Vérif est sortie, et la promesse "le code QR ne peut être falsifié" en a fait rire plus d'un. Voici comment j'ai relevé le défi de M. Caire en créant mon propre code QR falsifié...

Note

Cet article est publié après vérification que la vulnérabilité présentée a été corrigée. Il se veut un moyen d'attirer l'attention sur l'importance du développement sécuritaire d'applications.

Un peu de contexte

Depuis le 13 mai 2021, les Québécoises et les Québécois peuvent récupérer leur code QR sur un site web développé par Akinox pour le gouvernement du Québec 1 . Ce n'est que 3 mois plus tard, le 24 août 2021, que le détail de l'application du passeport vaccinal a été communiqué 2 . Trois jours plus tard, le Québec se réveille avec un article qui annonce qu'un informaticien anonyme aurait trouvé une vulnérabilité dans l'application du passeport vaccinal 3 .

Comme beaucoup, j'ai voulu partir à la chasse aux vulnérabilités lorsque l'application est devenue disponible. Je me suis dit que si je dois faire confiance à cette application, il faut que je confirme qu'elle a un minimum de crédibilité. Ça s'est passé à peu près comme ça...

Un mercredi matin

Mercredi le 25, je me réveille à quelques jours de la fin de mes vacances estivales et je me rappelle que l'application du passeport vaccinal était dû pour sortir aujourd'hui. Je fais le tour du web et je trouve le nom de l'application : Vaxicode. Un nom un peu funky, mais un nom qui donne assez peu de résultats sur Github donc j'ai pas trop chialé.

Je suis rapidement tombé sur cet issue qui pointe vers le code source de l'application sur iOS . N'ayant pas d'appareil iOS à portée de la main, je décide de tenter ma chance à installer l'application Expo Go et ça me permet de rouler le code sur un appareil Android en utilisant un des liens présent dans l'issue sur Github.

J'utilise beautifier.io pour rendre le code un peu plus lisible et j'ouvre dans VS Code pour pouvoir faire des Ctrl+F plus facilement. Quand j'ai vu le nombre de lignes, j'ai fait le saut, mais j'étais bien content d'avoir choisi un éditeur avec une fonction de recherche.

J'ai commencé par rechercher covid et répertorier les différents liens qui me semble intéressant: - covid19.quebec.ca - https://covid19.quebec.ca/PreuveVaccinaleApi/issuer - https://smarthealth.cards#covid19 - https://covid19.quebec.ca/vaxiupdates/vaxi.json - https://covid19.quebec.ca/vaxicode/confidentialite.html

Un de ces liens attire particulièrement mon attention, c'est covid19.quebec.ca puisqu'il se trouve dans une fonction nommée payloadSigningStatus . Le nom de la fonction me semble assez intéressant, donc j'ai commencé par là. Voici donc un snippet :

JavaScript
var h = (function() {
[... snip ...]
    key: "payloadSigningStatus",
    get: function() {
        var n, t = {
                valid: !1,
                isQCProof: !1
            },
            l = this.getLogFromLevel(v.LogLevels.ERROR);
        if (t.valid = !l || 0 == l.length, t.valid && null != (n = this.payload) && n.iss) {
            var o = new c.URL(this.payload.iss);
            t.signingDomain = o.hostname,
            t.isQCProof = "covid19.quebec.ca" == t.signingDomain,
            t.isTestProof = "aki-shc-generator.vercel.app" == t.signingDomain,
            t.entityName = y[t.signingDomain]
        }
        return t
[... snip ...]
})();
e.VxHealthCard = h
Le code est pas très joli, mais c'est ça qui est ça. Il faut saigner des yeux si on veut trouver des vulnérabilités (please prove me wrong😢).

En regardant le contexte autour, je remarque la chaîne VxHealthCard et une couple d'assignations de variable et je me dis que ça ferait du sens que ce soit l'objet qui contient les informations sur le code QR scanné.

De ce que je comprend du snippet, l'objectif de la fonction payloadSigningStatus est de retourner un résultat qui dit : 1. si la signature du code QR est valide; et 2. si l'entité qui a signé le code est le gouvernement du Québec.

Pour détecter si le code QR est valide, la fonction en appelle une autre qui retourne un tableau de toutes les erreurs que l'objet a rencontré depuis sa création. Si cette fonction ne retourne pas d'erreur, t.valid sera vrai. Ensuite, le if valide que le payload (le contenu du QR code) existe et qu'il contient un élément iss . Le iss , c'est pas l'International Space Station, mais l'entité qui a signé le token contenu dans le code QR (le issuer ). Comment ça se fait que je sais ça? On s'en reparle tantôt.

Une fois qu'on a confirmé qu'on avait bien un code QR qui n'a pas fait d'erreur jusqu'à présent et qu'il a bien un iss , c'est l'heure de valider que le iss est valide. Pour faire simple, il faut que le hostname soit égal à covid19.quebec.ca pour confirmer que c'est bien une preuve du Québec.

En lisant ce morceau de code-là (et en le re-lisant et le re-re-lisant), je suis de plus en plus convaincu qu'il y a un gros problème de validation de la signature du code QR. Pourquoi? Je ne vois nulle part une vérification cryptographique de la signature. Nulle part, mais j'ai vu des messages passer sur différents Discord comme quoi qu'ils avaient identifié la librairie qui valide la signature. Je me dis que j'ai regardé le code un peu trop longtemps et que pour y voir plus clair, je devrais me changer les idées en allant prendre une marche...

Après une marche un peu longue

Je reviens à mon ordi et je me dis que je vais rechercher comment fonctionne les codes QR. Je me souvenais avoir utilisé l'outil de fproulx quelques mois auparavant pour lire le contenu de mon code, donc je décide de sortir mon code QR des boules à mites (aka le redownload du site web) et m'amuser avec.

Je réalise en jouant avec mon jouet pixelisé que nos codes QR, c'est juste un JWT glorifié. Okay fine, le standard est un peu plus détaillé , mais pour notre besoin, c'est suffisant de se dire que c'est juste ça. Un JWT, c'est un JSON Web Token, une sorte de code avec une signature numérique qui permet de confirmer que son contenu n'a pas été modifié depuis qu'il a été créé. Pas mal cool, assez bien documenté, mais surtout assez facile à générer, autant pour le gouvernement du Québec que pour le "gouvernement" de blog.oki.moe . :)

Le jeudi matin

Après avoir jouer avec mon code QR la veille, je me sens motivé pour essayer de fabriquer mon propre code QR. On va commencer par en faire des invalides, pis on verra pour la suite.

Équipé de l'exemple donné dans la documentation de smarthealth.cards , j'ouvre l'application du lecteur et je le scan. Un beau résultat négatif (je m'y attendais), mais ça me donne une idée de ce que je peux arriver à faire de mon bord :

Image du résultat de l'exemple de smarthealth.cards

Cette journée-là, je feelais pas fort fort, donc j'ai essayé un peu sans trop réfléchir à ce que je faisais. J'ai fini par reprendre tel quel le code QR de l'exemple et à juste modifier l'élément iss pour celui du gouvernement que j'ai récupérer de mon propre code QR: https://covid19.quebec.ca/PreuveVaccinaleApi/issuer . Sans surprise, ça s'est soldé par un échec et un nouveau message d'erreur :

Image du résultat lorsque la signature n'est pas valide

Après mon échec lamentable, je me dis qu'il y a probablement pas de vulnérabilité, ou que je suis juste pas capable de l'exploiter si ce que j'ai trouvé en est bien une. Je me dis que je devrais laisser ça de côté pour quelques jours et revenir dessus à tête reposée. Who knows, je pourrais peut-être trouver des nouveaux éléments.

Un article qui choque

Et oui, il fallait arrêter d'essayer pour que quelqu'un viennent trouver la vulnérabilité et l'expose dans les médias... J'aurais peut-ête pu la trouver avant qu'il y en aille une d'annoncée, mais j'ai choisi de prendre un break. Je vais le savoir pour la prochaine fois...

J'ai décidé de passer la journée à écouter des séries, mais j'arrivais pas à m'enlever de la tête que si j'avais fait un peu plus d'effort, j'aurais pu le trouver. Fack je me suis motivé pis j'ai recommencé le lendemain (oui oui, je prend beaucoup de pauses, chuttt).

Me sentant un peu plus motivé, je reprend le flambeau le vendredi matin. Je vais m'inspirer du code source du shc-covid19-decoder de fproulx pour savoir quelles librairies utiliser, reprendre la logique pour le décodage, mais l'appliquer pour encoder un code QR. J'utilise keytool.online pour me générer une clé privée qui va me permettre de signer mes codes QR et je suis parti pour la gloire.

Je reprends l'exemple de smarthealth.cards et je le modifie pour qu'il contienne le domaine de mon "gouvernement". Je m'assure de rendre ma clé publique accessible en ligne comme le demande le standard et je scanne le code QR :

Image du résultat de l'exemple de smarthealth.cards lorsque je change le iss et que je le signe avec ma propre clé

On fait du progrès! On a maintenant un code qui se scanne par l'application, mais on se fait dire que ça vient pas du gouvernement du Québec. Est-ce qu'on peut juste changer le iss pour celui du gouvernement et le signer avec notre propre clé? On va s'essayer :

Image du résultat de l'exemple de smarthealth.cards avec le iss du Québec, mais signé avec ma propre clé

Quoi? Euhh, c'est une erreur ou un succès? Je me suis posé la question et c'est sur le Discord du Hackfest qu'on m'a pointé dans la bonne direction : il semblerait que ce soit le message d'erreur qui s'affiche lorsque la signature est bonne, mais que les vaccins reçus n'offrent pas une protection suffisante. C'est super, j'ai réussi à forger un code QR valide et il ne me manque qu'à m'assurer que le code affiche que la protection est correcte. On m'a suggéré d'aller voir le code QR de Jean Untel qui est disponible sur la page du App Store puisqu'il contient des données de vaccination valide.

En comparant le fhirBundle des deux codes, je réalise qu'ils diffèrent au niveau des données qu'ils contiennent :

Image qui compare le fhirBundle des deux codes

Ne sachant pas trop quoi prendre, je décide de copier la section entry du code de M. Untel pour l'intégrer au code de Mr. Anyperson (j'adore leurs noms). Je conserve https://covid19.quebec.ca/PreuveVaccinaleApi/issuer comme iss , je signe le nouveau code et je le scanne :

Image du résultat du code falsifié

Victoire! J'ai désormais réussi à produire un code QR de toutes pièces et de le faire apparaître comme légitime dans le validateur officiel. Un peu effrayant.

Pour m'assurer que c'était pas de la chance, je décide de fermer l'application et de l'ouvrir à nouveau. Je me disais qu'après avoir fait plusieurs tests, peut-être que l'application était dans un état particulier et c'est ce qui a fait que la signature a été validée. Je teste à nouveau et, malheur, je reçois une erreur, la même qu'au début :

Image du résultat après redémarrage de l'application

Après avoir perdu la capacité de produire des codes falsifié, j'étais un peu déçu. J'ai réssayé plein d'affaires pour finalement comprendre le pourquoi, mais j'ai pas pris assez de notes pour montrer toutes les tentatives. Ce que je savais, c'était que si je rescannais tout mes codes dans le même ordre, ça fonctionnait à nouveau.

La vulnérabilité, en 5 étapes faciles

  1. Il faut avoir un issuer correctement configuré selon le standard de smarthealth.cards .
  2. Il faut signer un code QR qui contient le iss configuré précédemment. Ça va télécharger la clé dans le keystore local et rendre toutes les signatures par cette clé valide.
  3. Il faut signer un deuxième code QR avec la même clé, mais en utilisant https://covid19.quebec.ca/PreuveVaccinaleApi/issuer comme iss .
  4. ...
  5. Profit!

Cas d'utilisation

Mettons que le premier code s'appelle un code malicieux et que le deuxième s'appelle un code falsifié.

Dans un scénario, un individu a en sa possession ces deux codes et il veut entrer dans un lieu réservé à la population vaccinée. Il présente d'abord le code malicieux qui afficherait une erreur sur le téléphone du commerçant. Puisque le nom peut être choisi arbitrairement, on pourrait afficher Please Scan Again . Le commerçant serait peut-être un peu confus, mais ça donne le temps à l'individu de sortir son code falsifié et de demander au commerçant de le "rescanner". Le deuxième code se fait scanner sans erreur et l'individu peut accéder au lieu restreint.

Une fois le code malicieux scanné, on peut faire valider autant de code falsifié qu'on veut, tant et aussi longtemps qu'on ne ferme pas complètement l'application via le multi-tâches.

Réflexions

C'est assez intéressant de voir qu'en très peu de temps après que l'application soit devenue disponible publiquement, la communauté a réussie à trouver et exploiter une vulnérabilité qui permet de briser la seule fonctionnalité de l'application : valider si le code QR est légitime.

Je suis satisfait d'avoir réussi à la découvrir et à l'exploiter, mais je suis déçu de voir que si personne ne s'était essayé à briser l'application, on l'aurait pas su avant un bon bout. Je veux dire, ça fait dur. Si des bénévoles qui n'ont pas accès au code source sont capable de trouver des vulnérabilités en quelques heures, est-ce qu'il y a vraiment eu des audits qui ont été faits avant que l'application devienne publique? Je mettrais un gros (x) doubt là-dessus.

Anyway, on verra ce qui se passe avec ça, mais en ce qui me concerne, j'espère que mon write-up a pas été trop chaotique à votre goût et je souhaite que si vous avez utilisé une méthode différente pour exploiter l'application que vous fassiez un write-up à votre tour pour que j'aille le lire. Si vous avez des commentaires, des write-ups, avez vu des fautes/typos, mon courriel est sur la front page :)

Merci à la communauté du Hackfest pour les hints qui m'ont encouragés à continuer de chercher et à finalement réussir ce CTF-là :)