乾貨|CVE-2019-11043: PHP-FPM在Nginx特定配置下任意代碼執行漏洞分析

Alt

近期,國外安全研究員Andrew Danau,在參加奪旗賽(CTF: Capture the
Flag)期間,偶然發現php-fpm組件處理特定請求時存在缺陷:在特定Nginx配置下,特定構造的請求會形成php-fpm處理異常,進而致使遠程執行任意代碼。當前,做者已經在github上公佈了相關漏洞信息及自動化利用程序。鑑於Nginx+PHP組合在Web應用開發領域擁有極高的市場佔有率,該漏洞影響範圍較爲普遍。

漏洞概述

PHP-FPM在Nginx特定配置下存在任意代碼執行漏洞。具體爲:
使用Nginx + PHP-FPM搭建的服務器在使用相似以下配置的nginx.conf時:php

location ~ [^/]\.php(/|$) {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        fastcgi_param PATH_INFO       $fastcgi_path_info;
        fastcgi_pass   php:9000;
        ...

Nginx中fastcgi_split_path_info 在處理存在"n"(%oA) 的path_info時,會將傳遞給PHP-FPM的PATH_INFO置爲空(PATH_INFO=""),影響關鍵指針的指向,致使後續path_info[0]=0的置零操做位置可控,經過構造特定長度和內容的請求,能夠覆蓋寫特定位置數據,插入特定環境變量,進而致使代碼執行。html

漏洞分析

首先,分析其補丁:在進行request_info結構體初始化的static void init_request_info(void)函數中,增添對pilen 和slen的大小校驗,規避了指針的非預期回溯移動。nginx

// php-src/sapi/fpm/fpm/fpm_main.c
     ...
     if (pt) {
        while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) {
             // 對傳入PATH_INFO 進行校驗。經過判斷文件狀態,獲取真實PATH_INFO
             *ptr = 0;
             f (stat(pt, &st) == 0 && S_ISREG(st.st_mode)) {
             int ptlen = strlen(pt); # Path-translated CONTENT_LENGTH
             int slen = len - ptlen;  //script length
            int pilen = env_path_info ? strlen(env_path_info) : 0;  //Path info 長度 0
            int tflag = 0;
            char *path_info;

            if (apache_was_here) {
                /* recall that PATH_INFO won't exist */
                path_info = script_path_translated + ptlen;
                tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
            } else {
        -       path_info = env_path_info ? env_path_info + pilen - slen : NULL; // 經過偏移設置新env_path_info,可是未對偏移量作校驗
        -       tflag = (orig_path_info != path_info);
        +       path_info = (env_path_info && pilen > slen) ? env_path_info + pilen - slen : NULL;
        +       tflag = path_info && (orig_path_info != path_info);
            }

            if (tflag) {
                if (orig_path_info) {
                char old;

                FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
                old = path_info[0];
                path_info[0] = 0; //置零操做
                if (!orig_script_name ||
                    strcmp(orig_script_name, env_path_info) != 0) {
                    if (orig_script_name) {
                        FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);//觸發入口
                    }
                    SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
                    } else {
                    SG(request_info).request_uri = orig_script_name;
                    }
                    path_info[0] = old;
                }
        ...

其中git

//以http://localhost/info.php/test?a=b爲例
     PATH_INFO=/test
     PATH_TRANSLATED=/docroot/info.php/test
     SCRIPT_NAME=/info.php
     REQUEST_URI=/info.php/test?a=b
     SCRIPT_FILENAME=/docroot/info.php
     QUERY_STRING=a=b
 
     pt = script_path_translated; // = env_script_filename => "/docroot/info.php/test"
    len = script_path_translated_len  // 爲"/docroot/info.php/test"

    // 通過從新計算處理後
    int ptlen = strlen(pt); // strlen("/docroot/info.php")
    int pilen = env_path_info ? strlen(env_path_info) : 0;  // 即len(PATH_INFO) "/test"
    int slen = len - ptlen;   // len("/test")

    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即是這樣一處位置。這裏須要說明一下各變量的存儲方式。github

經過fastcgi協議傳入的各環境變量會存儲到_fcgi_request->env 這個fcgi_hash結構體中,供後續執行取用,結構具體定義以下:web

// php-src/sapi/fpm/fpm/fastcgi.c
     typedef struct _fcgi_hash_bucket {
         unsigned int              hash_value;
         unsigned int              var_len;
         char                     *var;
         unsigned int              val_len;
         char                     *val;
         struct _fcgi_hash_bucket *next;
         struct _fcgi_hash_bucket *list_next;
   } fcgi_hash_bucket;

    typedef struct _fcgi_hash_buckets {
        unsigned int               idx;
        struct _fcgi_hash_buckets *next;
        struct _fcgi_hash_bucket   data[FCGI_HASH_TABLE_SIZE];
    } fcgi_hash_buckets;

    typedef struct _fcgi_data_seg {
        char                  *pos;
        char                  *end;
        struct _fcgi_data_seg *next;
        char                   data[1];
    } fcgi_data_seg;

    typedef struct _fcgi_hash {
        fcgi_hash_bucket  *hash_table[FCGI_HASH_TABLE_SIZE];
        fcgi_hash_bucket  *list;
        fcgi_hash_buckets *buckets;
        fcgi_data_seg     *data;
    } fcgi_hash;
    ...
    /* hash table */
    //初始化操做
    static void fcgi_hash_init(fcgi_hash *h)
    {
        memset(h->hash_table, 0, sizeof(h->hash_table));
        h->list = NULL;
        h->buckets = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets));
        h->buckets->idx = 0;
        h->buckets->next = NULL;
        h->data = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + FCGI_HASH_SEG_SIZE); // 默認分配 (4*8 - 1) + 4096
        h->data->pos = h->data->data; //指向環境變量初始寫入位置
        h->data->end = h->data->pos + FCGI_HASH_SEG_SIZE; 指向//data_seg末尾
        h->data->next = NULL;
    }
    ...

