CVE-2022-31888 (published 2023-04-05T22:15Z)

Session Fixation vulnerability in in function login in class.auth.php in osTicket through 1.16.2.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-384
cwe_name: Session Fixation
description: Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
extended_description: 
            Such a scenario is commonly observed when:
            
               A web application authenticates a user without first invalidating the existing session, thereby continuing to use the session already associated with the user.
               An attacker is able to force a known session identifier on a user so that, once the user authenticates, the attacker has access to the authenticated session.
               The application or container uses predictable session identifiers.
	    
	    In the generic exploit of session fixation vulnerabilities, an attacker creates a new session on a web application and records the associated session identifier. The attacker then causes the victim to associate, and possibly authenticate, against the server using that session identifier, giving the attacker access to the user's account through the active session.
         
         

Fix information:
repo_url: https://github.com/osTicket/osTicket
commit_details: repo_url: https://github.com/osTicket/osTicket
author: Peter Rotich
author_date: 2022-04-24 20:49:20+00:00
msg: Security: Session Fixation

This commit addresses possible session fixation on both agent and end user
login. Upon login users are now issued new session with the old one
invalidated.
repository: repo_url: https://github.com/osTicket/osTicket
repo_name: osTicket/osTicket
description: The osTicket open source ticketing system official project repository, for versions 1.8 and later
repo_language: PHP
owner: osTicket
file_changes: filename: class.auth.php
change_type: ModificationType.MODIFY
diff: @@ -538,6 +538,8 @@ abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
             || !($authkey=$bk->getAuthKey($staff)))
             return false;
 
