#!/usr/bin/env python3 """ Extract OpenSSL hardcoded paths from Zabbix Agent binaries. Uses strings/radare2 to find OPENSSLDIR, ENGINESDIR, MODULESDIR paths. """ import subprocess import sys import re import json from pathlib import Path def extract_with_strings(binary_path: str) -> dict: """Extract OpenSSL paths using strings command.""" result = { "binary": binary_path, "openssl_version": None, "openssldir": None, "enginesdir": None, "modulesdir": None, "has_conf_modules_load": False, "has_engine_by_id": False, "has_dynamic_path": False, } try: # Run strings on the binary proc = subprocess.run( ["strings", binary_path], capture_output=True, text=True, timeout=60 ) output = proc.stdout # Find OpenSSL version version_match = re.search(r'OpenSSL (\d+\.\d+\.\d+[^\s]*)', output) if version_match: result["openssl_version"] = version_match.group(1) # Find OPENSSLDIR openssldir_match = re.search(r'OPENSSLDIR:\s*"([^"]+)"', output) if openssldir_match: result["openssldir"] = openssldir_match.group(1) # Find ENGINESDIR enginesdir_match = re.search(r'ENGINESDIR:\s*"([^"]+)"', output) if enginesdir_match: result["enginesdir"] = enginesdir_match.group(1) # Find MODULESDIR modulesdir_match = re.search(r'MODULESDIR:\s*"([^"]+)"', output) if modulesdir_match: result["modulesdir"] = modulesdir_match.group(1) # Check for vulnerable functions result["has_conf_modules_load"] = "CONF_modules_load" in output result["has_engine_by_id"] = "ENGINE_by_id" in output result["has_dynamic_path"] = "dynamic_path" in output except subprocess.TimeoutExpired: print(f"Timeout extracting strings from {binary_path}", file=sys.stderr) except Exception as e: print(f"Error: {e}", file=sys.stderr) return result def extract_with_r2(binary_path: str) -> dict: """Extract OpenSSL paths using radare2 for more accurate results.""" result = { "binary": binary_path, "openssl_version": None, "openssldir": None, "openssldir_offset": None, "enginesdir": None, "enginesdir_offset": None, "modulesdir": None, "modulesdir_offset": None, } try: # Use r2 to find strings with offsets proc = subprocess.run( ["r2", "-q", "-c", "izz~OPENSSLDIR", binary_path], capture_output=True, text=True, timeout=120 ) for line in proc.stdout.strip().split('\n'): if 'OPENSSLDIR' in line: # Parse r2 output format: offset file_offset vaddr length type string match = re.search(r'0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+\d+\s+\d+\s+\S+\s+(.+)', line) if match: result["openssldir_offset"] = f"0x{match.group(2)}" full_str = match.group(3) dir_match = re.search(r'OPENSSLDIR:\s*"([^"]+)"', full_str) if dir_match: result["openssldir"] = dir_match.group(1) # Get ENGINESDIR proc = subprocess.run( ["r2", "-q", "-c", "izz~ENGINESDIR", binary_path], capture_output=True, text=True, timeout=120 ) for line in proc.stdout.strip().split('\n'): if 'ENGINESDIR' in line: match = re.search(r'0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+\d+\s+\d+\s+\S+\s+(.+)', line) if match: result["enginesdir_offset"] = f"0x{match.group(2)}" full_str = match.group(3) dir_match = re.search(r'ENGINESDIR:\s*"([^"]+)"', full_str) if dir_match: result["enginesdir"] = dir_match.group(1) # Get MODULESDIR proc = subprocess.run( ["r2", "-q", "-c", "izz~MODULESDIR", binary_path], capture_output=True, text=True, timeout=120 ) for line in proc.stdout.strip().split('\n'): if 'MODULESDIR' in line: match = re.search(r'0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+\d+\s+\d+\s+\S+\s+(.+)', line) if match: result["modulesdir_offset"] = f"0x{match.group(2)}" full_str = match.group(3) dir_match = re.search(r'MODULESDIR:\s*"([^"]+)"', full_str) if dir_match: result["modulesdir"] = dir_match.group(1) # Get OpenSSL version proc = subprocess.run( ["r2", "-q", "-c", "izz~OpenSSL [0-9]", binary_path], capture_output=True, text=True, timeout=120 ) version_match = re.search(r'OpenSSL (\d+\.\d+\.\d+[^\s"]*)', proc.stdout) if version_match: result["openssl_version"] = version_match.group(1) except subprocess.TimeoutExpired: print(f"Timeout running r2 on {binary_path}", file=sys.stderr) except FileNotFoundError: print("radare2 not found, falling back to strings only", file=sys.stderr) except Exception as e: print(f"r2 error: {e}", file=sys.stderr) return result def analyze_vulnerability(result: dict) -> dict: """Analyze if the binary is vulnerable based on extracted paths.""" vuln = { "vulnerable": False, "exploitability": "unknown", "openssl_cnf_path": None, "engine_dll_path": None, "notes": [] } openssldir = result.get("openssldir") enginesdir = result.get("enginesdir") if not openssldir: vuln["notes"].append("No OPENSSLDIR found - may not be statically linked") return vuln # Check if path has proper separators if openssldir and not ('/' in openssldir or '\\' in openssldir): vuln["notes"].append("OPENSSLDIR path appears malformed (no path separators)") vuln["exploitability"] = "unlikely" # Determine openssl.cnf path if openssldir: # Normalize path cnf_path = openssldir.rstrip('/\\') + "/openssl.cnf" vuln["openssl_cnf_path"] = cnf_path if enginesdir: vuln["engine_dll_path"] = enginesdir # Check exploitability based on path location if openssldir: path_lower = openssldir.lower() if "program files" in path_lower: vuln["exploitability"] = "requires_admin" vuln["notes"].append("Path in Program Files - requires admin to exploit") elif "vcpkg" in path_lower: vuln["vulnerable"] = True vuln["exploitability"] = "user_writable" vuln["notes"].append("vcpkg path - likely user-writable on Windows") elif "usr/local" in path_lower: vuln["vulnerable"] = True vuln["exploitability"] = "user_writable" vuln["notes"].append("C:\\usr\\local path - likely user-writable on Windows") elif path_lower.startswith("c:") and "/" in openssldir: vuln["vulnerable"] = True vuln["exploitability"] = "potentially_user_writable" vuln["notes"].append("Non-standard Windows path - check if user-writable") # Check for vulnerable functions if result.get("has_conf_modules_load") and result.get("has_dynamic_path"): vuln["notes"].append("Has CONF_modules_load and dynamic_path - engine loading possible") return vuln def main(): if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} [binary2] ...") print(f" {sys.argv[0]} *.exe") sys.exit(1) binaries = sys.argv[1:] results = [] for binary in binaries: if not Path(binary).exists(): print(f"File not found: {binary}", file=sys.stderr) continue print(f"\n{'='*60}", file=sys.stderr) print(f"Analyzing: {binary}", file=sys.stderr) print(f"{'='*60}", file=sys.stderr) # Try r2 first, fall back to strings result = extract_with_r2(binary) # Supplement with strings for function checks strings_result = extract_with_strings(binary) result["has_conf_modules_load"] = strings_result["has_conf_modules_load"] result["has_engine_by_id"] = strings_result["has_engine_by_id"] result["has_dynamic_path"] = strings_result["has_dynamic_path"] # If r2 didn't find paths, use strings results if not result.get("openssldir") and strings_result.get("openssldir"): result["openssldir"] = strings_result["openssldir"] if not result.get("openssl_version") and strings_result.get("openssl_version"): result["openssl_version"] = strings_result["openssl_version"] # Analyze vulnerability vuln_analysis = analyze_vulnerability(result) result["vulnerability"] = vuln_analysis results.append(result) # Print summary print(f"\nOpenSSL Version: {result.get('openssl_version', 'Unknown')}") print(f"OPENSSLDIR: {result.get('openssldir', 'Not found')}") print(f"ENGINESDIR: {result.get('enginesdir', 'Not found')}") print(f"MODULESDIR: {result.get('modulesdir', 'Not found')}") print(f"\nopenssl.cnf path: {vuln_analysis.get('openssl_cnf_path', 'Unknown')}") print(f"Vulnerable: {vuln_analysis.get('vulnerable')}") print(f"Exploitability: {vuln_analysis.get('exploitability')}") if vuln_analysis.get('notes'): print("Notes:") for note in vuln_analysis['notes']: print(f" - {note}") # Output JSON for programmatic use print("\n\n--- JSON Output ---") print(json.dumps(results, indent=2)) if __name__ == "__main__": main()