记一次某推上的session利用trick

逆向病毒分析 1年前 (2023) admin
497 0 0

记一次某推上的session利用trick

本文为看雪论坛优秀文章

看雪论坛作者ID:RoboTerh



在一次浏览某推中发现了发现了了一个web challenge的赏金ctf,这里从来学习一下由于使session_start()报错引发的危害。


正文


题目环境地址

http://18.185.14.202/chall1/index.php?page=showMeTheCode


程序分析


index.php

<?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

发现在这里存在有这样一段话:

记一次某推上的session利用trick

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

记一次某推上的session利用trick

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

记一次某推上的session利用trick

成功登陆上了admin(中途刷新了一下cookie的,所以导致cookie不一样。


之后就是通过这个cookie进行flag的获取。

curl 'http://18.185.14.202/chall1/index.php?page=flag' -H 'Cookie: PHPSESSID=ugr7im9314nhn283bse6j0gf4r' -v

记一次某推上的session利用trick

那么为什么会产生这种错误呢,主要是因为没有对PHPSESSID的值进行校验,使得产生错误,执行了Session类的stop方法中的session_write_close()方法。


总结


算是总结了关于php session利用的一个小trick。




记一次某推上的session利用trick


看雪ID:RoboTerh

https://bbs.kanxue.com/user-home-962655.htm

*本文由看雪论坛 RoboTerh 原创,转载请注明来自看雪社区

记一次某推上的session利用trick

# 往期推荐

1、[译]大疆无人机安全与DroneID漏洞

2、记录一下从编译的角度还原VMP的思路

3、Galgame汉化中的逆向:动态汉化分析-以AZsystem引擎为例

4、记录调试Windows服务操作

5、Xposed检测绕过

6、源代码静态分析方法——代码属性图Code Property Graphs


记一次某推上的session利用trick


记一次某推上的session利用trick

球分享

记一次某推上的session利用trick

球点赞

记一次某推上的session利用trick

球在看


记一次某推上的session利用trick

点击“阅读原文”,了解更多!

原文始发于微信公众号(看雪学苑):记一次某推上的session利用trick

版权声明:admin 发表于 2023年3月8日 下午6:00。
转载请注明:记一次某推上的session利用trick | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...