HOME/Articles/

CVE-2019-11043

Article Outline

概要

NginxとPHP-FPMで構成される環境において、特定の設定値が設定されている場合にリモートからの任意コード実行が可能となる脆弱性です。
Nginxが以下の設定を満たす場合、任意コードの実行が可能になります。

  • locationディレクティブでリクエストをPHP-FPMに転送するようになっている
  • PATH_INFO変数を割り当てる際にfastcgi_paramディレクティブが使用されている
  • fastcgi_split_path_infoディレクティブが存在し、「^」で始まり「$」で終わる正規表現が用いられている
  • try_files $uri =404のようなファイルの有無を判断するためのチェックがない

具体的には、Nginxのconfigが以下のようであり、php-fpmにリクエストを転送している場合にこの脆弱性の影響を受ける可能性があります。

location ~ [^/]\.php(/|$) {
     fastcgi_split_path_info ^(.+?\.php)(/.*)$;
     fastcgi_param PATH_INFO $fastcgi_path_info;
     fastcgi_pass php:9000;
     ...
}

この設定はNginxのかなり標準的な設定であり、この脆弱性の影響が広範囲に及んでいる可能性があります。

環境

この脆弱性の影響を受けるバージョンは以下の通りです。

  • PHP7.1系:〜7.1.32
  • PHP7.2系:〜7.2.23
  • PHP7.3系:〜7.3.10

攻撃手順

https://github.com/neex/phuip-fpizdam
こちらのPoCの攻撃手順です。

1. PHP-FPMに新しいバッファーを割り当てる

PHP-FPMでは、CGI環境は構造体fcgi_data_segに格納され、構造体fcgi_hashによって管理されます。
バッファーの現在位置であるfcgi_data_seg->posが終端fcgi_data_seg->endまで進んだ時、PHP-FPMには新しいバッファーが割り振られ、それまでのバッファーはfcgi_data_seg->nextに移動されます。

URLに大量のクエリ文字列を付与し、fcgi_data_segfcgi_data_seg->endまで進めて新しいバッファーを確保します。

こうすることにより、path_infoからfcgi_data_seg->posへのオフセットが34だとわかります。

2. path_infoのアドレスをfcgi_data_seg->posと一致させる

概要で述べた設定のNginxにおいて、URLに改行コード%0Aが含まれる場合、path_infoという変数が空の状態でPHP側に渡されます。
これとsapi/fpm/fpm/fpm_main.cにおけるバッファーアンダーフローの脆弱性を利用し、path_infofcgi_data_seg->posと一致させます。

// fpm_main.c
int ptlen = strlen(pt);
int slen = len - ptlen;
int pilen = env_path_info ? strlen(env_path_info) : 0;
int tflag = 0;
char *path_info;
if (apache_was_here) {
    /* recall that PATH_INFO won't exist */
    path_info = script_path_translated + ptlen;
    tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
} else {
    path_info = env_path_info ? env_path_info + pilen - slen : NULL;
    tflag = (orig_path_info != path_info);
}

ここで使う主要な変数は以下の通りです。

  • 変数pilen:path_infoの長さ(つまりpath_infoが空の時は0)
  • 変数env_path_info:path_infoのアドレス
  • 変数slen:URLのリクエスト部分の長さ(例:http://127.0.0.1:8080/script.php/test だとすると、[test]部分の長さ)

この時、path_info = env_path_info ? env_path_info + pilen - slen : NULL;の部分でバッファーアンダーフローが発生します。
slenが34になるようにクエリ文字列を付与することで、path_infofcgi_data_seg->posと一致します。

3. Nullバイト書き込みを行い、バッファーを上書きする

path_infoにNullバイト書き込みをします。

// fpm_main.c
path_info[0] = 0;

これによりfcgi_data_seg->posのアドレスも変更されます。 末尾のアドレスに00を書き込んだため、fcgi_data_seg->posは現在のバッファーの中央付近にシフトされ、新たな環境変数を追記した際にバッファーが上書きされます。  

// fpm_main.c
old = path_info[0];
   path_info[0] = 0;
   if (!orig_script_name ||
       strcmp(orig_script_name, env_path_info) != 0) {
       if (orig_script_name) {
           FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
       }
       SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
   } else {
       SG(request_info).request_uri = orig_script_name;
   }
   path_info[0] = old;

この部分で新たな環境変数ORIG_SCRIPT_NAMEを上書きできるようになりました。

4. リモートコード実行を可能にする

環境変数を上書きしただけではリモートコードの実行はできません。
PHP-FPMは環境変数を構造体fcgi_hash_bucketに保存しています。
この時、PHP-FPMはハッシュテーブルを使用して管理を行っていて、まずハッシュテーブルから環境を取得し、その後hash_valuevar_len、および内容を確認します。

// fastcgi.c
static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len)
{
   unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;
   fcgi_hash_bucket *p = h->hash_table[idx];

   while (p != NULL) {
      if (p->hash_value == hash_value &&
          p->var_len == var_len &&
          memcmp(p->var, var, var_len) == 0) {
          *val_len = p->val_len;
          return p->val;
      }
      p = p->next;
   }
   return NULL;
}

PHP-FPMのハッシュ生成アルゴリズムは非常に単純であり、以下のようになっています。

// fastcgi.c
#define FCGI_HASH_FUNC(var, var_len) \
   (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
      (((unsigned int)var[3]) << 2) + \
      (((unsigned int)var[var_len-2]) << 4) + \
      (((unsigned int)var[var_len-1]) << 2) + \
      var_len)

つまり、HTTP_EBUTというヘッダーを用意すれば、PHP_VALUEとハッシュが衝突します。
この状態でHTTPリクエストを送信すると、PHP_VALUEの代わりにHTTP_EBUTがハッシュテーブルから参照されます。

さらに、以下の関数でHTTP_EBUTのvalueを書き換えることによって、任意のコードを実行できるようになります。

// fpm_main.c
FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);

検証

上述のPoCで検証を行います。 まずはNginx+PHP-FPMでローカルに環境を作ります。

go get github.com/neex/phuip-fpizdam

phuip-fpizdamコマンドを入手し、先ほど建てたローカル環境に対してコマンドを叩くと、任意のPHPコードが実行できる状態になります。

phuip-fpizdam http://127.0.0.1:8080/script.php

うまく動かない時は、数回試行することで実行できる可能性があります。

参考URL