记一次某推上的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。
