Jump to content

Title: An analysis and thought about recently PHP-FPM RCE (CVE-2019-11043)

Featured Replies

Posted

First of all, this is such a really interesting bug! From a small memory defect to code execution. It combines both binary and web technique so that’s why it interested me to trace into. This is just a simple analysis, you can also check the bug report and the author neex’s exploit to know the original story :D

Originally, this write-up should be published earlier, but I am now traveling and don’t have enough time. Sorry for the delay :(

The root cause

PHP-FPM wrongly handles the PATH_INFO, which leads to a buffer underflow. Although it’s not vulnerable by default, there are still numerous vulnerable configurations that sysadmins would copy paste from Google and StackOverflow.

When the fastcgi_split_path_info directive is parsing a URI with newline, the env_path_info becomes an empty value. And due to the cgi.fix_pathinfo, the empty value is used(fpm_main.c#L1151) to calculate the real path_info later.

12345678910111213int ptlen=strlen(pt);int slen=len - ptlen;int pilen=env_path_info ? strlen(env_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 + piloten - slen : NULL; tflag=(orig_path_info !=path_info);}Please note that the pilot is zero and slen is the original URI length minus the real file-path length, so there is a buffer underflow. path_info can point to somewhere before it should be.

The exploitation

With this buffer underflow, we have a limited(and small) buffer access. What can we do? The author leverages the fpm_main.c#L1161 to do further actions.

1path_info[0]=0;As the path_info points ahead of PATH_INFO, we can write a single null-byte to the position before path_info.

A. From null-byte writing to CGI environment overwritten

OK, now we can write a single null-byte to somewhere before PATH_INFO, and then? In PHP-FPM, the CGI environments are stored in fcgi_data_seg structure, and managed by structure fcgi_hash.

12345678910111213typedef 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;The fcgi_data_seg in memory looks like:

1234567891011121314151617181920212223242526gdb-peda$ p *request.env.data$3={ pos=0x556578555537 '7UUxeU', end=0x5565785564d8 '', next=0x556578554490, data='P'}gdb-peda$ x/50s request.env.data.data0x5565785544a8: 'FCGI_ROLE'0x5565785544b23: 'RESPONDER'0x5565785544bc: 'SCRIPT_FILENAME'0x5565785544cc: '/var/www/html/test.php'0x5565785544e3: 'QUERY_STRING'0x5565785544f03: '0x5565785544f13: 'REQUEST_METHOD'0x556578554500: 'GET'.0x556578554656: 'SERVER_NAME'0x556578554662: '_'0x556578554664: 'REDIRECT_STATUS'0x556578554674: '200'0x556578554678: 'PATH_INFO'0x556578554682: '/', 'a' repeats 13 times, '.php' --- the `path_info` points to0x556578554695: 'HTTP_HOST'0x55657855469f3: '127.0.0.1'The structure member fcgi_data_seg-pos points to the current buffer - fcgi_data_seg-data to let PHP-FPM know where to write, and fcgi_data_seg-end points to the buffer end. If the buffer reaches the end(pos end). PHP-FPM creates a new buffer and moves the previous one to the structure member fcgi_data_seg-next.

So, the idea is to make path_info points to the location of fcgi_data_seg-pos. Once we achieve that, we can abuse the CGI environment management! For example, here we adjust the path_info points to the fcgi_data_seg-pos.

12345678910111213141516171819202122232425262728293031323334gdb-peda$ frame#0 init_request_info () at /home/orange/php-src/sapi/fpm/fpm/fpm_main.c:11611161 path_info[0]=0;gdb-peda$ x/xg path_info0x5565785554c0:0x0000556578555537gdb-peda$ x/g request.env.data0x5565785554c0:0x0000556578555537gdb-peda$ p (fcgi_data_seg)*request.env.data$2={ pos=0x556578555537 '', end=0x5565785564d8 '', next=0x556578554490, data='P'}gdb-peda$ x/15s (char **)request.env.data.data0x5565785554d8: 'PATH_INFO'0x5565785554e23: ''0x5565785554e33: 'HTTP_HOST'0x5565785554ed: '127.0.0.1'0x5565785554f7: 'HTTP_ACCEPT_ENCODING'0x55657855550c: 'A' repeats 11 times0x556578555518: 'HTTP_LAYS'0x556578555522: 'NOGG'0x556578555527: 'ORIG_PATH_INFO'0x556578555536: ''0x556578555537: '' --- the original `request.env.data.pos`0x556578555538: ''0x5565785555393: ''0x55657855553a: ''0x55657855553b: ''This is the memory layout of request.env.data.

25ed41355c5c976b-02.png

Once the line path_info[0]=0; has been executed, the memory layout becomes:

630b35c33f200b20-03.png

As the request.env.data.pos has been written, and changed to a new location:

12345678910111213141516171819202122gdb-peda$ next.gdb-peda$ p (fcgi_data_seg)*request.env.data$4={ pos=0x556578555500 'PT_ENCODING', end=0x55657855564d8 '', next=0x556578554490, data='P'}gdb-peda$ x/10s (char **)request.env.data.pos0x556578555500: 'PT_ENCODING'0x55657855550c: 'A' repeats 11 times0x556578555518: 'HTTP_LAYS'0x556578555522: 'NOGG'0x556578555527: 'ORIG_PATH_INFO'0x556578555536: ''0x556578555537: ''0x556578555538: ''0x5565785555393: ''0x55657855553a: ''As you can see, the request.env.data.pos is shifted to the middle of an environment variable. The next time PHP-FPM put a new CGI environment, it will overwrite the existing one.

12345678910111213141516171819202122232425262728293031323334353637383940#define FCGI_PUTENV(request, name, value) \ fcgi_quick_putenv(request, name, sizeof(name)-1, FCGI_HASH_FUNC(name, sizeof(name)-1), value)char* fcgi_putenv(fcgi_request *req, char* var, int var_len, char* val){ if (!req) return NULL; if (val==NULL) { fcgi_hash_del(req-env, FCGI_HASH_FUNC(var, var_len), var, var_len); return NULL; } else { return fcgi_hash_set(req-env, FCGI_HASH_FUNC(var, var_len), var, var_len, val, (unsigned int)strlen(val)); }}static char* fcgi_hash_set(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, char *val, unsigned int val_len){ unsigned int idx=hash_value FCGI_HASH_TABLE_MASK; fcgi_hash_bucket *p=h-hash_table[idx]; //. 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){ char *ret; //. ret=h-data-pos; --- we have corrupted the `pos` :D memcpy(ret, str, str_len); ret[str_len]=0; h-data-pos +=str_len + 1; return ret;}And it’s lucky, there is a FCGI_PUTENV right after the null-byte writing:

123456789101112old=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); --- here } 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;It puts the name ORIG_SCRIPT_NAME and our controllerable value into the CGI environments so that we can overwrite some important environments! …and then?

