Skip to content

Vulnerability: $_SESSION spoofing can give superadmin access  #25

@brainexcerpts

Description

@brainexcerpts

I was working on two different instances of PivotX on a local server (Xampp).
I discovered that, login on one instance, would allow me to get access to the other instance's administration panel...
By instance I mean: two copies of PivotX in different folders placed in /htdocs.
Even when the two versions have distinct user names, passwords etc.
It allows some level of accessibility in the unrelated instance.
Worse, using the same username gives you the same level of credential in the other instance.

Multisites scenarios on the same web server might be rare, but I think it is still a security issue.
It's also a bug since user sessions of different instances interferes with each other.

Cause

It turns out the function isLoggedIn() blindly trust $_SESSION:

function isLoggedIn() {
    global $PIVOTX;
    $this->loadPermsessions();
    $sessioncookie = (!empty($_COOKIE['pivotxsession'])) ? $_COOKIE['pivotxsession'] : $_POST['pivotxsession'];
    if (isset($_SESSION['user']) && isset($_SESSION['user']['username']) && ($_SESSION['user']['username']!="") ) {
        // User is logged in!
        // Check if we're in the saved sessions..
        if (!empty($sessioncookie) && !isset($this->permsessions[$sessioncookie])) {
    
            $this->permsessions[ $sessioncookie ] = array(
                'username' => $_SESSION['user']['username'],
                'ip' => $_SERVER['REMOTE_ADDR'],
                'lastseen' => time()
            );
            $this->savePermsessions();
        }
        return true;
    } else {
         ...
        skipping stuff
        ...
        return false;
    }
}

It seems that $_SESSION['user']['username'] only needs to be set to some value for pivotx to view the user as "logged in"
(and this regardless of its actual value or checking anything else).

If the attacker finds out the username of the superadmin,
(for instance when the nickname is the same as the user name and displayed in a blog post),
he can get full privileges on the other instance without knowing its password.

If one finds a way to set $_SESSION['user']['username'] by somehow executing some lines of php, another whole PivotX instance is not even necessary (but I'll admit that in this case you are doomed anyway 😅 ).

Aside from the security issue, it also means you have to log out of one instance before using the other instance.
Otherwise the two instances will interfere with each other by login you with the user privileges of the instance you have just used.
You will end up with the wrong user name and settings to publish. So this behavior is also a bug in itself.

Fix

My proposed fix involves isLoggedIn() and function login($username, $password, $stay) we make sure to check $_SESSION user name actually exists in the db, and that the stored password actually matches the $_SESSION password.
This version will log you out if you change your password, which makes more sens as well.
Now we can login on two different instances without conflicts even when the usernames are exactly the same.

I did not push it as far as comparing the settings of the two users to be exactly the same,
from a security standpoint that would be useless, however,
someone using various instances may use the same passwords and username but different settings (pretty niche feature maybe).

// fix and refactoring of 'if' imbrications to avoid unnecessary indentation.
function isLoggedIn() {
    global $PIVOTX;

    $this->loadPermsessions();

    $sessioncookie = (!empty($_COOKIE['pivotxsession'])) ? $_COOKIE['pivotxsession'] : $_POST['pivotxsession'];

    $isTrustedSession = false;
    $hasSession = isset($_SESSION['user']) && isset($_SESSION['user']['username']) && ($_SESSION['user']['username']!="");
    if ( $hasSession ) {
        if ( $PIVOTX['users']->isUser($_SESSION['user']['username']) ) {
            $userInfos = $PIVOTX['users']->getUser($_SESSION['user']['username']);
            if( $userInfos['password'] == $_SESSION['user']['password'] ) {
                $isTrustedSession = true;
            }
        }
    }

    if ( $isTrustedSession )
    {
        // Check if we're in the saved sessions..
        if (!empty($sessioncookie) && !isset($this->permsessions[$sessioncookie])) {
            $this->permsessions[ $sessioncookie ] = array(
                'username' => $_SESSION['user']['username'],
                'ip' => $_SERVER['REMOTE_ADDR'],
                'lastseen' => time(),
                'password' => $_SESSION['user']['password'] // we can trust this password
            );
            $this->savePermsessions();
        }

        return true;
    }

    // See if we can continue a stored session..
    // Check if we have a pivotxsession cookie that matches a saved session..
    if ( empty($sessioncookie) || !isset($this->permsessions[$sessioncookie]) ) {
        return false;
    }

    $savedsess = $this->permsessions[ $sessioncookie ];
    // Check if the IP in the saved session matches the IP of the user..
    if ($_SERVER['REMOTE_ADDR'] != $savedsess['ip']) {
        return false;
    }

    // Check if the 'lastseen' wasn't too long ago..
    if (time() >= ($savedsess['lastseen'] + $this->cookie_lifespan) ) {
        return false;
    }

    // Finally check if the user in the stored session still exists.
    if (!$PIVOTX['users']->isUser($savedsess['username'])) {
        return false;
    }

    // check password
    $userInfos = $PIVOTX['users']->getUser($savedsess['username']);

    // In the off chance that an attacker has managed to aquire the cookie,
    // we still need to check the password.
    // changed password should invalidate the session anyway.
    if( $userInfos['password'] != $savedsess['password'] ) {
        return false;
    }

    // If we get here, we can restore the session!

    $_SESSION['user'] = $userInfos;

    // Update the 'lastseen' in permanent sessions.
    $this->permsessions[ $sessioncookie ]['lastseen'] = time();
    $this->savePermsessions();

    // Add the 'lastseen' to the user settings.
    $PIVOTX['users']->updateUser($savedsess['username'], array('lastseen'=>time()) );
    $_SESSION['user']['lastseen'] = time();

    // Set the session cookie as session variable.
    $_SESSION['pivotxsession'] = $sessioncookie;

    return true;
}

function login($username, $password, $stay) { 
     ...
     ...
    // Only the following needs to be patched:
    // we add the hashed password in the session file for future checks with $_SESSION:
    $this->permsessions[ $key ] = array(
                    'username' => $username,
                    'ip' => $_SERVER['REMOTE_ADDR'],
                    'lastseen' => time(),
                    'password' => $_SESSION['user']['password']
                );
    ...
    ...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions