xdebugをuninstall/disableするとsegfault

表題のとおりですが、

# PHP7.4.3(Ubuntu 20.04)にxdebugをインストール
apt install php-xdebug
phpenmod xdebug
systemctl restart apache2

# xdebugを無効にする
phpdismod xdebug
systemctl restart apache2

ApachePHPのページにアクセスするとsegmentation faultになる。

[Thu Dec 16 19:32:08.276046 2021] [core:notice] [pid 421823] AH00051: child pid 421937 exit signal Segmentation fault (11), possible coredump in /etc/apache2

CLIも同じで、gdbで見ると以下のような感じです。

Program received signal SIGSEGV, Segmentation fault.
0x0000000000000000 in ?? ()
(gdb) where
#0  0x0000000000000000 in ?? ()
#1  0x00005555558793ad in ?? ()
#2  0x000055555587bb33 in execute_ex ()
#3  0x00007fffecd2cf81 in tideways_xhprof_execute_ex (execute_data=<optimized out>)
    at /usr/local/src/php-xhprof-extension/tideways_xhprof.c:72
#4  tideways_xhprof_execute_ex (execute_data=0x7ffff5413020)
    at /usr/local/src/php-xhprof-extension/tideways_xhprof.c:61
#5  0x000055555588324b in zend_execute ()
#6  0x00005555557fa2ec in zend_execute_scripts ()
#7  0x0000555555799fd0 in php_execute_script ()
#8  0x0000555555885382 in ?? ()
#9  0x0000555555661938 in ?? ()
#10 0x00007ffff76460b3 in __libc_start_main (main=0x555555661530, argc=2, argv=0x7fffffffe478,
    init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe468)
    at ../csu/libc-start.c:308
#11 0x0000555555661ade in _start ()

原因はtidewaysなのかxdebugなのか分かりませんが、とりあえずopcacheのファイルをクリア(全削除)してapacheを再起動すると直ります。おそらくxdebugがある時にhookが入ったopcacheが出来上がる→無効化してもそれが残ってsegfaultになる、みたいな感じではないかと思います。


それはそうと、なぜxdebugをoffにしたのかというと、xdebugのextentionをロードすると、それだけで実行速度が遅くなるからです。

Xdebug 2 vs Xdebug 3 Performance Comparison • PHP.Watch

この記事によると、xdebug2では、xdebugのextensionをロードしているだけで(xdebugをoffにしていたとしても)実行時間が8-9倍になっています。

テストを流す際の時間節約のためにoffにしたのですが、上の記事にあるようにxdebug3では性能低下は非常に小さいようなので、xdebug3を入れるべきなのでしょう。

PHPのExtensionのTips ― Ubuntu (20.04.3)での開発

PHPは既定のPHP 7.4.3を入れている前提です。

これまでCentOS 7で開発していたPHP Extensionを、Ubuntuでビルドしようとした際にエラー(Warning)が出ました。

warning: implicit declaration of function ‘php_mb_convert_encoding’ [-Wimplicit-function-declaration]
  199 |     php_mb_convert_encoding(input, len, to_enc, from_enc, out_len)

ざっくり言うと「php_mb_convert_encodingの定義がありません」というエラーです。

ビルドするとsoファイルはできますが、実行するとやはりSegfaultになってしまいます。

ググっても情報が無く、仕方がないのでmbstring.hを見たところ、メインのAPI定義部分が以下のようなマクロで囲われてました。

// snip

#ifdef COMPILE_DL_MBSTRING
#undef HAVE_MBSTRING
#define HAVE_MBSTRING 1
#endif

// snip
#if HAVE_MBSTRING
... API(php_mb_convert_encoding等)の定義

どうやらUbuntuPHPではCOMPILE_DL_MBSTRINGが定義されていないようで、そのためにphp_mb_convert_encoding等のAPIがExtention側にexposeされず、上のエラーになったようです。一方でCentOSPHP(remiのphp_config.h)ではCOMPILE_DL_MBSTRINGが定義されているため、CentOSでは問題なくビルドできたようです。

正しい対処方法かは分かりませんが、諸々のヘッダファイルをincludeする手前で、

#define COMPILE_DL_MBSTRING 1

を定義をしてやれば、正常にビルド/実行できます。

Ubuntu 20.04 PHP7.4 OCI8+PDO_OCIのインストール

OSはUbuntu 20.04.3で、PHPは既定のPHP7.4.3をインストールしている前提。

### 事前準備
apt install php-dev build-essential libaio1
cd /usr/local/src
curl -O https://www.php.net/distributions/php-7.4.3.tar.gz
tar xzvf php-7.4.3.tar.gz
curl -O https://download.oracle.com/otn_software/linux/instantclient/214000/instantclient-basic-linux.x64-21.4.0.0.0dbru.zip
unzip instantclient-basic-linux.x64-21.4.0.0.0dbru.zip
curl -O https://download.oracle.com/otn_software/linux/instantclient/214000/instantclient-sdk-linux.x64-21.4.0.0.0dbru.zip
unzip instantclient-sdk-linux.x64-21.4.0.0.0dbru.zip
curl -O https://pecl.php.net/get/oci8-2.2.0.tgz
tar xzvf oci8-2.2.0.tgz

### Instant client
echo /usr/local/src/instantclient_21_4 > /etc/ld.so.conf.d/oracle-instantclient.conf
ldconfig

### oci8
cd /usr/local/src/oci8-2.2.0
./configure --with-oci8=instantclient,/usr/local/src/instantclient_21_4
make
make install
echo 'extension=oci8.so' > /etc/php/7.4/mods-available/oci8.ini
phpenmod oci8

### pdo_oci
cd /usr/local/src/php-7.4.3/ext/pdo_oci
phpize
./configure -with-pdo-oci=instantclient,/usr/local/src/instantclient_21_4,21.4
make
make install
echo 'extension=pdo_oci.so' > /etc/php/7.4/mods-available/pdo_oci.ini
phpenmod pdo_oci

### 確認
php -m | grep -i oci
(oci8, PDO_OCIが出力されたらOK)

上の手順は下の記事を参考に、最新化 & 必要な手順を追加 & 不要な手順を削除したものです。

Install OCI8 dan PDO_OCI ubuntu 20.04 (Ahmad Afandi氏、インドネシア語

実はもっと簡単な方法があるかもしれませんけど、とりあえず上記でOracle 18にアクセスできるようになりました。

PHPのExtensionのTips ― ZEND_STRLマクロ

PHPのExtensionを作る時に、モヤっとするところ。

$obj->type = 123;

上のPHPコードをExtensionにすると、下のC言語のコードになります。

zend_update_property_long(Z_OBJCE_P(obj), obj, "type", sizeof("type")-1, 123);

モヤッとするのは"type"を二回書かなければならないところです。

ですが、最近になってZEND_STRLマクロを発見しました。

このマクロを使うと、

zend_update_property_long(Z_OBJCE_P(obj), obj, ZEND_STRL("type"), 123);

となり、同じことを2度書かなくてよくなります。

この辺のマクロの定義はZend/zend_portability.hの中にされています。

#define ZEND_STRL(str)      (str), (sizeof(str)-1)
#define ZEND_STRS(str)      (str), (sizeof(str))

確かにマクロでできるよね~、マクロってすごいね~、という感じですが、C言語力が低い私は今まで律儀に2度書いてました。

もうひとつモヤッとするのは参照カウントです。

  • zend_update_property() ― オブジェクトにzvalを追加する
  • add_assoc_zval() ― 配列にzvalを追加する

上の2つはある意味で似ている機能ですが、前者ではzvalの参照カウントは上がり、後者だと上がらないという違いがあり、この辺を間違うとメモリリークや解放済みメモリの参照のようなバグになります。

Extension開発(PHP7以降)でつらいのは、上のようなものを含めて情報(特にリファレンス的に使えるもの)が少ないことです。(Extensionではなく)PHP本体の方はドキュメントが充実しているので、それと比べると雲泥の差があります。まあ、そもそもの話として「PHPソースコードをサクサク読めるような人以外は、Extensionを作らない方が良い」というのはあるかもしれませんが、私のようなたまにしかExtensionをいじらない人にとっては割と難儀するところです。


参考:Extension開発をするにあたり有用なサイト(PHP7)を挙げておきます。

Writing PHP Extensions(Zend社)

PHP Internals Book(PHPの開発者)

PHP Extension 開発入門(Michihide Hotta氏)

C言語で書くのはヒリヒリする感じがして好きなので、これからも精進しようと思います。

PHP Extension(C/C++)の中でPHPのコードを実行する

PHPで処理速度を上げたい時や、メモリ使用量を減らしたい時に、「C/C++でExtensionを自作する」という最終手段があります。

Extensionは非常に強力ですが、一方でどうしようもなく開発効率が悪い。

そういう意味で、C/C++のExtensionの中でPHPを実行したいこともある(かもしれません)。

そんな時に使えるのがzend_eval_string()です。

本来であれば下のようなコードを書かなければならないところを...

PHP_FUNCTION(foobar_test)
{
    zval zv;

    // zvを [10,20,30] という配列にしたい!
    array_init(&zv);
    add_next_index_long(&zv, 10);
    add_next_index_long(&zv, 20);
    add_next_index_long(&zv, 30);

こんな風に書ける。

#include <zend_execute.h>

PHP_FUNCTION(foobar_test)
{
    zval zv;

    // 1行で [10,20,30] ができる!
    zend_eval_string("[10,20,30]", &zv, "test desukara");

関数名から分かるようにeval()しているのと同じです。

当然ですが、

  • 性能のためにC/C++にしているのに、PHPを実行したら本末転倒になる
  • 動的にPHPコードを作ると脆弱性になる可能性がある

という問題はあります。ただし、込み入った連想配列を定義するときなどに使えば楽だろうなとは思います(個人的には使ったことないですが)。

ちなみに、PHP本体(7.4)のソースコードzend_eval_stringgrepすると、下のものを含めていくつかヒットしました。

PCREのe修飾子はPHP7で廃止されたんですが、mb_ereg系ではまだ現役で使えるようです(知らなかった)。

HTTPヘッダについての調査(4/?)

1回目2回目3回目、の続き。前提条件はこれまでと同じ。

ブラウザ以外で処理されるヘッダ

前回のHTTP/2についての記事の中でCGIStatusヘッダに少し触れたので、ブラウザ以外(サーバやリバースプロキシ)で処理される応答ヘッダについて書く。

その種のヘッダには以下のようなものがある。

  • Status: 404 ... CGI環境などで応答ステータス変更
  • X-Sendfile: /etc/hosts ... Sendfileが有効な環境でサーバ上のファイルを取得
  • X-Accel-Redirect: /internal/secret ... nginxの内部リダイレクトで非公開ファイルを取得

他にもWebサーバや中間の機器で処理されうるヘッダというのは多くあるだろう。ただし、この手のものはサーバやリバースプロキシがヘッダとして解釈しなければならないので、LFが入ることが条件になることが多いと思われる。

ところで、普通のHTTPヘッダインジェクションは他人に踏ませる受動的な攻撃だが、Sendfileなどでは能動的な攻撃になりうる。この辺は、XSSが能動的なSSI(Server Side Includes)インジェクションなどに化けることがあるのに似ている。

HTTP/2におけるHTTPヘッダインジェクション

前記事でHTTP/2の話が出たので、ちょっと脱線して「HTTP/2におけるHTTPヘッダインジェクション」に関連して行った実験について書く。

既に何年も前に語られているように(abend氏, yasulib氏)、HTTP/2では「ヘッダの区切り文字」という概念は無くなっているものの、HTTP/2を使うWebアプリケーションでもCR/LFを使ったヘッダインジェクションの可能性はある。

実験

HTTP/2のヘッダに改行を含めた不正な応答をサーバからブラウザに送り、ブラウザ側の挙動を調べる実験を行った。上述のようにHTTP/2では「区切り文字」という概念は無いため、改行等を入れた応答ヘッダを作れる。

今回の実験では、HTTP/2のヘッダの値に加えてヘッダ名についても改行を入れてみた。

結果は下のとおり。

  • Chrome/Safari/IE
    • エラー画面を表示した(期待通り)。
  • Firefox
    • ヘッダ値に含まれる改行をスペース(0x20)に変換する。
    • ヘッダ名に改行を入れることで、新たなヘッダを作り出せる。

ヘッダ名に改行を入れた例を下に挙げる。

aaa:aaa[0x0D][0x0A]set-cookie:hello=111;path=/;

サーバから上のようなHTTP/2応答のヘッダ名を受け取ると、Firefoxは改行の後のset-cookieを新たなヘッダとして解釈してしまった。FirefoxはHTTP/2のヘッダをhttp/1.1のテキスト形式にした上でparseをしているが、テキストにする際のチェックが漏れていたのだろう。

ちなみにFirefoxHttp2Compression.cpp内でヘッダをhttp/1.1のテキストにしているようだ。そんな処理をするのかと意外な感じがしたが、工数を減らすために同様の方式にしている実装は割とあるのかもしれない。

HTTPヘッダインジェクションの文脈で(ヘッダ値ではなく)ヘッダ名をいじれる状況は殆どありえないし、変なヘッダ名をHTTP/2ヘッダにそのまま送り込むようなWebアプリケーション実行環境が存在するか甚だ疑問ではあるが、一応この問題をMozillaに報告したところ、既に別の方が「スペース等がヘッダ名に許されている」というバグを報告していることが分かった。先日リリースされたFirefox 93からはエラー画面を表示するよう変更されている。

なお、Firefoxに関してはこの辺のチェックが緩かったようで、v92にてHTTP/3のヘッダインジェクションも修正されている。

HTTP/2の仕様

HTTP/2の仕様(RFC 7540)はどうなっているかと言うと、10.3等に規定がある。

  • ヘッダ値にCR, LF, NULが含まれる場合はmalformedと扱う。
  • ヘッダ名についてもhttp/1.1的に不正なものはmalformedと扱う。

当然、http/1.1の仕様はヘッダ名に改行文字を許可していないので、ヘッダ名/値ともに改行が含まれるならば何らかのエラーとしなければならない。

ちなみに「malformedと扱う」のは個々のヘッダではなくて要求/応答メッセージ全体であり、クライアントであればエラー画面を表示し、サーバであればステータス400等を応答するべきなのだろう。

HTTP/2固有の応答ヘッダ

最後に、HTTP/2に特有のヘッダで、ヘッダインジェクションで使えるものがあるか?という点について。

例えば、前記事のAlt-Svcは、ChromeではHTTP/2でしか動かないようなので、ChromeにとってはHTTP/2に特有のヘッダと言える。他に何かHTTP/2特有のヘッダがあったかなと思ってざっと見直してみた。結論を言うと「今のところ使えそうなものは無さそう」なのだが、一応調べたことを書いておく。

HTTP/2のヘッダとしては以下の2種類がある。

  • 通常のヘッダ
  • 疑似ヘッダ

まず通常のヘッダをざっと調べたがめぼしい情報は見つからなかった。

次の疑似ヘッダだが、これは先頭に:が付いたHTTP/2固有のヘッダだ。応答で使える疑似ヘッダは:statusの1つのみだが、これをうまく使えば(CGIstatusヘッダと同じように)応答のステータスコードが変えられるかもと思い、NginxのCGIで試してみた。

ヘッダインジェクションがあるCGIに、以下のパラメータを与える。

redirect=ZZZZZ%0D%0A:status: 200

CGIプログラムはパラメータの値をLocationヘッダに出力する。

サーバが出力する応答ヘッダの全体は以下のようになる(HTTP/1的な形式で表現)。

:status: 302
server: nginx/1.20.1
date: Tue, 21 Sep 2021 14:59:34 GMT
content-type: text/html; charset=iso-8859-1
location: ZZZZZ
:status: 200  ← インジェクトされた部分

CGIなので単一のHTTP/2ヘッダの中に改行が入るのではなく、インジェクトした:statusは独立したヘッダとなる。

意図どおり応答には2つの:statusが含まれるが、全てのメジャーブラウザがこの応答をエラーとした。これはおそらく、HTTP/2仕様(8.1.2.1)の「疑似ヘッダは通常のヘッダより前に位置しなければならない」というチェックをブラウザが行っているためだろう*1

ちなみにBurp経由にすると、下のようにBurpのEvent logに「Normalized malformed HTTP/2 response」というメッセージが出る。

しかし、Burpが2つ目の:statusに一本化する形で正常なヘッダに書き換えるため、ブラウザに届くのは正常なステータス200の応答になる。要はBurpがある時だけは:statusヘッダのインジェクションは成功する。

調べていないが、一般のリバースプロキシ等で同じような挙動をするものも、ひょっとしたらあるかもしれない(ただし、使える可能性で言えば、:無しのstatusヘッダの方が大きいだろう)。

診断の観点では、HTTP/2においては「Burpを挟んでいる時だけ動作するHTTPヘッダインジェクションのPoC」というのが存在するので気に留めておく必要があるかもしれない(BurpはHTTP/1でも応答をnormalizeするので、HTTP/2でも/1でも発生しうることではある)。

*1:本来は、ヘッダを出力するWebサーバ側でも、何らかのチェックが必要なのだろう。