2026-10-25
Webアプリの脆弱性について、言葉では知っていても実際にどう成立するのかはピンときていませんでした。そこで DVWA(Damn Vulnerable Web Application) という「わざと脆弱に作られた練習用アプリ」を隔離環境で立てて、ファイルアップロード脆弱性とSQLインジェクションを自分の手で再現してみました。触ってみると、この2つは見た目が全然違うのに原因は同じだと腑に落ちたので、その学びをまとめます。
きっかけは「PHP2Shell」というPHPの脆弱性がニュースになったことでした。 個人情報流出のニュースが連日放送されているのも見ていて、脆弱性に関してもっと理解を深めたいと思い、DVWAを触ってみることにしました。
脆弱性を試すアプリは、文字どおり「穴だらけ」です。そのまま外部からアクセスできる場所に置くのは危険なので、Dockerで隔離して立てました。
ポイントは -p 127.0.0.1:8080:80 の部分です。0.0.0.0 ではなく 127.0.0.1 に限定してバインドすることで、自分のマシンの中からしかアクセスできなくなり、外部には公開されません。練習用の脆弱なアプリを扱うときの最低限の自衛策だと思います。
起動したら http://127.0.0.1:8080 にアクセスし、初期ユーザー admin / password でログインします。DVWAには難易度設定があるので、まずは Security を Low(穴あきモード)にして挙動を観察しました。
終わったら後片付けも忘れずに。
コンテナを強制削除して、脆弱なアプリを残さないようにします。
DVWAの File Upload ページで、画像の代わりにPHPファイルをアップロードしてみます。成立までの流れはこうでした。
shell.php を用意する※ ちなみに、このphpファイルをWindows側に置こうとしたら、保存した瞬間にWindows Defenderが削除してしまいました。Webシェルはウイルス対策ソフトに「典型的な攻撃ファイル」として検知される、という防御が実際に効いた一例です。手元では開発環境がWSL上にあったので、WSL側でファイルを用意してアップロードしました。
hackable/uploads/)に保存されるhttp://127.0.0.1:8080/hackable/uploads/shell.php にアクセスすると、ApacheがPHPとして実行するhttp://127.0.0.1:8080/hackable/uploads/shell.php?cmd=id のようにURLで渡した文字列が、system() を通してそのままOSコマンドとして実行される?cmd=... を書き換えるだけで何度でもコマンドを打てる=シェルを取られた状態たった1行ですが、分解すると何をしているかがわかります。
<?php ... ?>: この中がPHPとしてサーバー上で実行される、という囲み$_GET['cmd']: URLの ?cmd=○○ で渡された文字列を受け取るsystem(...): 受け取った文字列をOSコマンドとして実行し、結果を画面に返すPHPの関数たとえば ?cmd=id にアクセスすると、$_GET['cmd'] が "id" を受け取り、system("id") としてサーバー上でコマンド id が実行されます。id を ls や cat /etc/passwd に書き換えれば、ブラウザから好きなコマンドを打ててしまうわけです。
本来 cmd は「ただの文字列データ」であるはずなのに、system() に素通しされることで命令として実行されてしまう。この記事の核である「データを命令として解釈させてしまう」の、一番わかりやすい見本だと思います。攻撃の手順そのものより、何が起きているか を理解するのが目的なので、コードはこの最小限に留めます。
この攻撃は1つの原因で成立するわけではなく、2つの不備が重なって初めて 通ってしまいます。裏を返すと、どちらか一方でも塞げば止められるということです。
| # | 不備 | これがあると |
|---|---|---|
| 1 | アップロードで拡張子/MIMEを検証していない | .php が通ってしまう |
| 2 | 保存先がWebから実行可能になっている | アクセスでPHPが動いてしまう |
シェルを取れたと言っても、何でもできるわけではありませんでした。コマンドを実行しているのは www-data、つまり権限を絞られたWebサーバー専用のユーザーです。
ちなみにこの www-data、自分で作った覚えがなかったのですが、、ApacheやNginxを apt install などでインストールすると、そのセットアップ処理が裏で www-data というユーザーとグループを自動的に作ります(今回のDVWAはDockerイメージの中に最初から用意されています)。
試してみると、cat /etc/passwd は読めても、パスワードの本体が入っている cat /etc/shadow(root専用)は読めませんでした。侵入されても、そのユーザーにできる範囲までしか被害が及ばない。これが 最小権限の原則 の効き目で、「被害に天井がある」状態を体感できました。
逆に言えば、もしWebサーバーをrootで動かしていたら、この壁は無くなります。改めてWebサーバーは絶対にrootで動かしてはいけないと思いました。
次はSQLインジェクションです。これはDBに投げるSQL文の中に、入力を通して悪意ある命令を混ぜ込む攻撃で、原因はファイルアップロード脆弱性とまったく同じ(入力を命令として解釈させる)です。DVWAの SQL Injection ページで試しました。
OR '1'='1' によって条件が常に真になり、本来は1件だけのはずが全ユーザーが返ってきます。実際にDVWAの User ID 欄に 1' OR '1'='1 を入れて Submit すると、登録されている全ユーザーの名前が一覧で返ってきました。

