Heute zeige ich euch, was eine SQL-Injection ist, und wie man sich davor schützt.
Zuerst haben wir am Anfang eine PHP-Datei und eine MySQL-Datenbank. In der PHP-Datei sind ein paar Lücken, die wir Stück für Stück finden und schließen werden.
Code der Datei ( nicht XHML valide, ich weiß
<?php
/* Diese PHP-Datei nicht auf einem "oeffentlichen" Webserver benutzen !
* Sie enthaelt Luecken, ueber die dann SQL-Injections durchgefuehert werden koennten !
*/
mysql_connect('localhost', 'root', '');
mysql_select_db('test');
if(isset($_GET['id']))
{
$query = 'SELECT * FROM `blockentrys` WHERE `id` = '. $_GET['id'];
$result = mysql_query($query);
$row = mysql_fetch_assoc($result);
echo'<center><h2>'. $row['title'] .'</h2></center>';
echo $row['content'];
}
else
{
echo'<center><h1>Alle Einträge</h1></center>';
$query = 'SELECT * FROM `blockentrys` ORDER BY id DESC';
$result = mysql_query($query);
while($row = mysql_fetch_assoc($result))
{
echo'<h2>'. $row['title'] .'</h2>';
echo $row['content'];
echo'<br />';
echo'<small><a href="'. $_SERVER['PHP_SELF'] .'?id='. $row['id'] .'">Lesen</a></small>';
echo'<hr />';
}
}
?>
Alles anzeigen
Datenbank am Anfang:
CREATE TABLE IF NOT EXISTS `blockentrys` (
`id` int(2) NOT NULL AUTO_INCREMENT,
`title` text NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`content` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=3;
INSERT INTO `blockentrys` (`id`, `title`, `date`, `content`) VALUES
(1, 'Testeintrag', '2012-03-02 07:41:23', 'Hallo \r\nDies ist ein Testeintrag '),
(2, 'Weiterer Eintrag', '2012-03-02 07:42:25', 'Hier haben wir noch einen Eintrag \r\n\r\nAllerdings sollten wir uns langsam mal über die Sicherheit des Blockes klar werden :S');
Alles anzeigen
Rufen wir nun die Webseite auf, sehen wir die Blogs, darunter sind Links zu der “Vollansicht” des Textes. Nicht sehr hübsch, aber funktionierend.
Was hierbei nicht beachtet wurde, ist der Parameter der “weiterführenden” Links, das “$_GET['id']“.
Stellt euch mal vor, ihr ruft das wie folgt auf:
Könnt ihr ja mal machen, heraus kommt ein
Warning: mysql_fetch_assoc() expects parameter 1 to be resource, boolean given in C:\Users\Tion\Desktop\Framework\htdocs\sqlinjection.php on line 12
Hier würde man jetzt sehen, dass eine Lücke vorliegt. Und jetzt machen wir es noch mal eine Stufe anders und lesen gleich alles aus, um es uns dann wohl geordnet runterzuladen.
http://eureseite.de/index.php?id=2 OR 1=1 INTO OUTFILE "C:/Users/Tion/Desktop/Framework/htdocs/database.txt"
Den Pfad haben wir ja aus der Fehlermeldung oben, MySQL liest alles aus und speichert es in der “database.txt”. Und tada, das steht drinne:
1 Testeintrag 2012-03-02 08:41:23 Hallo
Dies ist ein Testeintrag
2 Weiterer Eintrag 2012-03-02 08:42:25 Hier haben wir noch einen Eintrag
Allerdings sollten wir uns langsam mal über die Sicherheit des Blockes klar werden
Scheint auch harmlos zu sein, hat er alle Blogeinträge, soll er doch lesen. Der Spaß hört allerdings auf, wenn die Tabelle “user” mit Benutzer + Passwort + Zugriffsrechten ausgelesen wird und im Netz steht.
Also, wie schützen wir uns dagegen ?
Der magische Befehl lautet… mysql_real_escape_string. ( Bei SQLite-Benutzern: sqlite_escape_string ) Wir ändern also die Zeile
zu
$query = 'SELECT * FROM `blockentrys` WHERE `id` = "'. mysql_real_escape_string($_GET['id']) .'"';
Und tada, der Blogeintrag wird wieder angezeigt, allerdings keine Datei mehr erzeugt.
Für die, die allerdings keine Lust haben, jeden einzelnden “$_GET”- / “$_POST”- / “$_COOKIE”- Eintrag zu maskieren, können das ja in einer foreach-Schleife erledigen
Den Code liefere ich euch mal:
<?php
/* Diese PHP-Datei nicht auf einem "oeffentlichen" Webserver benutzen !
* Sie enthaelt Luecken, ueber die dann SQL-Injections durchgefuehert werden koennten !
*/
mysql_connect('localhost', 'root', '');
mysql_select_db('test');
$_GET = escapearray($_GET); // NEU
$_POST = escapearray($_POST); // NEU
$_COOKIE = escapearray($_COOKIE); // NEU
if(isset($_GET['id']))
{
$query = 'SELECT * FROM `blockentrys` WHERE `id` = "'. $_GET['id'] .'"';
echo $query .'<br />';
$result = mysql_query($query);
if($result)
echo'VALID<br />';
else
echo'INVALID<br />';
$row = mysql_fetch_assoc($result);
echo'<center><h2>'. $row['title'] .'</h2></center>';
echo $row['content'];
}
else
{
echo'<center><h1>Alle Einträge</h1></center>';
$query = 'SELECT * FROM `blockentrys` ORDER BY id DESC';
$result = mysql_query($query);
while($row = mysql_fetch_assoc($result))
{
echo'<h2>'. $row['title'] .'</h2>';
echo $row['content'];
echo'<br />';
echo'<small><a href="'. $_SERVER['PHP_SELF'] .'?id='. $row['id'] .'">Lesen</a></small>';
echo'<hr />';
}
}
/* Geht ein Array zeilenweise durch und maskiert den Inhalt
* zum anwenden einer MySQL-Query.
* Wichtig: den Code danach nicht erneut maskieren !
*/
function escapearray($array)
{
foreach($array as $key => $value)
$array[$key] = mysql_real_escape_string($value);
return $array;
}
?>
Alles anzeigen
Anmerkung:
Ich wurde in den Kommentaren in meinem Blogeintrag von deadinat0r darauf hingewiesen, dass auch array_map genutzt werden könnte.
Der Code sähe dann etwa so aus:
Quelle: tionsys.de