この記事の内容は執筆当時のものです。ご利用条件の詳細はサイト利用規約をご確認ください。
どうも、カヌです。
最近、趣味で合格した第二種電気工事士の免状が届いたので、さっそく自宅のコンセントの工事とかやってます。
さて、早速本題に入りましょう。
今日は、 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インジェクション攻撃を試してみたいと思っていますが、その前にツールの使い方を学んでおきましょう。
極力安全に使う方法もお伝えできればと思います。
--risk
--level
--data
--dump
--current-db
--current-user
--passwords
--A
-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インジェクションとは何かということと
ツールに関する簡単な説明をしてきました。
ここからは、実際にツールを使ったデモを行いたいと思います。
検査レベルとリスク設定はそれぞれ検査レベルが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製品に応じた対応を行う必要も出て来ますが、漏れなく実装するのは非常に困難だと考えられます。
製品改良時に設計の幅が狭まる可能性や、コードのリファクタリングに手間がかかる可能性があるため、極力避けるべきです。
この記事では、SQLインジェクション攻撃に対する脆弱性を持つログインフォームを使用して、攻撃を行った結果、どのようなことが実行できるかを検証しました。
以下に、本記事の重要なポイントをまとめます。
これらの対策を施すことで、SQLインジェクション攻撃からの保護が強化され、安全で信頼性の高いシステムの構築に寄与します。