2024全网最全面及最新且最为详细的网络安全技巧五 之 SSRF 漏洞EXP技巧,典例分析以及 如何修复 (上册)———— 作者:LJS

news2025/2/26 5:44:47
  • 五——SSRF漏洞 EXP技巧,典例分析以及 如何修复

  • 目录

    五——SSRF EXP技巧,典例分析以及 如何修复

    5.1Apache mod_proxy SSRF(CVE-2021-40438)的一点分析和延伸

    0x01 Apache Module综述

    0x02 漏洞原理分析

    Apache在配置反代的后端服务器时,有两种情况:

    当满足这三个条件后,将unix:后面的内容进行解析,设置成uds_path的值;将字符|后面的内容,设置成rurl的值。

    这个函数中有三个主要的部分,

    0x03 限制绕过

    那么如何让ap_runtime_dir_relative的返回值是null?

    0x04 mod_proxy_fcgi是否存在漏洞?

    0x05 哪些模块受到影响

    5.2Dict协议是什么

    dict 的初体验

    dict 协议是啥

    5.3Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

    Fastcgi Record

    Fastcgi Type

    PHP-FPM(FastCGI进程管理器)

    Nginx(IIS7)解析漏洞

    security.limit_extensions配置

    任意代码执行

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

    那么,我们怎么设置auto_prepend_file的值?

    EXP编写


  • 5.1Apache mod_proxy SSRF(CVE-2021-40438)的一点分析和延伸

  • 0x01 Apache Module综述

  • 如果我们要部署一个PHP运行环境,且将Apache作为Web应用服务器,那么常用的有三种方法:
  • Apache以CGI的形式运行PHP脚本

  • PHP以mod_php的方式作为Apache的一个模块运行

  • PHP以FPM的方式运行为独立服务,Apache使用mod_proxy_fcgi模块作为反代服务器将请求代理给PHP-FPM

  • 第一种方式比较古老,性能较差,基本已经淘汰;第二种方式在Apache环境下使用较广,配置最为简单;第三种方法也有较大用户体量,不过Apache仅作为一个中间的反代服务器,更多新的用户会选择使用性能更好的Nginx替代。
  • 这其中,第三种方法使用的mod_proxy_fcgi就是本文主角mod_proxy模块的一个子模块。mod_proxy是Apache服务器中用于反代后端服务的一个
  • 一个模块,而它拥有数个不同功能的子模块,分别用于支持不同通信协议的后端,比如常见的有:
  • mod_proxy_fcgi 用于反代后端是fastcgi协议的服务,比如php-fpm

  • mod_proxy_http 用于反代后端是http、https协议的服务

  • mod_proxy_uwsgi 用于反代后端是uwsgi协议的服务,主要针对uWSGI

  • mod_proxy_ajp 用于反代后端是ajp协议的服务,主要针对Tomcat

  • mod_proxy_ftp 用于反代后端是ftp协议的服务

  • 除去mod_proxy_fcgi用于反代PHP,我们在使用Node.js、Python等脚本语言编写的应用也常常会使用mod_proxy_http作为一层反代服务器,这样中间层可以做ACL、静态文件服务等。
  • 这次的SSRF漏洞是出在mod_proxy这个模块中的,我们就来从代码的层面分析一下它的原理是什么,究竟影响有多大。
  • 0x02 漏洞原理分析

  • 《Building a POC for CVE-2021-40438》这篇文章中提到了这个漏洞的复现方法:当目标环境使用了mod_proxy做反向代理,比如ProxyPass / "http://localhost:8000/",此时通过请求http://target/?unix:{'A'*5000}|http://example.com/即可向http://example.com发送请求,造成一个SSRF攻击。
  • 这里面,Apache代码中犯得错误是在modules/proxy/proxy_util.c的fix_uds_filename函数:
  • /*
     * In the case of the reverse proxy, we need to see if we
     * were passed a UDS url (eg: from mod_proxy) and adjust uds_path
     * as required.
     */
    static void fix_uds_filename(request_rec *r, char **url) 
    {
        char *ptr, *ptr2;
    
        // 检查传入的 request_rec 是否为空,以及是否存在有效的 r->filename
        if (!r || !r->filename) return;
    
        // 检查 r->filename 是否以 "proxy:" 开头,并且包含 "unix:" 字符串,并且包含 '|' 字符
        if (!strncmp(r->filename, "proxy:", 6) &&
            (ptr2 = ap_strcasestr(r->filename, "unix:")) &&
            (ptr = ap_strchr(ptr2, '|'))) {
            
            apr_uri_t urisock;
            apr_status_t rv;
    
            // 将 '|' 替换为字符串结束符 '\0',以便后续解析
            *ptr = '\0';
    
            // 解析 URI,填充 urisock 结构体
            rv = apr_uri_parse(r->pool, ptr2, &urisock);
            
            if (rv == APR_SUCCESS) {
                char *rurl = ptr + 1; // rurl 指向 '|' 后的路径部分
                char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path);
                
                // 将解析得到的 Unix 域套接字路径存储到请求的 notes 表中
                apr_table_setn(r->notes, "uds_path", sockpath);
                
                // 复制 rurl 到 url,保留 scheme 部分(因为需要用于 UDS)
                *url = apr_pstrdup(r->pool, rurl);
    
                // 将 r->filename 中 "proxy:" 后的部分替换为 rurl,以更新请求的 filename
                memmove(r->filename + 6, rurl, strlen(rurl) + 1);
    
                // 记录调试日志,指示由于 UDS 的重写 URL
                ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
                              "*: rewrite of url due to UDS(%s): %s (%s)",
                              sockpath, *url, r->filename);
            }
            else {
                // 如果解析 URI 失败,恢复 '|' 字符
                *ptr = '|';
            }
        }
    }
    
  • Apache在配置反代的后端服务器时,有两种情况:
  • 直接使用某个协议反代到某个IP和端口,比如ProxyPass / "http://localhost:8080"

  • 使用某个协议反代到unix套接字,比如ProxyPass / "unix:/var/run/www.sock|http://localhost:8080/"

  • 第一种情况比较好理解,第二种情况的设计我觉得不是很好,相当于让用户可以使用一个Apache自创的写法来配置后端地址。那么这时候就会涉及到parse的过程,需要将这种自创的语法转换成能兼容正常socket连接的结构,而fix_uds_filename函数就是做这个事情的。
  • 使用字符串文法来表示多种含义的方式通常暗藏一些漏洞,比如这里,进入这个if语句需要满足三个条件:
  • r->filename的前6个字符等于proxy:

  • r->filename的字符串中含有关键字unix:

  • unix:关键字后的部分含有字符|

  • 当满足这三个条件后,unix:后面的内容进行解析,设置成uds_path的值;将字符|后面的内容,设置成rurl的值。
  • 举个例子,前面介绍中的ProxyPass / "unix:/var/run/www.sock|http://localhost:8080/",在解析完成后,uds_path的值等于/var/run/www.sockrurl的值等于http://localhost:8080/
  • 看到这里其实都没有什么问题,那么我们肯定会思考,r->filename是从哪来的,用户可控吗,为什么?
  • 这时就要说到另一个函数,proxy_hook_canon_handler,这个函数用于注册canon handler,比如:
  • image-20211017234354615.png

  • 可以看到,每一个mod_proxy_xxx都会注册一个自己的canon handler,canon handler会在反代的时候被调用,用于告诉Apache主程序它应该把这个请求交给哪个处理方法来处理。
  • 比如,我们看到mod_proxy_httpproxy_http_canon函数:
  • static int proxy_http_canon(request_rec *r, char *url)
    {
        const char *scheme = NULL;  // 存储 URL 的 scheme,如 "http" 或 "https"
        const char *host = NULL;    // 存储主机名或 IP 地址
        const char *path = NULL;    // 存储 URL 的路径部分
        const char *search = NULL;  // 存储 URL 的查询字符串部分
        int port, def_port;         // 端口号和默认端口号
        char sport[7];              // 存储端口号的字符串形式,如 ":80"
        char enc_path[512];         // 存储经过编码处理的路径部分
    
        // first part: 检查 URL 的 scheme,并处理
        if (strncasecmp(url, "http:", 5) == 0) {
            url += 5;               // 移动指针,跳过 "http:"
            scheme = "http";        // 设置 scheme 为 "http"
        }
        else if (strncasecmp(url, "https:", 6) == 0) {
            url += 6;               // 移动指针,跳过 "https:"
            scheme = "https";       // 设置 scheme 为 "https"
        }
        else {
            return DECLINED;        // 如果不是以 "http:" 或 "https:" 开头,则返回处理失败
        }
    
        port = def_port = ap_proxy_port_of_scheme(scheme);  // 获取对应 scheme 的默认端口号
    
        // second part: 根据请求类型和标记处理 URL 的主机名和端口号
        ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port);
        
        switch (r->proxyreq) {
        default: /* wtf are we doing here? */
        case PROXYREQ_REVERSE:
            // 对于反向代理,检查是否设置了 "proxy-nocanon" 标记
            if (apr_table_get(r->notes, "proxy-nocanon")) {
                path = url;   // 如果设置了标记,直接使用原始的路径部分
            }
            else {
                // 否则,对路径进行规范化和编码处理
                path = ap_proxy_canonenc(r->pool, url, strlen(url),
                                         enc_path, 0, r->proxyreq);
                search = r->args;  // 获取请求的查询字符串
            }
            break;
        case PROXYREQ_PROXY:
            path = url;  // 对于正向代理,直接使用原始的路径部分
            break;
        }
    
        // 检查路径是否有效
        if (path == NULL)
            return HTTP_BAD_REQUEST;
    
        // 如果端口不是默认端口,则格式化端口号字符串
        if (port != def_port)
            apr_snprintf(sport, sizeof(sport), ":%d", port);
        else
            sport[0] = '\0';  // 否则置为空字符串
    
        // 如果主机名包含 ':',则将其视为 IPv6 地址,加上方括号
        if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */
            host = apr_pstrcat(r->pool, "[", host, "]", NULL);
        }
    
        // fourth part: 构建最终的 filename 字符串,表示代理请求的目标 URL
        r->filename = apr_pstrcat(r->pool, "proxy:", scheme, "://", host, sport,
                "/", path, (search) ? "?" : "", (search) ? search : "", NULL);
    
        return OK;  // 返回处理成功
    }
    
  • 这个函数中有三个主要的部分,
  • 第一部分检查了配置中的url的开头是不是http:https:,如果不是,说明这个请求不该由mod_proxy_http模块处理,后续的过程跳过;
  • 第二部分,用各种方式获取到scheme、host、port、path、search等几个URL的组成变量;
  • 第三部分,拼接proxy:、scheme、://、host、sport、/、path、search,成为一个字符串,赋值给r->filename
  • 这里面,scheme、host、sport来自于配置文件中配置的ProxyPass,而path、search来自于用户发送的数据包。也就是说,r->filename中的后半部分是用户可控的。
  • 那我们回看前面的fix_uds_filename函数,它在r->filename中查找关键字unix:,并将这个关键字后面直到|的部分作为unix套接字地址,而将|后面的部分作为反代的后端地址。
  • 我们可以通过请求的path或者search来控制这两个部分,控制了反代的后端地址,这也就是为什么这里会出现SSRF的原因。
  • 0x03 限制绕过

  • 当然,这里面有一个问题,那就是Apache在正常情况下,因为识别到了unix套接字,所以会把用户请求发送给这个本地文件套接字,而不是后端URL。
  • 可以来做个测试,我们发送这样一个请求:
  • GET /?unix:/var/run/test.sock|http://example.com/ HTTP/1.1
    
  • 此时会得到一个503错误,错误日志会反馈这样的结果:
  • [Mon Oct 18 00:14:38.634795 2021] [proxy:error] [pid 782180:tid 140737306797824] (2)No such file or directory: AH02454: HTTP: attempt to connect to Unix domain socket /var/run/test.sock (192.168.1.1) failed
    [Mon Oct 18 00:14:38.634875 2021] [proxy_http:error] [pid 782180:tid 140737306797824] [client 192.168.1.142:59696] AH01114: HTTP: failed to make connection to backend: httpd-UDS
  • 找不到unix套接字/var/run/test.sock,这是当然。
  • 我们不能让他把请求发送到unix套接字上,而是发送给我们需要的|后面的地址。
  • 国外那位作者给出了一个非常巧妙的方法,在fix_uds_filename函数中,unix套接字的地址来自于下面这两行代码:
  • char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path);
    apr_table_setn(r->notes, "uds_path", sockpath);
  • 如果这里ap_runtime_dir_relative函数返回值是null,则后面获取uds_path时将不会使用unix套接字地址,而变成普通的TCP连接:
  • uds_path = (*worker->s->uds_path ? worker->s->uds_path : apr_table_get(r->notes, "uds_path"));
    if (uds_path) {
        if (conn->uds_path == NULL) {
            /* use (*conn)->pool instead of worker->cp->pool to match lifetime */
            conn->uds_path = apr_pstrdup(conn->pool, uds_path);
        }
        // ...
        conn->hostname = "httpd-UDS";
        conn->port = 0;
    }
    else {
        // ...
        conn->hostname = apr_pstrdup(conn->pool, uri->hostname);
        conn->port = uri->port;
        // ...
    }
  • 那么如何让ap_runtime_dir_relative的返回值是null?
  • ap_runtime_dir_relative函数最后引用了apr库中的apr_filepath_merge函数,它的主要作用就是路径的join,用于处理相对路径、绝对路径、../连接。
  • 这个函数中,当待join的两段路径长度+4大于APR_PATH_MAX,也就是4096的时候,则函数会返回一个路径过长的状态码,导致最后unix套接字的值是null
  • rootlen = strlen(rootpath);
    maxlen = rootlen + strlen(addpath) + 4; /* 4 for slashes at start, after
                                                 * root, and at end, plus trailing
                                                 * null */
    if (maxlen > APR_PATH_MAX) {
        return APR_ENAMETOOLONG;
    }
  • 也就是说,我们只需要在unix:|之间传入内容长度大概超过4092的字符串,就能构造出uds_path为null的结果,让Apache不再发送请求给unix套接字。
  • 最后,这样构造出的请求成功触发SSRF漏洞:
  • image-20211018010400594.png

  •  Apache官方对这个漏洞的修复也比较简单,因为用户只能控制r->filename的后半部分,而前半部分proxy:{scheme}://{host}{sport}/来自于配置文件,所以最新版改成检查其开头是不是proxy:unix:,这一部分用户无法控制
  • 0x04 mod_proxy_fcgi是否存在漏洞?

  • 我们前文都以mod_proxy_http作为例子来研究,而在Apache+PHP环境下,mod_proxy_fcgi的使用频率更高,那么它是否也会被SSRF漏洞影响呢?
  • 这个漏洞出现在modules/proxy/proxy_util.c的fix_uds_filename函数,理论上是mod_proxy的漏洞,那么它的子模块应该都会被影响,但这个漏洞中有一个很关键的变量是r->filename,他是否可控决定了后面的利用是否可以成功。
  • 我们看一下mod_proxy_fcgi的canon函数:
  • static int proxy_fcgi_canon(request_rec *r, char *url)
    {
        char *host, sport[7];  // 定义变量 host 和 sport 数组,sport 数组用于存储端口号字符串
        const char *err;       // 错误信息字符串
        char *path;            // 路径字符串
        apr_port_t port, def_port;  // 端口号变量
        fcgi_req_config_t *rconf = NULL;  // FastCGI 请求配置
        const char *pathinfo_type = NULL;  // 路径信息类型
    
        if (ap_cstr_casecmpn(url, "fcgi:", 5) == 0) {
            url += 5;  // 如果 url 以 "fcgi:" 开头,则移动指针到路径的起始位置
        }
        else {
            return DECLINED;  // 如果不以 "fcgi:" 开头,则拒绝处理该请求
        }
    
        // ...
    
        if (apr_table_get(r->notes, "proxy-nocanon")) {
            path = url;   // 如果在请求的 notes 中找到了 "proxy-nocanon" 键,则使用原始路径
        }
        else {
            path = ap_proxy_canonenc(r->pool, url, strlen(url), enc_path, 0, r->proxyreq);
            // 否则,使用 ap_proxy_canonenc 函数对 URL 进行规范化编码
        }
        if (path == NULL)
            return HTTP_BAD_REQUEST;  // 如果路径为空,返回 HTTP_BAD_REQUEST 错误
    
        // 构建完整的文件名,格式为 "proxy:fcgi://host:sport/path"
        r->filename = apr_pstrcat(r->pool, "proxy:fcgi://", host, sport, "/", path, NULL);
    
        // ...
    }
    
  • 可见,这里的r->filename等于proxy:fcgi://{host}{sport}/{path}相比于mod_proxy_http少了search。不过,path仍然是用户可以控制的,我们可以尝试发送这样的数据包:
  • GET /unix:testtest|http://example.com/1.php HTTP/1.1
    ...
  • 经过调试可见,path中的|ap_proxy_canonenc函数编码成了%7C:
  • image-20211018013708745.png

  • 没有|,后面也就无法完成SSRF利用了。
  • 0x05 哪些模块受到影响

  • 那么,我们其实可以认为,如果r->filename有部分可控,且可控的部分没有被编码(不是path),这个模块就会受到SSRF漏洞的影响。
  • 对这个结论我没有逐一测试考证,我仅挑选另一个较为常用的模块mod_proxy_ajp来复现漏洞。
  • mod_proxy_ajp是用于反代Tomcat的一个Apache模块,Tomcat在8.5.51版本以前默认会开启两个端口8080和8009,分别对应HTTP协议和AJP协议。HTTP协议好理解,AJP协议是一个二进制协议,通信协议相比起来效率更高。所以以前很多运维人员会将Tomcat假设在Apache之后,然后二者之间使用AJP协议通信。
  • Tomcat 8.5.51之后的版本受到Ghostcat漏洞影响不再默认开放8009端口。

  • Apache下有两个模块能实现AJP的反代通信:
  • mod_proxy_ajp 这就是mod_proxy的一个子模块,由Apache HTTPd官方维护

  • mod_jk 这是Tomcat官方维护的一个Apache模块,更加出名用户也更多

  • 由于mod_jk不是用mod_proxy的代码,所以不受到影响,我们今天仅测试mod_proxy_ajp。
  • 简单部署一个开放8009端口的Tomcat服务器,并配置好mod_proxy_ajp进行调试,可见其proxy_ajp_canon函数r->filename中是包含search的:
  • static int proxy_ajp_canon(request_rec *r, char *url)
    {
        char *host, *path, sport[7];
        char *search = NULL;
        const char *err;
        apr_port_t port, def_port;
    
        /* ap_port_of_scheme() */
        if (strncasecmp(url, "ajp:", 4) == 0) {
            url += 4;  // 如果 URL 以 "ajp:" 开头,移动指针到路径的起始位置
        }
        else {
            return DECLINED;  // 如果不以 "ajp:" 开头,则拒绝处理该请求
        }
    
    
        // 构建完整的文件名,格式为 "proxy:ajp://host:sport/path?search"
        r->filename = apr_pstrcat(r->pool, "proxy:ajp://", host, sport,
                                  "/", path, (search) ? "?" : "",
                                  (search) ? search : "", NULL);
        return OK;  // 返回 OK 表示处理成功
    }
    
  • 那么按照我们的预测,这里也会存在SSRF漏洞。果然测试成功:
  •  那么,mod_proxy_http2、mod_proxy_balancer、mod_proxy_wstunnel等这些模块也会受到影响,而mod_proxy_uwsgi、mod_proxy_scgi等模块不受影响。我没有严格验证,有兴趣的同学可以自己下去调试一下,也许还能找到绕过方法。

  • 5.2Dict协议是什么

  • 最近在学校 ssrf 攻击,看到可以用 dict 协议可以加载一个 tcp 端口的提供的服务所返回的部分数据。但是网上很少 dict 协议相关的说明,直到我找到了这个网站:感谢这个博客,让俺明白了啥是 dict 协议
  • dict 的初体验

  • 多说无益,直接上一个用了 dict 协议的服务让你们来体验一下
  • 首先在你的电脑上安装一个 telnet 客户端 Windows 和 Mac / Linux 上应该都有对应的客户端
  • 安装好了以后用这个命令来登录(由于编码原因,有些非英文字符在某些系统上可能会乱码)
  • telnet dict.org 2628
  • 之后如果连接上了,能看到对应的提示:
  • 220 dict.dict.org dictd 1.12.1/rf on Linux 4.19.0-10-amd64 <auth.mime><56180310.14213.1628480435@dict.dict.org>
  • 在终端中输入 h 来获取帮助
  • 113 help text follows
    DEFINE database word         -- look up word in database
    MATCH database strategy word -- match word in database using strategy
    SHOW DB                      -- list all accessible databases
    SHOW DATABASES               -- list all accessible databases
    SHOW STRAT                   -- list available matching strategies
    SHOW STRATEGIES              -- list available matching strategies
    SHOW INFO database           -- provide information about the database
    SHOW SERVER                  -- provide site-specific information
    OPTION MIME                  -- use MIME headers
    CLIENT info                  -- identify client to server
    AUTH user string             -- provide authentication information
    STATUS                       -- display timing information
    HELP                         -- display this help information
    QUIT                         -- terminate connection
    
    The following commands are unofficial server extensions for debugging
    only.  You may find them useful if you are using telnet as a client.
    If you are writing a client, you MUST NOT use these commands, since
    they won't be supported on any other server!
    
    D word                       -- DEFINE * word
    D database word              -- DEFINE database word
    M word                       -- MATCH * . word
    M strategy word              -- MATCH * strategy word
    M database strategy word     -- MATCH database strategy word
    S                            -- STATUS
    H                            -- HELP
    Q                            -- QUIT
  • 在终端中输入以下命令(这个东西貌似不区分大小写的样子)来列出所有的字典
  • image.png

  • 最后我们看到了 english 这个字典
  • 在最后我们输入 define [字典名] [单词] 这样的命令来获取一个单词的解释
  • 比如说
  •  define english hello
  • image.png

  • 服务器就会返回对应的单词解释
  • dict 协议是啥

  • dict 协议是一个在线网络字典协议,这个协议是用来架设一个字典服务的。不过貌似用的比较少,所以网上基本没啥资料(包括谷歌上)。可以看到用这个协议架设的服务可以用 telnet 来登陆,说明这个协议应该是基于 tcp 协议开发的。
  • 所以像 mysql 的服务,因为也是基于 tcp 协议开发,所以用 dict 协议的方式打开也能强行读取一些 mysql 服务的返回内容
  • 比如说下面这段程序:
  • <?php
    
    // 文件名: main.php
      
    $url = "dict://localhost:3306"; // localhost:3306 上架设了我的 mysql 服务
    
    $ch = curl_init($url);
    curl_exec($ch);
    curl_close($ch);
  • 输出结果:
  • image.png

  • 可以看到虽然乱码,但是还是强行读取出来了一些可以辨识的数据,比如说 mysql 的版本号

  • 5.3Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

  • 搭过php相关环境的同学应该对fastcgi不陌生,那么fastcgi究竟是什么东西,为什么nginx可以通过fastcgi来对接php?
  • Fastcgi Record

  • Fastcgi其实是一个通信协议,和HTTP协议一样,都是进行数据交换的一个通道。
  • HTTP协议是浏览器和服务器中间件进行数据交换的协议,浏览器将HTTP头和HTTP体用某个规则组装成数据包,以TCP的方式发送到服务器中间件,服务器中间件按照规则将数据包解码,并按要求拿到用户需要的数据,再以HTTP协议的规则打包返回给服务器。
  • 类比HTTP协议来说,fastcgi协议则是服务器中间件和某个语言后端进行数据交换的协议。
  • Fastcgi协议由多个record组成,record也有header和body一说,服务器中间件将这二者按照fastcgi的规则封装好发送给语言后端,语言后端解码以后拿到具体数据,进行指定操作,并将结果再按照该协议封装好后返回给服务器中间件。
  • 和HTTP头不同,record的头固定8个字节,body是由头中的contentLength指定,其结构如下:
  • 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,也就是65536字节。
  • Fastcgi Type

  • 刚才我介绍了fastcgi一个record中各个结构的含义,其中第二个字节type我没详说。
  • type就是指定该record的作用。因为fastcgi一个record的大小是有限的,作用也是单一的,所以我们需要在一个TCP流里传输多个record。通过type来标志每个record的作用,用requestId作为同一次请求的id。
  • 也就是说,每次请求,会有多个record,他们的requestId是相同的。
  • 借用该文章中的一个表格,列出最主要的几种type
  • 14931267923354.jpg

  • 看了这个表格就很清楚了,服务器中间件和后端语言通信,第一个数据包就是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个结构,至于用哪个结构,有如下规则:
  • key、value均小于128字节,用FCGI_NameValuePair11

  • key大于128字节,value小于128字节,用FCGI_NameValuePair41

  • key小于128字节,value大于128字节,用FCGI_NameValuePair14

  • key、value均大于128字节,用FCGI_NameValuePair44

  • 为什么我只介绍type为4的record?因为环境变量在后面PHP-FPM里有重要作用,之后写代码也会写到这个结构。type的其他情况,大家可以自己翻文档理解理解。
  • PHP-FPM(FastCGI进程管理器)

  • 那么,PHP-FPM又是什么东西?
  • FPM其实是一个fastcgi协议解析器,Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。
  • 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',         # FastCGI协议版本
        'REQUEST_METHOD': 'GET',                    # HTTP请求方法
        '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',         # 包含查询字符串的完整请求URI
        'DOCUMENT_ROOT': '/var/www/html',           # 服务器的文档根目录
        'SERVER_SOFTWARE': 'php/fcgiclient',        # 服务器软件和版本信息
        'REMOTE_ADDR': '127.0.0.1',                 # 客户端的IP地址
        'REMOTE_PORT': '12345',                     # 客户端的端口号
        'SERVER_ADDR': '127.0.0.1',                 # 服务器的IP地址
        'SERVER_PORT': '80',                        # 服务器的端口号
        'SERVER_NAME': "localhost",                 # 服务器的名称
        'SERVER_PROTOCOL': 'HTTP/1.1'               # 使用的HTTP协议版本
    }
    
  • 这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉fpm:“我要执行哪个PHP文件”。
  • PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php
  • Nginx(IIS7)解析漏洞

  • Nginx和IIS7曾经出现过一个PHP相关的解析漏洞(测试环境),该漏洞现象是,在用户访问http://127.0.0.1/favicon.ico/.php时,访问到的文件是favicon.ico,但却按照.php后缀解析了。
  • 用户请求http://127.0.0.1/favicon.ico/.php,nginx将会发送如下环境变量到fpm里:
  • {
        ...
        'SCRIPT_FILENAME': '/var/www/html/favicon.ico/.php',  # 正在执行的脚本文件的文件系统路径,这里看起来像是一个错误的路径,包含了不应该有的"/.php"
        'SCRIPT_NAME': '/favicon.ico/.php',                   # 当前脚本的路径,同样反映了错误的路径结构
        'REQUEST_URI': '/favicon.ico/.php',                   # 包含查询字符串的完整请求URI,同样反映了错误的路径结构
        'DOCUMENT_ROOT': '/var/www/html',                     # 服务器的文档根目录,正常情况下是正确的路径
        ...
    }
    
  • 正常来说,SCRIPT_FILENAME的值是一个不存在的文件/var/www/html/favicon.ico/.php,是PHP设置中的一个选项fix_pathinfo导致了这个漏洞。
  • PHP为了支持Path Info模式而创造了fix_pathinfo,在这个选项被打开的情况下,fpm会判断SCRIPT_FILENAME是否存在,如果不存在则去掉最后一个/及以后的所有内容,再次判断文件是否存在,往次循环,直到文件存在。
  • 所以,第一次fpm发现/var/www/html/favicon.ico/.php不存在,则去掉/.php,再判断/var/www/html/favicon.ico是否存在。显然这个文件是存在的,于是被作为PHP文件执行,导致解析漏洞。
  • 正确的解决方法有两种:
  • 一是在Nginx端使用fastcgi_split_path_info将path info信息去除后,用tryfiles判断文件是否存在;
  • 二是借助PHP-FPM的security.limit_extensions 配置项,避免其他后缀文件被解析。
  • security.limit_extensions配置

  • 写到这里,PHP-FPM未授权访问漏洞也就呼之欲出了。PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信。
  • 此时,SCRIPT_FILENAME的值就格外重要了。因为fpm是根据这个值来执行php文件的,如果这个文件不存在,fpm会直接返回404:
  • 在fpm某个版本之前,我们可以将SCRIPT_FILENAME的值指定为任意后缀文件,比如/etc/passwd;但后来,fpm的默认配置中增加了一个选项security.limit_extensions
  • ; 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 .php7
  •  其限定了只有某些后缀的文件允许被fpm执行,默认是.php所以,当我们再传入/etc/passwd的时候,将会返回Access denied.
  • 14931290357686.jpg

  •  ps. 这个配置也会影响Nginx解析漏洞,我觉得应该是因为Nginx当时那个解析漏洞,促成PHP-FPM增加了这个安全选项。另外,也有少部分发行版安装中security.limit_extensions默认为空,此时就没有任何限制了。
  • 由于这个配置项的限制,如果想利用PHP-FPM的未授权访问漏洞,首先就得找到一个已存在的PHP文件。
  • 万幸的是,通常使用源安装php的时候,服务器上都会附带一些php后缀的文件,我们使用
  • find / -name "*.php"
  • 来全局搜索一下默认环境:
  • 14931297810961.jpg

  • 找到了不少。这就给我们提供了一条思路,假设我们爆破不出来目标环境的web目录,我们可以找找默认源安装后可能存在的php文件,比如/usr/local/lib/php/PEAR.php
  • 任意代码执行

  • 那么,为什么我们控制fastcgi协议通信的内容,就能执行任意PHP代码呢?
  • 理论上当然是不可以的,即使我们能控制SCRIPT_FILENAME让fpm执行任意文件,也只是执行目标服务器上的文件,并不能执行我们需要其执行的文件。
  • 但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_fileauto_append_file
  • auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;
  • auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。
  • 那么就有趣了,假设我们设置auto_prepend_filephp://input,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include
  • 那么,我们怎么设置auto_prepend_file的值?
  • 这又涉及到PHP-FPM的两个环境变量,PHP_VALUEPHP_ADMIN_VALUE。这两个环境变量就是用来设置PHP配置项的,
  • PHP_VALUE可以设置模式为PHP_INI_USERPHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)
  • 所以,我们最后传入如下环境变量:
  • {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',       # FastCGI协议的版本号
        'REQUEST_METHOD': 'GET',                 # 请求方法是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',     # 请求的URI,包括查询字符串
        'DOCUMENT_ROOT': '/var/www/html',        # 文档根目录
        'SERVER_SOFTWARE': 'php/fcgiclient',     # 服务器软件信息
        'REMOTE_ADDR': '127.0.0.1',              # 客户端的IP地址
        'REMOTE_PORT': '12345',                  # 客户端的端口号
        'SERVER_ADDR': '127.0.0.1',              # 服务器的IP地址
        'SERVER_PORT': '80',                     # 服务器的端口号
        'SERVER_NAME': "localhost",              # 服务器的主机名
        'SERVER_PROTOCOL': 'HTTP/1.1'            # 使用的协议版本
        'PHP_VALUE': 'auto_prepend_file = php://input',  # PHP配置:在请求处理之前自动包含php://input中的内容
        'PHP_ADMIN_VALUE': 'allow_url_include = On'       # PHP管理员配置:允许使用URL包含(远程文件包含)
    }
    
  • 设置auto_prepend_file = php://inputallow_url_include = On,然后将我们需要执行的代码放在Body中,即可执行任意代码。
  • 效果如下:
  • EXP编写

  • 上图中用到的EXP,就是根据之前介绍的fastcgi协议来编写的,代码如下:https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75 。兼容Python2和Python3,方便在内网用。
  • 之前好些人总是拿着一个GO写的工具在用,又不太好用。实际上理解了fastcgi协议,再看看这个源码,就很简单了。

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

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

