web-dev-qa-db-ja.com

PHPでSQLインジェクションを使用して情報を抽出する方法

私はSQLインジェクションが初めてなので、これが少しアマチュアのように見える場合はご容赦ください。

私は認証をバイパスして通常のユーザーとしてログインすることができたウェブサイトで作業していました:

username: ' OR 1 -- -
password: <empty>

結果のページには、「あなたは次のようにログインしています: 'OR 1--」と表示されます。そこで、データベースの詳細を表示する必要があるユーザー名フィールドにダンプできるかどうか考えました。

ただし、データベースのバージョン/情報を列挙してさらに一歩進んだら、機能しません(認証をバイパスできなくなったため)。

私は以下を試しました:

username: ' OR 1;SELECT @@VERSION -- -
username: ' OR 1;SELECT user_name -- -

WebサイトはApache/2.4.7(Ubuntu)サーバーを実行しており、データベースはMySQLを実行していると思います。

7
Nicholas Lim

SQLインジェクションの新機能に問題はありませんが(これはかなり古典的な例のように思われるため、完全なスタートを構成します:))、SQLリクエストに関するいくつかの信頼できる背景を持つことが非常に重要だと思います:構文、テーブル、結果セット、それらは何であり、どのように処理されるかなどです。

SQLインジェクションの目標は、可能な限りあらゆる方法で要求をねじることによって数値を計算することです(状況に応じてバレルを保持しません!) 、最終的には、サーバー側の処理を実際にcontrol制御する能力を獲得します。

簡単に悪用可能なSQLインジェクションを見つける

説明のために、「 SQLインジェクションとは 」の主な回答で示した例を再利用します。 SQLインジェクションに関する背景情報がさらに必要な場合は、それを開始するのに適しています。

したがって、サーバーが次のコードを実行していると想像します。

_sql = "select id, username from users'
      + ' where username='" + username + "' and password='" + password +"'";
_
  • 認証が成功すると、このリクエストは次のような結果セットを1行で返します。

    _+----+----------+
    | id | username |
    +----+----------+
    | 42 | jdoe     |
    +----+----------+
    _
  • 認証に失敗すると、空の結果セットが生成され、行は生成されません。

    _+----+----------+
    | id | username |
    +----+----------+
    +----+----------+
    _

これで何とかして、サーバー側のコードが脆弱であることを示しているのは、ユーザー名を_' OR 1 --_に設定して認証をバイパスすることです。

上記と同じ例をとると、コードは次のSQLリクエストを生成します。

_select id, username from users where username='' OR 1 --
_

(_--_は、リクエストの残りの行を効果的にコメント化しているため、ここには表示されていません)

ここで、このリクエストの機能を理解します。このリクエストは、すべてのユーザーのIDとユーザー名を取得します(ユーザー名が空のすべてのユーザーまたはtrue、つまりすべてのユーザーを意味します)。

これはあなたにとって良いニュースではありません(結果セットの最初の行がたまたまWebサイトの管理者である場合を除きますが、これは情報の抽出に焦点を当てるこの投稿では対象外です)。

どうして?ここでも、サーバー側で何が起こっているのかを想像する必要があります。

これは、「ログインしている」Webページに表示される内容が表示されることを意味するため、良いニュースではありません。

  • データベースクエリの結果(_$result[0]['username']_)から抽出されません。そうしないと、代わりにランダムなユーザー名が表示されます。
  • 代わりに、サーバーによって受信されたフォームのフィールド値から直接取得される可能性が最も高いです(getPostData("username");)。

正当な使用例では、どちらも同じ値を返す必要があるため、これは何も変更しません。私たちの場合、これは悪いことです。これは、この場所でデータベース情報を抽出して表示できない可能性が高いためです。

より高度なSQLインジェクション方法(エラーベースのSQLi、ブラインドSQLiなど)があり、Webサイトが明示的に表示するつもりがない場合にデータベースから情報を抽出できます。ただし、それらには 独自の問題のセット が付属しており、この投稿ではトピック外と見なします。実際、認証ページを保護するためにWebサイト開発者が注意を払っていると、他のページにも脆弱性が存在する可能性が高く、今度は他のページがデータベースから取得した情報を表示します(ユーザーのプロフィールページがあなたの親友かもしれません:) )。

