web-dev-qa-db-ja.com

Mysql_real_escape_string()を回避するSQLインジェクション

mysql_real_escape_string()関数を使ってもSQLインジェクションの可能性はありますか?

このサンプル状況を検討してください。 SQLは以下のようにPHPに構築されます。

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

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

私は、そのようなコードはまだ危険であり、mysql_real_escape_string()関数を使用してもハッキングする可能性があると多くの人が私に言うのを聞いたことがあります。しかし、私はどんな悪用も考えられませんか?

このような古典的な注射:

aaa' OR 1=1 --

動作しない。

上記のPHPコードを通過する可能性のある注入を知っていますか?

557
Richard Knop

次のようなクエリを考えます。

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

mysql_real_escape_string()はこれに対してあなたを守りません。 クエリ内で変数の周りに一重引用符(' ')を使用しているという事実が、これに対してあなたを保護しています。 以下もオプションです。

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

短い答えははい、はい、mysql_real_escape_string()を回避する方法があります。

非常に不明瞭なエッジケースの場合!!!

長い答えはそれほど簡単ではありません。これは攻撃に基づいています ここで説明

攻撃

それでは、攻撃を見せることから始めましょう...

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");

特定の状況では、それは複数の行を返します。ここで何が起こっているのかを分析しましょう:

  1. 文字セットの選択

    mysql_query('SET NAMES gbk');
    

    この攻撃が機能するためには、接続でサーバーが期待するエンコードが必要です。エンコードはASCIIのように'をエンコードする必要があります。つまり0x27and ASCII \すなわち0x5c。結局のところ、MySQL 5.6ではデフォルトで、big5cp932gb2312gbkおよびsjisという5つのエンコーディングがサポートされています。ここでgbkを選択します。

    ここで、SET NAMESの使用に注意することが非常に重要です。これにより、文字セットサーバー上が設定されます。 C API関数mysql_set_charset()の呼び出しを使用した場合、問題ありません(2006年以降のMySQLリリース)。しかし、その理由についてはあと1分で...

  2. ペイロード

    このインジェクションに使用するペイロードは、バイトシーケンス0xbf27で始まります。 gbkでは、これは無効なマルチバイト文字です。 latin1では、文字列¿'です。 latin1andgbkでは、0x27自体が'文字であることに注意してください。

    このペイロードを選択したのは、addslashes()を呼び出した場合、\文字の前にASCII 0x5c、つまり'を挿入するためです。そこで、gbkの2文字のシーケンスである0xbf5c27を作成しました:0xbf5cの後に0x27が続きます。または、言い換えると、valid文字の後にエスケープされていない'が続きます。ただし、addslashes()は使用していません。次のステップに進みます...

  3. mysql_real_escape_string()

    mysql_real_escape_string()へのC API呼び出しは、接続文字セットを知っているという点でaddslashes()と異なります。そのため、サーバーが予期している文字セットに対して適切にエスケープを実行できます。ただし、この時点まで、クライアントは接続にlatin1を使用していると考えています。 servergbkを使用していると伝えましたが、clientはまだlatin1であると考えています。

    したがって、mysql_real_escape_string()を呼び出すとバック​​スラッシュが挿入され、「エスケープ」されたコンテンツに'文字がぶら下がります!実際、gbk文字セットの$varを見ると、次のように表示されます。

    縗 'OR 1 = 1/*

    これは 正確に何 攻撃に必要です。

  4. クエリ

    この部分は単なる形式ですが、レンダリングされたクエリは次のとおりです。

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

おめでとうございます、あなたはmysql_real_escape_string()を使用してプログラムを攻撃することに成功しました...

悪い人

ひどくなる。 PDOのデフォルトは、MySQLで準備されたステートメントをemulatingにします。つまり、クライアント側では、基本的に(Cライブラリ内の)mysql_real_escape_string()を介してsprintfを実行します。つまり、次のようにするとインジェクションが成功します。

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

さて、エミュレートされた準備済みステートメントを無効にすることでこれを防ぐことができることに注意してください:

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

これにより、通常の結果、真の準備済みステートメント(つまり、クエリとは別のパケットでデータが送信されます)になります。ただし、PDOは黙って フォールバック MySQLがネイティブに準備できないステートメントをエミュレートします:マニュアルでは listed ですが、適切なものを選択するように注意してくださいサーバーのバージョン)。

ぶさいく

最初に、SET NAMES gbkの代わりにmysql_set_charset('gbk')を使用していた場合、これをすべて防止できたと言いました。 2006年以降、MySQLのリリースを使用している場合、それは事実です。

以前のMySQLリリースを使用している場合、mysql_real_escape_string()bug は、ペイロード内の文字などの無効なマルチバイト文字が、クライアントがエスケープしても1バイトとして扱われることを意味しました接続エンコーディングが正しく通知されていたため、この攻撃は引き続き成功します。このバグはMySQLで修正されました 4.1.25.0.22 および 5.1.11

しかし、最悪の部分は、PDOは5.3.6までmysql_set_charset()のC APIを公開しなかったため、以前のバージョンではcannot考えられるすべてのコマンドに対してこの攻撃を防ぎます。現在、 DSNパラメーター として公開されています。

救いの恵み

最初に述べたように、この攻撃が機能するには、脆弱な文字セットを使用してデータベース接続をエンコードする必要があります。 utf8mb4脆弱ではないでありながら、すべてのUnicode文字をサポートできます。 MySQL 5.5.3以降でのみ利用可能です。代替手段は utf8 で、これも脆弱ではなく、Unicode全体をサポートできます Basic Multilingual Plane

または、 NO_BACKSLASH_ESCAPES SQLモードを有効にすることができます。これにより、(特に)mysql_real_escape_string()の操作が変更されます。このモードを有効にすると、0x270x2727ではなく0x5c27に置き換えられるため、エスケーププロセスcannotは、以前に存在しなかった脆弱なエンコーディングで有効な文字を作成します(つまり0xbf27はまだ0xbf27など)-サーバーは引き続き文字列を無効として拒否します。ただし、このSQLモードの使用から生じる可能性のある別の脆弱性については、 @ eggyal's answer を参照してください。

安全な例

次の例は安全です。

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");

サーバーがutf8を期待しているため...

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

クライアントとサーバーが一致するように文字セットを適切に設定したためです。

$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 /*"));

エミュレートされた準備済みステートメントをオフにしたためです。

$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 /*"));

文字セットを適切に設定したからです。

$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();

MySQLiは常に真のプリペアドステートメントを実行するためです。

まとめ

もし、あんたが:

  • MySQLの最新バージョン(5.1以降、すべて5.5、5.6など)を使用しますおよびmysql_set_charset()/$mysqli->set_charset()/PDOのDSN charsetパラメーター(PHP≥5.3.6)

または

  • 接続エンコードに脆弱な文字セットを使用しないでください(utf8/latin1/ascii /などのみを使用します)

あなたは100%安全です。

そうでなければ、あなたは脆弱ですmysql_real_escape_string() ...を使用していても.

597
ircmaxell

実は、それを通過できるものは何もありません、%ワイルドカード以外に。攻撃者としてLIKEステートメントを使用していて、それを除外しない場合はログインとして%だけを入力することができ、ユーザーのパスワードをブルートフォースする必要があります。データはそのようにクエリ自体に干渉することができないため、100%安全にするためにプリペアドステートメントを使用することをお勧めします。しかし、そのような単純なクエリでは、おそらく$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);のようなことをするほうが効率的でしょう。

19
Slava

私はこれに直面しています、そして私はあなたがPDOで働くことをお勧めしますが、場合によってはこの方法を試してみたいかもしれません。それはうまくいくし、とても簡単です。なぜ人々はそれを無視したのだろうか。

コードサンプル//私のフレームワークを使う Moorexa

それは豊富なORMとそれ以上のものをサポートしています。しかし私は生のSQL文を書く人々に代わるものを確実にするためにこのテストケースを実行しなければなりませんでした。

例です。

// 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'

要点は何ですか。

  1. タイプチェック.
  2. ユーザー入力を検証します。 <1つだけ信頼する>
  3. 文字列のエスケープ引用符

選択クエリを実行するコード例を紹介します。

// 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