Files
feadulta/scripts/cutover/post-import-seguridad.sh
T
rafa 0ee9a2eef5 cutover: retirar en cada import la inyeccion de SEO spam del #183
Buscando si Wordfence habia que reinstalarlo limpio (#183 comment-492) resulta
que Wordfence NO esta instalado en produccion. Lo que si queda es una opcion
llamada 'wordfence_core_options' que no es suya: es el payload del atacante,
disfrazado con ese nombre para pasar por legitimo.

Cifrado con un cesar de una letra (ejw->div, tuzmf->style, isfg->href). Una vez
descifrado:

  <div style="position: absolute;margin-top: -110px;">
    <div style="position: absolute; left: -7585px;">
      <a href="https://1xbetjap.com/ja/casino/">

Un div fuera de pantalla con enlaces a un casino japones, con el campo output
apuntando a wp_footer. Esto explica los "dos POST a wp-admin/options.php" del
comment-492, que se habian atribuido a que el atacante toco la configuracion de
Wordfence: lo que hacia era crear esta opcion.

Hoy esta inerte porque quien la leia era el plugin wp-highlits, en cuarentena.
Pero viaja dentro del dump, asi que CADA import la reintroduce en el servidor
nuevo: por eso va aqui y no como limpieza manual de una vez.

Se preserva el valor en /data/feadulta-migracion/evidencia-183/ antes de borrar.

Barrido de la BD del destino: es la unica. 0 en post_content, 0 enlaces ocultos
fuera de pantalla en el contenido, 0 referencias a las IPs del atacante.

Nota sobre el escaneo previo del dump: lo di por limpio buscando <script, eval(,
base64_decode y gzinflate. Este payload no lleva ninguno de esos patrones y paso
el filtro. El escaneo por firmas conocidas no acredita limpieza.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:34:12 -04:00

106 lines
5.1 KiB
Bash

#!/bin/bash
#
# Higiene de seguridad posterior a importar la base de datos en el servidor nuevo.
# Parte del cutover CDMON -> Hetzner (gitea rafa/feadulta #180, criterio del comment-516).
#
# SE EJECUTA DESPUES DE CADA IMPORT. El dump trae las cuentas de administrador de
# produccion con sus hashes, incluidas las tres deshabilitadas durante el incidente #183,
# asi que reimportar las revive. Por eso esto es un script y no una tarea manual.
#
# Lee las contrasenas de ~/.hermes/profiles/feadulta/.env (claves FEA_WP_PASS_*).
# Nunca las imprime ni las pasa por argv: viajan por variable de entorno del proceso.
#
# Uso: bash post-import-seguridad.sh [--dry-run]
#
set -euo pipefail
DRY=0
[ "${1:-}" = "--dry-run" ] && DRY=1
ENVF=~/.hermes/profiles/feadulta/.env
[ -f "$ENVF" ] || { echo "Falta $ENVF"; exit 1; }
set -a; . "$ENVF"; set +a
SRV=root@188.40.120.157
U=r2ssjifwj0r0ghyoqd528uaa # servicio feadulta-wp en Coolify
WPC=wordpress-$U
# usuario:ID:variable_de_password
ADMINS_OK="eqpyk:1:FEA_WP_PASS_EQPYK icalvotorre:1048:FEA_WP_PASS_ICALVOTORRE calvo:396:FEA_WP_PASS_CALVO"
ADMINS_KO="pabloarias:1047:FEA_WP_PASS_PABLOARIAS josek:1049:FEA_WP_PASS_JOSEK andrey:1087:FEA_WP_PASS_ANDREY"
wp() { ssh -o BatchMode=yes "$SRV" "docker exec -u www-data $WPC php wp-content/wp-cli.phar --path=/var/www/html --skip-plugins --skip-themes $*" 2>/dev/null; }
echo "=== 1. Rotar la contrasena de los 6 administradores ==="
for entry in $ADMINS_OK $ADMINS_KO; do
login=${entry%%:*}; rest=${entry#*:}; id=${rest%%:*}; var=${rest##*:}
pass="${!var:-}"
[ -n "$pass" ] || { echo " $login: falta $var en el .env"; exit 1; }
if [ $DRY -eq 1 ]; then
echo " [dry-run] rotaria la contrasena de $login (ID $id)"
else
ssh -o BatchMode=yes "$SRV" \
"docker exec -e NEWPASS=\"$pass\" -u www-data $WPC php wp-content/wp-cli.phar --path=/var/www/html --skip-plugins --skip-themes eval 'wp_set_password(getenv(\"NEWPASS\"), $id);'" 2>/dev/null
echo " $login (ID $id): contrasena rotada"
fi
done
echo ""
echo "=== 2. Deshabilitar las tres cuentas del incidente #183 ==="
echo " (doble cinturon: se les quita el rol Y el mu-plugin fea-security-blocked-users.php"
echo " bloquea wp_authenticate_user y allow_password_reset para estos mismos IDs)"
for entry in $ADMINS_KO; do
login=${entry%%:*}; rest=${entry#*:}; id=${rest%%:*}
if [ $DRY -eq 1 ]; then
echo " [dry-run] quitaria el rol administrator a $login (ID $id)"
else
wp user remove-role "$id" administrator >/dev/null || true
echo " $login (ID $id): rol administrator retirado"
fi
done
echo ""
echo "=== 3. Limpiar los cron huerfanos de plugins ya desinstalados ==="
for hook in wpseo-reindex wpseo_permalink_structure_check wppb_review_check cozmos_wppb_plugin_optin_sync; do
if [ $DRY -eq 1 ]; then
echo " [dry-run] borraria el evento $hook"
else
out=$(wp cron event delete "$hook" 2>&1 || true)
echo " $hook: ${out:-sin eventos}"
fi
done
echo ""
echo "=== 3b. Retirar la inyeccion de SEO spam del incidente #183 ==="
# La opcion 'wordfence_core_options' NO es de Wordfence (que ni siquiera esta instalado):
# es el payload que dejo el atacante, cifrado con un cesar de una letra, con un div oculto
# (left:-7585px) y enlaces a un casino japones, enganchado a wp_footer. Hoy esta inerte
# porque quien lo leia era el plugin wp-highlits, en cuarentena. Pero VIAJA EN EL DUMP,
# asi que cada import lo reintroduce. Se preserva antes de borrar (evidencia del #183).
SPAMOPT=wordfence_core_options
if [ $DRY -eq 1 ]; then
echo " [dry-run] preservaria y borraria la opcion $SPAMOPT"
else
ssh -o BatchMode=yes "$SRV" "bash -s" <<REMOTE
RP=\$(docker inspect mysql-$U --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^MYSQL_ROOT_PASSWORD=//p')
m() { docker exec mysql-$U mysql --default-character-set=utf8mb4 -uroot -p"\$RP" -N -e "\$1" wordpress 2>/dev/null; }
EV=/data/feadulta-migracion/evidencia-183; mkdir -p "\$EV"
m "SELECT option_value FROM wp_options WHERE option_name='$SPAMOPT';" > "\$EV/${SPAMOPT}.\$(date +%Y%m%d%H%M%S).txt"
m "DELETE FROM wp_options WHERE option_name='$SPAMOPT';"
n=\$(m "SELECT COUNT(*) FROM wp_options WHERE option_name='$SPAMOPT';")
echo " $SPAMOPT: preservada y borrada (quedan \$n)"
REMOTE
fi
echo ""
echo "=== 4. Verificacion ==="
echo " administradores que quedan:"
wp user list --role=administrator --fields=ID,user_login,user_email --format=csv | sed 's/^/ /'
echo " el mu-plugin de bloqueo esta presente?:"
ssh -o BatchMode=yes "$SRV" "docker exec $WPC test -f /var/www/html/wp-content/mu-plugins/fea-security-blocked-users.php && echo ' SI' || echo ' NO -- FALTA, las cuentas del incidente quedarian solo con el rol retirado'"
echo ""
echo " application passwords existentes (NO se tocan: las usan los bots de la carta;"
echo " su rotacion se coordina con Inma):"
wp db query "SELECT u.user_login, LENGTH(m.meta_value) AS bytes FROM wp_usermeta m JOIN wp_users u ON u.ID=m.user_id WHERE m.meta_key='_application_passwords';" 2>/dev/null | sed 's/^/ /' || \
echo " (wp db query no disponible; consultar por SQL directo)"