本文为看雪论坛优秀文章
看雪论坛作者ID:RoboTerh
在一次浏览某推中发现了发现了了一个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。
看雪ID:RoboTerh
https://bbs.kanxue.com/user-home-962655.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!
原文始发于微信公众号(看雪学苑):记一次某推上的session利用trick