OSコマンドのエスケープ – シェルの仕様とコマンドの実装

(Last Updated On: 2018年8月16日)

OSコマンドのエスケープの続きです。OSコマンドインジェクションを防ぐための、OSコマンドのエスケープはSQLのエスケープに比べるとかなり難しいです。

難しくなる理由は多くの不定となる条件に依存する事にあります。

  • OSコマンドを実行するシェルはシステムによって異なる
  • シェルはプログラミング言語+複雑なエスケープ仕様を持っている(コマンド以後はクオートなしでも文字リテラルのパラメーター+各種展開処理)
  • WebアプリはCGIインターフェースで動作するため環境変数にインジェクションできる
  • コマンドパラメーターの取り扱いはコマンド次第である
  • 実行されるコマンドの実装により、間接インジェクションが可能になる

SQLの場合、出力先のシステムは一定です。PostgreSQL用にエスケープした文字列をMySQLで実行したり、MySQL用にエスケープした文字列をPostgreSQLで実行することはまずありません。DB抽象化レイヤーを使用し同じユーザーコードでエスケープした場合でも、それぞれのデータベース用に適切な出力となるようDB抽象化レイヤーが適切な処理となるように処理してくれます。

PHPのescapeshellarg/escapeshellcmd関数は、それら自体がDB抽象化レイヤーの様に実装されています。入力文字列は環境によってUNIX系のシェル用とWindowsのcommand.exe/cmd.exe用で別の処理となるように実装されています。PHPがどのように処理するのかは、コンパイル時に決まります。(configureスクリプトが自動的にシステムを識別)

UNIX系OSのシェル

UNIX系OSのシェルには多くの実装があります。大きく別けてBourne Shell系とC Shell系があります。Bourne Shell系にはsh, bash, ksh, zshなどがあります。C Shell系にはcsh, tcshがあります。全て詳しく解説すると膨大な量になるので概要のみ解説します。

UNIX系のシェルはsh(Bourne Shell)の仕様を踏襲しています。文字列のエスケープ処理はほぼ同じです。シングルクォートで囲まれた文字列は変数展開や特殊文字の解釈は行われません。シングルクォートのエスケープ処理は定義されていませんが、文字列の中でエスケープはできるようになっています。

例えば、クオートなしで\’を出力した場合、’のみを出力します。

$ echo \'
'

escapeshellarg関数はシェルが\’を’と出力する事を利用して、シングルクォートを無効化しています。例えば、escapeshellarg関数は Lets’ PHP をパラメーター値として安全な ‘Lets’\” PHP’ に変換します。
(参考:このエスケープ方法はXPath 1.0のエスケープ方法と基本的な考え方が同じです)

シェルはコマンド実行を行う為に特化しています。シェルはシェル自身の特殊文字となる文字を除き、コマンドとなる(上記の場合はecho)文字列以外は文字として扱い、コマンドのパラメーターとして渡します。

コマンド実行 − Cプログラムの場合

C言語でプログラムを作った事がある方なら、main関数は

int main(int argc, char *argv[]);

というプロトタイプを持っている事を知っていると思います。argcは引数の数、argvが実際の引数となる文字列の配列です。引数は空白文字で区切られて渡されるか、シングルクォートまたはダブルクオートで囲まれた文字列が渡されます。

PHPでコマンドプログラムを作成した場合も、$argcと$argvグローバル変数に引数の数と引数の値が保存されます。引数をパラメーター名(オプション名)とパラメーター値(オプション値)に分離する処理はCプログラムでもPHPプログラムでも、パラメーターを受け取ったプログラムの役割です。C、PHPの場合、getopt関数を利用する場合が多いですが、必ずしもgetopt関数を利用する必要はありません。

実行されたコマンドの仕様は、コマンドが安全に実行されることを保証する為に非常に重要です。コマンドが更に別のコマンドを呼び出す場合、別のコマンドを呼び出す時にインジェクションされる可能性があります。コマンドがセキュリティ維持の為に指定されては困るオプションを持っている場合、そのオプションを指定できないようにしなければなりません。

シェルのエスケープ

シェルの文字列は通常の言語の文字列とは異なります。前のecho文でも紹介しましたが、基本的にクオート無しで文字列として扱えます。シェルはインタラクティブにコマンド実行を行い易くするため、コマンドライン中でシェルが持つ特殊文字をエスケープで解除できるようになっています。つまり、手動でコマンドを実行する場合は文字列をクオートで囲まなくても\でエスケープすれば特殊文字の意味を解除できます。

