こんにちは、Marsです。今回はShell shockの話です。
2014年9月頃、Bashについての一群の脆弱性(例:CVE-2014-6271等)が発見されました。これらの脆弱性は、俗にShell shock(またはbashdoor)と呼ばれています。この脆弱性は、当初遠隔からBashコマンドの実行を許してしまうものです。主に、サーバへリクエスト時に巧妙に細工されたデータを注入することで実現します。直接的あるいは間接的に相手のサーバOSでコマンドを実行できるので、この攻撃はOSコマンドインジェクション攻撃に属します。
理解のために、まず一つの典型例(CVE-2014-6271)をご紹介します。
CVE-2014-6271は、環境変数に特殊な構文のbashコマンドが読み込まれていると、与えられたbashコマンドが実行されてしまう脆弱性です。外部から環境変数を変えることができれば、CVE-2014-6271等のShell shock脆弱性がある限り、攻撃されてしまう危険性があります。
ここで、CVE-2014-6271を例にご説明します。
bashのバージョンはCVE-2014-6271の脆弱性のあるバージョンなのかどうかをチェックするのも非常に簡単で、以下のコマンドを入力します。
env x='() { :;}; echo vulnerable' bash -c 'echo this is a test'
実行すると、CVE-2014-6271の脆弱性がある場合、vulnerableと出力されます。問題なければ、this is a testだけ出力されます。
なぜこれで分かるかについては、後でご説明します。
攻撃者は、HTTPリクエストを送信する際に、User-agentという環境変数を
() { :;}; 攻撃コード1
に変えれば良いです。
例:
すると、以下のレスポンスが来ました。
・説明:
HTTPリクエストヘッダの中に、User-agentという環境変数があります。サーバにHTTPリクエストを送る際に、ユーザのシステム種類およびバージョン、ブラウザ種類およびバージョンなどの情報をセットしてから送ります。
bashが起動されると、以下のように環境変数が処理されます。
これは、bashのソースコードvariables.cの抜粋です。
/* Initialize the shell variables from the current environment.
If PRIVMODE is nonzero, don't import functions from ENV or
parse $SHELLOPTS. */
void
initialize_shell_variables (env, privmode)
char **env;
int privmode;
{
char *name, *string, *temp_string;
int c, char_index, string_index, string_length, ro;
SHELL_VAR *temp_var;
create_variable_tables ();
for (string_index = 0; string = env[string_index++]; )
{
char_index = 0;
name = string;
while ((c = *string++) && c != '=')
;
if (string[-1] == '=')
char_index = string - name - 1;
/* If there are weird things in the environment, like `=xxx' or a
string without an `=', just skip them. */
if (char_index == 0)
continue;
/* ASSERT(name[char_index] == '=') */
name[char_index] = '';
/* Now, name = env variable name, string = env variable value, and
char_index == strlen (name) */
上記のコードにより、変数初期化用の関数initialize_shell_variablesは、bashが起動される際に設定される予定の環境変数(まだ設定されていない)を順に読み取り、区切り文字「=」によって名前とその値に分割します。
temp_var = (SHELL_VAR *)NULL;
/* If exported function, define it now. Don't import functions from
the environment in privileged mode. */
if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
{
string_length = strlen (string);
temp_string = (char *)xmalloc (3 + string_length + char_index);
strcpy (temp_string, name);
temp_string[char_index] = ' ';
strcpy (temp_string + char_index + 1, string);
さらに、先頭の4文字が「() {」( ')'と'{'の間に半角空白1個あり )にマッチした場合、これと名前、半角スペース、値を順に連結したものをtemp_stringとして定義します。つまり、関数として見なします。
「名前+半角スペース+値」=「function() { :;}」(理解のため、functionという名前を付けましたが、実際にfunctionという名前はありません)
簡単に言うと、ただの字句&構文解析です。「() {」を含む場合は関数として見なします。それを含まない場合は、そのままif文の中の内容をパスします。
次の作業は非常に重要です。なぜなら、先ほどセットされたtemp_stringの内容は、bashスクリプトとして解析され、そのまま実行されるからです。
if (posixly_correct == 0 || legal_identifier (name))
parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);
ここまでは、まだ環境変数が設定されていません。環境変数が読み込み終わって環境変数として設定する前準備の段階です。この段階で普通の文字列として設定されるのではなく、通常とは違って関数として、実行できるコードとして解析されます。
() { :;}; cat /etc/shadow
これは、ただただ「;」で区切られた二つのコマンドとして解析されますので、「function() { :;}」という特に何も実行しないコードを実行した後、「cat /etc/shadow」を実行することになっています。
ここで抜粋します。攻撃コードに「;」を付ければ、複数のコマンドを実行するのが可能です。「;」の後に続きがある場合、また次のコマンドを実行するからです。
parse_and_execute関数:
int
parse_and_execute (string, from_file, flags)
char *string;
const char *from_file;
int flags;
{
if (parse_command () == 0)
{
if (interactive_shell == 0 && read_but_dont_execute)
{
last_result = EXECUTION_SUCCESS;
dispose_command (global_command);
global_command = (COMMAND *)NULL;
}
else if (command = global_command)
{
}
else
last_result = execute_command_internal
(command, 0, NO_PIPE, NO_PIPE, bitmap);
コマンドはさらにこの中の関数に実行されます。
↓
execute_command_internal関数:
case cm_function_def:
exec_result = execute_intern_function (command->value.Function_def->name,
command->value.Function_def->command);
break;
↓
さらに、
execute_intern_function関数:
case cm_connection:
exec_result = execute_connection (command, asynchronous,
pipe_in, pipe_out, fds_to_close);
break;
↓
さらに
execute_connection
case ';':
if (ignore_return)
{
if (command->value.Connection->first)
command->value.Connection->first->flags |= CMD_IGNORE_RETURN;
if (command->value.Connection->second)
command->value.Connection->second->flags |= CMD_IGNORE_RETURN;
}
QUIT;
execute_command (command->value.Connection->first);
QUIT;
exec_result = execute_command_internal (command->value.Connection->second,
asynchronous, pipe_in, pipe_out,
fds_to_close);
break;
ここで分かるのは、続きがある場合に新たにexecute_command_internal関数(下から四行目)が呼び出され、引き続きのコマンドを実行されます。
以上は説明でした。
shell shock攻撃は、一つではなく一群の脆弱性と話した通り、様々な攻撃に活用できてしまいます。例えば、victimホストを乗っ取ってmailコマンドでスパムメールを送信させたり、pingコマンドでDoS攻撃を行わせたりすることができます。”Shell Shock”についてそのほかの脆弱性をまた別の記事で紹介したいと思います。
参考サイト:
bash の脆弱性 "Shell Shock" のめっちゃ細かい話 (CVE-2014-6271) - もろず blog