JPCERT コーディネーションセンター

ENV33-C. コマンドプロセッサが必要ない場合は system() を呼び出さない

ENV33-C. コマンドプロセッサが必要ない場合は system() を呼び出さない

C の関数 system() は、指定されたコマンドを実行するために UNIX シェルや Windows NT 以降の CMD.EXE など、処理系定義のコマンドプロセッサを呼び出す。POSIX の popen() 関数もコマンドプロセッサを呼び出すが、呼び出し元のプログラムと実行するコマンドの間にパイプを作成し、そのパイプの読み書きに使用できるストリームへのポインタを返す [Open Group 2004]。

システムが必要とする機能を実行するために外部プログラムを呼び出すことがある。これはコンポーネント型ソフトウェア開発における再利用の原始的な形態と見ることもできる。

ただし、POSIX のシェルである sh や Microsoft Windows のコマンド言語インタプリタである CMD.EXE などは、単純なコマンドの実行以外の機能も提供する。この機能が必要ないのであれば、system() 関数やコマンドインタプリタを呼び出す関数は使用しないほうが良い。使用すると、コマンド文字列の無害化が非常に複雑になる(「ENV03-C. 外部プログラムを呼び出す際は環境を無害化する」を参照)。

違反コード

以下のコード例は system() 関数を使用してホスト環境で any_cmd を実行している。しかし、コマンドプロセッサの呼び出しは必要ない。

char *input = NULL;

/* input はユーザによって初期化される */

char cmdbuf[512];
int len_wanted = snprintf(
  cmdbuf, sizeof(cmdbuf), "any_cmd '%s'", input
);
if (len_wanted >= sizeof(cmdbuf)) {
  perror("Input too long");
}
else if (len_wanted < 0) {
  perror("Encoding error");
}
else if (system(cmdbuf) == -1) {
  perror("Error executing input");
}

たとえば、Linux システムでこのコードをコンパイルし、スーパーユーザ権限で実行すると、攻撃者は以下の文字列を入力することでアカウントを作成できる。

happy'; useradd 'attacker

シェルは、これを次の2つの独立したコマンドとして認識する。

any_cmd 'happy';
useradd 'attacker'

新しいユーザアカウントが作成され、攻撃者によってシステムへのアクセスに利用される可能性がある。

このコードは、「STR02-C. 複雑なサブシステムに渡すデータは無害化する」にも違反している。

適合コード (POSIX)

以下の解決法では、system() の呼び出しを execve() の呼び出しに置き換えている。exec() 系関数は、そのパラメータに応じた様々な形で、外部プログラムを実行できる。

関数 execlp()execvp() および(非標準の) execvP() は、指定されたファイル名にスラッシュ "/" が含まれていなければ、シェルと同じ動作で実行可能ファイルを探す。それゆえ、「ENV03-C. 外部プログラムを呼び出す際は環境を無害化する」に記載されているように、スラッシュ文字なしで使用できるのは、PATH 環境変数が安全な値に設定されている場合だけである。

関数 execl()execle()execv()、および execve() は、パス名の置換を行わない。

exec() 関数はシェルインタプリタを全面的に使用するわけではないため、違反コードに示されているようなコマンドインジェクション攻撃に対する脆弱性はない。

さらに、信頼のないユーザによって外部の実行可能プログラムが書き換えられないことを保証するために、ユーザによる実行可能ファイルの書き込みを不可にするなどの予防策を講じる必要がある。

char *input = NULL;

/* input はユーザによって初期化される */

pid_t pid;
int status;
pid_t ret;
char *const args[3] = {"any_exe", input, NULL};
char **env;
extern char **environ;

/*... 引数を無害化 ... */

pid = fork();
if (pid == -1) {
  perror("fork error");
}
else if (pid != 0) {
  while ((ret = waitpid(pid, &status, 0)) == -1) {
    if (errno != EINTR) {
      perror("Error waiting for child process");
      break;
    }
  }
  if ((ret != -1) &&
      (!WIFEXITED(status) || !WEXITSTATUS(status)) ) {
    /* 子プロセスの予期せぬステータスをレポートする */
  }
} else {

  /*... env を environ の無害化したコピーとして初期化 ...*/

  if (execve("/usr/bin/any_exe", args, env) == -1) {
    perror("Error executing any_exe");
    _exit(127);
  }
}

この解決法は、違反コードとは大きく異なっている。第 1 に、inputargs 配列に組み込まれており、execve() に引数として渡されている。このためコマンド文字列の作成時に、バッファオーバーフローや文字列の切り捨てが発生する心配がない。第 2 に、子プロセスで "/usr/bin/any_exe" を実行する前に、新しいプロセスを fork している。これにより system() を呼び出すよりも複雑になるが、労力に見合ったセキュリティ向上を図ることができる。

