記一次某推上的session利用trick

在一次瀏覽某推中發現了發現了了一個web challenge的賞金ctf,這里從來學習一下由于使session_start()報錯引發的危害。
正文
題目環境地址
http://18.185.14.202/chall1/index.php?page=showMeTheCode
程序分析
index.php
define('DEV_MODE', false); class Session{ public static $id = null; protected static $isInit = false; protected static $started = false; public static function start(){ self::$isInit = true; if (!self::$started) { if (!is_null(self::$id)) { session_id(self::$id); self::$started = session_start(); } else { self::$started = session_start(); self::$id = session_id(); } } } public static function stop(){ if (self::$started) { session_write_close(); self::$started = false; } } public static function destroy() { session_destroy(); } public static function set($key, $value){ if (!isset($_SESSION) || self::get($key) == $value) { return; } if (!self::$started) { self::start(); $_SESSION[$key] = $value; self::stop(); } else { $_SESSION[$key] = $value; } } public static function get($key){ if (isset($_SESSION)) { return $_SESSION[$key]; } return null; } public static function isInit(){ return self::$isInit; }} class User { private $users; private $states = ['start', 'checkCreds', 'credsValid', 'userState', 'connected', 'error']; private $banlist = ['blackhat', 'notkindguy']; function __construct(){ $userFile = '/users.txt'; $fp = fopen($userFile,'r'); while(($userLine = fgets($fp))!==false){ $user = explode(':',trim($userLine),2); $this->users[] = $user; } } function login($username, $password){ $state = Session::get('state'); if($state === 'connected' && Session::get('authenticated') === true) exit; if(method_exists($this,$state)){ $this->$state($username, $password); } else { $this->start($username, $password); } } function start($username, $password) { // NOT IN USE FOR NOW Session::set('state', 'checkCreds'); $this->login($username, $password); } function checkCreds($username, $password) { foreach($this->users as $user) { if($username === $user[0] && $password === $user[1]) { Session::set('state', 'credsValid'); $this->login($username, $password); return; } } Session::set('state', 'error'); $this->login($username, $password); } function credsValid($username, $password) { Session::set('user', $username); Session::set('state', 'userState'); $this->login($username, $password); } function userState($username, $password) { if(in_array($username, $this->banlist)) { Session::set('user',null); Session::set('state','error'); $this->login($username, $password); return; } else { Session::set('state', 'connected'); $this->login($username, $password); } } function connected($username, $password) { Session::set('authenticated',true); echo "Welcome $username, you're connected! Have a great day."; } function error($username, $password) { echo "Your login or password is incorrect, or you're banned :("; Session::destroy(); return; } function getFlag() { if(Session::get('user') === 'admin' && Session::get('authenticated')) { echo file_get_contents('/flag.txt'); } else { echo "No flag for you"; } }} Session::start();$users = file_get_contents('/users.txt');if(isset($_GET['page'])) { switch($_GET['page']) { case 'login': $user = new User(); $user->login($_GET['username'],$_GET['password']); break; case 'flag': $user = new User(); $user->getFlag(); break; case 'showMeTheCode': highlight_file(__FILE__); exit; }}
users.txt
user:user
我們僅僅只有一個賬戶username為user, password為user。
我們又怎么能夠達到在登陸admin賬戶之后進行flag的獲取?
那么肯定是需要越權的實現了。
簡單分析一下代碼吧。
class Session{ public static $id = null; protected static $isInit = false; protected static $started = false; public static function start(){ self::$isInit = true; if (!self::$started) { if (!is_null(self::$id)) { session_id(self::$id); self::$started = session_start(); } else { self::$started = session_start(); self::$id = session_id(); } } } public static function stop(){ if (self::$started) { session_write_close(); self::$started = false; } } public static function destroy() { session_destroy(); } public static function set($key, $value){ if (!isset($_SESSION) || self::get($key) == $value) { return; } if (!self::$started) { self::start(); $_SESSION[$key] = $value; self::stop(); } else { $_SESSION[$key] = $value; } } public static function get($key){ if (isset($_SESSION)) { return $_SESSION[$key]; } return null; } public static function isInit(){ return self::$isInit; }}
這個Session類主要是封裝了一些有關session的創建銷毀及擴展了一些功能。
至于在其下的User類的邏輯。
存在有一個__construct這個魔術方法,在創建對象的時候將會進行調用。
主要是從users.txt中讀取賬戶。
存在有login函數:
function login($username, $password){ $state = Session::get('state'); if($state === 'connected' && Session::get('authenticated') === true) exit; if(method_exists($this,$state)){ $this->$state($username, $password); } else { $this->start($username, $password); }}
傳入username和password參數,首先從session中獲取state值,如果其為connected 并且已經被被認證了就會直接退出。
對應的如果存在有從state中獲得的方法,就會調用其方法。
如果沒有,就調用start函數。
function start($username, $password) { // NOT IN USE FOR NOW Session::set('state', 'checkCreds'); $this->login($username, $password);}
他會創建一個$_SESSION['state'] = checkCreds,之后再次調用login方法,根據上面的描述將會調用checkCreds方法。
function checkCreds($username, $password) { foreach($this->users as $user) { if($username === $user[0] && $password === $user[1]) { Session::set('state', 'credsValid'); $this->login($username, $password); return; } } Session::set('state', 'error'); $this->login($username, $password);}
在這個方法中,進行了身份的校驗,通過從users.txt中獲取的賬戶對傳入的參數username和password進行了判斷,這里進行了強比較,所以也就不存在php的弱比較繞過了。
如果不滿足校驗將創建一個$_SESSION['state'] = error,之后調用login方法,進而調用了error方法。
function error($username, $password) { echo "Your login or password is incorrect, or you're banned :("; Session::destroy(); return;}
在error方法中將會銷毀掉session并返回null。
如果通過了前面的校驗,就會創建一個$_SESSION['state'] = credsValid, 之后再次調用login,進而調用了credsValid方法。
function credsValid($username, $password) { Session::set('user', $username); Session::set('state', 'userState'); $this->login($username, $password);}
在這個方法中將會將傳入的username參數創建一個$_SESSION['user'] = $username 和 $_SESSION['state'] = 'userState',之后調用了login方法,進而調用了userState方法。
function userState($username, $password) { if(in_array($username, $this->banlist)) { Session::set('user',null); Session::set('state','error'); $this->login($username, $password); return; } else { Session::set('state', 'connected'); $this->login($username, $password); }}
如果傳入的useranme參數在banlist名單中將會出現異常(有一說一,我感覺沒有任何作用)。
private $banlist = ['blackhat', 'notkindguy'];
如果不存在,就會調用connected方法。
function connected($username, $password) { Session::set('authenticated',true); echo "Welcome $username, you're connected! Have a great day.";}
賦予$_SESSION['authenticated'] = true
那么我們最后需要達到的目標就是:
function getFlag() { if(Session::get('user') === 'admin' && Session::get('authenticated')) { echo file_get_contents('/flag.txt'); } else { echo "No flag for you"; }}
不僅需要username 為admin, 而且還是需要認證的admin才會得到flag。
我們通過上面的分析似乎走進了死胡同,但是還是有可以突破的點。
突破
我們通過上面的分析,相信我們能夠注意到在credsValid方法中存在和我們傳入參數進行交互的點。
他在代碼中將其傳入給了session中的user值,如果我們能夠在這一步使得傳入的username為admin,是不是后面獲取flag就是格外的輕松了呢?
那是直接傳入admin還是有一個問題!
那就是調用credsValid方法之前還有一步通過調用了checkCreds判斷了username和password的可用性。
但是如果我們能夠獲取到credsValid那一步的cookie, 修改我們的cookie值在傳入username=admin是不是就可以成功突破了呢?
基于這樣的思路,我們翻閱php manual的文檔
https://www.php.net/manual/en/function.session-id.php
發現在這里存在有這樣一段話:

If id is specified and not null, it will replace the current session id. session_id() needs to be called before session_start() for that purpose. Depending on the session handler, not all characters are allowed within the session id. For example, the file session handler only allows characters in the range a-z A-Z 0-9 , (comma) and - (minus)!
在session_id調用過程中不是所有的字符都能夠存在于cookie中的
他只允許在a-z A-Z 0-9 , -等字符,如果我們使用特殊字符他是否會報錯呢?報什么錯?有什么危害?
它能夠使得session_start方法發生錯誤。
我們可以嘗試一下他的作用。
curl 'http://18.185.14.202/chall1/index.php?page=login&username=user&password=user' -H 'Cookie: PHPSESSID=$' -v

what's this !
我們居然得到了很多程序運行中set的cookie值,那么其中一個是否是我們需要的呢?
當然有!
curl 'http://18.185.14.202/chall1/index.php?page=login&username=admin&password=123' -H 'Cookie: PHPSESSID=ugr7im9314nhn283bse6j0gf4r' -v

成功登陸上了admin(中途刷新了一下cookie的,所以導致cookie不一樣。
之后就是通過這個cookie進行flag的獲取。
curl 'http://18.185.14.202/chall1/index.php?page=flag' -H 'Cookie: PHPSESSID=ugr7im9314nhn283bse6j0gf4r' -v

那么為什么會產生這種錯誤呢,主要是因為沒有對PHPSESSID的值進行校驗,使得產生錯誤,執行了Session類的stop方法中的session_write_close()方法。
總結
算是總結了關于php session利用的一個小trick。