【コマンドインジェクション編】OSコマンドに混ざる悪意とRCEの経路|CTF思考フレームワーク #16
こんにちは、アンペンです!
前回は、信頼できないデータの復元(デシリアライズ)から起きるRCEを扱いました。
今回は、サーバがOS上のコマンドを直接呼ぶ処理で起きるコマンドインジェクションを見ていきます。OSコマンドに混ざる悪意とRCEの経路を、一緒に追ってみましょう。

サーバがOSコマンドを呼ぶ場面って、そんなにあるの?

意外と多いんだ。画像処理・ZIP圧縮・PDF変換・ping確認など、便利な機能の裏でユーザ入力がコマンド文字列に混ざるとコマンドインジェクションになる。
RCE(サーバ乗っ取り)につながる攻撃を、ここ数回続けて見てきました。SSTI、デシリアライズ、そして今日のコマンドインジェクション。この“RCE三兄弟”の中でも、コマンドインジェクションは仕組みが一番シンプルで、いちばんイメージしやすいかもしれません。要は『サーバに、頼んでいないコマンドまで実行させる』こと。今日は、お使いリストへの“追記”にたとえて、その入口を見ていきましょう。
コマンドインジェクションは「利用者入力をシェル経由でOSコマンドに渡す」と発生し、メタ文字(; | `)で追加コマンドを実行されてしまいます。守り方の基本は「シェルを経由しない」「引数を配列で渡す」です。
この記事で分かること
- コマンドインジェクションが成立する仕組み
- シェルメタ文字とブラインド型攻撃の考え方
- シェル非経由・引数分離・許可リストの守り方
📖 はじめてのWebセキュリティ #16|コマンドインジェクション編
『便利な機能の裏のOSコマンド』が、入力一つでRCEに化ける経路を学びます。 シリーズ一覧を見る →
⚠️ 大事なお約束
この記事の確認は、CTF・公式ラボ・自分で作った検証環境だけで行ってください。実在のサービスにメタ文字を含めたペイロードを送る行為は、不正アクセスに該当する可能性があります。
コマンドインジェクションは「シェル経由」が鍵
サーバがOSコマンドを呼ぶときの「シェル(bashやcmd)」は、コマンド文字列を解析して、複数のコマンドを区切る機能を持っています。 cmd1 ; cmd2 のように書けば、両方が順番に実行されます。
もしサーバが「convert input.png output.png」のような文字列を組み立てるときに、利用者入力をそのまま埋め込んでいたら、攻撃者はメタ文字を混ぜることで別のコマンドを追加で実行できます。
つまり問題は、シェルが“親切すぎる”ことにあります。サーバは「画像を変換して」とだけ頼んだつもりでも、利用者の入力に ; 別のコマンド という“追記”が混ざっていれば、シェルは律儀に「画像変換のあと、そのコマンドも実行」してしまう。コンピュータには悪意の有無は分かりません。リストに書いてあるから、ただ順番にこなすだけ。この“疑わない実直さ”こそが、コマンドインジェクションを成立させる土台なんです。
カギになるのは『シェル』という存在です。シェル(bashやcmd)は、人間が打ったコマンドを解釈してくれる“通訳さん”のようなもの。とても気がきく通訳で、たとえば ;(セミコロン)を見つけると「あ、ここでコマンドが区切れてるな、次のも実行しよう」と気をきかせてくれます。ふだんは便利なこの“気のきき方”が、利用者の入力を通してしまうと、そのまま攻撃の入口になるんです。
図解:普通の呼び出しと、メタ文字混入の違い

同じ「画像変換」処理でも、入力にメタ文字が混ざる前と後で、シェルが実行するコマンドの本数が変わります。

受付の人に「牛乳を買ってきて」と頼むつもりだったのに、お使いリストの空欄に「; 倉庫の鍵を持ってきて」と書き足されたら、リスト通りに動く人は両方やってしまいます。シェルもまさにこの「リスト通りに動く人」です。区切り文字(セミコロンなど)を見たら、その後ろも別の指示として実行します。
ここで覚える用語:シェルメタ文字
シェルが特別な意味で解釈する記号(; & | ` $() < > など)のことです。これらを含む入力がそのままシェルに渡されると、想定外のコマンドが追加実行される原因になります。
これらのメタ文字、私たちがターミナルで作業するときは「複数の作業を一度にこなす」ための便利な道具です。でも、利用者の入力欄に入り込むと一転して牙をむく。『便利な道具は、置き場所を間違えると凶器になる』——コマンドインジェクションは、その典型例です。だから入力をシェルに渡すときは、これらの記号が“ただの文字”として扱われるよう、特別な配慮が要ります。
代表的なコマンドインジェクションのパターン
コマンドインジェクションの狙われ方は、大きく3つです。『追記して別コマンドを実行する』『応答に出なくても時間差などで成功を確かめる(ブラインド型)』『便利ライブラリの裏で呼ばれるシェルに混ぜ込む』。とくに3つ目は盲点で、自分では“コマンドなんて呼んでいない”つもりでも、使っているライブラリが裏でこっそり呼んでいることがあります。
よくある狙われ方