相关文章

跟《经济学人》学英文:2024年07月06日这期:Finishing schools for the age of TikTok

Finishing schools for the age of TikTok Unsure how to be polite at work? Ask a digital etiquette guru 不确定如何在工作中保持礼貌&#xff1f;请教一位数字礼仪大师 “Finishing schools” 是指专门为年轻女性提供礼仪、社交技巧、文化修养等教育的学校&#xff0c;…

深度解析 Raft 分布式一致性协议

本文参考转载至&#xff1a;浅谈 Raft 分布式一致性协议&#xff5c;图解 Raft - 白泽来了 - 博客园 (cnblogs.com) 深度解析 Raft 分布式一致性协议 - 掘金 (juejin.cn) raft-zh_cn/raft-zh_cn.md at master maemual/raft-zh_cn (github.com) 本篇文章将模拟一个KV数据读写服…

华为开发者大会2024纪要:鸿蒙OS的全新篇章与AI大模型的革命

华为开发者大会2024纪要:鸿蒙OS的全新篇章与AI大模型的革命 在科技的浪潮中,华为再次引领潮流,2024年的开发者大会带来了一系列令人瞩目的创新成果。从鸿蒙操作系统的全新Beta版到盘古大模型的震撼发布,华为正以前所未有的速度重塑智能生态。以下是本次大会的亮点,让我们…

