Nach dem Artikel über Cross-Site Scripting (XSS) folgt hier der Bericht über eine ebenfalls oft genutzte Schwachstelle und zwar SQL Injections. Im Gegensatz zu XSS kann man damit Abfragen an die Datenbank erstellen und hat damit Zugriff auf möglicherweise sensible Daten.
Wie beim Ausnutzen jeder Schwachstelle ist es notwendig, das zugrunde liegende System etwas zu kennen. Bei SQL Injections bedeutet das ein Wissen über die Tabellenstruktur zu haben. Für TYPO3 im Speziellen heißt das zu wissen wo die Admin- und FE-User gespeichert sind, wo interessante Datensätze wie die eines Shops liegen, aber es ist absolut nicht notwendig dafür TypoScript zu lernen. Umso wichtiger ist das Wissen über PHP und MySql.
Wie funktionierts?
Das Problem bei SQL-Injections ist es, durch User eingegebenen Daten zu vertrauen, denn das verletzt bereits die ersten 3 Regeln des sicheren Programmierens:
- Traue nie dem User!
- Traue nie dem User!
- Traue nie dem User!
Das mag auf dem ersten Blick lustig erscheinen, ist es aber so wirklich gar nicht und die nächsten Kapitel sollten zeigen warum nicht. Eine typische Abfrage an die Datenbank schaut so aus (man verzeihe mir das nicht benützen der TYPO3 API):
SELECT * FROM tx_foo_record WHERE deleted = 0 AND uid = $_GET[id]
Diese Abfrage wird auch genau das gewünschte Ergebnis liefern und zwar den einen Datensatz der Tabelle tx_foo_record. Problematisch wird es allerdings, sobald der User hier nicht mehr einen Integer übergibt sondern etwas gänzlich anderes.
Theorieeinschub in MySql
An dieser Stelle erfolgt eine kurze Theorieeinheit über eine Möglichkeit zur SQL Injection. Wie auch bei XSS gibt Google für „sql injection cheat sheet“ genügend weitere Anleitungen an.
Die Abfrage
SELECT uid,pid,title,description FROM tx_foo_record WHERE deleted = 0 AND tstamp > 3
liefert folgendes beispielhafte Ergebnis:
uid | pid | title | description -------------------------------------- 1 | 10 | Test 1 | Ich bin ein Test 2 | 23 | Hallo | Lorem Lipsum
Wird nun der Befehl UNION SELECT benützt, so kann man damit eine weitere Abfrage in das gleiche Resultset aufnehmen. Wichtig dabei ist, dass beide Resultsets die gleiche Anzahl an Spalten besitzen. In diesem Beispiel sind das 4 Spalten (uid, pid, title, description).
SELECT uid,pid,title,description FROM tx_foo_record WHERE deleted = 0 AND tstamp > 3
UNION SELECT uid,pid,username,password FROM admin_users WHERE deleted = 0 AND admin = 1
Das Ergebnis ist nun:
uid | pid | title | description -------------------------------------- 1 | 10 | Test 1 | Ich bin ein Test 2 | 23 | Hallo | Lorem Lipsum -------------------------------------- 30 | 0 | admin | geheimespwd 31 | 0 | redakteur| test123
In einem typischen Anwendungsfall werden einfach alle Ergebnisse nacheinander abgearbeitet und auf der Website ausgegeben. Verkürzt und ohne die CGL zu beachten schaut das dann so aus:
$content.= ‚<h1>$row[title]</h1><p>$row[description]</p>‘;
Das wirklich schöne ist nun, dass man mit der vorher beschriebenen Abfrage neben den Datensätzen aus der Tabelle tx_foo_record auch den Usernamen und das Passwort der User aus der Tabelle admin_users erhält. Ist man ein fauler bequemer Hacker, erweitert man die Abfrage noch auf:
SELECT uid,pid,title,description FROM tx_foo_record WHERE deleted = 0 AND tstamp > 3
AND pid = 0
UNION SELECT uid,pid,username,password FROM admin_users WHERE deleted = 0 AND admin = 1
und erhält damit nur mehr die admin user, aber das ist dann schon mehr das Sahnehäubchen.
uid | pid | title | description -------------------------------------- 30 | 0 | admin | geheimespwd 31 | 0 | redakteur| test123
Neben dem UNION SELECT gibt es noch eine weitere Hilfestellung seitens MySql und zwar die 2 Bindestriche –, die MySql angeben, dass damit das Query endet, egal was sonst noch im Code dahinter stehen würde. Das wird dann verwendet, wenn beispielsweise der PHP-Code folgendermaßen aussieht:
SELECT * FROM tx_foo_record WHERE uid = $_GET[id] AND tstamp > 3 ORDER BY tstamp;
Lösungsmöglichkeiten
Um SQL Injections zu verhindern braucht es nicht viel Code.
- Für Zahlen bietet sich das Prüfen durch intval() oder das Casten auf Integers durch (int) an.
- Für jegliche Strings sollte man die Funktion $GLOBALS[‚TYPO3_DB‘]->quoteStr($value, $table) an.
Ein Beispiel aus der Praxis
Ich habe für einen Vortrag über Sicheres Programmieren eine unsichere Extension gesucht und gefunden. Diese wurde für eine Suche nach FE-Usern benutzt, es hätte aber auch eine Suche nach jeder anderen Tabelle sein können. Da die Extension mit POST-Parametern arbeitete und aus Select-Feldern bestand, konnte man die zusätzlichen Parameter nicht einfach in die URL packen sondern den Umweg über Firebug gehen. Das macht die Sache aber nicht wesentlich komplizierter. Einfach den String in ein vorhandenes option-Feld packen, dieses auswählen und abschicken.
Die Schwierigkeit bestand eher darin, die gleiche Anzahl an Spalten der 2 verschiedenen Tabellen zu ermitteln. Da die Suche mit fe_users arbeitete und ich die be_users ausgeben wollte, wusste ich, dass das Select auf die BE-User solange zu ergänzen hatte bis es passte. Schlussendlich hat es aber dann funktioniert und es wurde der Inhalt der Backend-User-Tabelle ausgegeben – und das alles nur weil ein simples intval() fehlte.
Du hast eine fehlerhafte Extension gefunden?
Wenn du auf eine fehlerhafte Extension stößt, bitte unverzüglich an das Security-Team von TYPO3 melden. Poste den Fehler nicht im Bugtracker, auf keiner Mailingliste, nicht auf Facebook und schicks auch nicht an den Autor! Das Security Team nimmt anschließend Kontakt zum Extension-Autor auf und dieser wird eine gefixte Lösung bereitstellen.
Wie immer bitte ich um zahlreiche Kommentare!