- シンプルな追加実行:入力に
; whoamiのようなメタ文字+コマンドを混ぜ、追加コマンドの結果を奪う - ブラインド型:応答に結果が出ない場合、
sleepやDNS問い合わせで「実行された」事実だけを観測する - 外部コマンド呼び出し系ライブラリ経由:画像変換・ZIP・PDF・pingなど、内部でシェルを呼ぶ機能の引数に混入する
典型的な危険関数は、Pythonの os.system / subprocess.run(..., shell=True)、PHPの shell_exec / system、Javaの Runtime.exec 文字列形式、Node.jsの child_process.exec などです。
ここで一つ、覚えておくと一生役立つキーワードがあります。それが shell=True(や、文字列でコマンドをまるごと渡す書き方)。これは“通訳さん(シェル)を間に挟む”スイッチで、コマンドインジェクションのほぼすべてが、この一語から生まれると言っても過言ではありません。逆に言えば、ここを shell=False +配列指定に変えるだけで、多くの穴が一気に塞がります。自分のコードに shell=True がないか、ぜひ一度探してみてください。
今日の練習も、攻撃ではなく“宝探し”です。自分のコードを検索して、外部コマンドを呼んでいる場所と、そこに渡る引数をたどるだけ。「この引数、利用者の入力が混ざらない?」「シェルを経由していない?」と確認していきます。本物のサービスにメタ文字付きの入力を送るのは厳禁。あくまで自分のコードとラボの中で、です。
CTFでやってみよう:コマンド呼び出し箇所を探す
自分のラボのコードで、外部コマンドを呼ぶ箇所と引数を整理しよう
目的は実際の攻撃ではなく、「どこで利用者入力がコマンド文字列に届くか」を見抜くことです。
- CTFや自分の検証環境のコードで、上記の危険関数を文字列検索する
- 渡されている引数を追い、ユーザ入力が含まれていないか確認する
- 引数が「シェルに解釈される文字列」か「コマンドと引数の配列」かを区別する
- ping/dig/imagemagick等の便利ライブラリが内部でシェルを呼ぶか調べる
- shell=Trueの利用箇所を一覧化し、配列指定や安全なAPIに置き換えられるか検討する
守りの考え方は、実はとてもスッキリしています。

メタ文字をぜんぶエスケープして無害化すればいいんじゃないの?

それも一つの手だけど、エスケープは“漏れ”が怖いんだ。シェルのメタ文字は種類が多くて、OSやシェルによって挙動も違う。一個でも見落とすと、そこから抜けられてしまう。だからもっと確実なのは、そもそも“通訳さん(シェル)を呼ばない”こと。コマンドと引数を『料理名』と『材料』に分けて、配列でそのまま渡す。そうすれば、入力に何が書いてあっても“ただの材料”として扱われて、別の命令には化けない。エスケープで戦うより、土俵に上げないほうが強いんだよ。
守る側なら、「シェルを経由しない」が最強の防御
コマンドインジェクションの守りは、「シェルを経由せず、コマンドと引数を分離して渡す」が鉄則です。エスケープよりも、設計でシェルを介入させない方が安全です。
- 外部コマンド呼び出しは配列形式の引数で渡す(Pythonなら
subprocess.run([...], shell=False)) - そもそも外部コマンドを呼ばないAPI(ライブラリ)に置き換えられないかを最優先で検討する
- どうしてもユーザ入力を含める場合は、許可リストと最小限の文字種チェックを行う
- 絶対パスでコマンドを指定し、PATH依存を排除する
- プロセスを最小権限で実行し、被害範囲を縮小する
- 監査ログ・WAFで異常パターン(セミコロン、バッククォート等)を検知する

『エスケープする』より『シェルを使わない』方が確実なんだね。

そう。シェルが混ざる時点で『次のコマンド』が成立しうる、と覚えるのが安全だよ。
ここまでをひと言で言うと、コマンドインジェクションの守りは『通訳さんを呼ばない』。シェルを挟まず、コマンドと引数を分けて渡す。そして可能なら、そもそも外部コマンドではなく、言語のライブラリ機能で済ませる。“便利だから”とシェルに頼った瞬間に、追記の隙が生まれる——この感覚を持っておけば大丈夫です。
まとめ:『シェルなし・配列引数』が基本
- コマンドインジェクションはシェル経由のOS呼び出しに潜む
- メタ文字とブラインド型攻撃で、応答が見えなくても成立する
- 守りはシェル非経由+引数配列+許可リスト
- ライブラリAPIで完結できるなら、そもそも外部コマンドを呼ばないのが理想
今日の持ち帰りは『入力をシェルに混ぜない、ただそれだけ』です。コマンドインジェクションは、仕組みこそシンプルですが、決まると一発でサーバを乗っ取られる重大な穴。でも裏を返せば、対策もシンプルです。shell=True を避け、引数は配列で渡す。この一手を習慣にするだけで、RCE三兄弟の一角はしっかり封じられます。
次回は、XMLに混ざる悪意でファイル漏洩や内部リクエストにつながるXXE(XML External Entity)を扱います。「外部参照」が招くトラブルを見ていきましょう。
