Comment fonctionne l'injection SQL à partir de la bande dessinée XKCD «Bobby Tables»?

1094

En regardant simplement:

Bande XKCD (Source: https://xkcd.com/327/ )

Que fait ce SQL:

Robert'); DROP TABLE STUDENTS; --

Je connais les deux 'et je suis --pour les commentaires, mais le mot n'est-il pas DROPaussi commenté puisqu'il fait partie de la même ligne?

Blankman
la source
16
Si vous écoutez Stack Overflow Podcast # 31 (27 novembre 2008), ils en discutent.
EBGreen
93
Dans MySQL, ce 'n'est pas pour les commentaires . Même si c'était le cas, il n'y a pas d'espace avant, donc il ne peut que terminer la chaîne qui le précède.
Courses de légèreté en orbite le
45
En ce qui concerne XKCD, s'il y a des questions sur certaines bandes dessinées, vous pouvez toujours aller à Explain XKCD et avoir votre réponse. Il existe même un wiki XKCD , qui est très utile pour certaines bandes dessinées délicates comme le géohashing XKCD
Anatoli
13
Je crois que ce lien doit être enregistré ici: bobby-tables.com
Arioch 'Le
2
beta.companieshouse.gov.uk/company/10542519 est l'enregistrement d'un cabinet de conseil nommé; DROP TABLE "COMPANIES"; - LTD
Alex Dupuy

Réponses:

1117

Il laisse tomber la table des étudiants.

Le code d'origine du programme de l'école ressemble probablement à quelque chose comme

q = "INSERT INTO Students VALUES ('" + FNMName.Text + "', '" + LName.Text + "')";

C'est la façon naïve d'ajouter une entrée de texte dans une requête, et c'est très mauvais , comme vous le verrez.

Après que les valeurs du prénom, de la zone de texte du deuxième prénom FNMName.Text (qui est Robert'); DROP TABLE STUDENTS; --) et du nom de la zone de texte LName.Text (appelons-le Derper) sont concaténées avec le reste de la requête, le résultat est maintenant en fait deux requêtes séparées par le terminateur d'instruction (point-virgule). La deuxième requête a été injectée dans la première. Lorsque le code exécute cette requête sur la base de données, il ressemblera à ceci

INSERT INTO Students VALUES ('Robert'); DROP TABLE Students; --', 'Derper')

qui, en anglais simple, se traduit grosso modo par les deux requêtes:

Ajouter un nouvel enregistrement à la table des étudiants avec une valeur de nom de «Robert»

et

Supprimer le tableau des étudiants

Tout ce qui est passé la deuxième requête est marqué comme un commentaire : --', 'Derper')

Le nom 'de l'élève n'est pas un commentaire, c'est le délimiteur de la chaîne de fermeture . Étant donné que le nom de l'élève est une chaîne, il est nécessaire syntaxiquement pour terminer la requête hypothétique. Les attaques par injection ne fonctionnent que lorsque la requête SQL qu'elles injectent aboutit à un SQL valide .

Modifié à nouveau selon le commentaire astucieux de dan04

Volonté
la source
3
Mmm, le WHERE avec des parenthèses autour des arguments est plutôt inhabituel, mais au moins il évite une erreur de syntaxe ... :-)
PhiLho
60
@PhiLho: Si la déclaration d'origine était un INSERT, alors la parenthèse aurait plus de sens. Cela expliquerait également pourquoi la connexion à la base de données n'est pas en mode lecture seule.
dan04
3
Comme l'explique @ dan04, la parenthèse a plus de sens avec un INSERT. En y repensant, le SELECTne fonctionnerait pas de toute façon car l'insertion des petites tables Bobby dans le tableau aurait déjà laissé tomber le tableau.
ypercubeᵀᴹ
10
En fait, dans cet exemple, la première requête ("ajouter un nouvel enregistrement ...") échouera car Studentsattend plus qu'une seule colonne (l'instruction d'origine / correcte a fourni deux colonnes). Cela dit, la présence de la deuxième colonne est utile pour montrer pourquoi les commentaires sont nécessaires; et comme on ne peut pas changer le nom de Bobby, il vaut probablement mieux laisser tel quel avec un peu plus que cette observation en note de bas de page.
eggyal
7
Le nom de famille de Bobby - ou du moins celui de sa mère, est Roberts , selon Explain XKCD . Cependant, je ne suis pas sûr que la correction améliorerait la clarté de la réponse.
WBT
611

Disons que le nom a été utilisé dans une variable $Name.

Vous exécutez ensuite cette requête :

INSERT INTO Students VALUES ( '$Name' )

Le code place par erreur tout ce que l'utilisateur a fourni comme variable.

Vous vouliez que le SQL soit:

INSÉRER DANS LES VALEURS DES ÉTUDIANTS (' Robert Tables')

Mais un utilisateur intelligent peut fournir ce qu'il veut:

INSÉRER DANS LES VALEURS DES ÉTUDIANTS (' Robert'); DROP TABLE Students; --')

Vous obtenez:

INSERT INTO Students VALUES ( 'Robert' );  DROP TABLE STUDENTS; --' )

Le --seul commentaire le reste de la ligne.

sinoth
la source
87
C'est beaucoup mieux que le vote le plus élevé, car cela explique la parenthèse fermante.
Tim Büthe
1
Soit dit en passant, il n'y a aucun moyen pour le directeur de l'école dans les bandes dessinées d'être au courant ou pour le XSS puisque la table des élèves est supprimée, il ne peut pas savoir qui a fait cela.
xryl669
@ xryl669 Les journaux sont très utiles dans des situations comme celle-ci ... Parfois, toutes les requêtes sont enregistrées, et parfois d'autres informations enregistrées peuvent vous aider à déduire le coupable.
inemanja
165

Comme tout le monde l'a déjà souligné, le ');ferme la déclaration d'origine, puis une deuxième déclaration suit. La plupart des frameworks, y compris des langages comme PHP, ont désormais des paramètres de sécurité par défaut qui ne permettent pas plusieurs instructions dans une même chaîne SQL. En PHP, par exemple, vous ne pouvez exécuter que plusieurs instructions dans une chaîne SQL en utilisant la mysqli_multi_queryfonction.

