2024全網最全面及最新且最為詳細的網絡安全技巧五 之 SSRF 漏洞EXP技巧,典例分析以及 如何修復 (上冊)———— 作者:LJS

  • 五——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->filenameif (!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,以更新請求的 filenamememmove(r->filename + 6, rurl, strlen(rurl) + 1);// 記錄調試日志,指示由于 UDS 的重寫 URLap_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);elsesport[0] = '\0';  // 否則置為空字符串// 如果主機名包含 ':',則將其視為 IPv6 地址,加上方括號if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */host = apr_pstrcat(r->pool, "[", host, "]", NULL);}// fourth part: 構建最終的 filename 字符串,表示代理請求的目標 URLr->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 connectionThe 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對應的請求idunsigned 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.pswp.cn/web/40913.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/40913.shtml
英文地址,請注明出處:http://en.pswp.cn/web/40913.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Vue的學習之生命周期

一、生命周期 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>Vue的學習</title><script src"vue.js" type"text/javascript" charset"utf-8"></script></head>&l…

C#如何從中級進階到高級開發

從中級C#開發進階到高級開發&#xff0c;需要深入理解和掌握更復雜的技術和架構&#xff0c;同時培養解決問題的能力和創新思維。以下是一些關鍵的技能和步驟&#xff0c;可以幫助你從中級向高級開發邁進&#xff1a; 1. 深入理解C#語言特性 泛型&#xff1a;熟練使用泛型提高…

Java實現登錄驗證 -- JWT令牌實現

目錄 1.實現登錄驗證的引出原因 2.JWT令牌2.1 使用JWT令牌時2.2 令牌的組成 3. JWT令牌&#xff08;token&#xff09;生成和校驗3.1 引入JWT令牌的依賴3.2 使用Jar包中提供的API來實現JWT令牌的生成和校驗3.3 使用JWT令牌驗證登錄3.4 令牌的優缺點 1.實現登錄驗證的引出 傳統…

強化Linux系統安全性:從基礎命令到高級管理

強化Linux系統安全性&#xff1a;從基礎命令到高級管理 引言 在網絡安全領域&#xff0c;Linux系統因其穩定性和安全性而廣受歡迎。作為一名網絡安全專家&#xff0c;我將分享如何通過Linux基礎命令和高級管理技巧來加強系統的安全性。本文將基于《學神 IT 教育》提供的Linux…

Debezium報錯處理系列之第110篇: ERROR Error during binlog processing.Access denied

Debezium報錯處理系列之第110篇:ERROR Error during binlog processing. Last offset stored = null, binlog reader near position = /4 Access denied; you need at least one of the REPLICATION SLAVE privilege for this operation 一、完整報錯二、錯誤原因三、解決方法…

python 切入點(EntryPoints)使用

文章目錄 EntryPoints 介紹EntryPoints案例EntryPoints 介紹 官網參考 EntryPoints 是發布的python 項目的一種機制,可以提供對自身項目的切入點,供其他項目代碼使用。在python環境中可以通過importlib.metadata.entry_points 函數發現所有的切入點插件,并在代碼中加載、調…

08_排序

基本概念與分類 假設含有n個記錄的序列為 { r 1 , r 2 , . . . , r n } \{r_1,r_2,...,r_n\} {r1?,r2?,...,rn?}&#xff0c;其相應的關鍵字分別為 { k 1 , k 2 , . . . , k n } \{k_1,k_2,...,k_n\} {k1?,k2?,...,kn?}&#xff0c;需確定1&#xff0c;2&#xff0c;…&…

微服務: Nacos部署安裝與properties配置

Nacos 是阿里巴巴開源的一款用于動態服務發現、配置管理和服務管理的基礎設施。Nacos 這個名稱源自于 “Dynamic Naming and Configuration Service”。它主要是用于解決微服務架構中服務發現和配置管理的問題。 Nacos 單機模式的部署安裝 1. 安裝(Windows環境) Nacos是Java…

Java線程基礎知識總結

基礎概念 Java 線程是并發編程的基礎&#xff0c;涉及到線程的創建、管理、同步以及通信。理解和掌握線程的使用對于編寫高效和響應快速的應用程序至關重要。 1. 線程基礎 線程是程序中的執行流。每個Java程序至少有一個線程 — 主線程&#xff08;main&#xff09;。通過使…

從入門到深入,Docker新手學習教程

編譯整理&#xff5c;TesterHome社區 作者&#xff5c;Ishaan Gupta 以下為作者觀點&#xff1a; Docker 徹底改變了我們開發、交付和運行應用程序的方式。它使開發人員能夠將應用程序打包到容器中 - 標準化的可執行組件&#xff0c;將應用程序源代碼與在任何環境中運行該代碼…

InspireFace-商用級的跨平臺開源人臉分析SDK

InspireFace-商用級的跨平臺開源人臉分析SDK InspireFaceSDK是由insightface開發的?款?臉識別軟件開發?具包&#xff08;SDK&#xff09;。它提供了?系列功能&#xff0c;可以滿?各種應?場景下的?臉識別需求&#xff0c;包括但不限于閘機、?臉?禁、?臉驗證等。 該S…

ubuntu22 sshd設置

專欄總目錄 一、安裝sshd服務 sudo apt updatesudo apt install -y openssh-server 二、配置sshd 使用文本編輯器打開/etc/ssh/sshd_config sudo vi /etc/ssh/sshd_config &#xff08;一&#xff09;配置sshd服務的偵聽端口 建議將ssh的偵聽端口改為7000以上的端口&#…

【bazel】快速下載教程

bazel下載鏈接&#xff1a; https://github.com/bazelbuild/bazel/releases?page11 直接在github上下載&#xff0c;會因為網絡不穩定&#xff0c;而頻繁下載錯誤 這里提供一個超級快速的方法&#xff01;&#xff01;&#xff01; 用迅雷下載&#xff01; 1.從github上復…

cpp http server/client

httplib 使用httplib庫 basedemo server.cpp #include "httplib.h" #include <iostream> using namespace httplib;int main(void) {Server svr;svr.Get("/hello", [](const Request& req, Response& res) {std::cout << "lo…

實現Java Web應用的高性能負載均衡方案

實現Java Web應用的高性能負載均衡方案 大家好&#xff0c;我是微賺淘客系統3.0的小編&#xff0c;也是冬天不穿秋褲&#xff0c;天冷也要風度的程序猿&#xff01; 在高并發的網絡環境中&#xff0c;負載均衡是確保Web應用程序高性能和可靠性的關鍵策略之一。本文將探討如何…

【力扣 - 每日一題】3115. 質數的最大距離(一次遍歷、頭尾遍歷、空間換時間、埃式篩、歐拉篩、打表)Golang實現

原題鏈接 題目描述 給你一個整數數組 nums。 返回兩個&#xff08;不一定不同的&#xff09;質數在 nums 中 下標 的 最大距離。 示例 1&#xff1a; 輸入&#xff1a; nums [4,2,9,5,3] 輸出&#xff1a; 3 解釋&#xff1a; nums[1]、nums[3] 和 nums[4] 是質數。因此答…

算法系列--分治排序|再談快速排序|快速排序的優化|快速選擇算法

前言:本文就前期學習快速排序算法的一些疑惑點進行詳細解答,并且給出基礎快速排序算法的優化版本 一.再談快速排序 快速排序算法的核心是分治思想,分治策略分為以下三步: 分解:將原問題分解為若干相似,規模較小的子問題解決:如果子問題規模較小,直接解決;否則遞歸解決子問題合…

策略模式的應用

前言 系統有一個需求就是采購員審批注冊供應商的信息時&#xff0c;會生成一個供應商的賬號&#xff0c;此時需要發送供應商的賬號信息&#xff08;賬號、密碼&#xff09;到注冊填寫的郵箱中&#xff0c;通知供應商賬號信息&#xff0c;當時很快就寫好了一個工具類&#xff0…

Python 學習中什么是字典,如何操作字典?

什么是字典 字典&#xff08;Dictionary&#xff09;是Python中的一種內置數據結構&#xff0c;用于存儲鍵值對&#xff08;key-value pair&#xff09;。字典的特點是通過鍵來快速查找值&#xff0c;鍵必須是唯一的&#xff0c;而值可以是任何數據類型。字典在其他編程語言中…

vue實現搜索文章關鍵字,滑到指定位置并且高亮

1、輸入搜索條件&#xff0c;點擊搜索按鈕 2、滑到定位到指定的搜索條件。 <template><div><div class"search_form"><el-inputv-model"searchVal"placeholder"請輸入關鍵字查詢"clearablesize"small"style&quo…