(偽のユーザー名が単一の行ではなく複数の行を返すという事実に満足できないページがある場合は、LIMITステートメントをユーザー名に追加してください:_' OR 1 LIMIT 1, 1 --_、もちろん、パラメータを自由に変更して、あるユーザーから別のユーザーに切り替えることができます...有効なユーザー名を知っている場合は、それも使用してみてください:_admin' --_。)

この投稿では引き続き認証ページを例として取り上げますが、これは他のページに影響を与えるSQLインジェクションでも同じように機能します。

結果セットのレイアウトを決定する

この投稿の冒頭では、結果セットが2列のテーブルで、2番目の列にユーザー名が格納されていると想定していました。

_+----+----------+
| id | username |
+----+----------+
| 42 | jdoe     |
+----+----------+
_

しかし、これがいくつかの仮定に過ぎないことを知るまで、私たちは気にしませんでした。残念ながら、これを知っている必要があります:

  • 技術的な理由から、列の正確な数を知る必要があります。そうしないと、後で使用するUNIONステートメントが満足できなくなります。
  • サーバーが表示されたデータをフェッチする列がわかれば、注入された値を結果セットの適切な場所に配置できます(実際の列名を知る必要はなく、位置だけを知る必要があります)。

(サーバーがMySQLの代わりにPostgreSQLを使用する場合、UNIONを満たすために正しい列タイプを決定する必要があるかもしれません。実際の問題よりも厄介ですが、それは覚えておいてください。)

列の数を決定する

最も簡単な方法は、次のようにサーバーに結果を注文するよう依頼することです。

_username: ' OR 1 ORDER BY 1 -- -
_

エラーが発生するまで、_ORDER BY 1_句をインクリメントします。最後に機能する値は、結果セットの列数に対応します。

これをサンプルの結果セットに対して実行すると、次のようになります。

  • _username: ' OR 1 ORDER BY 1 -- -_:OK
  • _username: ' OR 1 ORDER BY 2 -- -_:OK
  • _username: ' OR 1 ORDER BY 3 -- -_:エラー、例の結果セットには2つの列が含まれています。

使用可能な列を特定する

正しい列数がわかったので、次に進んで次のようなコマンドを挿入できます。

_username: ' OR 1 UNION SELECT 1,2 -- -
_

3つの列がある場合は、5つの列に対して_SELECT 1,2,3_を実行します__SELECT 1,2,3,4,5_など。

ここでの目標は、実際の数値を結果セットに挿入し、サーバーによって表示されるWebページ(レンダリングされたものまたはそのソースコード)をチェックインすることです。

数値を使用する利点は、カラムタイプで必要な場合、MySQLによって文字列として簡単にキャストできることです。ここでは例として「1,2,3」を示していますが、お好きな数字を自由に使用してください(「1234,2345,3456」も同様に機能し、結果を見つけやすくなります)。

この例では、結果のWebページは「次のようにログインしています:2」と表示されます。ユーザー名が結果セットの2番目の列からフェッチされることがわかります。

注意UNION演算子がどのように機能するかを理解します。サーバーは、左側がリクエストは行を生成しません!

実際、UNION演算子は2つの結果セットを連結します。

_+----+----------+         +----+----------+
| 42 | jdoe     |  UNION  | 1  | 2        |
+----+----------+         +----+----------+
_

結果は:

_+----+----------+
| id | username |
+----+----------+
| 42 | jdoe     |
+----+----------+
| 1  | 2        |
+----+----------+
_

これは期待される結果ではない可能性があります。

取得するため:

_+----+----------+
| id | username |
+----+----------+
| 1  | 2        |
+----+----------+
_

左側のクエリがエントリを返さないようにすることができます。

_username: ' AND 0 UNION SELECT 1,2 -- -
_

_OR 1_が_AND 0_に反転されていることを確認して、データベース行の内容に関係なく、左側のクエリがfalseとして評価されることを確認します。