其中咱們主要關注其中的get/set操做,實現以下:shell

static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len)
     // 關聯 FCGI_GETENV()
     {
         unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;
        fcgi_hash_bucket *p = h->hash_table[idx];
 
         while (p != NULL) {
         //須要hast_value值相同,var_len相同才能取出值
             if (p->hash_value == hash_value &&
                p->var_len == var_len &&
                memcmp(p->var, var, var_len) == 0) {
                *val_len = p->val_len;
                return p->val;
            }
            p = p->next;
        }
        return NULL;
    }

    static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len)
    // 關聯 FCGI_PUTENV()
    {
        unsigned int      idx = hash_value & FCGI_HASH_TABLE_MASK;  // 計算hash_value肯定 index
        fcgi_hash_bucket *p = h->hash_table[idx];  //獲取原有hash_table中的對應值

        while (UNEXPECTED(p != NULL)) {
            if (UNEXPECTED(p->hash_value == hash_value) &&
                p->var_len == var_len &&
                memcmp(p->var, var, var_len) == 0) {

                p->val_len = val_len;
                p->val = fcgi_hash_strndup(h, val, val_len);
                return p->val;
            }
            p = p->next;
        }

       if (UNEXPECTED(h->buckets->idx >= FCGI_HASH_TABLE_SIZE)) {
            fcgi_hash_buckets *b = (fcgi_hash_buckets*)malloc(sizeof(fcgi_hash_buckets));
            b->idx = 0;
            b->next = h->buckets;
            h->buckets = b;
        }

        p = h->buckets->data + h->buckets->idx;
        h->buckets->idx++;
        p->next = h->hash_table[idx];
        h->hash_table[idx] = p;
        p->list_next = h->list;
        h->list = p;

        p->hash_value = hash_value;
        p->var_len = var_len;
        p->var = fcgi_hash_strndup(h, var, var_len);
        p->val_len = val_len;
        p->val = fcgi_hash_strndup(h, val, val_len);
        return p->val;
    }

    static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len)
    // 實際操做request->env->data,進行數據寫入。
    {
        char *ret;

        if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) {
        //若是準備寫入的數據長度大於當前指向的fcgi_hash_seg大小,則向前插入新的fcgi_hash_seg
                unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE;//較長值,不跨越兩個seg進行寫入。
                fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size);
                p->pos = p->data;
                p->end = p->pos + seg_size;
                p->next = h->data;
                h->data = p;
            }

            ret = h->data->pos;
           memcpy(ret, str, str_len); //於h->data->pos後寫入數據
            ret[str_len] = 0;
            h->data->pos += str_len + 1; //後移h->data->pos到新的可寫入位置
            return ret;
    }