springboot马拉松赛事志愿者管理系统-计算机毕业设计源码80251

摘 要 随着马拉松运动的兴起和发展&#xff0c;马拉松赛事的组织和管理面临着越来越多的挑战&#xff0c;其中志愿者的招募、培训和管理是至关重要的一环。传统的人力资源管理方式已经无法满足大型马拉松赛事对志愿者团队的需求&#xff0c;因此基于现代信息技术的马拉松赛事志…

电表读数检测数据集VOC+YOLO格式18156张12类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;18156 标注数量(xml文件个数)&#xff1a;18156 标注数量(txt文件个数)&#xff1a;18156 标…

oracle数据库默认表空间详解

文章目录 oracle数据库默认表空间列表 oracle数据库默认表空间列表 系统表空间&#xff08;System Tablespace&#xff09; 系统表空间包含了系统级别的元数据&#xff0c;如数据字典、系统表和存储过程等。例如SYSTEM表空间用于保存数据库的数据字典、PL/SQL程序的源代码和解释…

Windows如何查看端口是否占用,并结束端口进程

需求与问题&#xff1a;前后端配置了跨域操作&#xff0c;但是仍然报错&#xff0c;可以考虑端口被两个程序占用&#xff0c;找不到正确端口或者后端接口书写是否规范&#xff0c;特别是利用Python Flask书写时要保证缩进是否正确&#xff01; Windows操作系统中&#xff0c;查…

