Mon premier hack: un site qui vous permet de définir n'importe quel mot de passe utilisateur

Récemment, j'ai trouvé une vulnérabilité intéressante qui permet à tout utilisateur de définir un site spécifique pour définir n'importe quel mot de passe. Cool, hein?

C'était drôle et je pensais pouvoir écrire un article intéressant.

Vous êtes tombé dessus.



Remarque: l' auteur de l'article traduit n'est pas un spécialiste de la sécurité de l'information, et c'est sa première excursion dans le monde de l'injection SQL. Il demande à être "condescendant envers sa naïveté".

Attention: l' auteur de l'article traduit ne dévoilera pas le site présentant cette vulnérabilité. Non pas parce qu'il en a informé le propriétaire et qu'il est lié par le silence, mais parce qu'il veut se préserver de la vulnérabilité. Si vous découvrez ce site, veuillez garder la bouche fermée (tsyts).

Vous savez, c'est ainsi que vous ouvrez parfois un site Web dans la boîte à outils pour un développeur, examinez le code minifié et les demandes de réseau sans but. Et soudain, vous remarquez que quelque chose ne va pas ici. Pas du tout comme ça. J'ai donc fait quelque chose de similaire avec la page de profil utilisateur sur l'un des sites et j'ai remarqué que lorsque vous activez et désactivez la notification de réception, la page envoie une demande réseau:

/api/users?email=no 

Et j'ai pensé: je me demande s'ils ont permis une certaine bêtise? Peut-être que je devrais essayer l'injection SQL?

J'ai cherché sur le net des «petites tables de bobby xkcd» pour rafraîchir ma mémoire sur la façon de faire des injections SQL - je ne les aime pas - et je me suis mis au travail.