Vous pouvez cependant manipuler une instruction SQL existante via l'injection SQL sans avoir à ajouter une deuxième instruction. Disons que vous avez un système de connexion qui vérifie un nom d'utilisateur et un mot de passe avec cette simple sélection:

$query="SELECT * FROM users WHERE username='" . $_REQUEST['user'] . "' and (password='".$_REQUEST['pass']."')";
$result=mysql_query($query);

Si vous fournissez peterle nom d'utilisateur et secretle mot de passe, la chaîne SQL résultante ressemblera à ceci:

SELECT * FROM users WHERE username='peter' and (password='secret')

Tout va bien. Imaginez maintenant que vous fournissiez cette chaîne comme mot de passe:

' OR '1'='1

La chaîne SQL résultante serait alors la suivante:

SELECT * FROM users WHERE username='peter' and (password='' OR '1'='1')

Cela vous permettrait de vous connecter à n'importe quel compte sans connaître le mot de passe. Vous n'avez donc pas besoin de pouvoir utiliser deux instructions pour utiliser l'injection SQL, bien que vous puissiez faire des choses plus destructrices si vous êtes en mesure de fournir plusieurs instructions.

Johannes Fahrenkrug
la source
71

Non, ce 'n'est pas un commentaire en SQL, mais un délimiteur.

Maman a supposé que le programmeur de base de données avait fait une demande ressemblant à ceci:

INSERT INTO 'students' ('first_name', 'last_name') VALUES ('$firstName', '$lastName');

(par exemple) pour ajouter le nouvel étudiant, où le $xxxcontenu de la variable a été directement extrait d'un formulaire HTML, sans vérifier le format ni échapper les caractères spéciaux.

Donc, si $firstNamecontient Robert'); DROP TABLE students; --le programme de base de données exécutera la demande suivante directement sur la base de données:

