web-dev-qa-db-fra.com

Comment extraire des informations à l'aide d'une injection SQL sur PHP?

Je suis nouveau dans les injections SQL, alors pardonnez-moi si cela semble un peu amateur.

Je travaillais sur un site Web où j'ai réussi à contourner l'authentification et la connexion en tant qu'utilisateur normal:

username: ' OR 1 -- -
password: <empty>

La page résultante affiche "Vous êtes connecté en tant que: 'OR 1 - -". J'ai donc pensé que si je pouvais vider les détails de la base de données dans le champ du nom d'utilisateur qu'elle devrait afficher.

Cependant, lorsque j'essaie d'aller plus loin en énumérant la version/les informations de la base de données, cela ne fonctionne pas (comme dans Je ne pouvais plus contourner l'authentification).

J'ai essayé ce qui suit:

username: ' OR 1;SELECT @@VERSION -- -
username: ' OR 1;SELECT user_name -- -

Le site Web exécute le serveur Apache/2.4.7 (Ubuntu) et je soupçonne que la base de données exécute MySQL.

7
Nicholas Lim

Il n'y a aucun problème à être nouveau dans l'injection SQL (celui-ci semble être un exemple assez classique et constitue donc un début parfait :)), mais je pense qu'il est très important d'avoir une base fiable sur les requêtes SQL: syntaxe, tableaux, jeux de résultats , quels sont-ils et comment ils sont traités, etc.

Le but de l'injection SQL est en effet de réussir à comprendre ce qui se passe côté serveur en tordant la requête de toutes les manières possibles (pas de barils quand la situation le mérite!) Et, éventuellement, gagner la capacité de réellement contrôler le traitement côté serveur.

Trouvez une injection SQL facilement exploitable

Pour des raisons d'illustration, je vais réutiliser l'exemple donné dans la réponse principale de " Qu'est-ce que l'injection SQL? ". Si vous avez besoin de plus d'informations sur l'injection SQL, cela peut être un bon point de départ.

J'imagine donc que le serveur exécute le code suivant:

sql = "select id, username from users'
      + ' where username='" + username + "' and password='" + password +"'";
  • Une fois l'authentification réussie, cette demande renverra un jeu de résultats similaire à celui-ci, avec une seule ligne:

    +----+----------+
    | id | username |
    +----+----------+
    | 42 | jdoe     |
    +----+----------+
    
  • L'authentification a échoué produire un jeu de résultats vide, pas de ligne:

    +----+----------+
    | id | username |
    +----+----------+
    +----+----------+
    

Maintenant, ce que vous avez déjà réussi à faire et qui montre que le code côté serveur est vulnérable est de contourner l'authentification en définissant le nom d'utilisateur sur ' OR 1 --.

Prenant toujours le même exemple que ci-dessus, le code produirait la requête SQL suivante:

select id, username from users where username='' OR 1 --

(-- Commente efficacement le reste de la ligne de la requête qui n'est donc pas affichée ici)

Ici, il est temps de comprendre ce que fait cette demande: cette demande récupère l'ID et le nom d'utilisateur de chaque utilisateur (chaque utilisateur avec un nom d'utilisateur vide ou vrai, ce qui signifie pratiquement chaque utilisateur).

Ce n'est pas une bonne nouvelle pour vous (sauf si la première ligne de l'ensemble de résultats est l'administrateur du site Web, mais cela est hors de portée pour ce post où nous voulons nous concentrer sur l'extraction d'informations).

Pourquoi? Là encore, vous devez essayer d'imaginer ce qui peut se passer côté serveur.

Ce n'est pas une bonne nouvelle pour vous car cela signifie que ce que vous voyez s'affiche sur la page Web "Vous êtes connecté":

  • N'est pas extrait du résultat de la requête de base de données ($result[0]['username']), Sinon vous verriez un nom d'utilisateur aléatoire affiché à la place.
  • Est plutôt directement directement extrait de la valeur du champ du formulaire reçue par le serveur (getPostData("username");).

Dans les cas d'utilisation légitimes, cela ne change rien car les deux doivent retourner la même valeur. Dans notre cas, cela est mauvais car cela signifie que nous serons très probablement incapables d'extraire et d'afficher les informations de la base de données à cet endroit.

Il existe des méthodes d'injection SQL plus avancées (SQLi basé sur les erreurs, SQLi aveugle, ...) qui vous permettent d'extraire des informations de la base de données lorsque le site Web n'a pas l'intention de les afficher explicitement. Cependant, ils viennent avec leur propre ensemble de problèmes et je les considérerai hors sujet dans ce post. En effet, étant donné le soin apporté par le développeur du site à sécuriser la page d'authentification, la plupart des chances sont que d'autres pages soient également vulnérables, d'autres pages qui cette fois afficheront des informations extraites de la base de données (la page de profil de l'utilisateur peut être votre meilleur ami :) ).