B. From CGI environment overwritten to Remote Code Execution

Now we can overwrite environments, how to turn it into the RCE?

After the null-byte writing, the PHP-FPM retrieves the environment PHP_VALUE to initial the PHP stuff. So that’s our target!

However, although we can overwrite the environment data. To forge the PHP_VALUE is still not easy. We can not just overwrite the existing environments key to PHP_VALUE and profit. After checking the source, we found the problem is PHP-FPM uses a hash table to manage environments. Without corrupting the table, we can’t insert a new environment!

PHP-FPM stores each environment variable in structure fcgi_hash_bucket.

123456789typedef 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;There are also some checks before PHP-FPM retrieve the environment variable:

1234567891011121314151617static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len){ unsigned int idx=hash_value FCGI_HASH_TABLE_MASK; fcgi_hash_bucket *p=h-hash_table[idx]; while (p !=NULL) { 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;}PHP-FPM first retrieves the environment structure from the hash table, and then check the hash_value, var_len and content. We can forge the content, but how to forge the hash_value and var_len? OK, let’s do it!

The hash algorithm in PHP-FPM is simple.

123456#define FCGI_HASH_FUNC(var, var_len) \ (UNEXPECTED(var_len 3) ? (unsigned int)var_len : \ (((unsigned int)var[3]) 2) + \

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

Important Information

HackTeam Cookie PolicyWe have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.