INSERT INTO 'students' ('first_name', 'last_name') VALUES ('Robert'); DROP TABLE students; --', 'XKCD');

c'est à dire. il terminera tôt l'instruction d'insertion, exécutera tout code malveillant souhaité par le pirate, puis commentera le reste du code.

Mmm, je suis trop lent, je vois déjà 8 réponses avant les miennes dans la bande orange ... :-) Un sujet populaire, semble-t-il.

PhiLho
la source
39

TL; DR

- L'application accepte l'entrée, dans ce cas «Nancy», sans tenter de - aseptiser l'entrée, par exemple en échappant les caractères spéciaux 
école => INSÉRER LES VALEURS des élèves ( «Nancy» ); INSÉRER 0 1
   
  

- l' injection SQL se produit lorsque l' entrée dans une commande de base de données est manipulée pour - la cause du serveur de base de données pour exécuter SQL arbitraire 
école => INSERT INTO étudiants VALEURS ( « Robert » ); Étudiants DROP TABLE ; - '); INSÉRER 0 1 TABLEAU DROP
      
  
 

- Les dossiers des étudiants ont maintenant disparu - cela aurait pu être encore pire! 
école => CHOISIR * DES ÉTUDIANTS ; 
ERREUR :   relation « étudiants » ne pas exister   
LIGNE 1 : CHOISIR * DES ÉTUDIANTS ; ^   
                      

Cela supprime (supprime) la table des étudiants.

( Tous les exemples de code de cette réponse ont été exécutés sur un serveur de base de données PostgreSQL 9.1.2. )

Pour clarifier ce qui se passe, essayons ceci avec un simple tableau contenant uniquement le champ de nom et ajoutons une seule ligne:

école => CREATE TABLE élèves ( nom TEXTE PRINCIPAL CLES ); 
AVIS : CREATE TABLE / PRIMAIRE KEY va créer implicite index "students_pkey" pour la table "élèves" CREATE TABLE 
école => INSERT INTO étudiants VALEURS ( 'John' ); INSÉRER 0 1             
    
  

Supposons que l'application utilise le SQL suivant pour insérer des données dans la table:

INSÉRER DANS LES VALEURS des élèves ( «foobar» );  

Remplacez foobarpar le nom réel de l'élève. Une opération d'insertion normale ressemblerait à ceci:

- Input: 
école de Nancy => INSÉRER LES VALEURS des élèves ( 'Nancy' ); INSÉRER 0 1   
  

Lorsque nous interrogeons la table, nous obtenons ceci:

école => CHOISIR * DES ÉTUDIANTS ;   
 Nom
-------
 John
 Nancy
( 2 rangées ) 

Que se passe-t-il lorsque nous insérons le nom de Little Bobby Tables dans la table?

- Entrée: Robert '); Étudiants DROP TABLE; - 
école => INSÉRER LES VALEURS des élèves ( «Robert» ); Étudiants DROP TABLE ; - '); INSÉRER 0 1 TABLEAU DROP      
  
 

L'injection SQL ici est le résultat du nom de l'élève qui termine l'instruction et inclut une DROP TABLEcommande distincte ; les deux tirets à la fin de l'entrée sont destinés à commenter tout code restant qui autrement provoquerait une erreur. La dernière ligne de la sortie confirme que le serveur de base de données a supprimé la table.

Il est important de noter que pendant l' INSERTopération, l'application ne vérifie pas la présence de caractères spéciaux dans l'entrée et permet donc la saisie arbitraire dans la commande SQL. Cela signifie qu'un utilisateur malveillant peut insérer, dans un champ normalement destiné à la saisie par l'utilisateur, des symboles spéciaux tels que des guillemets avec du code SQL arbitraire pour obliger le système de base de données à l'exécuter, d'où l' injection SQL  .

Le résultat?

école => CHOISIR * DES ÉTUDIANTS ; 
ERREUR :   relation « étudiants » ne pas exister   
LIGNE 1 : CHOISIR * DES ÉTUDIANTS ; ^   
                      