【算法专题】模拟算法题

模拟算法题往往不涉及复杂的数据结构或算法&#xff0c;而是侧重于对特定情景的代码实现&#xff0c;关键在于理解题目所描述的情境&#xff0c;并能够将其转化为代码逻辑。所以我们在处理这种类型的题目时&#xff0c;最好要现在演草纸上把情况理清楚&#xff0c;再动手编写代…

pytest使用报错(以及解决pytest所谓的“抑制print输出”)

1. 测试类的类名问题 #codingutf-8import pytestclass TestClass1:def setup(self) -> None:print(setup)def test_01(self) -> None:print(test_01111111111111111111111)def test_02(self) -> None:print(test_02)以上述代码为例&#xff0c;如果类名是Test开头&am…

基于Android Studio订餐管理项目

目录 项目介绍 图片展示 运行环境 获取方式 项目介绍 能够实现登录&#xff0c;注册、首页、订餐、购物车&#xff0c;我的。 用户注册后&#xff0c;登陆客户端即可完成订餐、浏览菜谱等功能&#xff0c;点餐&#xff0c;加入购物车&#xff0c;结算&#xff0c;以及删减…

Mysql慢日志、慢SQL

慢查询日志 查看执行慢的SQL语句&#xff0c;需要先开启慢查询日志。 MySQL 的慢查询日志&#xff0c;记录在 MySQL 中响应时间超过阀值的语句&#xff08;具体指运行时间超过 long_query_time 值的SQL。long_query_time 的默认值为10&#xff0c;意思是运行10秒以上(不含10秒…