UNION SELECT で、本来の画面には出ないはずの列(ユーザー名やパスワード)まで引っ張り出せてしまいます。末尾の -- はSQLのコメントで、元のSQL文に残っている余分な ' を無効化するためのテクニックです。
ただしMySQL/MariaDBでは、-- をコメントとして認識させるには直後に半角スペースが必要です(上の例の ␣ がその位置)。スペースなしの --' だと、自分の環境では You have an error in your SQL syntax ... near '--'' というエラーになりました。スペースが見えにくいので、スペース不要の # をコメントに使う手もあります。
ここまでは UNION SELECT のように、抜いたデータがそのまま画面に出るケースでした。でも実際のアプリは、エラーも結果も画面に出さないことが多いです。「それなら安全では?」と思ったのですが、そうではありませんでした。Yes/No の反応さえ返れば、1文字ずつ値を特定できてしまうのです。これを Blind SQLi(ブラインド) と呼びます。
ページの表示が「正常に出る/出ない」で変わることを利用します。たとえば「パスワードの1文字目は a か?」という条件を注入します。
(末尾の -- ␣ は、-- の後に半角スペースが必要という前述のルールです)
a なら条件が真 → ページが普通に表示される表示の違いを見れば、画面に文字が出ていなくても「1文字目は a だ」と分かります。あとは 2文字目、3文字目…と同じことを繰り返すだけ。= で総当たりする代わりに > 'm' のような大小比較で範囲を半分ずつ絞れば(二分探索)、ぐっと少ない回数で特定できます。
実際にDVWAの SQL Injection (Blind) ページで試してみました。このページは結果を一切表示せず、User ID exists in the database.(該当あり=条件が真)か User ID is MISSING from the database.(該当なし=条件が偽)の2つのメッセージだけを返します。これがそのまま Yes/No の反応になります。
まず1文字目が a かを試すと、MISSING(偽)。「1文字目は a ではない」と分かります。

次に 5 を試すと、exists(真)に変わりました。これで「1文字目は 5」が、画面にデータを一切表示させずに確定できたことになります。

ちなみにDVWAはパスワードを平文ではなくMD5ハッシュで保存しているので、ここで1文字ずつ抜けるのは md5("password") = 5f4dcc3b5aa765d61d8327deb882cf99 というハッシュ値です(1文字目が 5 なのはそのため)。平文そのものではない分、ここからさらに解析という別の壁はありますが、「結果を見せていないのに1文字ずつ特定できる」ことは、この2枚のメッセージの違いだけで十分に確認できました。
まず、なぜ刺さったのかをおさらいします。原因は、ユーザーの入力をそのまま文字列としてSQL文にくっつけていたことです。
ここに id として 1' OR '1'='1 が来ると、出来上がるSQLはこうなります。
ただのデータのつもりだった ' OR '1'='1 が、SQLの一部(命令)として読まれてしまっています。これが問題の核心でした。
対策は プリペアドステートメント です。SQL文の中で値が入る場所を ? という目印(プレースホルダ)にしておき、実際の値はあとから別で渡します。
ポイントは、SQL文の形が先に確定してから、値があとではめ込まれることです。? の場所は「ここは値を入れる箱」と決まっているので、そこに 1' OR '1'='1 を入れても、
1' OR '1'='1 という文字列に一致する id を探す」というただの検索条件の値として扱われるこれだけで、今回見た OR '1'='1'・UNION SELECT・Blind SQLi は、すべてまとめて効かなくなります。文字列連結をやめてプレースホルダに置き換えるのが、一番効く対策です。
SQLインジェクションもファイルアップロードの脆弱性も、名前は聞いたことがあるものの、正直「なんか危ないらしい」という雰囲気しか分かっていませんでした。具体的に何がどうヤバいのかは、ピンときていませんでした。
しかし、実際に手を動かして、自分でサーバーのコマンドを実行したりユーザー一覧を表示してみると、脆弱性の怖さを体験することができました。