L'injection SQL est l'équivalent dans la base de données d'une vulnérabilité d' exécution de code arbitraire à distance dans un système d'exploitation ou une application. L'impact potentiel d'une attaque par injection SQL réussie ne peut pas être sous-estimé - selon le système de base de données et la configuration de l'application, il peut être utilisé par un attaquant pour provoquer une perte de données (comme dans ce cas), obtenir un accès non autorisé aux données, ou même exécuter code arbitraire sur la machine hôte elle-même.

Comme indiqué par la bande dessinée XKCD, une façon de se protéger contre les attaques par injection SQL consiste à assainir les entrées de la base de données, par exemple en échappant les caractères spéciaux, afin qu'elles ne puissent pas modifier la commande SQL sous-jacente et ne puissent donc pas provoquer l'exécution de code SQL arbitraire. Si vous utilisez des requêtes paramétrées, par exemple en utilisant SqlParameterdans ADO.NET, l'entrée sera, au minimum, automatiquement filtrée pour se prémunir contre l'injection SQL.

Cependant, le nettoyage des entrées au niveau de l'application peut ne pas arrêter des techniques d'injection SQL plus avancées. Par exemple, il existe des moyens de contourner la mysql_real_escape_stringfonction PHP . Pour plus de protection, de nombreux systèmes de base de données prennent en charge les instructions préparées . Si elles sont correctement implémentées dans le backend, les instructions préparées peuvent rendre l'injection SQL impossible en traitant les entrées de données comme sémantiquement distinctes du reste de la commande.

bwDraco
la source
30

Disons que vous avez naïvement écrit une méthode de création d'élève comme celle-ci:

void createStudent(String name) {
    database.execute("INSERT INTO students (name) VALUES ('" + name + "')");
}

Et quelqu'un entre le nom Robert'); DROP TABLE STUDENTS; --

Ce qui s'exécute sur la base de données est cette requête:

INSERT INTO students (name) VALUES ('Robert'); DROP TABLE STUDENTS --')

Le point-virgule termine la commande d'insertion et en démarre une autre; le - commente le reste de la ligne. La commande DROP TABLE est exécutée ...

C'est pourquoi les paramètres de liaison sont une bonne chose.

Dan Vinton
la source
26

Une citation simple correspond au début et à la fin d'une chaîne. Un point-virgule est la fin d'une instruction. Donc, s'ils faisaient une sélection comme celle-ci:

Select *
From Students
Where (Name = '<NameGetsInsertedHere>')

Le SQL deviendrait:

Select *
From Students
Where (Name = 'Robert'); DROP TABLE STUDENTS; --')
--             ^-------------------------------^

Sur certains systèmes, le selectserait exécuté en premier, suivi de l' dropinstruction! Le message est le suivant: NE PAS EMBALLER DE VALEURS DANS VOTRE SQL. Utilisez plutôt des paramètres!

CodeAndCats
la source
18

La ');termine la requête, elle ne démarre pas de commentaire. Ensuite, il supprime la table des étudiants et commente le reste de la requête qui devait être exécutée.

Jorn
la source
17

L'auteur de la base de données a probablement

sql = "SELECT * FROM STUDENTS WHERE (STUDENT_NAME = '" + student_name + "') AND other stuff";
execute(sql);

Si nom_étudiant est celui donné, cela fait la sélection avec le nom "Robert", puis supprime le tableau. La partie "-" transforme le reste de la requête donnée en commentaire.

Paul Tomblin
la source
C'était ma première pensée, mais vous obtenez une erreur de syntaxe avec la parenthèse fermante de fin, non?
PhiLho
3
C'est pourquoi il y a un - à la fin, indiquant que le texte restant est un commentaire et doit être ignoré.
17

Dans ce cas, «n'est pas un caractère de commentaire. Il est utilisé pour délimiter les littéraux de chaîne. L'artiste de bande dessinée mise sur l'idée que l'école en question a un sql dynamique quelque part qui ressemble à ceci:

$sql = "INSERT INTO `Students` (FirstName, LastName) VALUES ('" . $fname . "', '" . $lname . "')";