(Si certaines pages ne sont pas satisfaites du fait que votre nom d'utilisateur falsifié renvoie plusieurs lignes au lieu d'une seule, ajoutez simplement une instruction LIMIT à votre nom d'utilisateur: ' OR 1 LIMIT 1, 1 --, Et de Bien sûr, vous êtes libre de modifier ses paramètres pour passer d'un utilisateur à un autre ... Si vous connaissez un nom d'utilisateur valide, vous pouvez également essayer de l'utiliser: admin' --.)

Alors que dans cet article, je continuerai à prendre la page d'authentification comme exemple, cela fonctionnera de la même manière sur les injections SQL affectant toutes les autres pages.

Déterminer la disposition du jeu de résultats

Au début de ce post, nous avons supposé que le jeu de résultats était un tableau à deux colonnes avec le nom d'utilisateur stocké dans la deuxième colonne:

+----+----------+
| id | username |
+----+----------+
| 42 | jdoe     |
+----+----------+

Cependant, jusqu'à ce que vous sachiez que c'était juste une supposition et que cela nous était égal. Malheureusement, nous allons maintenant devoir savoir ceci:

  • Connaître le nombre exact de colonnes est nécessaire pour des raisons techniques, sinon l'instruction UNION que nous utiliserons plus tard ne sera pas satisfaite.
  • Savoir à partir de quelle colonne le serveur récupère les données affichées nous permettra de placer la valeur injectée au bon endroit dans le jeu de résultats (nous n'aurons pas besoin de connaître le nom réel de la colonne, seulement sa position).

(Si le serveur utilise PostgreSQL au lieu de MySQL, vous devrez peut-être même déterminer les types de colonnes corrects pour satisfaire UNION, plus une nuisance qu'un vrai problème mais c'est quelque chose à garder à l'esprit .)

Déterminer le nombre de colonnes

Le moyen le plus simple est de demander au serveur de commander le résultat comme ci-dessous:

username: ' OR 1 ORDER BY 1 -- -

Incrémentez la clause ORDER BY 1 Jusqu'à ce que vous obteniez une erreur, la dernière valeur de travail correspond au nombre de colonnes dans le jeu de résultats.

Si je l'exécutais contre l'ensemble de résultats d'exemple, j'obtiendrais:

  • username: ' OR 1 ORDER BY 1 -- -: OK
  • username: ' OR 1 ORDER BY 2 -- -: OK
  • username: ' OR 1 ORDER BY 3 -- -: Erreur, donc l'exemple de jeu de résultats contient deux colonnes.

Déterminez quelle (s) colonne (s) est/sont utilisable (s)

Maintenant que vous connaissez le nombre correct de colonnes, vous pouvez aller de l'avant et injecter une commande comme celle-ci:

username: ' OR 1 UNION SELECT 1,2 -- -

Si vous aviez trois colonnes, vous feriez SELECT 1,2,3, Pour cinq colonnes SELECT 1,2,3,4,5, Etc.

Le but ici sera d'injecter les nombres réels dans le jeu de résultats et de vérifier dans la page Web (celle rendue ou son code source) laquelle est affichée par le serveur.

L'avantage d'utiliser des nombres est qu'ils sont facilement transtypés en chaîne par MySQL si le type des colonnes le requiert. Alors que "1,2,3" est donné à titre d'exemple ici, n'hésitez pas à utiliser les chiffres que vous aimez ("1234,2345,3456" fonctionnerait également et produirait des résultats plus faciles à repérer).

Dans l'exemple, la page Web résultante indiquerait-elle "Vous êtes connecté en tant que: 2", vous sauriez que le nom d'utilisateur est récupéré dans la deuxième colonne du jeu de résultats.

Attention : comprendre le fonctionnement de l'opérateur UNION: le serveur n'utiliserait-il que la première ligne dont vous pourriez avoir besoin pour vous assurer que le côté gauche la demande ne produit aucune ligne!

En fait, les opérateurs UNION concaténent deux jeux de résultats:

+----+----------+         +----+----------+
| 42 | jdoe     |  UNION  | 1  | 2        |
+----+----------+         +----+----------+

Aura pour résultat:

+----+----------+
| id | username |
+----+----------+
| 42 | jdoe     |
+----+----------+
| 1  | 2        |
+----+----------+

Ce qui n'est peut-être pas le résultat escompté.

Obtenir:

+----+----------+
| id | username |
+----+----------+
| 1  | 2        |
+----+----------+

Vous souhaiterez peut-être vous assurer que la requête de gauche ne renvoie aucune entrée:

username: ' AND 0 UNION SELECT 1,2 -- -

Voyez comment OR 1 A été inversé en AND 0 Pour vous assurer que la requête de gauche sera évaluée comme fausse, quel que soit le contenu des lignes de la base de données.

Exploitez l'injection SQL

Et maintenant, le plaisir commence :)!

Avec toutes ces informations en main, vous êtes maintenant libre d'extraire les informations que vous souhaitez de la base de données.

Par exemple:

  • Pour obtenir la version MySQL:

    username: ' AND 0 UNION SELECT 1,@@VERSION -- 
    
  • Pour récupérer les noms de table et de colonne:

    username: ' AND 0 UNION SELECT 1,GROUP_CONCAT(table_name,0x2e,column_name) FROM information_schema.columns WHERE table_schema=database() -- 
    

Il existe de nombreux sites Web référençant les requêtes SQL adaptées à l'injection. Faites attention à la version du serveur MySQL que vous avez devant vous car certaines syntaxes peuvent dépendre de la version.

Conclusion

La facilité d'injection SQL peut varier considérablement en fonction des détails de la requête SQL du serveur et du traitement appliqué au résultat. Tout dépend de votre facteur de chance ici.

Néanmoins, la construction manuelle d'une chaîne d'injection SQL réussie se résume toujours à cette boucle:

  1. Injectez une requête dans le serveur.
  2. Observez le résultat.
  3. Essayez d'interpréter ce résultat du point de vue du serveur: quel type de traitement aurait pu conduire à un tel résultat? (c'est votre connaissance de SQL qui compte vraiment)
  4. Créez une nouvelle chaîne d'injection conçue pour résoudre votre problème ou vérifier certaines hypothèses sur le comportement du serveur.
  5. Revenez à l'étape 1.

