SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

実例で学ぶ脆弱性対策コーディング

Linuxのcrontabコマンドの脆弱性をつぶす

実例で学ぶ脆弱性対策コーディング 第7回


  • このエントリーをはてなブックマークに追加

 本連載では、脆弱性を含むサンプルコードを題材に、修正方法の例を解説していきます。今回はジョブを定期実行するための仕組み「Cron」をとりあげます。

  • このエントリーをはてなブックマークに追加

はじめに

 今回はCronをとりあげます。CronはOSの持っている時計に基づき、あらかじめ設定しておいたコマンドを実行するための仕組みで、Unix系システムには必ず備えられているといっていい機能でしょう。ログファイルのローテーションやログインアカウントの利用状況集計など、システム管理上のジョブを定期的に実行するために活用されています。

 英語版Wikipediaのページによると、Cronの歴史はVersion7 Unix(1979年リリース)までさかのぼるそうです。Linuxディストリビューションの多くが現在使っているものは、Paul Vixie氏が実装したVixie Cronが元になっています。

サンプルコード

 Cronでは、crontabという設定ファイルでいつどのようなジョブを実行するかを指定します。この設定ファイルはユーザごとに用意されており、必要に応じてユーザが自分で編集します。この設定ファイルを編集するためのコマンドもcrontabという名前です。設定ファイルのcrontabとコマンドのcrontabを区別するために、マニュアルページのセクション番号を付けて、設定ファイルはcrontab(5)、コマンドはcrontab(1)などと表記します。

 以下に引用したコードは、crontab(1)のソースコードの一部、edit_cmd()という関数です。ユーザが自分の設定ファイルを編集するために「crontab -e」というようにコマンドを起動したときにこの関数が呼び出されます。

 edit_cmd()関数が内部から参照している関数や変数で他のファイルに定義されているものも、一緒に展開して並べてあります。実際のコードを確認する場合には注意してください。また、今回のトピックに関係ないシグナル処理の部分などは省略してあります。

 ちなみに、文字列の連結操作のためにglue_strings()という関数を定義しています。今ならこの機能はsnprintf()を使うべきところです。Vixie Cronが書かれた当時はまだsnprintf()が普及していなかったために自前でこのような関数を用意したのかもしれませんね。

cronie-1.4.3 の edit_cmd() から
static uid_t save_euid;
static gid_t save_egid;

static char Filename[MAX_FNAME];
static FILE *NewCrontab;


int swap_uids(void) {
  save_egid = getegid();
  save_euid = geteuid();
  return ((setegid(getgid()) || seteuid(getuid()))? -1 : 0);
}


int swap_uids_back(void) {
  return ((setegid(save_egid) || seteuid(save_euid)) ? -1 : 0);
}


/*
 * glue_strings is the overflow-safe equivalent of
 *    sprintf(buffer, "%s%c%s", a, separator, b);
 *
 * returns 1 on success, 0 on failure.  'buffer' MUST NOT be used if
 * glue_strings fails.
 */
int
glue_strings(char *buffer, size_t buffer_size, const char *a, const char *b,
  char separator) {
  char *buf;
  char *buf_end;

  if (buffer_size <= 0)
    return (0);
  buf_end = buffer + buffer_size;
  buf = buffer;

  for ( /* nothing */ ; buf < buf_end && *a != '\0'; buf++, a++)
    *buf = *a;
  if (buf == buf_end)
    return (0);
  if (separator != '/' || buf == buffer || buf[-1] != '/')
    *buf++ = separator;
  if (buf == buf_end)
    return (0);
  for ( /* nothing */ ; buf < buf_end && *b != '\0'; buf++, b++)
    *buf = *b;
  if (buf == buf_end)
    return (0);
  *buf = '\0';
  return (1);
}


static char *tmp_path() {
  char *tmpdir = NULL;

  if ((getuid() == geteuid()) && (getgid() == getegid())) {
    tmpdir = getenv("TMPDIR");
  }
  return tmpdir ? tmpdir : "/tmp";
}