SQLインジェクションを利用する

そして今、楽しみが始まります:)!

このすべての情報を入手したら、データベースから必要な情報を自由に抽出できます。

例えば:

  • MySQLバージョンを取得するには:

    _username: ' AND 0 UNION SELECT 1,@@VERSION -- 
    _
  • テーブルと列の名前を取得するには:

    _username: ' AND 0 UNION SELECT 1,GROUP_CONCAT(table_name,0x2e,column_name) FROM information_schema.columns WHERE table_schema=database() -- 
    _

インジェクションに適したSQLリクエストを参照するWebサイトはたくさんあります。一部の構文はバージョンに依存する可能性があるため、目の前にあるMySQLサーバーのバージョンに注意してください。

結論

SQLインジェクションの容易さは、サーバーのSQLリクエストの詳細と結果に適用される処理によって大きく異なります。ここでの運の要素次第です。

それにもかかわらず、成功したSQLインジェクション文字列を手動で作成すると、常に次のループになります。

  1. サーバーにリクエストを挿入します。
  2. 結果を観察します。
  3. サーバーの観点からこの結果を解釈してみてください:どのような処理がそのような結果につながる可能性がありますか? (これはSQLに関するあなたの知識が本当に重要でした)
  4. 問題を解決するか、サーバーの動作に関するいくつかの仮説を確認するために設計された新しい注入文字列を作成します。
  5. 手順1に戻ります。

ついに、これを手動で行うと、十分にスケーリングできない場合があります。必要に応じて、 sqlmap のようないくつかのツールがこれをすべて自動化するのに役立ちます。

12
WhiteWinterWolf

PHP/MySQLのスタッククエリ

MySQL + PHPは、実際にはほとんど使用されない multi_query を使用したスタッククエリのみをサポートします。

そのため、攻撃は機能しません。 ;を使用して2番目のクエリを追加しようとしましたが、これはサポートされていません。

ただし、この注入でもデータを抽出できます。 MySQLに関する限り、エラーベースの注入とブラインド注入の2つのオプションがあります。

エラーベースの注入

まず、名前が示すように、エラーベースのインジェクションは、MySQLエラーメッセージが実際に表示される場合にのみ機能します。

アイデアは単純です。注入でデータを抽出できない場合、結果が表示されないため、必要なデータを含むMySQLエラーを作成するだけです。これは多くの場合、extractvalueを介して行われます。

' OR extractvalue(1,version()) -- -

ブラインドSQLインジェクション(コンテンツベース)

エラーベースのインジェクションと比較して、エラーメッセージが表示されない場合でも、ブラインドインジェクションは常に機能します。

アイデアは再び単純です。データを表示することはできませんが、クエリが結果をフェッチしたかどうかはわかります。あなたのケースでは、ログインしている(True状態)か、ログインしていない(False状態)のどちらかです。

非常に単純な攻撃は次のとおりです。

' OR substring(version(),1,1)='5

これによりログインが成功する可能性が高いため、データベースのバージョンが5で始まることがわかります。別の値を指定してもログインは行われません。

現在のアイデアは、一度に1つの文字を取得することです。

バージョンを尋ねる代わりに、より複雑なクエリを追加することもできます。結果を一度に1行だけLIMITすることを忘れないでください。

この攻撃はかなりうるさいですが、改善することができます。 1つの可能性は、ascii関数を使用して文字をそのASCII値に変換することです。これにより、小なり比較と大なり比較を使用できます。

ブラインドSQLインジェクション(タイミングベース)

場合によっては、注入から真/偽のフィードバックを得ることもありません。そのような場合でも、応答時間を使用して真/偽の質問をすることができます。

' AND if(substring(version(), 1, 1)='5',BENCHMARK(50000000,ENCODE('msg','msg')),null) -- -

ここでの考え方は、特定の条件が真の場合にいくつかの高価な関数が呼び出されるため、応答時間が長くなるということです。

以前と同様に、ascii関数を使用して、より複雑なクエリを実行し、パフォーマンスを向上させることもできます。

2
tim