webentwicklung-frage-antwort-db.com.de

SQL-Injection, die umgeht mysql_real_escape_string ()

Gibt es eine SQL-Injection-Möglichkeit auch bei Verwendung der Funktion mysql_real_escape_string()?

Betrachten Sie diese Beispielsituation. SQL ist in PHP folgendermaßen aufgebaut:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

Ich habe zahlreiche Leute gehört, die mir sagen, dass ein solcher Code immer noch gefährlich ist und sogar mit mysql_real_escape_string()-Funktion hacken kann. Ich kann mir aber keinen möglichen Exploit vorstellen.

Klassische Injektionen wie diese:

aaa' OR 1=1 --

arbeite nicht.

Kennen Sie eine mögliche Injektion, die den obigen Code PHP durchlaufen würde?

539
Richard Knop

Betrachten Sie die folgende Abfrage:

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string() schützt Sie nicht davor. . Die Tatsache, dass Sie die Variablen in Ihrer Abfrage in einfache Anführungszeichen (' ') setzen, schützt Sie dagegen. Folgendes ist auch eine Option:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
328

Die kurze Antwort lautet ja, ja, es gibt einen Weg, um mysql_real_escape_string() herumzukommen.

Für sehr OBSCURE Edge FÄLLE !!!

Die lange Antwort ist nicht so einfach. Es basiert auf einem Angriff hier demonstriert .

Der Angriff

Beginnen wir also damit, den Angriff zu zeigen ...

mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Unter bestimmten Umständen gibt dies mehr als eine Zeile zurück. Sehen wir uns an, was hier los ist:

  1. Auswählen eines Zeichensatzes

    mysql_query('SET NAMES gbk');
    

    Damit dieser Angriff funktioniert, muss die Codierung, die der Server auf der Verbindung erwartet, sowohl ' als auch ASCII, dh 0x27 und , codieren, um ein Zeichen zu haben, dessen letztes Byte ein ASCII \ dh 0x5c. Wie sich herausstellt, werden in MySQL 5.6 standardmäßig 5 solche Codierungen unterstützt: big5, cp932, gb2312, gbk und sjis. Wir wählen hier gbk aus.

    Nun ist es sehr wichtig, die Verwendung von SET NAMES hier zu beachten. Dies setzt den Zeichensatz ON THE SERVER. Wenn wir den Aufruf der C-API-Funktion mysql_set_charset() verwenden würden, wären wir in Ordnung (in MySQL-Releases seit 2006). Aber mehr darüber, warum in einer Minute ...

  2. Die Nutzlast

    Die Payload, die wir für diese Injection verwenden werden, beginnt mit der Bytesequenz 0xbf27. In gbk ist das ein ungültiges Multibyte-Zeichen. In latin1 ist es die Zeichenfolge ¿'. Beachten Sie, dass in latin1ndgbk0x27 für sich genommen ein wörtliches '-Zeichen ist.

    Wir haben diese Nutzdaten ausgewählt, weil wir, wenn wir addslashes() aufrufen, ASCII \, d. H. 0x5c, vor dem Zeichen ' einfügen würden. Wir würden also mit 0xbf5c27 enden, was in gbk eine Folge von zwei Zeichen ist: 0xbf5c gefolgt von 0x27. Oder mit anderen Worten, ein gültiges Zeichen, gefolgt von einem ' ohne Flucht. Wir verwenden jedoch nicht addslashes(). Also weiter zum nächsten Schritt ...

  3. mysql_real_escape_string ()

    Der C-API-Aufruf von mysql_real_escape_string() unterscheidet sich von addslashes() darin, dass er den Verbindungszeichensatz kennt. Auf diese Weise kann die Maskierung für den vom Server erwarteten Zeichensatz ordnungsgemäß ausgeführt werden. Bis jetzt glaubt der Client jedoch, dass wir latin1 immer noch für die Verbindung verwenden, da wir es nie anders angegeben haben. Wir haben dem Server gesagt, dass wir gbk verwenden, aber der Client denkt immer noch, dass es latin1 ist.

    Der Aufruf von mysql_real_escape_string() fügt daher den umgekehrten Schrägstrich ein, und wir haben ein freihängendes '-Zeichen in unserem "entkommenen" Inhalt! In der Tat, wenn wir uns $var im Zeichensatz gbk ansehen würden, würden wir sehen:

    縗 'OR 1 = 1/*

    Welches ist genau was der Angriff erfordert.

  4. Die Abfrage

    Dieser Teil ist nur eine Formalität, aber hier ist die gerenderte Abfrage:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

