先來了解一下關於session
的一些基礎知識
什么是session
在計算機中,尤其是在網絡應用中,稱為“會話控制”。Session 對象存儲特定用戶會話所需的屬性及配置信息。這樣,當用戶在應用程序的 Web 頁之間跳轉時,存儲在 Session 對象中的變量將不會丟失,而是在整個用戶會話中一直存在下去。當用戶請求來自應用程序的 Web 頁時,如果該用戶還沒有會話,則 Web 服務器將自動創建一個 Session 對象。當會話過期或被放棄后,服務器將終止該會話。
session是如何起作用的
當第一次訪問網站時,Seesion_start()函數就會創建一個唯一的Session ID,並自動通過HTTP的響應頭,將這個Session ID保存到客戶端Cookie中。同時,也在服務器端創建一個以Session ID命名的文件,用於保存這個用戶的會話信息。當同一個用戶再次訪問這個網站時,也會自動通過HTTP的請求頭將Cookie中保存的Seesion ID再攜帶過來,這時Session_start()函數就不會再去分配一個新的Session ID,而是在服務器的硬盤中去尋找和這個Session ID同名的Session文件,將這之前為這個用戶保存的會話信息讀出,在當前腳本中應用,達到跟蹤這個用戶的目的。
session_start()函數以及該函數所起的作用:
當會話自動開始或者通過 session_start() 手動開始的時候, PHP 內部會依據客戶端傳來的PHPSESSID來獲取現有的對應的會話數據(即session文件), PHP 會自動反序列化session文件的內容,並將之填充到 $_SESSION 超級全局變量中。如果不存在對應的會話數據,則創建名為sess_PHPSESSID(客戶端傳來的)的文件。如果客戶端未發送PHPSESSID,則創建一個由32個字母組成的PHPSESSID,並返回set-cookie。
session存儲機制
php中的session中的內容並不是放在內存中的,而是以文件的方式來存儲的,存儲方式就是由配置項session.save_handler來進行確定的,默認是以文件的方式存儲。
存儲的文件是以sess_sessionid來進行命名的,文件的內容就是session值的序列話之后的內容。
假設我們的環境是xampp,默認配置為:
session.save_path="D:\xampp\tmp"
session.save_handler=files
session.auto_start=0
session.serialize_handler=php
在默認配置的情況下:
最后的session的存儲和顯示如下:
可以看到PHPSESSID的值是jo86ud4jfvu81mbg28sl2s56c2,而在xampp/tmp下存儲的文件名是sess_jo86ud4jfvu81mbg28sl2s56c2,文件的內容是name|s:6:"spoock";
。name是鍵值,s:6:"spoock";
是serialize("spoock")
的結果。
了解了有關session的概念后,還需要了解php.ini中一些Session配置:
以上的選項就是與PHP中的Session存儲和序列話存儲有關的選項。
在使用xampp組件安裝中,上述的配置項的設置如下:
session.save_path="D:\xampp\tmp" 表明所有的session文件都是存儲在xampp/tmp下
session.save_handler=files 表明session是以文件的方式來進行存儲的
session.auto_start=0 表明默認不啟動session
session.serialize_handler=php 表明session的默認序列話引擎使用的是php序列話引擎
在上述的配置中,session.serialize_handler是用來設置session的序列話引擎的,除了默認的PHP引擎之外,還存在其他引擎,不同的引擎所對應的session的存儲方式不相同。想要知道為什么為出現這個session反序列化漏洞,就需要了解session機制中對序列化是如何處理的。
在php中session有三種序列化的方式,分別是php_serialize, php_binary和php
這個便是在相應的處理器處理下,session
所存儲的格式,這里舉個例子來了解一下在不同的處理器下,session所儲存的格式有什么不一樣(測試的時候php版本一定要大於5.5.4,不然session寫不進文件)):
比如這里我get進去一個值為shy,查看一下各個存儲格式:
這有什么問題,其實PHP中的Session的實現是沒有的問題,危害主要是由於程序員的Session使用不當而引起的。如:使用不同處理器來處理session文件。
使用不同的引擎來處理session文件
php引擎的存儲格式是鍵名
| serialized_string
,而php_serialize引擎的存儲格式是serialized_string
。如果程序使用兩個引擎來分別處理的話就會出現問題。
先以php_serialize
的格式存儲,從客戶端接收參數並存入session
變量
(1.php)
接下來使用php
處理器讀取session文件
(2.php)
攻擊思路:
首先訪問1.php
,在傳入的參數最開始加一個'|'
,由於1.php
是使用php_serialize
處理器處理,因此只會把'|'
當做一個正常的字符。然后訪問2.php
,由於用的是php
處理器,因此遇到'|'
時會將之看做鍵名與值的分割符,從而造成了歧義,導致其在解析session文件時直接對'|'
后的值進行反序列化處理。
這里可能會有一個小疑問,為什么在解析session文件時直接對'|'
后的值進行反序列化處理,這也是處理器的功能?這個其實是因為session_start()
這個函數,可以看下官方說明:
首先生成一個payload:
攻擊思路中說到了因為不同的引擎會對'|'
,產生歧義,所以在傳參時在payload前加個'|'
,作為a參數,訪問1.php
,查看一下本地session文件,發現payload已經存入到session
文件
訪問一下2.php
看看會有什么結果
成功觸發了student類的__wakeup()
方法,所以這種攻擊思路是可行的。但這種方法是在可以對session
的進行賦值的,那如果代碼中不存在對$_SESSION
變量賦值的情況下又該如何利用
沒有$_SESSION變量賦值
在PHP中還存在一個upload_process機制,即自動在$_SESSION中創建一個鍵值對,值中剛好存在用戶可控的部分,可以看下官方描述的,這個功能在文件上傳的過程中利用session實時返回上傳的進度。在session.upload_process.enabled開啟時會啟用這個功能,在php.ini中會默認啟用這個功能。
上傳文件時,如果 POST 一個名為 PHP_SESSION_UPLOAD_PROGRESS
的變量,就可以將 filename 的值賦值到session 中,filename 的值如果包含雙引號,還需要進行轉義,上傳的頁面的寫法如下:
1 |
<form action="http://example.com/index.php" method="POST" enctype="multipart/form-data"> |
最后 Session 就會保存上傳的文件名。如果沒有提供寫入 Session 的地方,可以用這種方法。POST 請求的數據包:
1 |
POST / HTTP/1.1 |
這種攻擊方法與上一部分基本相同,不過這里需要先上傳文件,同時POST
一個與session.upload_process.name
的同名變量(默認為PHP_SESSION_UPLOAD_PROGRESS)。后端會自動將POST
的這個同名變量作為鍵進行序列化然后存儲到session
文件中。下次請求就會反序列化session文件,從中取出這個鍵。所以攻擊點還是跟上一部分一模一樣,程序還是使用了不同的session處理引擎。
舉個栗子:
當我們隨便傳入一個值時,便會觸發__construct()
魔法函數,從而出現phpinfo
頁面,在phpinfo頁面發現
可以看到題目環境中的 session.serialize_handler 默認為 php_serialize 處理器,而程序使用的卻是 php 處理器,而且開頭 第4行 使用了 session_start() 函數,那么我們就可以利用 session.upload_progress.enabled 來偽造 session ,然后在 PHP 反序列化 session 文件時,還原 OowoO 類,最終執行 eval 函數。
通過POST
方法來構造數據傳入$_SESSION
,首先構造POST
提交表單
接下來構造序列化payload
將payload改為如下代碼:print_r(scandir(dirname(__FILE__)));
#scandir 目錄中的文件和目錄
#dirname 函數返回路徑中的目錄部分
#__FILE__ php中的魔法常量,文件的完整路徑和文件名。如果用在被包含文件中,則返回被包含的文件名
#序列化后的結果 O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
為防止雙引號被轉義,在雙引號前加上\
,除此之外還要加上|
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
在這個頁面隨便上傳一個文件,然后抓包修改filename的值
可以看到Here_1s_7he_fl4g_buT_You_Cannot_see.php
這個文件,flag肯定在里面,但還有一個問題就是不知道這個路徑,路徑的問題就需要回到phpinfo頁面去查看
$_SERVER['SCRIPT_FILENAME'] 也是包含當前運行腳本的路徑,與 $_SERVER['SCRIPT_NAME'] 不同的是,這是服務器端的絕對路徑。
既然知道了路徑,就繼續構造payload即可
print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));
#file_get_contents() 函數把整個文件讀入一個字符串中。
接下來的就還是序列化然后改一下格式傳入即可,后面的就不再寫了
再舉個栗子:
class.php
index.php
通過代碼發現,我們最終是要通過foo3中的execute來執行我們自定義的函數。
那么我們首先在本地搭建環境,構造我們需要執行的自定義的函數。如下:
myindex.php
在foo1中的構造函數中定義$varr的值為foo2的實例,在foo2中定義$obj為foo3的實例,在foo3中定義$varr的值為echo "spoock"。最終得到的序列話的值是:
O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:14:"echo "spoock";";}}}
這樣當上面的序列話的值寫入到服務器端,然后再訪問服務器的index.php,最終就會執行我們預先定義的echo "spoock";
的方法了。
寫入的方式主要是利用PHP中Session Upload Progress來進行設置,具體為,在上傳文件時,如果POST一個名為PHP_SESSION_UPLOAD_PROGRESS的變量,就可以將filename的值賦值到session中,上傳的頁面的寫法如下:
最后就會將文件名寫入到session中,具體的實現細節可以參考PHP手冊。
那么最終寫入的文件名是|O:4:\"foo1\":1:{s:4:\"varr\";O:4:\"foo2\":2:{s:4:\"varr\";s:1:\"1\";s:3:\"obj\";O:4:\"foo3\":1:{s:4:\"varr\";s:12:\"var_dump(1);\";}}}
。注意與本地反序列化不一樣的地方是要在最前方加上|