Dans l'onglet réseau Chrome, j'ai copié la demande (Copier> Copier en tant que récupération) et collé le résultat dans un fragment afin que la demande puisse être lue:

 fetch('https://blah.com/api/users', { credentials: 'include', headers: { authorization: 'Bearer blah', 'content-type': 'application/x-www-form-urlencoded', 'sec-fetch-mode': 'cors', 'x-csrf-token': 'blah', }, referrer: 'https://blah.com/blah', referrerPolicy: 'no-referrer-when-downgrade', body: 'email=no', // < -- The bit we're interested in method: 'POST', mode: 'cors', }); 

Le reste de l'article est consacré à s'occuper de la ligne du body - c'est un moyen de transport pour envoyer des instructions au serveur.

Tout d'abord, j'ai essayé de changer mon nom de famille en définissant la valeur dans la colonne lastName , en lastName concentrant simplement sur son nom:

 { // ... body: `email=no', lastName='testing` } 

Rien d'intéressant ne s'est produit. Ensuite, j'ai fait de même avec last_name , puis j'ai tenté ma chance avec le surname de surname - et oups! - la page a remplacé mon nom de famille par «testing».

C'était très excitant. J'ai toujours considéré les injections SQL comme une légende de livre. Le fait qu'il n'ouvre pas vraiment le monde au code qui insère les entrées utilisateur directement dans les expressions SQL.

Un peu de philosophie
Récemment, j'aborde de nombreuses questions dans ma vie du point de vue de la loi de Sturgeon: "90% de tout ce qui est autour est des ordures." J'ai réalisé que si vous supposez que tout est fait correctement, vous perdez beaucoup d'opportunités. Je pense que cette nouvelle incrédulité dans l'humanité m'a donné suffisamment de confiance pour même entreprendre cette expérience.

Pour tous les non-initiés, je vais expliquer ce que signifie le résultat que j'ai découvert.
Je crois que quelque chose de similaire se produit sur le serveur:

 const userId = someSessionStore.userId; const email = request.body.email; const sql = `UPDATE users SET email = '${email}' WHERE id = '${userId}'`; 

Je suis sûr que leur serveur est écrit en PHP, mais je ne parle pas ce langage, je vais donc écrire des exemples en JavaScript. De plus, je ne suis pas particulièrement doué pour les requêtes SQL. Je n'ai aucune idée si la table s'appelle user , ou users , ou user_table , et cela n'a pas d'importance.

Si mon ID utilisateur est 1234 et que j'envoie un e-mail = non, le SQL se présente comme suit:

 UPDATE users SET email = 'no' WHERE id = '1234' 

Et si vous remplacez no par la chaîne no', surname = 'testing , alors SQL sera valide, mais délicat:

 UPDATE users SET email = 'no', surname = 'testing' WHERE id = '1234' 

Comme vous vous en souvenez, j'envoie des demandes à partir d'un extrait de code dans les outils de développement, pendant que je suis sur la page de profil. Ainsi, à partir de maintenant, vous pouvez considérer le champ de nom de famille sur cette page (élément HTML <input>) comme une petite sortie dans laquelle vous pouvez écrire des informations en définissant la valeur de mon compte d'utilisateur dans la colonne de surname de la base de données.

Ensuite, je me suis demandé si je pouvais copier les données d'une autre colonne vers la colonne du surname ?

Je ne comprenais pas quoi faire, quoi faire avec SQL, et d'ailleurs, je ne savais pas quelle base de données est utilisée sur le serveur. Donc après chaque étape, j'ai passé 20 minutes à chercher sur le net, puis à me gratter la tête pendant encore 20 minutes, car j'insérais régulièrement mes guillemets dans la mauvaise direction. Il est étrange que je n'ai pas détruit toute la base de données.

La copie des données d'une colonne à une autre s'est avérée un peu plus difficile, car je voulais envoyer une telle demande (il était supposé qu'il devait y avoir une colonne de password ):

 UPDATE users SET email = 'no', surname = password WHERE id = '1234' 

Notez qu'il n'y a pas de guillemets dans le code autour du password . Comme vous vous en souvenez, un concepteur de requêtes ultra-moderne devrait ressembler à ceci ...

 const sql = `UPDATE users SET email = '${email}' WHERE id = '${userId}'`; 

... c'est-à-dire que lorsque vous essayez de passer no', surname = password chaîne résultante ne sera pas une requête SQL valide. Au lieu de cela, j'avais besoin de toute la chaîne injectée pour devenir la deuxième partie de la demande, et tout ce qui vient après devrait être ignoré. En particulier, je devais passer WHERE et; à la fin de l'instruction SQL, ainsi que le commentaire # afin que les informations à droite soient ignorées. Oui, j'explique terriblement.

Bref, j'ai envoyé une nouvelle ligne:

 { // ... body: `email=no', surname = password WHERE username = 'me@email.com'; #` } 

Et la ligne suivante sera envoyée à la base de données:

 UPDATE users SET email = 'no', surname = password WHERE username = 'me@email.com'; # WHERE id = '1234' 

Veuillez noter que la base de données ignorera WHERE id = '1234' , car cette partie vient après le commentaire # (l'interdiction des commentaires dans les requêtes SQL semble être un bon moyen de se protéger contre le code bâclé).

J'espérais que mon mot de passe P @ ssword1 apparaîtrait sous forme de texte dans le champ du nom de famille, mais à la place, j'ai obtenu 00fcdde26dd77af7858a52e3913e6f3330a32b31.

Cela m'a déçu, même si cela ne m'a pas surpris, et j'ai continué à essayer de copier le hachage de mon mot de passe dans la colonne de mot de passe d'un autre utilisateur.

Je m'explique pour les débutants: lorsque vous créez un compte quelque part et envoyez un nouveau mot de passe P @ ssword1, il se transforme en hachage comme 00fcdde26dd77af7858a52e3913e6f3330a32b31 et est stocké dans la base de données. En regardant ce hachage, personne ne pourra déterminer votre mot de passe (du moins c'est ce qu'ils disent).

La prochaine fois que vous vous connectez et entrez le mot de passe Password @ 1, le serveur le hache à nouveau et le compare avec le hachage stocké dans la base de données. Cela confirmera la conformité sans même enregistrer votre mot de passe.

Cela signifie que si je veux donner à quelqu'un le mot de passe P @ ssword1, je dois définir la colonne de mot de passe de cet utilisateur sur 00fcdde26dd77af7858a52e3913e6f3330a32b31.

Poids léger.

J'ai ouvert un autre navigateur, créé un nouvel utilisateur avec un courrier différent et tout d'abord vérifié si je pouvais lui définir les données. Mis à jour la propriété du body :

 { // ... body: `email=no', surname = 'WOOT!!' WHERE username = 'user-two@email.com'; #` } 

J'ai exécuté le code, mis à jour la page de cet utilisateur et, ofiget, cela a fonctionné! Maintenant, il avait le nom de famille "WOOT !!" (nom de jeune fille de ma grand-mère).

Ensuite, j'ai essayé de définir un mot de passe pour cet utilisateur:

  // ... body: `email=no', password = '00fcdde26dd77af7858a52e3913e6f3330a32b31' WHERE username = 'user-two@email.com'; #` } 

Et vous savez quoi?!?!?

Ça n'a pas marché. Maintenant, je n'avais pas accès au deuxième compte.
Il s'est avéré que j'ai commis deux erreurs dont le calcul a pris plusieurs heures. Les experts en sécurité de l'information qui lisent cet article ont déjà compris de quoi ils parlent et se moquent probablement du fou qui écrit ses «exploits» répertoriés sur la première page de Hacking for the Youngest.

Nuuuuuu, à la fin, j'ai cherché sur le net «hachage de mot de passe» et j'ai remarqué que de nombreux hachages sont plus longs que mon 00fcdde26dd77af7858a52e3913e6f3330a32b31. On dirait qu'il recadre quelque part.
J'ai essayé d'entrer un morceau de texte dans le champ du nom de famille et j'ai trouvé une limite de 40 caractères (il est bon qu'ils définissent l'attribut maxlength pour <input> pour correspondre à la contrainte de base de données).

Maintenant, je n'étais intéressé que par les 40 premiers caractères du hachage, qui pourraient être beaucoup plus longs. J'ai recherché «sql substring» et j'ai rapidement envoyé la demande suivante au serveur:

 { // ... body: `email=no', surname = SUBSTRING(password, 30, 1000) WHERE username = 'me@email.com'; #` } 

Commencé par 30 pour vous assurer que les 10 premiers caractères se chevauchent avec les 10 derniers caractères 00fcdde26dd77af7858a52e3913e6f3330a32b31. Ou le dernier 9. Ou 11.

Digression lyrique
Je pense que quand je mourrai et irai en enfer, ils me forceront à regarder toutes mes erreurs pour toujours au ralenti, les unes après les autres. Un gros plan montrant mon visage pendant que je réalise encore et encore ma stupidité sans fin.

Revenons aux réalités: les personnages se chevauchaient et en combinant les lignes, j'ai obtenu un hachage de 64 caractères. Encore une fois, j'ai essayé de le copier sur le deuxième utilisateur:

  { // ... body: `email=no', password = '00fcdde26dd77af7858a52e3913e6f3330a32b3121a61bce915cc6145fc44453' WHERE username = 'user-two@email.com'; #` } 

Et tu sais quoi?!?!
Eh bien, vous l'avez deviné, car j'ai mentionné deux erreurs.

Je ne pouvais toujours pas me connecter au deuxième compte, mais j'en étais déjà proche (ce serait bien pour moi de le savoir à ce moment-là).

J'ai recherché le «mot de passe de la base de données des meilleures pratiques» et j'ai découvert / retenu une chose telle que «sel».

L'utilisation de salt signifie que si vous créez un hachage pour P @ ssword1 pour un utilisateur, alors pour l'autre utilisateur le même mot de passe donnera un hachage différent (un autre salt est utilisé). Bien sûr, un hachage de mot de passe ne fonctionnera pas pour deux utilisateurs, les sels sont différents.

Cela semble intelligent, mais en même temps stupide. Dans tous les exemples du tableau, il y avait simplement une autre colonne appelée sel. Cela ne signifie-t-il pas que je dois copier les données de deux colonnes, pas une? Cela ne ressemble-t-il pas à une deuxième serrure, à laquelle correspond la même clé?

J'ai changé la requête dans l'espoir de copier la valeur d'une colonne qui pourrait s'appeler sel,
à la colonne du nom de famille:

  { // ... body: `email=no', surname = salt WHERE username = 'myemail@email.com'; #` } 

Un ensemble aléatoire de caractères est apparu dans le champ du nom de famille, un bon signe. Pour obtenir ce qui s'est avéré être un sel de 64 caractères, j'ai à nouveau utilisé SUBSTRING.

Tout était prêt. J'ai un hachage de mot de passe et le sel qui a été utilisé pour le créer, il vous suffit de les copier vers un autre utilisateur. Et j'ai envoyé ma dernière demande de réseau ce soir-là:

 fetch('https://blah.com/api/users', { credentials: 'include', headers: { authorization: 'Bearer blah', 'content-type': 'application/x-www-form-urlencoded', 'sec-fetch-mode': 'cors', 'x-csrf-token': 'blah', }, referrer: 'https://blah.com/blah', referrerPolicy: 'no-referrer-when-downgrade', body: `email=no', password = '00fcdde26dd77af7858a52e3913e6f3330a32b3121a61bce915cc6145fc44453', salt = '8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52' WHERE username = 'user-two@gmail.com'; #`, method: 'POST', mode: 'cors', }); 

Ça a marché! Maintenant, je pouvais me connecter au deuxième compte avec un mot de passe du premier compte.
N'est-ce pas fou?

* * *

Il y a eu beaucoup d'essais et d'erreurs, mais lorsque je sélectionne un véritable utilisateur, je vais d'abord récupérer son sel et son hachage et le garder avec moi. Ensuite, je remplacerai son hachage salé par le mien, comme décrit dans l'article, connectez-vous et remplacez instantanément le sel et le hachage par les valeurs d'origine. J'ai seulement besoin de changer le mot de passe de quelqu'un d'autre pendant une fraction de seconde pendant que je me connecte, donc ils ne me trouveront certainement pas.

En théorie, bien sûr. Je ne ferais jamais ça.

* * *

Vous vous demandez peut-être s'il s'agit d'une histoire fictive. Pas inventé. J'ai changé quelques petits détails pour me protéger des charges, mais tout le reste était comme décrit. Et, bien sûr, en fait, j'ai signalé la vulnérabilité aux propriétaires du site.

Mais je ne peux pas m'empêcher de me demander si ce n'était qu'une chance pour les débutants. C'est littéralement le premier site sur lequel j'ai essayé l'injection SQL, et tout était comme préparé pour moi, comme si j'avais réussi l'examen de piratage pour les enfants.

Le site que j'ai décrit est petit, il a peu d'utilisateurs (34 718). Il s'agit d'un service payant, donc pour les pirates expérimentés, cela n'a pas d'intérêt. Et pourtant, cela m'a frappé que c'était possible.

En bref, maintenant je suis accro à tout ce sujet avec la sécurité de l'information. Pour moi, deux activités préférées y étaient combinées: écrire un code et le hooliganisme. Donc googler «sécurité de l'information des salaires en Australie», je pense que je me suis trouvé un nouvel emploi.
Merci d'avoir lu!

PS: la traduction de l'article essaie de préserver au maximum le style de l'auteur :)

Source: https://habr.com/ru/post/fr468695/


All Articles