4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / extract_openssl_paths.py PY
#!/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()