Enfin, faire cela manuellement peut ne pas évoluer très bien. Selon vos besoins, vous pouvez trouver des outils comme sqlmap utiles pour automatiser tout cela.

12
WhiteWinterWolf

Requêtes empilées en PHP/MySQL

MySQL + PHP ne supporte que les requêtes empilées avec multi_query qui est rarement utilisé dans la pratique.

C'est pourquoi votre attaque ne fonctionne pas. Vous essayez d'ajouter une deuxième requête en utilisant ;, qui n'est tout simplement pas pris en charge.

Vous pouvez toujours extraire des données avec cette injection! Il existe deux options en ce qui concerne MySQL: l'injection basée sur l'erreur et l'injection aveugle.

Injection basée sur les erreurs

Tout d'abord, comme son nom l'indique, l'injection basée sur les erreurs ne fonctionne que si le message d'erreur MySQL est réellement affiché.

L'idée est simple: si vous ne pouvez pas extraire de données avec votre injection, car les résultats ne sont pas affichés, créez simplement une erreur MySQL qui contient les données que vous souhaitez. Cela se fait souvent via extractvalue, dans votre cas ce serait:

' OR extractvalue(1,version()) -- -

Injection SQL aveugle (basée sur le contenu)

Par rapport à l'injection basée sur l'erreur, l'injection aveugle fonctionnera toujours, même lorsque vous ne voyez pas de messages d'erreur.

L'idée est à nouveau simple. Bien que vous ne puissiez pas afficher de données, vous savez si une requête a récupéré des résultats ou non. Dans votre cas, vous êtes connecté (l'état True) ou non (l'état False).

Une attaque très simple serait:

' OR substring(version(),1,1)='5

Cela entraînerait probablement une connexion réussie, vous savez donc que la version de la base de données commence par un 5. Une valeur différente n'entraînerait pas une connexion.

L'idée est maintenant de récupérer un personnage à la fois.

Au lieu de demander la version, vous pouvez également ajouter des requêtes plus complexes, n'oubliez pas de LIMIT votre résultat sur une seule ligne à la fois.

Cette attaque est assez bruyante, mais peut être améliorée. Une possibilité consiste à convertir le caractère en sa valeur ascii via la fonction ascii, qui vous permet d'utiliser des comparaisons inférieures et supérieures à.

Injection SQL aveugle (basée sur le temps)

Dans certains cas, vous n'obtiendrez même pas de retour vrai/faux de votre injection. Dans ces cas, vous pouvez utiliser les temps de réponse pour toujours poser des questions vraies/fausses:

' AND if(substring(version(), 1, 1)='5',BENCHMARK(50000000,ENCODE('msg','msg')),null) -- -

L'idée ici est qu'une fonction coûteuse est appelée si une condition spécifique est vraie, ce qui entraîne un temps de réponse plus long.

Comme précédemment, vous pouvez effectuer des requêtes plus complexes et également augmenter les performances en utilisant la fonction ascii.

2
tim