はじめに
いよいよ本連載も今回で最終回です。最終回ということで、近年報告件数が増加傾向にある
今回の脆弱性:CVE-2016-3185
CVE-2016-3185はPHPのインタプリタ内に実装されているmake_
関数の中に存在する型混同の脆弱性です。前回のおさらいになりますが、PHPのインタプリタ自体に脆弱性が存在した場合、不正なPHPのプログラムを解釈・
実際にそのようなプログラム
$ php CVE-2016-3185.php Segmentation fault (core dumped)
「Segmentation fault (core dumped)」
このプログラムの内部では、いったいどのような処理がなされているのか、読者としては気になるところだと思います。ですが、中身の解説を行う前に、その理解を促すためにも、まずは
型混同の脆弱性(C言語での例)
一般的に型混同の脆弱性は、型が存在するプログラミング言語
プログラミング言語によって型や変数の取り扱い方法が異なるため、一言に
#include <stdio.h>
struct Person {
int age;
union {
char *name;
int name_id;
};
};
int main(){
struct Person me;
me.age = 25;
me.name = "Hanako";
if(me.age >= 20){
printf("You are an adult!\n");
me.name_id = 100;
//return 0;
}
printf("Your name is %s\n", me.name);
return 0;
}
リスト1の場合はPersonという名前の構造体に、int型のageというメンバに加え、内部にunionで、charポインタ型のnameと、int型のname_
プログラム自体は至極単純で、Person構造体の変数meに入っているageの数値が20以上だった場合、me.
の直後にあるreturn文がコメントアウトされているのが見えます。つまり、ageが20以上の場合も、最終的にはme.
脆弱性の突き方
いったいどこが問題なのでしょうか。それは、printf関数を用いて名前を出力する際、me.
つまり、printf関数でme.
脆弱性を悪用する際は、このような挙動が利用されます。たとえば先ほどのプログラムのname_
そのほかにも、共用体のメンバに、仮に関数ポインタがあった場合はどうでしょうか。型混同の脆弱性が存在した場合、たとえばint型などいったん別の型を利用し、関数ポインタが持つアドレスを別の関数
CVE-2016-3185.phpの中身
では、型混同の脆弱性の概念について理解できたところで、いよいよPHPを異常終了させたプログラムであるCVE-2016-3185.
<?php
$client = new SoapClient(null, array('location' => "http://localhost/a.xml", 'uri' => "a"));
$value = array ('test' =>
array (
0 => 'test',
1 => 123456789,
2 => 123456789,
),
);
$client->_cookies = $value;
$client->__doRequest('test','http://localhost/','test', 1.0);
?>
SOAP HTTPリクエストとCookieについて
そもそも、今回の脆弱性が存在するmake_
関数は、PHPのプログラム内でSOAP HTTPリクエストを行う際に利用するものです。SOAP[3]とは、アプリケーション間で構造化されたデータを交換するための仕様で、おもにHTTPを利用してXML形式のデータ
今回の脆弱性は、このSOAPメッセージを送受信する際の、HTTP周りの実装に存在していました。具体的には送受信の際、クライアント側
ご存じの方も多いと思いますが、CookieとはWebブラウザなどでWebサイトに関する情報を一時的に保存するしくみです。Cookieには、各Webサイトで利用する情報

以上までわかったところで、CVE-2016-3185.
プログラムの解説
CVE-2016-3185.
そのあと、HTTP リクエストで送信するCookieの情報を、value変数に一度格納し、それを最終的にSoapClientのオブジェクトに格納
次に、Cookieの中身であるvalue変数を見ていきます。ここでは要素数1の連想配列の中に、図2のような3つの要素を持つ配列を格納しています。この配列では、配列の0番めの要素に、文字列型となるstring型で

最後に__
関数で、SOAP HTTPリクエストを発行しています。このときPHPインタプリタの内部では、今回の脆弱性箇所であるmake_
関数が呼ばれています。ちなみに__
関数に渡された引数は、どのようなデータでも脆弱性の検証をするうえでは変わらないので、説明を割愛します。
何が問題なのか?
一見なにも問題がないように見えるこのプログラムですが、実行すると最初見せたようにPHPが異常終了します。いったい何が問題なのでしょうか。実はCookieのデータの型に問題がありました。Cookieの情報はすべて、文字列を表すstring型で格納することを、PHPの開発者は想定していたのです

今回は、配列の1番めと2番めの要素には、int型で
実際の脆弱性箇所について
脆弱性の報告者によると、今回の脆弱性はphp_make_
という関数内の、リスト3の箇所に存在していました。この箇所は、SOAP HTTPリクエストを送信する際に、送信するべきCookieの情報のチェックとパースを行っている部分の抜粋です。ここでは、Cookieのデータを保持している配列の1番めと2番めの要素の中身を、if文を利用してチェックしています。

配列のデータの取り扱いに問題
リスト3の❶の白文字部分が、1番めの配列の要素をチェックしている部分です。前提として、配列のデータはdata
という引数に格納されています。最初の行では、zend_
という関数[4]を利用して配列から1番めの要素のデータを取り出し、それをtmpという名前の変数に格納しています。そのあと、簡単に言えばstrncmp
という文字列を比較するための関数を利用して指定の文字列とtmpを比較しています。
ここで問題なのは、strncmp
関数を利用しているのです。より詳しく説明すると、tmp変数に格納されているデータを、Z_
というマクロstrncmp
関数の第2引数に渡しています。つまり、簡単に言えば、本来ならば文字列を指すアドレスが引数として渡っているはずが、整数値strncmp
に渡されているのです。そして
これは配列の2番めの要素に対しても同様です。リスト3の❷の白文字部分が2番めの配列の要素をチェックしている部分です。こちらでも、まずzend_
関数を用いて、配列の2番めの要素のデータを取り出し、tmpに格納しています。そのあとin_
という、内部で文字列比較などを行っている関数の第2引数に、tmpのデータを文字列として解釈して渡しています。
PHPの内部を追う
先ほどの箇所を要約するとZ_
というマクロを利用して文字列として扱う」
この時点で脆弱性箇所の解説を終わらせても、次に解説する脆弱性の修正方法については理解できます。しかし、利用されていたtmp変数やZ_
マクロは、いったい何者なのでしょうか? 気になりますよね。そこで本節では、筆者と同じように細部まで気になるという方のためにも、これらを深掘りして説明していきます。
ではまずtmpから解析します。リスト3の1行目を見ると、zval *tmp;
という形で変数宣言が行われています。そして、この部分から上にさかのぼってソースコードを読んでいくと、zvalは、_zval_
という構造体であることがわかります。つまりこのtmpは、_zval_
という構造体のポインタ変数なのです。
次にこの_zval_
について筆者のほうで調べた結果、PHPの個々の変数のデータを保持・_zval_
構造体で表されているということです。リスト4がその定義です。
_zval_struct
の定義(zend_types.h
)struct _zval_struct {
zend_value value;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar type,
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved)
} v;
uint32_t type_info;
} u1;
union {
uint32_t var_flags;
(..略..)
} u2;
};
この構造体では、PHP上の変数のデータをvalueで、そしてそのデータにひもづく型をu1という名前のメンバ内で管理しています。今回の場合
PHP上の型の内部表現
前述のとおり、型の情報自体はu1に格納されています。具体的には、ここでは型に対応している数字が格納されています。たとえばPHP上の変数がstring型であった場合、対応する数値であるIS_
)
zend_types.h
)#define IS_UNDEF 0
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE 5
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
今回の場合は整数型で、少しまぎらわしいですが、簡単に言えばC言語のlong
型のサイズまでの整数値を入れられるということで、IS_
)
zend_value共用体の内部
では次に、_zval_
という構造体の中の、value内に格納されています。ではvalueが何者であるかと言うと、zend_
という名の共用体の変数にあたります
zend_value
共用体の定義(zend_types.h
)typedef union _zend_value {
zend_long lval;
double dval;
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
zend_
共用体は大雑把に言えば、それぞれのメンバで、PHP上の変数のデータに対しての型ごとの扱いなどを定義しているのです。今回の場合は整数型のため、値は本来ならばzend_
経由でアクセスする必要があります。しかし、後ほど解説しますが、実際には内部でstring型の扱いを定義したzend_
を利用してアクセスされていたのです。
補足ですが、本当に文字列であった場合は、zend_
型
Z_STRVAL_Pマクロ
最後に、脆弱性箇所で頻繁に利用されていた、Z_
マクロについて説明します。PHPでは、valueの中に保持されている値を、指定の型で簡単にアクセスするためのマクロが用意されており、Z_
もそのうちの1つです。
おさらいになりますが、脆弱性箇所ではこのマクロを利用してtmp変数の中に入っているデータをstring型として扱い、文字列にアクセスしようとしていましたね。そこで
Z_STRVAL_P
に関連するマクロ(zend_types.h
とzend_string.h
から抜粋)#define ZSTR_VAL(zstr) (zstr)->val
#define Z_STR(zval) (zval).value.str
#define Z_STRVAL(zval) ZSTR_VAL(Z_STR(zval))
#define Z_STRVAL_P(zval_p) Z_STRVAL(*(zval_p))
まず一番下の行がZ_
マクロです。そしてこのZ_
では、tmpのような_zval_
構造体のポインタ変数を引数に、Z_
マクロを呼び出しています。このZ_
マクロも同様にたどっていくと、最終的には前述したように、srting型のデータを表現するzend_
構造体の中でも、文字列を表すvalにアクセスしています(zstr)->val
)。
ここまでのまとめ
駆け足になりましたが、以上が脆弱性箇所に関係するPHPの内部構造の話でした。本稿前半では、脆弱性の原因はZ_
というマクロを利用して文字列として扱っている」
PHPの内部構造をふまえたうえで、これを言い換えると次のようになります。zend_
共用体のlval
経由でアクセスする必要があったところ、格納されているのが文字列であると勝手に決めつけ、型を確認せずに、Z_
マクロを使ってstring型を表すstr
脆弱性の修正方法
では、この脆弱性が実際にどのように修正されたかを見ていきます[5]。リスト8が修正後のソースコードです。修正の方針としては至極単純で、string型か否かをチェックする箇所を追加しています。
zval *tmp;
if (((tmp = zend_hash_index_find(Z_ARRVAL_P(data), 1)) == NULL ||
+ Z_TYPE_P(tmp) != IS_STRING || ←★
strncmp(phpurl->path?phpurl->path:"/",Z_STRVAL_P(tmp),Z_STRLEN_P(tmp)) == 0) &&
((tmp = zend_hash_index_find(Z_ARRVAL_P(data), 2)) == NULL ||
+ Z_TYPE_P(tmp) != IS_STRING || ←★
in_domain(phpurl->host,Z_STRVAL_P(tmp))) &&
(use_ssl || (tmp = zend_hash_index_find(Z_ARRVAL_P(data), 3)) == NULL)) {
具体的には、string型が前提の処理
最後になりますが、この脆弱性はPHPの次のバージョンで確認されています。該当するバージョンを利用している方は、最新版にアップデートすることをお勧めします。
- PHP 5.
4.44未満 - PHP 5.
5.28未満の5. 5.x - PHP 5.
6.12未満の5. 6.x - PHP 7.
0.4未満の7. x
連載のおわりに
本連載は今回で終わりとなりますが、いかがでしたでしょうか[6]。連載開始時にも述べましたが
そのため、本連載を通じて読者のみなさんが、脆弱性を生み出さない、もしくは適切に修正するための知見を少しでも身につけ、そして結果として少しでも脆弱性を世界から減らすことができれば、著者冥利に尽きます。
それではみなさん、本連載をご愛読いただきありがとうございました!