Herzlichen Glückwunsch, Sie haben gerade ein Programm mit mysql_real_escape_string()... erfolgreich angegriffen.

Das Schlechte

Es wird schlimmer. PDO verwendet standardmäßig emulierende vorbereitete Anweisungen mit MySQL. Das bedeutet, dass auf der Clientseite im Grunde genommen ein Sprint durch mysql_real_escape_string() (in der C-Bibliothek) ausgeführt wird, was bedeutet, dass Folgendes zu einer erfolgreichen Injektion führt:

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Es ist erwähnenswert, dass Sie dies verhindern können, indem Sie emulierte vorbereitete Anweisungen deaktivieren:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Dies wird normalerweise zu einer wahrhaft vorbereiteten Anweisung führen (d. H. Die Daten werden in einem von der Abfrage getrennten Paket übertragen). Beachten Sie jedoch, dass PDO im Stillen Fallback Anweisungen emuliert, die MySQL nicht nativ vorbereiten kann: Die möglichen aufgelistet im Handbuch, aber achten Sie darauf, die entsprechenden auszuwählen Serverversion).

Das hässliche

Ich sagte gleich zu Beginn, dass wir all dies hätten verhindern können, wenn wir mysql_set_charset('gbk') anstelle von SET NAMES gbk verwendet hätten. Und das ist wahr, vorausgesetzt, Sie verwenden eine MySQL-Version seit 2006.

Wenn Sie eine frühere MySQL-Version verwenden, bedeutete ein Fehler in mysql_real_escape_string(), dass ungültige Multibyte-Zeichen, wie sie in unserer Payload enthalten sind, als einzelne Bytes behandelt wurden, auch wenn die Der Client wurde korrekt über die Verbindungscodierung informiert und dieser Angriff würde weiterhin erfolgreich sein. Der Fehler wurde in MySQL behoben 4.1.2 , 5.0.22 und 5.1.11 .

Das Schlimmste ist jedoch, dass PDO die C-API für mysql_set_charset() erst in Version 5.3.6 verfügbar gemacht hat. In früheren Versionen kann es nicht ​​diesen Angriff für jeden möglichen Befehl verhindern! Es wird jetzt als DSN-Parameter angezeigt.

Die Rettungsgnade

Wie eingangs erwähnt, muss die Datenbankverbindung mit einem anfälligen Zeichensatz codiert werden, damit dieser Angriff funktioniert. utf8mb4 ist nicht verwundbar und kann dennoch jedes Unicode-Zeichen unterstützen: Sie können sich dafür entscheiden - aber es ist erst seit MySQL 5.5.3 verfügbar. Eine Alternative ist utf8 , was ebenfalls nicht anfällig ist und den gesamten Unicode Basic Multilingual Plane unterstützen kann.

Alternativ können Sie den SQL-Modus NO_BACKSLASH_ESCAPES aktivieren, der (unter anderem) die Funktion von mysql_real_escape_string() ändert. Wenn dieser Modus aktiviert ist, wird 0x27 durch 0x2727 und nicht durch 0x5c27 ersetzt, und daher kann der Escape-Prozess keine gültige Zeichen in einer der anfälligen Codierungen erstellen, in denen sie zuvor nicht vorhanden waren (dh 0xbf27 ist noch 0xbf27 usw.) Der Server weist die Zeichenfolge weiterhin als ungültig zurück. In @ eggyals Antwort finden Sie jedoch eine andere Sicherheitsanfälligkeit, die sich aus der Verwendung dieses SQL-Modus ergeben kann.

Sichere Beispiele

