[Linux][セキュリティ] shellshockはPerl/CGIでsystem()にダメージありや無しや

bash脆弱性"shellshock"が非常に話題になっておりますが、これがいろいろと事情が入り組んでおり全容がつかみづらい。

ということでここでは全容を理解するのを早々に放棄して、以下のレガシー環境に絞った狭い話をします。レガシーとは言っても、それなりに鉄板の構成なので対象者は多いのでは無いでしょうか。私は老害なので、Perlしか分からないのです。PHPは嫌い。

なお、shellshock自体の解説はここでは行いません。

基礎知識:CentOS/bin/sh

はじめに、/bin/shbashの関係について予備知識を知っておく必要があります。

現在のCentOSおよびRed Hat Linuxにおいて、/bin/shbashへのシンボリックリンクとなっています。そのため、CentOSにおいてshとbashは「同じもの」です。

[ozuma@cent6 ~]$ ls -laF /bin/sh
lrwxrwxrwx. 1 root root 4  2月 15 02:06 2014 /bin/sh -> bash*

ただし、これはあまり単純な話ではありません。実はbashは、「自分がshという名前で起動されたときはshを真似して振る舞う」という仕様を持っています。実際、bashのmanを読んでみると、以下のように書いてあります。

shという名前でbashを起動すると、bashは古くからあるshの起動動作をできるだけ真似しようとします。またPOSIX標準にもできるだけ従おうとします。

このモードがきちんと実装されていればあまり悲劇はおきなかった(はず)のですが、残念ながらbashのshモードは、bash特有の機能もバリバリ動いてしまうという困った特徴を持っています。そのため、今回のshellshockの根本原因である環境変数内でのfunction定義も、bashのshモードで動きます。また、bash特有の機能を使っていることに気付かずに、Shebangが「#!/bin/sh」として書かれたシェルスクリプトが世の中にはたくさんあります。

この辺の事情は、以下のUbuntu Weekly Topicsの解説が大変詳しいので一読をおすすめします。

そしていくつかのスクリプト言語は、OSコマンドを実行する機能の実装に、「直接execする」のではなく「/bin/shを起動してコマンドを実行させる」という形式を取っています。このことから、今回の悲劇は起きました。

では、Perlのsystem()関数はどうなのかというと……これがややこしいので、それを本稿で解説します。

Perlのsystem関数とshellshock

Perlでは、system関数を利用して外部コマンドを呼び出すことができます。これはshellshockの影響を受けます……が、コードの書き方によって影響を受ける場合と受けない場合があります。以下に詳しく見てみましょう。

まず、ものすごく簡単な、次のようなPerlスクリプトを考えます。「ls *」するだけですね。

#!/usr/bin/perl

system("ls *");

ではこのPerlスクリプトを、straceしながら実行してみましょう。

$ strace -f ./system.pl
....(省略)....
read(3, "#!/usr/bin/perl\n\nsystem(\"ls *\");"..., 4096) = 34
read(3, "", 4096)                       = 0
close(3)                                = 0
pipe([3, 4])                            = 0
clone(Process 9839 attached
child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f39d943d9d0) = 9839
....(省略)....
[pid  9839] execve("/bin/sh", ["sh", "-c", "ls *"], [/* 28 vars */]) = 0
[pid  9839] brk(0)                      = 0x1854000
....(省略)....
```

あちゃー。system()は、/bin/shを起動し、そこに"ls *"を与える形で実行しています。なおその後ろにある[/* 28 vars */]というのはstraceが気を利かせて省略してくれており(省略させたくないときは-vオプションも付けましょう)、この中には環境変数がみつしりと詰まつてゐます。ですからここにshellshockの例の攻撃文字列が入っていると、見事に注入されたコマンドが実行されてしまうわけです。

では、「Perlのsystem()関数を使っているCGIはshellshockの影響を受ける」と断言していいのでしょうか? 実はそんなに単純ではありません。次のようなソースはどうでしょう。今度は、「ls」だけを実行します。

#!/usr/bin/perl

system("ls");

同じだろって感じなのですが、これをstraceしてみましょう。

....(省略)....
read(3, "#!/usr/bin/perl\n\nsystem(\"ls\");\n\n", 4096) = 32
read(3, "", 4096)                       = 0
close(3)                                = 0
pipe([3, 4])                            = 0
clone(Process 9833 attached
child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3201d489d0) = 9833
....(省略)....
[pid  9833] execve("/bin/ls", ["ls"], [/* 28 vars */]) = 0
[pid  9833] brk(0)                      = 0x1cee000
....(省略)....

おおおおお。なんとこの場合は、/bin/lsが直接execveされており、シェルを経由していません。そのためこの場合は、shellshockの影響を受けません。つまり、「PerlCGIでsystem関数を使っている場合、shellshockの影響を受けるケースと受けないケースがある」が正解です。

system()の引数とその振る舞い

これは、Perlのsystem関数が、もらった引数を見て「シェルを起動して引数を渡す」か「自分で直接execする」かを判断して分岐するためです。ここでゴチャゴチャ言ってないで、公式のPerlDocを読んでみましょう。

スカラの引数が一つだけの場合、引数はシェルのメタ文字をチェックされ、もしあればパースのために引数全体がシステムコマンドシェル (これは Unix プラットフォームでは /bin/sh -c ですが、他のプラットフォームでは異なります)に渡されます。シェルのメタ文字がなかった場合、引数は単語に分解されて直接 execvp に 渡されます; この方がより効率的です。

このようにPerlでは、system()への引数が一つだけのときはそこにシェルのメタ文字があるかをチェックされ、メタ文字がある場合はシェルに解釈させる(丸投げする)ために/bin/shが起動されます。すなわち、shellshockされます。

一方、固定文字列が指定された場合、もしくは複数の引数がある場合(つまり、コマンドとそのコマンドライン引数を別々に渡した場合)には、シェルは経由しないのでshellshockされません。

なおあまり無いとは思いますが、たとえ固定文字列であっても、system("/bin/bash ....."); みたいに明示的にbashを起動していたら、そりゃー当然影響を受けます。

自分のスクリプトが影響あるか無いか知りたい

system関数の解釈だけが分かればいいわけですから、検証したいコードのsystem関数の部分だけ取り出して、それをstraceしながら実行してみれば/bin/shに渡されるのか直接execするのかはすぐ分かります。

この検証は引数の解釈だけを見たいので、system()自体が成功するかしないかは関係ありません。ですから、検証したいコードのsystem()部分1行だけを切り出してきて、それを手元で1行のPerlスクリプトにしてstraceを試してみるだけで良いわけです。system()内のコマンドは、失敗して構いません。

なおよくある例としては、アスタリスク・パイプ・セミコロンなどを含むと、シェルに渡されてしまいます。

参考情報

その他補足

  • Debian系(Ubuntuなど)は慎重で、デフォルトでは/bin/shをdashという原始的なシェルにしています。これはCentOSとの大きな違いです。そのためDebianでの影響はRedHat系と比べてかなり限定的です。

宣伝

シェルスクリプトの本を書きました。ご興味があれば本屋でパラパラしてみてちょ

UNIXシェルスクリプト マスターピース132

UNIXシェルスクリプト マスターピース132