Nginx(http配置、https配置)访问Spring Boot 项目

前文 记录一下在linux服务器下配置nginx中nginx.conf文件代理访问springboot项目 1. spring boot.yml配置 其他mysql,redis,mybatis等之类的配置就不一一列出了 # 自定义配置 为了等下验证读取的配置文件环境 appName: productserver:port: 8083 # 应用服务 WEB 访问端口s…

Apache Seata 高可用部署实践

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Apache Seata 高可用部署实践 Seata 高可用部署实践 使用配置中心和数据库来实现 Seata 的高…

OpenAI突然停止中国API使用,出海SaaS产品如何化挑战为机遇?

2023年是AI爆发的年代&#xff0c;人工智能带来的信息裂变刷新了整个SaaS行业。在这个AI引领的时代&#xff0c;我们不应该单纯依赖工具本身&#xff0c;而是要理解如何将这些AI功能与行业相结合。 然而&#xff0c;上周OpenAI宣布禁止对中国提供API服务&#xff0c;有一些用户…

FreeU: Free Lunch in Diffusion U-Net——【代码复现】

这篇文章发表于CVPR 2024&#xff0c;官网地址&#xff1a;ChenyangSi/FreeU: FreeU: Free Lunch in Diffusion U-Net (CVPR2024 Oral) (github.com) 一、环境准备 提前准备好python、pytorch环境 二、下载项目依赖 demo下有一个requirements.txt文件&#xff0c; pip inst…

