この記事の内容は執筆当時のものです。ご利用条件の詳細はサイト利用規約をご確認ください。
各ブログ記事の内容は執筆当時のものであり、閲覧時点において最新の情報が掲載されていない場合があります。各ブログ記事の内容について試行する場合は、必ずご自身で内容の安全性・正当性を検証の上、ご自身の管理下にある機器・環境に対してのみ実施してください。
徹底解説!SQLインジェクションの影響とメカニズム - 攻撃手法と防御策
どうも、カヌです。
最近、趣味で合格した第二種電気工事士の免状が届いたので、さっそく自宅のコンセントの工事とかやってます。
さて、早速本題に入りましょう。
今日は、 SQLインジェクション【SQL Injection】攻撃に対する脆弱性が存在することで、何ができるのか、どのような影響があるかを、オープンソースのSQLインジェクションの検査ツールであるsqlmapを用いて検証していきたいと思います。
また、オプションの一部についてはマニュアルに記載がなかったのでGitHubのソースコードも参考にしています。
マニュアルやソースコードを見ながら、できるだけ効果的なオプションの組み合わせを、被害を発生させない安全な方法で実施していこうと思います。
なお、SQLインジェクションの詳細はSQLインジェクション【SQL Injection】とは|図でわかる脆弱性の仕組みを参照してください。
脆弱性の説明
当社内でおなじみのPHP言語で記述され、脆弱性が組み込まれたサイト(CatCommunityというサイト)を使っていきます。
このサイトは、ローカルマシンに構築しており、http://localhost/
でアクセスできるという前提で進めていきます。
また、DBMSはMySQLを前提としています。
今回はこのログインフォームにフォーカスします。
実はこのフォーム、内部にSQLインジェクション攻撃に対する脆弱性が存在します。
次のようなコードでログイン許可・拒否を検証しています。
ログイン対象のユーザ名とパスワードが一致するレコードが1件以上あればログインをする。
というアルゴリズムになっています。
// SQL文にユーザ入力値を直接挿入しており、SQLインジェクション攻撃に脆弱です。
$raw_sql="SELECT COUNT(*) FROM user_info WHERE user_id='$user_id' AND password='$password'";
$statement=$pdo->query($raw_sql);
$result = $statement->fetch();
$login_check = intval($result["COUNT(*)"]);
// 内容は省略しますが、大体こんな雰囲気でログイン実行処理が書かれているとします。(以降のソースコードでは不要なので省略します。)
if($login_check){/*ログイン完了後の処理*/}else{/*ログインできなかった時のエラー処理*/}
ですが、黄色マーカーの部分がSQLインジェクション攻撃に対して脆弱です。
このコードの変数について説明します。
$user_id:POSTパラメータ(user_id)から取得したログインユーザのIDです。
$password:POSTパラメータ(password)から取得したパスワードです。
具体的には以下のようなコードで取得されています。
$raw_sql:ユーザの入力(ユーザIDとパスワード)を基にして、動的に構築されたSQL文が入ります。
例えば、ユーザ名に admin
、パスワードにadminpassword
という文字列が入ってきた場合には
SELECT COUNT(*) from user_info WHERE user_id='admin' AND password='adminpassword';
という文字列になり、「user_idカラムにadminがあり、なおかつpasswordカラムにadminpasswordがあるレコードが存在したらログインを許可する」という意味になります。
これは当初想定したSQL文と意味合いは変わっていません。
次にこんなことも試してみましょう。
例えば、ユーザ名に admin or 1=1--
、パスワードにadminpassword
という文字列が入ってきた場合には
SELECT COUNT(*) from user_info WHERE user_id='admin' or 𝟣=𝟣--' AND password='adminpassword';
という文字列になります。
ここで重要なのはコメントの始まり記号である--
以降の--' and password='adminpassword'
という文字列は読み捨てられるということです。
つまりSELECT COUNT(*) from user_info WHERE user_id='admin' or 𝟣=𝟣
のようなSQL文に変更され、「user_idカラムにadminがある場合か、1=1である場合にログインする」という意味合いに変わってしまいます。
内部で定義したSQL文の意味合いを、攻撃者の都合に合わせて外部から変更する攻撃が「SQLインジェクション攻撃」です。
今回はこのSQLインジェクション攻撃に対する脆弱性を持つログインフォームを悪用してDBをダンプしてみましょう。
とはいえ、ポチポチ一つ一つ攻撃コードを打っていくのは非常に疲れますのでsqlmapというオープンソースのツールを使っていきたいと思います。
SQLインジェクション攻撃を実際にやってみる
早速SQLインジェクション攻撃を試してみたいと思っていますが、その前にツールの使い方を学んでおきましょう。
極力安全に使う方法もお伝えできればと思います。
sqlmapのオプションの説明
--risk
- リスクレベル1:AND演算をベースにしたSQLインジェクション攻撃のみを行います。
- リスクレベル2:大量のクエリを流し、その処理時間を計測するSQLインジェクション攻撃を追加で行います。(貧弱な対象の場合はサーバダウンする恐れがあるので、見極めて使います。)
- リスクレベル3:OR演算子を使ったSQLインジェクション攻撃を追加で行うため、DBの破壊を招く恐れがあります。(業務では原則使用しません。自前で用意した検証環境に使うかどうかといったところです。)
--level
- 検査レベル1:GETクエリストリング、POSTパラメータと最低限のリクエスト数でのテストを行います。
- 検査レベル2:Cookieを加えてテストを行います。
- 検査レベル3:UserAgent・Refererヘッダを加えてテストを行います。
- 検査レベル4:(ソースコードより)セッション・CSRFトークンなどのパラメータもテストの対象にします。
- 検査レベル5:ホストヘッダもテストの対象にします。また、検査値をパラメータの前後に付与したテストを行います。
--data
- POSTパラメータがある場合、このオプションで指定します。
- 今回のデモでは、パラメータが3つあるので使用しています。
--dump
- 現在選択中のDBテーブルの内容をダンプします。
--current-db
- 現在選択中のDB名をダンプします。
--current-user
- 現在SQLクエリを発行しているDBユーザ名をダンプします。
--passwords
- DBMSにアクセスする際のパスワードハッシュをダンプします。
--A
- アプリケーションがUser-Agentでクライアントを判別している場合には、本オプションを使うことで回避できます。
- MS Edgeに偽装する場合は次の通りにします。(FirefoxやChromeでもご自身がインストールしているバージョンのヘッダを調べることで容易に偽装可能です。)
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"
ここまでは、SQLインジェクションとは何かということと
ツールに関する簡単な説明をしてきました。
ここからは、実際にツールを使ったデモを行いたいと思います。
sqlmapの準備
検査レベルとリスク設定はそれぞれ検査レベルが3、リスクレベルを1で設定します。
python3.12.exe .\sqlmap.py -u "http://localhost/cheshirecat/Login.php" --level=3 --risk=1 --data "user_id=admin&password=admin&submit=" --batch --dump --current-user --current-db --passwords --flush-session -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"
※動画内ではUser-Agentの指定を省略しています。
攻撃の実施と結果
それでは、実際にSQLインジェクション攻撃をsqlmapを用いて実施しているデモ動画をご覧ください。
現在の操作ユーザ root@localhost
と、選択しているDBcheshire_cat
の名前と中身がダンプできました。
また、MySQLでは任意のファイルの読み取り(LOAD_FILE)、生成(INTO OUTFILE、INTO DUMPFILEを使用します)も可能であり、Webshellの配置などに悪用されることもあります。
ただし、SQLサーバとWebサーバが同一ホストであったり、何らかの形でインターネットに外接しているWebサーバにSQLサーバがファイルを配置できるような構成で有ることが条件です。
この他、SQLiteでは共有ライブラリを読み込むことで任意コードの実行も可能です。(拡張機能読み込み機能を明示的に有効化している場合に限ります。)
上記のように、DBのダンプからファイルの読み書き、OSコマンドの実行までできてしまう可能性があります。
対策
それでは、対策のお話に進みます。
対策自体は、特に複雑なことは無くてSQLの呼び出し方を少し変えるだけです。
以下のようにバインド機構を利用して、確実にユーザ入力がSQL文から分離されるようにします。
// プリペアド・ステートメントにプレースホルダ("?")を定義します。
$prepared = $pdo->prepare("SELECT COUNT(*) FROM user_info WHERE user_id=? AND password=?;");
// プレースホルダに値を紐づけします。
// 以下は、1つ目のプレースホルダ(?記号部分)に紐づけする、という意味になります。
$prepared->bindValue(1,$user_id);
// 以下は、2つ目のプレースホルダ(?記号部分)に紐づけする、という意味になります。
$prepared->bindValue(2,$password);
$prepared->execute();
$login_info = $prepared->fetchColumn();
フレームワークによっては、O/Rマッパーが使えることもあります。
O/Rマッパーを使える場合は生のSQL文を扱うことはせず、O/Rマッパーを積極的に使うことを推奨します。
バインド機構が使えない例外ケース
ORDER BY句やテーブル名など、固定値が入ると想定されている箇所についてはバインド機構を使用できません。
基本的にこのような箇所を動的に変更する設計は避けるべきです。
とはいえ、ソート機能を実装する場面においては必要になることもあるかもしれません。
この場合は、ASCやDESCと言った文字列を直接受け入れるのではなく、以下のような設計にするとよいと考えています。
受け入れパラメータ名(POSTパラメータ)をsort
とし、ASCもしくはDESCという文字列が入ってくる前提とします。
このパラメータを使って以下のように事前定義したテーブル(ホワイトリスト)からSQL文を動的に構成します。
$whitelist = ["ASC","DESC"];
$sort = $_POST["sort"];
// 配列に登録されている文字列でなければ ASC とする
if( !in_array($sort,$whitelist) ){
$sort = "ASC";
}
// sortを連結していますが、構文の意味合いを変更しない固定文字列であることを保証しているため、問題ありません。
// ソースコード診断ツールなどでは「SQL インジェクション」として検出される可能性がありますが、過剰に検知されてしまっているだけですので問題ありません。
$prepared = $pdo->prepare("SELECT * FROM purchase_history WHERE user_id=? ORDER BY id $sort");
$prepared->bindValue(1,$user_id);
$prepared->execute();
$login_check = $prepared->fetchColumn();
この書き方はテーブル名などに対しても応用が利きます。
ただし、設計上テーブル名を動的に書き換える必要がなければ、別々のSQL文を準備し、それぞれラッパー関数としてCRUD処理を用意したほうが良いと考えています。
動的な値を用いることでメンテナンス性の低下が考えられることと、本質的に単一責任の原則に反するので前述の通り推奨はしていません。
極力避けるべき対策
バインド機構を使用しない場合に、以下のような対策をとることがあります。
ただし、以下で挙げる実装はアプリケーションの使い勝手や拡張性が低下することが見込まれます。
また、DBMS製品に応じた対応を行う必要も出て来ますが、漏れなく実装するのは非常に困難だと考えられます。
製品改良時に設計の幅が狭まる可能性や、コードのリファクタリングに手間がかかる可能性があるため、極力避けるべきです。
- エスケープを行う
- OWASPでは「強く推奨されない」保険的(Optional)な対策とされています。
- 入力可能な文字数や文字種を減らす。
- アプリケーションの異常系対策(セキュリティ対策)を目的とした文字種や文字数の制限は行わないでください。
パスワードの入力文字数の上限や文字種に制限を掛けるなどにつながってしまい、総合的なセキュリティレベルが低下する恐れがあります。 - アプリケーションの正常系として受け入れる文字数の上限を決める場合には問題ありません。(電話番号の入力などで文字種・文字数を制限するなど。)
まとめ
この記事では、SQLインジェクション攻撃に対する脆弱性を持つログインフォームを使用して、攻撃を行った結果、どのようなことが実行できるかを検証しました。
以下に、本記事の重要なポイントをまとめます。
- SQLインジェクション攻撃に対する脆弱性の発生原理は、SQL文の意味合いを外部から変更されてしまうことで発生します。
- 被害はサイトへの不正ログインやDBのフルダンプに留まらず、設定によってはOSコマンドの実行やファイルの読み書きによるWebshellの配置・実行まで幅広く存在します。
- バインド機構を利用することや、フレームワークによってはO/Rマッパーを利用することで対策が可能です。
- テーブル名など、最初から固定されることが前提である箇所にはプレースホルダは使えません。もし、テーブル名を可変にする等の要求がある場合は、本当に必要な設計であるかを見直しましょう。
これらの対策を施すことで、SQLインジェクション攻撃からの保護が強化され、安全で信頼性の高いシステムの構築に寄与します。