+        // Regenerate session id and refresh (avoid session fixation)
+        $staff->regenerateSession();
         //Log debug info.
         $ost->logDebug(_S('Agent Login'),
             sprintf(_S("%s logged in [%s], via %s"), $staff->getUserName(),
@@ -563,9 +565,10 @@ abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
         $authsession['id'] = $staff->getId();
         $authsession['key'] =  $authkey;
         $authsession['2fa'] =  $auth2fa;
-
+        // Set session token
+        $staff->setSessionToken();
+        // Set Auth Key
         $staff->setAuthKey($authkey);
-        $staff->refreshSession(true); //set the hash.
         Signal::send('auth.login.succeeded', $staff);
 
         if ($bk->supportsInteractiveAuthentication())
@@ -755,15 +758,15 @@ abstract class UserAuthenticationBackend  extends AuthenticationBackend {
                 throw new AccessDenied(__('Account is administratively locked'));
         }
 
+        // Regenerate session id and refresh (avoid session fixation)
+        $user->regenerateSession();
         // Tag the user and associated ticket in the SESSION
         $this->setAuthKey($user, $bk, $authkey);
-
+        // Set Session Token
+        $user->setSessionToken();
         //The backend used decides the format of the auth key.
         // XXX: encrypt to hide the bk??
         $user->setAuthKey($authkey);
-
-        $user->refreshSession(true); //set the hash.
-
         //Log login info...
         $msg=sprintf(_S('%1$s (%2$s) logged in [%3$s]'
                 /* Tokens are <username>, <id>, and <ip> */),

programming_language: PHP
filename: class.usersession.php
change_type: ModificationType.MODIFY
diff: @@ -17,137 +17,163 @@
 include_once(INCLUDE_DIR.'class.client.php');
 include_once(INCLUDE_DIR.'class.staff.php');
 
-
 class UserSession {
    var $session_id = '';
-   var $userID='';
+   var $userID = 0;
    var $browser = '';
    var $ip = '';
-   var $validated=FALSE;
-
-   function __construct($userid){
+   var $validated = false;
 
-      $this->browser=(!empty($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : $_ENV['HTTP_USER_AGENT'];
-      $this->ip=(!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : getenv('REMOTE_ADDR');
-      $this->session_id=session_id();
-      $this->userID=$userid;
+   function __construct($userid) {
+      $this->browser = (!empty($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : $_ENV['HTTP_USER_AGENT'];
+      $this->ip = (!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : getenv('REMOTE_ADDR');
+      $this->session_id = session_id();
+      $this->userID = $userid;
    }
 
-   function isStaff(){
-       return FALSE;
+   function isStaff() {
+       return false;
    }
 
    function isClient() {
-       return FALSE;
+       return false;
    }
 
-
-   function getSessionId(){
+   function getSessionId() {
        return $this->session_id;
    }
 
-   function getIP(){
+   function getIP() {
         return  $this->ip;
    }
 
-   function getBrowser(){
+   function getBrowser() {
        return $this->browser;
    }
-   function refreshSession(){
-       //nothing to do...clients need to worry about it.
-   }
 
    function sessionToken(){
-
       $time  = time();
       $hash  = md5($time.SESSION_SECRET.$this->userID);
       $token = "$hash:$time:".MD5($this->ip);
-
-      return($token);
+      return $token;
    }
 
    function getLastUpdate($htoken) {
        if (!$htoken)
            return 0;
 
-       @list($hash,$expire,$ip)=explode(":",$htoken);
+       @list($hash, $expire, $ip) = explode(":", $htoken);
        return $expire;
    }
 
    function isvalidSession($htoken,$maxidletime=0,$checkip=false){
-        global $cfg;
+        global $cfgi;
 
-        $token = rawurldecode($htoken);
+        // Compare session ids
+        if (strcmp($this->getSessionId(), session_id()))
+            return false;
 
-        #check if we got what we expected....
-        if($token && !strstr($token,":"))
-            return FALSE;
+        // Is the session invalidated?
+        if (isset($_SESSION['KAPUT']) &&  $_SESSION['KAPUT'] < time())
+            return (session_destroy() && false);
 
-        #get the goodies
-        list($hash,$expire,$ip)=explode(":",$token);
+        $token = rawurldecode($htoken);
+        // Check if we got what we expected....
+        if ($token && !strstr($token,":"))
+            return false;
 
-        #Make sure the session hash is valid
-        if((md5($expire . SESSION_SECRET . $this->userID)!=$hash)){
-            return FALSE;
-        }
-        #is it expired??
+        // Get the goodies
+        list($hash, $expire, $ip) = explode(":",$token);
 
+        // Make sure the session hash is valid
+        if ((md5($expire . SESSION_SECRET . $this->userID) != $hash))
+            return false;
+
+        // is it expired??
+        if ($maxidletime && ((time()-$expire)>$maxidletime))
+            return false;
 
-        if($maxidletime && ((time()-$expire)>$maxidletime)){
-            return FALSE;
-        }
         #Make sure IP is still same ( proxy access??????)
-        if($checkip && strcmp($ip, MD5($this->ip)))
-            return FALSE;
+        if ($checkip && strcmp($ip, MD5($this->ip)))
+            return false;
+
+        $this->validated = true;
 
-        $this->validated=TRUE;
+        return true;
+   }
+
+   function regenerateSession($destroy=false) {
+       // Delayed kaput time for current session
+       $_SESSION['KAPUT'] = time() + 60;
+       // Save the session id as old
+       $old = session_id();
+       // Regenerate the session without destroying data
+       session_regenerate_id(false);
+       // Get new session id and close
+       $new = session_id();
+       session_write_close();
+       // Start new session
+       session_id($new);
+       session_start();
+       $this->session_id  = $new;
+       // Make sure new session is not set to KAPUT
+       unset($_SESSION['KAPUT']);
+       // Destroy ?
+       if ($destroy) {
+           // Destrory old session
+           $this->destroySession($old);
+           // Restore new session
+           session_id($new);
+           session_start();
+       }
+       return true;
+   }
 
-        return TRUE;
+   function destroySession($id) {
+       // Close current session
+       session_write_close();
+       // Start target session
+       session_id($id);
+       session_start();
+       // Destroy session
+       session_destroy();
+       session_write_close();
+       return true;
    }
 
    function isValid() {
-        return FALSE;
+        return  ($this->validated);
    }
 
 }
 
-class ClientSession extends EndUser {
 
+trait UserSessionTrait {
+    // Session Object
     var $session;
+    // Session Token
     var $token;
-
-    function __construct($user) {
-        parent::__construct($user);
-        $this->token = &$_SESSION[':token']['client'];
-        // XXX: Change the key to user-id
-        $this->session= new UserSession($user->getId());
-    }
-
-    function getSessionUser() {
-        return $this->user;
-    }
-
-    function isValid(){
-        global $_SESSION,$cfg;
-
-        if(!$this->getId() || $this->session->getSessionId()!=session_id())
-            return false;
-
-        return $this->session->isvalidSession($this->token,$cfg->getClientTimeout(),false)?true:false;
-    }
-
-    function refreshSession($force=false){
-        global $cfg;
-
+    // Maximum idle time before session is considered invalid
+    var $maxidletime = 0;
+    // Indicates if session is bound to the IP address
+    var $checkip = false;
+    // User class
+    var $class = '';
+
+    function refreshSession($force=false) {
         $time = $this->session->getLastUpdate($this->token);
         // Deadband session token updates to once / 30-seconds
         if (!$force && time() - $time < 30)
             return;
 
         $this->token = $this->getSessionToken();
-        //TODO: separate expire time from hash??
+        osTicketSession::renewCookie($time, $this->maxidletime);
+    }
 
-        osTicketSession::renewCookie($time, $cfg->getClientSessionTimeout());
+    function regenerateSession($destroy=false) {
+        $this->session->regenerateSession($destroy);
+        // Set cookie for the new session id.
+        $this->refreshSession(true);
     }
 
     function getSession() {
@@ -158,75 +184,76 @@ class ClientSession extends EndUser {
         return $this->session->sessionToken();
     }
 
-    function getIP(){
+    function setSessionToken($token=null) {
+        // Assign memory to token variable
+        $this->token = &$_SESSION[':token'][$this->class];
+        // Set token
+        $this->token = $token ?: $this->getSessionToken();
+    }
+
+    function getIP() {
         return $this->session->getIP();
     }
+
+    function isValidSession() {
+        return ($this->getId()
+                && $this->session->isvalidSession($this->token,
+                    $this->maxidletime, $this->checkip));
+    }
+
+    abstract function isValid();
 }
 
+class ClientSession extends EndUser {
+    use UserSessionTrait;
 
-class StaffSession extends Staff {
+    function __construct($user) {
+        global $cfg;
+        parent::__construct($user);
+        $this->class ='client';
+        // XXX: Change the key to user-id
+        $this->session = new UserSession($user->getId());
+        $this->setSessionToken();
+        $this->maxidletime = $cfg->getClientTimeout();
+    }
 
-    var $session;
-    var $token;
+    function getSessionUser() {
+        return $this->user;
+    }
+
+    function isValid() {
+        return $this->isValidSession();
+    }
+}
+
+class StaffSession extends Staff {
+    use UserSessionTrait;
 
     static function lookup($var) {
-        if ($staff = parent::lookup($var)) {
-            $staff->token = &$_SESSION[':token']['staff'];
-            $staff->session= new UserSession($staff->getId());
+        global $cfg;
+        if (($staff = parent::lookup($var))) {
+            $staff->class = 'staff';
+            $staff->session = new UserSession($staff->getId());
+            $staff->setSessionToken();
+            $staff->maxidletime = $cfg->getStaffTimeout();
+            $staff->checkip = $cfg->enableStaffIPBinding();
         }
         return $staff;
     }
 
     function clear2FA() {
+        unset($_SESSION['_auth']['staff']['2fa']);
         $_SESSION['_auth']['staff']['2fa'] = null;
         return true;
     }
 
     // If 2fa is set then it means it's pending
     function is2FAPending() {
-        if (!isset($_SESSION['_auth']['staff']['2fa']))
-            return false;
-
-        return true;
-    }
-
-    function isValid(){
-        global $cfg;
-
-        if(!$this->getId() || $this->session->getSessionId()!=session_id())
-            return false;
-
-        if ($this->is2FAPending())
-            return false;
-
-        return $this->session->isvalidSession($this->token,$cfg->getStaffTimeout(),$cfg->enableStaffIPBinding())?true:false;
-    }
-
-    function refreshSession($force=false){
-        global $cfg;
-
-        $time = $this->session->getLastUpdate($this->token);
-        // Deadband session token updates to once / 30-seconds
-        if (!$force && time() - $time < 30)
-            return;
-
-        $this->token=$this->getSessionToken();
-
-        osTicketSession::renewCookie($time, $cfg->getStaffSessionTimeout());
-    }
-
-    function getSession() {
-        return $this->session;
-    }
-
-    function getSessionToken() {
-        return $this->session->sessionToken();
+        return isset($_SESSION['_auth']['staff']['2fa']);
     }
 
-    function getIP(){
-        return $this->session->getIP();
+    function isValid() {
+        return (!$this->is2FAPending() && $this->isValidSession());
     }
-
 }
-
 ?>

programming_language: PHP
filename: open.php
change_type: ModificationType.MODIFY
diff: @@ -47,9 +47,9 @@ if ($_POST) {
         // Drop session-backed form data
         unset($_SESSION[':form-data']);
         //Logged in...simply view the newly created ticket.
-        if($thisclient && $thisclient->isValid()) {
-            session_regenerate_id();
-            session_write_close();
+        if ($thisclient && $thisclient->isValid()) {
+            // Regenerate session id
+            $thisclient->regenerateSession();
             @header('Location: tickets.php?id='.$ticket->getId());
         } else
             $ost->getCSRF()->rotate();

programming_language: PHP