例えば、引数の区切り文字となるスペースの意味を無くすには

ls this\ directory\ contains\ spaces/*.txt

などとします。変数の開始を表す$やワイルドカードの*なども\でエスケープして特殊文字としての意味を解除できます。

コマンドの区切り

シェルはコマンドを区切る区切り文字となる文字が定義されています。コマンドを区切る文字を入力させない事が、コマンド実行を防止する為に必須となります。コマンドを分離する文字がまとめて記載されているtcshを例に解説します。tcshのマニュアルには次のように記載されています。

The shell splits input lines into words at blanks and tabs. The special characters `&’, `|’, `;’,
`<‘, `>’, `(‘, and `)’ and the doubled characters `&&’, `||’, `<<‘ and `>>’ are always separate
words, whether or not they are surrounded by whitespace.

以下の文字は前後のスペースの有無に限らず、常に単語を分離すると記述されています。

`&’  `|’  `;’  `<‘  `>’  `(‘  `)’ `&&’ `||’  `<<‘  `>>’

つまり、これらの文字が現れた場合、別のコマンドを実行します。しかし、これらの文字をエスケープするだけで十分かと言うとそうではありません。

シェル展開

シェルは変数展開をサポートしているので、変数展開を利用したインジェクションが可能です。シェルは$variable が現れると$variableの内容を展開してからコマンドに渡します。WebアプリはCGIインターフェースを利用しているので、ユーザーは環境変数を設定可能です。つまり、変数展開も無効化しなければコマンド実行が可能になる場合があります。

安全にコマンドを実行するには、コマンドラインを構成する文字列のみでなく、変数展開も考慮したエスケープ処理が必要です。PHPのescapeshellcmd関数はtcshマニュアルに記載された文字以外もエスケープ処理します。

Following characters are preceded by a backslash: #&;`|*?~<>^()[]{}$\, \x0A and \xFF. ‘ and ” are escaped only if they are not paired. In Windows, all these characters plus % are replaced by a space instead.

$をエスケープする事により変数展開を無効化したり、ホームディレクトリに展開するチルダ(~)を無効化しています。セキュリティ対策的には、コマンド実行を防止するだけでなく意図しないファイルへのアクセスも禁止しなければなりません。このためチルダ展開も無効化しています。これでも、escapeshellcmd関数の動作はセキュリティ処理としては十分ではないので

Warning

escapeshellcmd() should be used on the whole command string, and it still allows the attacker to pass arbitrary number of arguments. For escaping a single argument escapeshellarg() should be used instead.

と警告文も記載されいます。

オプションの指定

escapeshellcmd関数にはクオートされた文字列処理の仕様にも問題がありますが、任意のオプションを指定できてしまう仕様もセキュリティ上の問題となりえます。指定されては困るようなコマンドオプションがある場合、ユーザー入力をバリデーションする事無く利用すると任意のオプション設定が可能です。

Windowsのcommand.exe/cmd.exeの場合の解説は省略しますが、仕様に則ったエスケープ処理を行わないとコマンドインジェクションが可能となる事は理解できたと思います。

コマンドインジェクションが可能になる例

安全なコマンド実行文を作成してもコマンドインジェクションが可能になる例を紹介します。最初に呼び出すコマンドを安全に実行しても、呼び出されたコマンドが更に別のコマンドを呼び出し、その実装が不十分であった場合はコマンドインジェクションが可能になります。

次の例はPHPのshell_exec関数からはコマンドインジェクションを防いでいますが、呼び出したコマンドの実装の不備によりインジェクションが可能になる例です。

PHPのコード

<?php
echo shell_exec('list_dir.sh '. escapeshellarg($_GET['dir']));

(ここでは任意ディレクトリが参照できる脆弱性を考慮しない)

list_dir.sh

#!/bin/bash
command="ls -la $*"
eval $command

$* は特殊なシェル変数でコマンドの引数を全て保存した特殊パラメーターです。

$_GET[‘dir’]に

/var ; cat /etc/passwd

と設定された場合、shell_exec関数からコマンドインジェクションは行われませんが、list_dir.shスクリプトにコマンドインジェクションが可能です。catコマンドが実行され、/etc/passwdの内容が送信されてしまいます。

list_dir.shが文字列として生成した$commandをevalで実行しているので、インジェクションが可能になります。

少なくとも手元のbashでは、以下のように引数パラメーターを直接コマンドに渡した場合、パラメーターはパラメーターとして渡されます。

#!/bin/bash
ls -la $*

コマンドとパラメーターの分離ができているので、インジェクションは行えません。

もう一つ例を挙げます。PHPはOpenSSHをサポートしています。Windows系OSからUNIX系OSに接続し、コマンドを実行することも可能です。この場合、Windows用にビルドされたPHPでescapeshellarg関数を利用し、UNIX系OSホスト上で実行すると脆弱になります。

Windows用PHPビルドのescapeshellarg関数はパラメーターを ’ シングルクォートでは無く ” ダブルクオートで囲みます。ここまでの説明で何故脆弱になるかは理解できると思います。

まとめ

OSコマンドのエスケープ処理を見れば、SQLのエスケープが難しい、などとは言っていられない事が分かります。OSコマンドのエスケープ処理が複雑であることを知っていなければ簡単に脆弱性を作ってしまいます。エスケープを知る事の重要性を理解できるのがOSコマンドのエスケープです。

UNIX系OSの場合はクオート無しのシェルエスケープは諦めて、引数のみをシングルクォート+エスケープで可変にした方が安全だと思います。多くのシェル実装が無いWindowsでも同様に、引数のみダブルクオート+危険な文字削除にする方が安全だと思います。この方法ならシェル特殊文字やスペースを無効化するなどの処理、つまりシェル実装への依存度を減らせます。

シェル特殊文字のエスケープ処理が複雑過ぎる(元々複雑な上に複数あり、全部のシェルの詳細仕様まで把握しきれない)ため、上級者でも正しくエスケープする事は非常に難しいです。プログラマの意図する全ての処理を機械的に完全にエスケープする事もできません。完璧にエスケープ処理できたとしてもクオート無しでは不正なオプション指定を防げません。
(注:escapeshellcmd関数の仕様を前提とした記述です)

パラメーター値のクオート+エスケープなら十分単純で安全性の確保も行い易いです。普通コマンド名やパラメーター名は決まっています。これらはハードコードするかバリデーションすれば良いです。

肝心のエスケープ処理は前のエントリのOSコマンドのエスケープに書いています。こちらご覧ください。

OSコマンドインジェクションは出力先のコマンド実装によって、安全に出力していても出力先のコマンドでインジェクションが可能になる場合があります。OSコマンドを出力する場合、出力先プログラムの安全性も確保しないとなりません。シェルスクリプトを呼び出す場合には特に注意が必要です。PHPでも同じですが、シェルスクリプトのevalの利用には細心の注意が必要です。

WindowsからUNIX系OSのシェルなど、エスケープ方式が異なる出力先に出力すると脆弱になります。サーバー環境でデフォルトシェルを変える事はほぼ無いとは思いますが、安易に一般的に利用されているシェルから特殊なシェルに変えると脆弱になる可能性があります。例えば、Windowsでbashを使うと脆弱になります。

あまり多くないとは思いますが、同じOSコマンドとは言ってもエスケープ処理が異なるシステム間では安全性が保てない事にも注意が必要です。SSH接続してコマンド実行を行う場合などでは十分注意してください。

WindowsネイティブでないCygwinなどでは困った事になるのでは?と思いましたが環境がないので調べていません。知っている方、教えて下さい!

追記

ところで「コマンドと引数を分離すれば完璧」は、「プリペアードクエリだけ使っていればOK」と同じく、間違いです。

コマンド実行時、コマンドと引数を分離すれば完璧?

Rubyのシェルエスケープがどうなっているのか調べてみました。検索して最初に出てきたShellwordsモジュールではこうなっているようです。

argv = Shellwords.escape("special's.txt")
argv #=> "special\\s.txt"
system("cat " + argv)

PerlのShellwordsのポート(?)のようで、POSIX / SUSv3 (IEEE Std 1003.1-2001) 準拠ということらしいです。これだけだと判りませんが、信用して使うと危なそうな感じにも見えます。大丈夫なのかな?UNIX(POSIX)用、ということでWindowsにこのモジュールは使えません。Windowsの場合は手動(?)でエスケープ処理が必要なのかも知れません。Rubyistの方、コメント頂けると助かります。

エスケープ関数なのにエスケープ処理仕様が書いてないのはちょっと問題です。使う前にソースコードを確認すべきですが、オンラインのソースコード参照リンクは壊れているようです。使う方はソースコードで仕様を確認し、適切であるか確認してから使ったほうが良いと思います。

他のRubyのシェルエスケープメソッドは、以下のまとめによるとこういう事のようです。
http://whynotwiki.com/Comparison_of_Escape_class_and_String.shell_escape

投稿者: yohgaki