static void edit_cmd(void) {
  char n[MAX_FNAME], q[MAX_TEMPSTR];
  FILE *f;
  int ch = '\0', t;
  struct stat statbuf;
  struct utimbuf utimebuf;
  WAIT_T waiter;
  PID_T pid, xpid;

  if (!glue_strings(n, sizeof n, SPOOL_DIR, User, '/')) {
     fprintf(stderr, "path too long\n");
     exit(ERROR_EXIT);
  }
  if (!(f = fopen(n, "r"))) {
    if (errno != ENOENT) {
      perror(n);
      exit(ERROR_EXIT);
    }
    fprintf(stderr, "no crontab for %s - using an empty one\n", User);
    if (!(f = fopen(_PATH_DEVNULL, "r"))) {
      perror(_PATH_DEVNULL);
      exit(ERROR_EXIT);
    }
  }

  if (!glue_strings(Filename, sizeof Filename, tmp_path(),
                    "crontab.XXXXXXXXXX", '/')) {
    fprintf(stderr, "path too long\n");
    exit(ERROR_EXIT);
  }
  if (swap_uids() == -1) {
    perror("swapping uids");
    exit(ERROR_EXIT);
  }
  if (-1 == (t = mkstemp(Filename))) {
    perror(Filename);
    goto fatal;
  }

  if (swap_uids_back() == -1) {
    perror("swapping uids back");
    goto fatal;
  }
  if (!(NewCrontab = fdopen(t, "r+"))) {
    perror("fdopen");
    goto fatal;
  }

  // copy the rest of the crontab (if any) to the temp file.
  if (EOF != ch)
    while (EOF != (ch = get_char(f)))
      putc(ch, NewCrontab);

  fclose(f);
  if (fflush(NewCrontab) < OK) {
    perror(Filename);
    exit(ERROR_EXIT);
  }
  // Set it to 1970
  utimebuf.actime = 0;
  utimebuf.modtime = 0;
  utime(Filename, &utimebuf);
again:
  rewind(NewCrontab);
  if (ferror(NewCrontab)) {
      fprintf(stderr, "%s: error while writing new crontab to %s\n",
                      ProgramName, Filename);
fatal:
      unlink(Filename);
      exit(ERROR_EXIT);
  }

  // we still have the file open.  editors will generally rewrite the
  // original file rather than renaming/unlinking it and starting a
  // new one; even backup files are supposed to be made by copying
  // rather than by renaming.  if some editor does not support this,
  // then don't use it.  the security problems are more severe if we
  // close and reopen the file around the edit.

  switch (pid = fork()) {

    // 子プロセスでroot権限を放棄しFilenameを引数にエディタを起動

  }

  // parent
  for (;;) {
    xpid = waitpid(pid, &waiter, 0);

    // エラー状態だったら異常終了する

  }

  // lstat doesn't make any harm, because 
  // the file is stat'ed only when crontab is touched
  if (lstat(Filename, &statbuf) < 0) {
    perror("lstat");
    goto fatal;
  }

  if (!S_ISREG(statbuf.st_mode)) {
    fprintf(stderr, "%s: illegal crontab\n", ProgramName);
    goto remove;
  }

  if (statbuf.st_mtime == 0) {
    fprintf(stderr, "%s: no changes made to crontab\n", ProgramName);
    goto remove;
  }

  fprintf(stderr, "%s: installing new crontab\n", ProgramName);
  fclose(NewCrontab);
  if (swap_uids() < OK) {
    perror("swapping uids");
    goto remove;
  }
  if (!(NewCrontab = fopen(Filename, "r+"))) {
    perror("cannot read new crontab");
    goto remove;
  }
  if (swap_uids_back() < OK) {
    perror("swapping uids back");
    exit(ERROR_EXIT);
  }
  if (NewCrontab == 0L) {
    perror("fopen");
    goto fatal;
  }

  // 以下、Filenameをcrontabに入れ換える

remove:
    unlink(Filename);
done:
}

 edit_cmd()の処理は、以下のような流れになります。

  • root権限でcrontab(1)実行開始
  • ユーザのcrontab(5)ファイルをopenする
  • 一般ユーザ権限で(swap_uids())、一時ファイルを生成
  • 一時ファイルに既存ファイルの内容をコピー
  • 一時ファイルのタイムスタンプを0(epoch time)にセットする(utime())
  • 子プロセスがfork()し、一般ユーザ権限でエディタを起動、ユーザは一時ファイルを編集する
  • 親プロセスは、子プロセスが終了するのを待つ(waitpid())
  • 一般ユーザ権限で、一時ファイルのタイムスタンプを確認、変更されていれば、新たなcrontab(5)ファイルとして入れ換える

 Fedora Linuxでは、各ユーザのcrontab(5)ファイルは専用のディレクトリ(/var/spool/cron/)に置いてあります。他ユーザのcrontab(5)ファイルをいたずらに編集できないよう、このディレクトリ以下にはrootしかアクセスできないようにパーミションが設定されており、各ユーザはcrontab(1)コマンドを通じてのみ、自分のcrontab(5)ファイルを編集できます。

 crontab(1)コマンドは「setuid root」されているため、一般ユーザが起動したときでもroot権限で動作し、/var/spool/cron/以下のファイルにアクセスすることができます。

 そして、crontab(1)を起動したユーザが自分のcrontab(5)ファイルのみ編集できるようにする仕掛けが上記コード中にあるswap_uids()とswap_uids_back()です。

 setuidされたプログラムは、起動したユーザのIDをプロセスの属性情報として覚えており、seteuid()などのシステムコールを使うことで、setuidされた権限で動作するか、それとも自分を起動した元のユーザの権限で動作するかを変更できます。edit_cmd()では、一時ファイルの生成やcrontab(5)ファイルの置き換えを元のユーザ権限で行うことにより、他のユーザのファイルを間違っていじれないようにしてあるのです。

 ところが、一か所、その仕組みをきちんと使っていないところがあります。さて、どこでしょう?

会員登録無料すると、続きをお読みいただけます

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

次のページ
脆弱性の解説

この記事は参考になりましたか?

  • このエントリーをはてなブックマークに追加
実例で学ぶ脆弱性対策コーディング連載記事一覧

もっと読む

この記事の著者

戸田 洋三(JPCERT コーディネーションセンター)(トダ ヨウゾウ(JPCERT コーディネーションセンター))

リードアナリストJPCERTコーディネーションセンター東京工業大学情報理工学研究科修士課程修了。学生時代は、型理論および証明からのプログラム抽出を研究。その後、千葉大学総合情報処理センターのスタッフとして、学内ネットワークの運営、地域ネットワーク、IPマルチキャストの実験ネットワークであるJP-MB...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/5360 2010/08/19 12:03

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング