還記得先前討論的 PHP Hash Collisions DoS 漏洞嗎?最初開發團隊的修補方案是:當輸入變數超過 max_input_vars 限制時,直接拋出 E_ERROR 導致 PHP 終止執行。後來為了採取更輕量的處理方式,機制改為發出 E_WARNING 警告並停止往目標陣列新增元素,但程式流程繼續執行。這項變更隨 PHP 5.3.9 釋出。
這項善意的修改卻在 PHP 5.3.9 中引入一個嚴重問題(已在 5.3.10 修復),由 Stefan Esser 發現。請看 php_register_variable_ex 的最終修補代碼:
while (1) {
if (zend_symtable_find(symtable1, escaped_index, index_len + 1, (void **) &gpc_element_p) == FAILURE
|| Z_TYPE_PP(gpc_element_p) != IS_ARRAY) { //(3)
if (zend_hash_num_elements(symtable1) <= PG(max_input_vars)) { // (4)
if (zend_hash_num_elements(symtable1) == PG(max_input_vars)) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Input variables exceeded %ld. ...", PG(max_input_vars)); // (1)
}
MAKE_STD_ZVAL(gpc_element);
array_init(gpc_element);
zend_symtable_update(symtable1, escaped_index, index_len + 1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p);
}
//......
}
//.....
symtable1 = Z_ARRVAL_PP(gpc_element_p); // (2)
goto plain;
}
當註冊一個陣列變數(如 GET 中的 a[]=2),且該變數剛好是第 max_input_vars 個時,會觸發警告 (1),此時行為正常。
但若註冊的陣列變數是第 max_input_vars + 1 個,gpc_element_p 將成為未初始化指標。由於邏輯繼續執行並走到位置 (2),導致解參考未初始化指標——系統崩潰。若伺服器啟用 Core Dump,攻擊效果極為明顯,可用於 DoS 攻擊。
外層迴圈處理類似 a[b]=2 的鍵值對時會執行兩次:第一次插入 a[],第二次往 a[] 插入 b。關鍵在判斷點 (3):若目標陣列找不到元素,或該元素非陣列,流程同樣會流向 (2),引發問題。
假設預設 max_input_vars=1000,發送以下 POST 資料:
1=1&1=2&..........&999=1&x="我是惡意的string"&x[0]=
1 到 999 正常插入x 為第 1000 個元素,觸發警告並正常插入x[0] 時,(3) 判斷發現非 Array 進入 if 區塊,但 (4) 判斷失敗,流程流向 (2)gpc_element_p 指向 x —— 我們偽造的字串PHP 內部使用 zval 結構儲存變數:
struct _zval_struct {
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
zvalue_value 是聯合體:
typedef union _zvalue_value {
long lval;
double dval;
struct { char *val; int len; } str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;
當字串被當作 HashTable 解讀時,記憶體佈局會對應:
typedef struct _hashtable {
uint nTableSize;
uint nTableMask;
uint nNumOfElements;
ulong nNextFreeElement;
Bucket *pInternalPointer;
Bucket *pListHead;
Bucket *pListTail;
Bucket **arBuckets;
dtor_func_t pDestructor; // 關鍵函式指標
zend_bool persistent;
unsigned char nApplyCount;
zend_bool bApplyProtection;
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;
HashTable 中的 pDestructor 是函式指標,當陣列元素被刪除時會被呼叫。攻擊者可精心構造字串內容,控制 pDestructor 指向任意位址,再誘使 PHP 刪除陣列元素,即可實現任意程式碼執行。
max_input_vars 為較小值並監控異常請求