README.md
Rendering markdown...
#!/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]} <binary> [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()