終了ステータスの 127 は、コマンドが見つからない場合にシェルが設定する値で、POSIX ではアプリケーションにも同様の動作を推奨している。XCU のセクション 2.8.2 [Open Group 2004] では、次のように記載されている。

コマンドが見つからない場合の終了ステータスは 127 とする。コマンド名は見つかったが、実行可能でない場合の終了ステータスは 126 とする。シェルを使用せずにユーティリティを呼び出すアプリケーションは、これらの終了ステータスを使用して、類似のエラーを報告するべきである。

違反コード (POSIX)

このコード例は C の system() 関数を呼び出して、ユーザのホームディレクトリ内の .config ファイルを削除する。

system("rm ~/.config");

このプログラムがスーパーユーザ権限で実行されると、攻撃者が HOME の値を操作することで、システム上にあるどのような .config という名前のファイルも削除できてしまう。

適合コード (POSIX)

system() による外部プログラム呼び出しをなくす 1 つの方法は、 既存のライブラリ呼び出しを使用して、同等の機能をプログラム内に直接実装することである。たとえば、system() を使用せずにファイルを削除する方法として、POSIX の unlink() 関数を使用できる [Open Group 2004]。

const char *file_format = "%s/foo";
const size_t len;
char *file;
struct passwd *pwd;

/* 現在のユーザの /etc/passwd エントリを取得 */
pwd = getpwuid(getuid());
if (pwd == NULL) {
  /* エラー処理 */
  return 1;
}

/* パスワード入力から絶対パス名のホームディレクトリを作成 */

len = strlen(pwd->pw_dir) + strlen(file_format);
file = (char *)malloc(len+1);
snprintf(file, len, file_format, pwd->pw_dir);
if (unlink(file) != 0) {
  /* unlink 内のエラーの処理 */
}

free(file);
file = NULL;

とくに unlink() の使用に注意。特権で unlink() を実行するときにはファイル関連の競合状態が発生しやすい(「FIO01-C. ファイル名を使用してファイルを識別する関数の使用に注意する」を参照)。

リスク評価

コマンドプロセッサを呼び出す system()popen() などの関数に渡したコマンド文字列が完全に無害化されていないと、文字列を悪用されるリスクが高くなる。最悪の場合、攻撃者は侵入先のマシンにおいて、脆弱性のあるプロセスの権限で任意のシェルコードを実行することができる。

以下の状況で system() 関数を使用すると、攻撃可能な脆弱性につながる恐れがある。

  1. 汚染されたソースから無害化されていないコマンド文字列または無害化が適切に行われていないコマンド文字列を渡す場合
  2. コマンドがパス名なしで指定されていて、攻撃者がコマンドプロセッサのパス名解決メカニズムにアクセスできる場合
  3. 実行ファイルへの相対パスが指定され、攻撃者が現在の作業ディレクトリに対する制御を行える場合
  4. 攻撃者が指定した実行プログラムに対してスプーフィングを行える場合

このルールの例外は必要だが、コードレビュー中に個別に特定するしかないため、このルールの範囲には含まれない。

ルール

深刻度

可能性

修正コスト

優先度

レベル

ENV33-C

P12

L1

自動検出(最新の情報はこちら

ツール

バージョン

チェッカー

説明

Compass/ROSE      

Klocwork

V. 9.1

SV.CODE_INJECTION.SHELL_EXEC
SV.TAINTED.INJECTION

 

LDRA tool suite

V. 8.5.4

588 S

実装済み
PRQA QA-C 8.1 Warncall -wc system 部分的に実装済み
関連するガイドライン
CERT C++ Secure Coding Standard ENV04-CPP. Do not call system() if you do not need a command processor
CERT Oracle Secure Coding Standard for Java IDS07-J. Do not pass untrusted, unsanitized data to the Runtime.exec() method
ISO/IEC TR 24772:2013 Unquoted Search Path or Element [XZQ]
ISO/IEC TR 17961 (ドラフト) Calling system [syscall]
MITRE CWE CWE-78, Failure to sanitize data into an OS command (aka "OS command injection")
CWE-88, Argument injection or modification
参考資料
[Open Group 2004] XCU Section 2.8.2, "Exit Status for Commands"
environ, execl, execv, execle, execve, execlp, execvp—Execute a File
popen()
unlink()
[Wheeler 2004]  
翻訳元

これは以下のページを翻訳したものです。

ENV33-C. Do not call system() if you do not need a command processor (revision 104)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter