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コードを通過する可能性のあるインジェクションを知っていますか?
回答
次のクエリについて考えてみます。
$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";
簡単な答えは「はい」です。「はい」を回避する方法があります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");
特定の状況では、それは複数の行を返します。ここで何が起こっているのかを分析しましょう:
文字セットの選択
mysql_query('SET NAMES gbk');
仕事へのこの攻撃のために、我々は、サーバーの両方のエンコードへの接続に期待していることエンコーディングを必要とする
'
ASCII、すなわちのように0x27
してその最後のバイトASCIIであるいくつかの文字持っている\
すなわち0x5c
。結局のところ、デフォルトでのMySQL 5.6でサポートされている5つのなどのエンコーディングがありますbig5
、cp932
、gb2312
、gbk
とsjis
。gbk
ここで選択します。さて、
SET NAMES
ここでの使用に注意することは非常に重要です。これにより、サーバー上の文字セットが設定されます。C API関数の呼び出しを使用した場合は、mysql_set_charset()
問題ありません(2006年以降のMySQLリリースの場合)。しかし、なぜすぐに...ペイロード
この注入に使用するペイロードは、バイトシーケンスで始まります
0xbf27
。ではgbk
、これは無効なマルチバイト文字です。でlatin1
、それは文字列¿'
です。latin1
とgbk
は0x27
、それ自体がリテラル'
文字であることに注意してください。このペイロードを選択したのは、それを呼び出すと、文字の前に
addslashes()
ASCII、\
つまり0x5c
、が挿入されるため'
です。我々が羽目になるだろうので0xbf5c27
、その中にはgbk
、2つの文字列です:0xbf5c
続きます0x27
。つまり、有効な文字の後にエスケープされていない'
。が続きます。ただし、は使用していませんaddslashes()
。次のステップに進みます...mysql_real_escape_string()
へのCAPI呼び出し
mysql_real_escape_string()
はaddslashes()
、接続文字セットを認識しているという点で異なります。そのため、サーバーが期待している文字セットに対して適切にエスケープを実行できます。ただし、この時点まで、クライアントは、他の方法で通知したことがないlatin1
ため、接続にまだ使用していると考えています。使用しているサーバーに通知しましたgbk
が、クライアントはそれでもそうだと考えていlatin1
ます。したがって、への呼び出し
mysql_real_escape_string()
は円記号を挿入'
し、「エスケープされた」コンテンツには自由にぶら下がっている文字があります。私たちが見にした場合、実際には、$var
中にgbk
文字セット、我々は参照してくださいね。縗 'または1 = 1 / *
これはまさに攻撃に必要なものです。
クエリ
この部分は形式的なものですが、レンダリングされたクエリは次のとおりです。
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
おめでとうございます、あなたはmysql_real_escape_string()
...を使用してプログラムを攻撃することに成功しました。
悪い人
ひどくなる。PDO
デフォルトでは、MySQLでプリペアドステートメントをエミュレートします。つまり、クライアント側では、基本的にmysql_real_escape_string()
(Cライブラリで)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がネイティブに準備できないステートメントのエミュレートにサイレントにフォールバックすることに注意してください。マニュアルにリストされているステートメントですが、適切なサーバーバージョンを選択するように注意してください)。
ぶさいく
のmysql_set_charset('gbk')
代わりに使用すれば、これらすべてを防ぐことができたはずだと最初に言いましたSET NAMES gbk
。2006年以降のMySQLリリースを使用している場合は、これが当てはまります。
あなたは以前のMySQLリリース、その後、使用している場合はバグではmysql_real_escape_string()
、そのような私たちのペイロードのものと無効なマルチバイト文字が目的を脱出するための単一のバイトとして扱われたことを意味し、クライアントが正しく接続エンコーディングを知らされていた場合でもので、この攻撃は希望とをまだ成功しています。バグがMySQLの中で修正されました4.1.20、5.0.22および5.1.11。
しかし、最悪の部分は、5.3.6までPDO
C APIを公開しなかったmysql_set_charset()
ため、以前のバージョンでは、考えられるすべてのコマンドに対してこの攻撃を防ぐことはできません。これで、DSNパラメーターとして公開されます。
節約の恵み
冒頭で述べたように、この攻撃が機能するには、脆弱な文字セットを使用してデータベース接続をエンコードする必要があります。utf8mb4は脆弱ではありませんが、すべてのUnicode文字をサポートできます。そのため、代わりにそれを使用することを選択できますが、MySQL5.5.3以降でのみ使用可能になっています。代替手段はですutf8。これも脆弱ではなく、Unicode Basic MultilingualPlane全体をサポートできます。
または、NO_BACKSLASH_ESCAPESSQLモードを有効にすることもできます。これにより、(とりわけ)の操作が変更されmysql_real_escape_string()
ます。このモードを有効に0x27
すると、0x2727
ではなくに置き換えられる0x5c27
ため、エスケーププロセスは、以前は存在しなかった(つまり、まだ存在するなど)脆弱なエンコーディングで有効な文字を作成できません。したがって、サーバーは文字列を無効として拒否します。 。ただし、このSQLモードの使用から発生する可能性のある別の脆弱性については、@ eggyalの回答を参照してください。0xbf27
0xbf27
安全な例
次の例は安全です。
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、など)AND
mysql_set_charset()
/$mysqli->set_charset()
(PHP 5.3.6≥中)/ PDOのDSNのcharsetパラメータ
または
- 接続エンコーディングに脆弱な文字セットを使用しないでください(
utf8
/latin1
/ascii
/などのみを使用します)
あなたは100%安全です。
それ以外の場合は、使用しているにもかかわらずmysql_real_escape_string()
脆弱です...
TL; DR
mysql_real_escape_string()
次の場合は、保護を提供しません(さらに、データを破壊する可能性があります)。
MySQLのNO_BACKSLASH_ESCAPESSQLモードが有効になっています(接続するたびに別のSQLモードを明示的に選択しない限り、有効になっている可能性があります)。そして
SQL文字列リテラルは二重引用符を使用して引用符で囲まれます
"
。これはバグ#72458として報告され、MySQL v5.7.6で修正されました(以下の「TheSavingGrace」という見出しのセクションを参照してください)。
これは別の、(おそらく少ないですか?)あいまいなエッジケースです!!!
@ircmaxellの優れた回答に敬意を表して(実際、これは剽窃ではなくお世辞であるはずです!)、私は彼の形式を採用します。
攻撃
デモンストレーションから始めます...
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- '); mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
これにより、test
テーブルからすべてのレコードが返されます。解離:
SQLモードの選択
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
文字列リテラルで文書化されているように:
文字列内に引用符を含めるには、いくつかの方法があります。
「
'
」で引用された文字列内の「'
」は、「」と書くことができます''
。「
"
」で引用された文字列内の「"
」は、「」と書くことができます""
。引用文字の前にエスケープ文字(“
\
”)を付けます。「
'
」で引用された文字列内の「"
」は、特別な処理を必要とせず、二重化またはエスケープする必要もありません。同様に、「"
」で引用された文字列内の「'
」は、特別な処理を必要としません。
サーバーのSQLモードにが含まれている場合、NO_BACKSLASH_ESCAPESこれらのオプションの3番目(で採用されている通常のアプローチ)
mysql_real_escape_string()
は使用できません。代わりに、最初の2つのオプションのいずれかを使用する必要があります。4番目の箇条書きの効果は、データの改ざんを避けるために、リテラルを引用するために使用される文字を必ず知っている必要があることに注意してください。ペイロード
" OR 1=1 --
ペイロードは、文字通り文字通りこの注入を開始し
"
ます。特定のエンコーディングはありません。特殊文字はありません。奇妙なバイトはありません。mysql_real_escape_string()
$var = mysql_real_escape_string('" OR 1=1 -- ');
幸い、
mysql_real_escape_string()
SQLモードをチェックし、それに応じて動作を調整します。参照libmysql.c:ulong STDCALL mysql_real_escape_string(MYSQL *mysql, char *to,const char *from, ulong length) { if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES) return escape_quotes_for_mysql(mysql->charset, to, 0, from, length); return escape_string_for_mysql(mysql->charset, to, 0, from, length); }
したがって、SQLモードが使用
escape_quotes_for_mysql()
されている場合は、別の基礎となる関数、が呼び出さNO_BACKSLASH_ESCAPES
れます。上記のように、このような関数は、他の引用文字がリテラルで繰り返されることなくそれを繰り返すために、リテラルを引用するためにどの文字が使用されるかを知る必要があります。ただし、この関数は、文字列が一重引用符を使用して引用符で囲まれることを任意に想定しています
'
。参照charset.c:/* Escape apostrophes by doubling them up // [ deletia 839-845 ] DESCRIPTION This escapes the contents of a string by doubling up any apostrophes that it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in effect on the server. // [ deletia 852-858 ] */ size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info, char *to, size_t to_length, const char *from, size_t length) { // [ deletia 865-892 ] if (*from == '\'') { if (to + 2 > to_end) { overflow= TRUE; break; } *to++= '\''; *to++= '\''; }
したがって、リテラルの引用に使用される実際の文字に関係なく、二重引用符はそのままにします
"
(そして、すべての一重引用符の文字を2倍にします'
)。私たちの場合、に提供された引数とまったく同じままです—それはまるでエスケープがまったく行われていないかのようです。$var
mysql_real_escape_string()
クエリ
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
形式的なもので、レンダリングされたクエリは次のとおりです。
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
私の学んだ友人が言ったように:おめでとうございます、あなたはちょうどmysql_real_escape_string()
...を使用してプログラムを攻撃することに成功しました。
悪い人
mysql_set_charset()これは文字セットとは何の関係もないので、仕方がありません。またmysqli::real_escape_string()、これは同じ関数のラッパーが異なるだけなので、できません。
問題は、まだ明らかではmysql_real_escape_string()
ないにしても、後で決定するのは開発者に任されているため、リテラルがどの文字で引用されるかを呼び出すことができないことです。したがって、NO_BACKSLASH_ESCAPES
モードでは、この関数がすべての入力を安全にエスケープして任意の引用符で使用する方法は文字通りありません(少なくとも、2倍にする必要のない文字を2倍にして、データを変更する必要はありません)。
ぶさいく
ひどくなる。NO_BACKSLASH_ESCAPES
標準SQLとの互換性のために使用する必要があるため、実際にはそれほど珍しいことではないかもしれません(たとえば、SQL-92仕様のセクション5.3 、つまり<quote symbol> ::= <quote><quote>
文法の生成とバックスラッシュに与えられた特別な意味の欠如を参照してください)。さらに、ircmaxellの投稿で説明されている(修正されてから長い間)バグの回避策として、その使用が明示的に推奨されていました。誰が知っているか、一部のDBAは、のような誤ったエスケープ方法の使用を思いとどまらせる手段として、デフォルトでオンになるように構成することさえあります。addslashes()
また、新しい接続のSQLモードは、サーバーの構成に従ってサーバーによって設定されます(SUPER
ユーザーはいつでも変更できます)。したがって、サーバーの動作を確認するには、接続後に常に目的のモードを明示的に指定する必要があります。
節約の恵み
だから、限り、あなたはいつものように明示的に含まれないSQLモードを設定しNO_BACKSLASH_ESCAPES
、それぞれ:、または単一引用符文字を使用してMySQLの文字列リテラルを引用し、このバグはリアないその醜い頭できescape_quotes_for_mysql()
使用されることはありません、または引用符が意志を繰り返す必要かについての仮定正しいこと。
このため、一重引用符で囲まれた文字列リテラルを習慣的に使用することになるため、モードをNO_BACKSLASH_ESCAPES
有効にすることをお勧めしANSI_QUOTESます。これは、二重引用符で囲まれたリテラルが使用された場合のSQLインジェクションを妨げるものではなく、その可能性を減らすだけであることに注意してください(通常の悪意のないクエリは失敗するため)。
PDOでは、同等の関数PDO::quote()とプリペアドステートメントエミュレーターの両方が要求します。mysql_handle_quoter()これはまさにこれを行います。エスケープされたリテラルが一重引用符で囲まれていることを保証するため、PDOは常にこのバグの影響を受けません。
MySQL v5.7.6以降、このバグは修正されています。変更ログを参照してください:
追加または変更された機能
互換性のない変更: SQLモードが有効になっmysql_real_escape_string_quote()ているmysql_real_escape_string()と、後者の関数が文字を適切にエンコードできない可能性があるため、の代わりに新しいCAPI関数が実装されNO_BACKSLASH_ESCAPESました。この場合、mysql_real_escape_string()引用符を2倍にする以外にエスケープすることはできません。これを適切に行うには、引用符のコンテキストについて、利用可能な情報よりも多くの情報を知っている必要があります。mysql_real_escape_string_quote()引用コンテキストを指定するための追加の引数を取ります。使用法の詳細については、 mysql_real_escape_string_quote()を参照してください。
注意
アプリケーションはmysql_real_escape_string_quote()、の代わりにを使用するように変更する必要がmysql_real_escape_string()ありCR_INSECURE_API_ERRますNO_BACKSLASH_ESCAPES。これは、有効にすると失敗してエラーを生成するようになりました。
参照:バグ#19211994も参照してください。
安全な例
ircmaxellによって説明されたバグと合わせて、次の例は完全に安全です(4.1.20、5.0.22、5.1.11以降のMySQLを使用している、またはGBK / Big5接続エンコーディングを使用していないと仮定)。 :
mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*'); mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
...を含まないSQLモードを明示的に選択したためNO_BACKSLASH_ESCAPES
です。
mysql_set_charset($charset); $var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
...文字列リテラルを一重引用符で引用しているためです。
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1'); $stmt->execute(["' OR 1=1 /*"]);
... PDOプリペアドステートメントはこの脆弱性の影響を受けないため(PHP≥5.3.6を使用していて文字セットがDSNで正しく設定されているか、プリペアドステートメントのエミュレーションが無効になっている場合はircmaxellも同様です) 。
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
... PDOのquote()
関数はリテラルをエスケープするだけでなく、(一重引用符で'
)引用符で囲むためです。この場合ircmaxellのバグを回避するためにそのノートは、あなたがしなければならないPHP≥5.3.6を使用することにし、正しくDSNの文字セットを設定しています。
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1'); $param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
... MySQLiで準備されたステートメントは安全だからです。
まとめ
したがって、次の場合:
- ネイティブのプリペアドステートメントを使用する
または
- MySQLv5.7.6以降を使用する
または
ircmaxellの要約にあるソリューションの1つを採用することに加えて、次の少なくとも1つを使用してください。
- PDO;
- 一重引用符で囲まれた文字列リテラル。または
- を含まない明示的に設定されたSQLモード
NO_BACKSLASH_ESCAPES
...その後、完全に安全である必要があります(文字列のエスケープの範囲外の脆弱性は脇に置きます)。
まあ、%
ワイルドカード以外に、それを通過できるものは何もありません。LIKE
ステートメントを使用している%
場合、それをフィルターで除外しないと攻撃者がログインと同じように配置する可能性があり、ユーザーのパスワードをブルートフォースする必要があるため、危険な場合があります。データがクエリ自体に干渉することはないため、プリペアドステートメントを使用して100%安全にすることを提案することがよくあります。しかし、そのような単純なクエリの場合、次のようなことを行う方がおそらく効率的です。$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);