Alors maintenant, le caractère 'termine la chaîne littérale avant que le programmeur ne s'y attende. Combiné avec le; pour terminer la déclaration, un attaquant peut maintenant ajouter le sql qu'il veut. Le commentaire - à la fin est de s'assurer que tout sql restant dans l'instruction d'origine n'empêche pas la requête de se compiler sur le serveur.

FWIW, je pense également que la bande dessinée en question contient un détail important: si vous songez à assainir vos entrées de base de données, comme le suggère la bande dessinée, vous le faites toujours mal. Au lieu de cela, vous devriez penser en termes de mise en quarantaine des entrées de votre base de données, et la bonne façon de le faire est via des requêtes paramétrées.

Joel Coehoorn
la source
16

Le 'caractère dans SQL est utilisé pour les constantes de chaîne. Dans ce cas, il est utilisé pour terminer la constante de chaîne et non pour commenter.

Rockcoder
la source
7

Voici comment cela fonctionne: Supposons que l'administrateur recherche des dossiers d'étudiant

Robert'); DROP TABLE STUDENTS; --

Étant donné que le compte administrateur dispose de privilèges élevés, la suppression de la table de ce compte est possible.

Le code pour récupérer le nom d'utilisateur de la demande est

Maintenant, la requête serait quelque chose comme ça (pour rechercher la table des étudiants)

String query="Select * from student where username='"+student_name+"'";

statement.executeQuery(query); //Rest of the code follows

La requête résultante devient

Select * from student where username='Robert'); DROP TABLE STUDENTS; --

Étant donné que l'entrée utilisateur n'est pas filtrée, la requête ci-dessus a été manipulée en 2 parties

Select * from student where username='Robert'); 

DROP TABLE STUDENTS; --

Le double tiret (-) ne fera que commenter la partie restante de la requête.

Ceci est dangereux car il peut annuler l'authentification par mot de passe, le cas échéant

Le premier fera la recherche normale.

Le second supprimera l'élève de la table si le compte dispose de privilèges suffisants (généralement, le compte administrateur de l'école exécutera une telle requête et disposera des privilèges mentionnés ci-dessus).

Sans nom
la source
SELECT* FROM sutdents ...- vous avez oublié un "s". C'est ce que vous abandonnez. DROP TABLE STUDENTS;
DevWL
4

Vous n'avez pas besoin de saisir des données de formulaire pour effectuer une injection SQL.

Personne ne l'a signalé auparavant, alors je pourrais en avertir certains d'entre vous.

Généralement, nous essaierons de patcher l'entrée des formulaires. Mais ce n'est pas le seul endroit où vous pouvez être attaqué par injection SQL. Vous pouvez faire une attaque très simple avec une URL qui envoie des données via une requête GET; Prenons l'exemple de la jachère:

<a href="/show?id=1">show something</a>

Votre URL ressemblerait à http://votresite.com/show?id=1

Maintenant, quelqu'un pourrait essayer quelque chose comme ça

http://yoursite.com/show?id=1;TRUNCATE table_name

Essayez de remplacer nom_table par le nom réel de la table. S'il obtenait le bon nom de votre table, ils videraient votre table! (Il est très facile de forcer brutalement cette URL avec un script simple)

Votre requête ressemblerait à quelque chose comme ceci ...

"SELECT * FROM page WHERE id = 4;TRUNCATE page"

Exemple de code vulnérable PHP utilisant PDO:

<?php
...
$id = $_GET['id'];

$pdo = new PDO($database_dsn, $database_user, $database_pass);
$query = "SELECT * FROM page WHERE id = {$id}";
$stmt = $pdo->query($query);
$data = $stmt->fetch(); 
/************* You have lost your data!!! :( *************/
...

Solution - utilisez les méthodes PDO prepare () et bindParam ():

<?php
...
$id = $_GET['id'];

$query = 'SELECT * FROM page WHERE id = :idVal';
$stmt = $pdo->prepare($query);
$stmt->bindParam('idVal', $id, PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetch();
/************* Your data is safe! :) *************/
...
DevWL
la source