Die folgenden Beispiele sind sicher:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Weil der Server utf8 erwartet ...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Da wir den Zeichensatz richtig eingestellt haben, stimmen Client und Server überein.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Weil wir emulierte vorbereitete Anweisungen deaktiviert haben.

$pdo = new PDO('mysql:Host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Weil wir den Zeichensatz richtig eingestellt haben.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Weil MySQLi die ganze Zeit richtig vorbereitete Anweisungen ausführt.

Einpacken

Wenn du:

  • Verwenden Sie moderne Versionen von MySQL (spätes 5.1, alle 5.5, 5.6 usw.) UNDmysql_set_charset()/$mysqli->set_charset()/PDOs DSN-Zeichensatzparameter (in PHP ≥ 5.3.6)

ODER

  • Verwenden Sie keinen anfälligen Zeichensatz für die Verbindungscodierung (Sie verwenden nur utf8/latin1/ascii/etc).

Du bist 100% sicher.

Andernfalls sind Sie verwundbar obwohl Sie mysql_real_escape_string() ... verwenden.

597
ircmaxell

Nun, es gibt nichts, was wirklich passieren kann, außer % Wildcard. Es könnte gefährlich sein, wenn Sie die LIKE-Anweisung verwenden, da der Angreifer nur % als Login verwenden kann, wenn Sie das nicht herausfiltern, und das Kennwort eines beliebigen Benutzers bruteforce bräuchen müsste Um es zu 100% sicher zu machen, da Daten die Abfrage selbst nicht auf diese Weise stören können. Aber für solche einfachen Abfragen wäre es wahrscheinlich effizienter, etwas wie $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login); zu tun.

19
Slava

ich war mit diesem Problem konfrontiert und schlage Ihnen vor, mit PDO zu arbeiten, aber in einigen Fällen möchten Sie vielleicht diese Methode ausprobieren. Es funktioniert und sehr einfach. Ich frage mich, warum die Leute es vernachlässigten.

Codebeispiel . // Verwenden meines Frameworks Moorexa

es unterstützt einen reichen ORM und vieles mehr ... Aber ich musste diesen Testfall ausführen, um eine Alternative zu Leuten zu finden, die rohe SQL-Anweisungen schreiben.

beispiel. 

// checking from a user table
$check = DB::table('api_users')->get(['username' => "admin' or password='1'"])->run();

// expected output
SELECT * FROM api_users  WHERE username='admin' or password='1'

//sql generated output
SELECT * FROM api_users  WHERE username='admin\' or password=\'1\''

// lets try something heavy
$check = DB::table($table)->get(['username' => "admin' or 1=1 UNION SELECT password FROM api_users where id=1"])->run();

// expected output
SELECT * FROM api_users  WHERE username='admin' or 1=1 UNION SELECT password FROM api_users where id=1

// this would pass and fail
SELECT * FROM api_users  WHERE username='admin\' or 1=1 UNION SELECT password FROM api_users where id=1'

was ist der Gist?.

  1. Typcheck.
  2. benutzereingabe überprüfen. <TRUST NO ONE>
  3. anführungszeichen für Zeichenfolgen

ich zeige Ihnen ein Codebeispiel, das eine Auswahlabfrage ausführt.

// let's assume. would all work
$input = ['username' => "moorexa"]; //or $_POST or $_GET

$sql = 'SELECT * FROM '.$table.' ';

$safe = "";

// let's grab the user input from the array
foreach ($input as $key => $val)
{
    switch($val)
    {
        case is_string($val):
            $safe .= $key .'=\''.addslashes($val).'\' AND ';
        break;

        case is_int($val):
            $safe .= $key .'='.((int) $val).' AND ';
        break;

        case is_float($val):
        case is_double($val):
            $safe .= $key .'='.(double) $val.' AND ';
        break;

        default:
            // this failed
    }
}

$safe = rtrim($safe, "AND ");
$sql .= ' WHERE '. $safe .' ';  

// now sql contains a valid statement. and would only fail when terms are not met.
// Hope you can apply this and also use more test cases.
0
Ifeanyi Amadi