ubuntu 22 安装 lua 环境 编译lua cjson 模块

在 windows 下使用 cygwin 编译 lua 和 cjson 简直就是灾难&#xff0c;最后还是到 ubuntu 下完成了。 1、下载lua源码&#xff08;我下载的 5.1 版本&#xff0c;后面还有一个小插曲), 直接解压编译&#xff0c;遇到一个 readline.h not found 的问题&#xff0c;需要安装 re…

探索 SecureCRT:强大的终端 SSH 工具

SecureCRT 是一款功能强大、备受赞誉的终端 SSH 工具&#xff0c;适用于 Mac 和 Windows 操作系统&#xff0c;为用户提供了便捷、高效、安全的远程连接和管理体验。 SecureCRT 拥有直观友好的用户界面&#xff0c;即使是初次使用的用户也能迅速上手。其支持多种协议&#xff…

基于Python爬虫的城市二手房数据分析可视化

基于Python爬虫的城市二手房数据分析可视化 一、前言二、数据采集(爬虫,附完整代码)三、数据可视化(附完整代码)3.1 房源面积-总价散点图3.2 各行政区均价3.3 均价最高的10个小区3.4 均价最高的10个地段3.5 户型分布3.6 词云图四、如何更换城市一、前言 二手房具有价格普…

提高论文发表机会:Nature Communications 最新研究教你如何巧妙回复审稿意见

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 对于科研搬砖人来说&#xff0c;在论文投稿过程中&#xff0c;如何有效回复审稿意见才能得到审稿人的认可&#xff0c;一直是一个让人困惑又带点玄学的问题。 但是&#xff0c…

代码随想录算法训练Day57|LeetCode200-岛屿数量、LeetCode695-岛屿的最大面积

岛屿数量 题目描述 力扣200-岛屿数量 给你一个由 1&#xff08;陆地&#xff09;和 0&#xff08;水&#xff09;组成的的二维网格&#xff0c;请你计算网格中岛屿的数量。 岛屿总是被水包围&#xff0c;并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此…