由此,咱們能夠得出:request->env->data->pos的指向直接影響咱們環境變量Key,Value的寫入位置,只要咱們控制了char* pos的指向,就可能覆蓋已有的數據。可是,要想達成RCE還存在如下要求及限制:apache

  1. 指針前移受當前fcgi_hash_seg空間結構影響,太短沒法將char*
    pos置零,過長會分配到新fcgi_hash_seg空間。(如傳遞"形如"http://127.0.0.1/Somefile_exits/AAAAA.php/"也可形成指針後移,)
  2. path_info[0] = 0 僅能將單字節置零,最好爲最低位,不然會形成指針位置偏離過多。
  3. 鑑於條件 2 被覆蓋寫的地址最低位應爲0,且其後爲符合條件的可覆蓋的環境變量。
  4. 被覆蓋位置環境變量的key必須與預期寫入的key知足:var、hash_value和var_len均相同,纔可能被讀取。
  5. 執行FCGI_PUTENV(request, "ORIG_PATH_INFO",
    orig_path_info);時,分別寫入ORIG_SCRIPT_NAME、orig_script_name("ORIG_SCRIPT_NAME/index.php/PHP_VALUEnAAAAAA")。

相應地,咱們能夠:api

  1. 經過控制query_string的長度,使path_info剛好處於新fcgi_hash_seg的data首位,這時咱們僅需移動8+8+8+len("PATH_INFO0")+N= 34 + N便可完成對char* pos的篡改。知足條件1,2的要求。
  2. 經過自定義http header,操縱request header的長度將預期覆蓋的環境變量放置到特定的位置(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會在後續處理中被嘗試讀取(ini =
    FCGI_GETENV(request, "PHP_VALUE");)。知足條件4的要求。

Alt

除此以外,鑑於PATH_INFO從新取值部分邏輯主要是處理PATH_INFO與真實path_info不一樣的狀況,對開頭說起的nginx配置項,存在一種狀況,發起形如http://localhost/index/info.p...,能夠構造如下場景安全

//以http://localhost/index/info.php/test?a=b爲例,index爲存在的文件
     PATH_INFO=/test
     PATH_TRANSLATED=/docroot/index/info.php/test
     SCRIPT_NAME=/index/info.php
     REQUEST_URI=/index/info.php/test?a=b
     SCRIPT_FILENAME=/docroot/index/info.php
     QUERY_STRING=a=b
 
     pt = script_path_translated; // = env_script_filename => "/docroot/index/info.php/test"
    len = script_path_translated_len  // 爲"/docroot/index/info.php/test"

    // 通過從新計算處理後
    int ptlen = strlen(pt); // strlen("/docroot/index")
    int pilen = env_path_info ? strlen(env_path_info) : 0;  // 即len(PATH_INFO) "/test"
    int slen = len - ptlen;   // len("/info.php/test ")

    path_info = env_path_info + pilen - slen;  // pilen < slen, 即偏移爲-N

此時URL中無需存在%0A,亦可完成指針移位,漏洞過程與上述相似,可是由於script_name無效,沒法直觀顯示攻擊狀態,利用難度較高,再也不贅述。

path_info指向了request->env->data->pos後的內存佈局

Alt

漏洞利用

Exp做者利用PHP_VALUE向PHP傳遞多個環境變量,使PHP產生錯誤,以錯誤日誌的形式將webshell輸出到/tmp/a,並經過auto_prepend_file自動執行/tmp/a中的惡意代碼,達成getshell。

var chain = []string{
         "short_open_tag=1", //開啓php短標籤
         "html_errors=0",   // 在錯誤信息中關閉HTML標籤。
         "include_path=/tmp",  //包含路徑
         "auto_prepend_file=a",  //指定腳本執行前自動包含的文件,功能相似require()。
         "log_errors=1",  //使能錯誤日誌
         "error_reporting=2",   //指定錯誤級別
         "error_log=/tmp/a",  //錯誤日誌記錄文件
         "extension_dir=\"<?=\`\"",   //指定extension的加載目錄
        "extension=\"$_GET[a]\`?>\"", //指定加載的extension
    }

影響範圍

在文初提到的配置下,該漏洞影響如下版本的PHP:
7.1.x < 7.1.33
7.2.x < 7.2.24
7.3.x < 7.3.11

漏洞修復

能夠經過 Nginx 增添配置try_files %uri = 404php設置cgi.fix_pathinfo=0選項,臨時規避漏洞影響。也能夠選擇使用官方已經釋出的更新進行徹底修復。

京東雲-WAF現已支持對該漏洞的防禦,點擊【閱讀】,獲取更多產品信息。

歡迎點擊「京東雲」瞭解更多精彩

Alt

Alt

相關文章
相關標籤/搜索