**Русский** · [English](SECURITY.en.md) # HostinPL 5.6 · Аудит безопасности: что нашли и что исправили Аудит исходников сборки HostinPL 5.6 («nulled»-копия). Проверялось: скрытые закладки и вебшеллы, обфускация, отправка данных на сторонние хосты, SQL-инъекции, RCE, обход авторизации, хранение паролей, установщик и Dockerfile. **Поддерживается и развивается с помощью [REDL.IO](https://redl.io) — Хостинг с искусственным интеллектом.** --- ## Главное в двух словах Форк заявлял: «в панели вырезаны все шеллы/дыры». Проверка показала — **заявление верно лишь частично**. Явных работающих закладок мы не нашли, обфускации в коде панели нет. Но нашлись: * **доказательство, что в этой родословной вебшеллы действительно были** — один найден обезвреженным; * **две SQL-инъекции, доступные без авторизации** — про них в описании форка ничего не сказано; * **пароли пользователей писались в базу в открытом виде**; * установка кода на игровые ноды **по обычному HTTP без проверки подписи**. Первые три пункта мы исправили. Ниже — детали. --- ## 1. Вебшелл в родословной сборки — обезврежен **Файл:** `panel/application/public/js/proxy/proxy.php` — 7 строк целиком: ```php ``` Файл принимает параметр `?cmd=` — это классическая сигнатура вебшелла, принимающего команды. Здесь тело подменено надписью, то есть **конкретно этот шелл обезврежен**. Но сам факт однозначен: в цепочке распространения этой сборки закладки размещались осознанно, и лежала одна из них в папке со скриптами, куда обычно никто не смотрит. **Наше решение:** файл оставлен как есть — как свидетельство. Он ничего не исполняет. Если он вам не нужен, удалите: `rm panel/application/public/js/proxy/proxy.php`. **Вывод:** любая другая копия этой панели, взятая не из этого репозитория, может содержать работающий шелл. Перед установкой чужой сборки проверяйте хотя бы так: ```bash grep -rIn --include=*.php -E "eval\(|assert\(|base64_decode|gzinflate|\\\$_REQUEST\[" . find . -path ./engine/libs -prune -o -name '*.php' -print | grep -E '(assets|public|tmp)/' ``` --- ## 2. Чего не нашли (это хорошая новость) Проверено отдельно, результат отрицательный: * **Обфускации в коде панели нет.** Ноль вхождений `eval`, `assert`, `create_function`, `gzinflate`, `$$`, `$_REQUEST` вне вендорных библиотек. Ни одного длинного base64-блоба. * **Посторонних PHP-файлов нет** в каталогах ассетов, загрузок и временных файлов — кроме описанного `proxy.php`. * **Утечки данных на хосты автора нет.** Упоминания `vipadmin.club` были только в мета-теге keywords и адресе отправителя писем, `osmp.ga` — ссылка в подвале. Никакой отправки паролей, лицензий или данных БД наружу. * **Админ-гейт цел.** Во всех контроллерах `application/controllers/admin/**` есть проверка `getAccessLevel()` (уровень 2 — админка, 3 — игры и локации). Ни одного файла без проверки. * **Панель не вызывает shell напрямую.** Наивный `grep` даёт 54 `exec` и 8 `system`, но все они внутри вендорных библиотек: в phpseclib `exec()` — это метод SSH, а не запуск процесса, остальное в elFinder. Код самой панели shell не вызывает. --- ## 3. Две SQL-инъекции без авторизации — ИСПРАВЛЕНО Самое серьёзное из найденного. В описании форка об этом не сказано ничего. **Файл:** `panel/application/models/users.php`, функция `createAuthLog()`. Функция пишет журнал попыток входа. Было так (значения подставлялись в SQL напрямую): ```php $query=$this->db->query("INSERT INTO `authlog` (... ,`ip`, ... ,`password`) VALUES (NULL, '".$userid."', '".$ip."', ... , '".$password."');"); ``` **Вектор 1 — через форму входа.** `$password` — это сырой пароль из `$_POST`, без экранирования. Вызывается в `account/login.php` при **каждой** попытке входа, в том числе неудачной, то есть **до какой-либо авторизации**. Любой человек с улицы мог инжектить SQL через форму логина. **Вектор 2 — через подделку заголовка.** `$ip` брался из `getRealIpAdress()` (`engine/main/user.php`), а та возвращала заголовок `CF-Connecting-IP` **без всякой проверки** — ранний `return` стоял до блока с `filter_var`: ```php if (!empty($_SERVER["HTTP_CF_CONNECTING_IP"])) { return $_SERVER["HTTP_CF_CONNECTING_IP"]; // как есть, без валидации } ``` Заголовок подставляется клиентом, значит атакующий полностью контролировал значение, попадавшее в SQL. Этот же неproверенный IP уходил в запрос к внешнему сервису `ip-api.com`. **Что сделано:** * все значения в `INSERT` проходят через `$this->db->escape()` либо приводятся к `(int)`; * `CF-Connecting-IP` проверяется через `filter_var($ip, FILTER_VALIDATE_IP)`, иначе игнорируется и берётся `REMOTE_ADDR`; * запрос к `ip-api.com` выполняется только для валидного IP и с `urlencode()`. **Проверено на живой установке:** попытка инъекции через поле пароля и через подделанный заголовок больше не проходит, таблица `authlog` на месте, все 27 таблиц целы. --- ## 4. Пароли в открытом виде — ИСПРАВЛЕНО `account/login.php` передавал в `createAuthLog()` **реальный пароль** из формы: ```php $this->usersModel->createAuthLog($userid['user_id'], $ip, '1', $password); // успешный вход $this->usersModel->createAuthLog($userid['user_id'], $ip, '0', $password); // неудачный тоже ``` То есть таблица `authlog` копила пароли всех пользователей в открытом виде — включая опечатки и пароли от других сервисов, которые люди случайно вводят. Читались они кем угодно с доступом к базе, а с учётом инъекции из пункта 3 — и снаружи. **Что сделано:** пароль больше не передаётся, в колонку пишется пустая строка. Проверено: после успешного и после неудачного входа колонка пустая. --- ## 5. XSS в форме регистрации — ИСПРАВЛЕНО `panel/application/views/common/loginheader.php`: параметр `?ref=` (реферальный код) выводился в скрытое поле формы без экранирования — ``. Добавлен `htmlspecialchars(..., ENT_QUOTES, 'UTF-8')`, в контроллере значение приводится к `(int)`. --- ## 6. Пароли хранятся как MD5 без соли — НЕ исправлено `login.php:114` — `md5($password)`, колонка `user_password varchar(32)`. MD5 без соли перебирается на бытовой видеокарте со скоростью миллиардов хешей в секунду, готовые радужные таблицы для типовых паролей есть в открытом доступе. При утечке базы пароли пользователей считайте раскрытыми. Мы это **не меняли**: переход на `password_hash()` затрагивает вход, регистрацию, восстановление и смену пароля и требует миграции существующих пользователей — это отдельная работа, а не правка одной строки. **Что делать:** если панель используется с реальными людьми — переводите на `password_hash()` с прозрачной миграцией (при успешном входе по старому MD5 пересохранять хеш новым алгоритмом). --- ## 7. Установка кода на игровые ноды по HTTP — источник убран `panel/engine/games/game_settings.php:54-69` — 18 модулей Node.js для RAGE:MP скачиваются **по обычному HTTP** с постороннего хоста `mc.hostinpl.ru`, в виде zip-архивов без подписи и без проверки контрольных сумм. Любой посредник на пути (или владелец того хоста) может подменить содержимое, и код выполнится на игровых нодах. Оригинальный установщик так же тянул сборки игр с `dl.und3x.ru` и `vipadmin.club`. **Что сделано:** из нашего установщика ноды скачивание сборок с этих хостов убрано — сборки вы кладёте сами в `/home/cp/gameservers/files/<код_игры>/`. Строки в `game_settings.php` не тронуты (это функциональность панели), но пользоваться ими не стоит: либо замените адреса на свои по HTTPS, либо разворачивайте модули вручную. Node.js в нашем `docker/Dockerfile` устанавливается из NodeSource **по HTTPS с проверкой ключа репозитория**. --- ## 8. Панель подключается к нодам паролем root — особенность устройства Таблица `locations` хранит `location_user` и `location_password` (`varchar(32)`) **в открытом виде**, подключение идёт через `ssh2_auth_password()`. Команды панели (`useradd`, `docker`, `chown /home`) требуют прав root, то есть на практике панель ходит на ноду root'ом с паролем. Изменить это без переработки панели нельзя: она не умеет ни ключи, ни `sudo`. Меры, которые снижают риск: ```bash # на ноде: SSH и MySQL — только с адреса панели ufw allow from IP_ПАНЕЛИ to any port 22 proto tcp ufw allow from IP_ПАНЕЛИ to any port 3306 proto tcp ufw deny 3306 ``` Установщик ноды печатает эти команды в конце работы. Дополнительно: отдельный пароль на каждую ноду и доступ к базе панели по минимуму — кто прочитает `locations`, получит root на всех нодах. --- ## 9. MariaDB на ноде слушает все интерфейсы — так требует панель Панель создаёт базы игровых серверов на самой ноде и подключается к ним по сети, поэтому `bind-address = 0.0.0.0` обязателен. Установщик так и делает, но **выводит предупреждение** и печатает команды файрвола — в отличие от оригинального, который менял настройку молча. --- ## 10. Регистрационное письмо содержит пароль `panel/application/views/mail/account/register.php` отправляет новому пользователю его пароль в открытом виде в письме. Так задумано в оригинале; мы не меняли, чтобы не ломать сценарий регистрации и активации. Учтите, что письмо остаётся в почтовом ящике и на промежуточных серверах. --- ## Итоговая оценка | Что | Статус | |-----|--------| | Работающие закладки в коде панели | не найдены | | Вебшелл в родословной сборки | найден обезвреженным, оставлен как свидетельство | | Обфускация, утечка данных наружу | не найдены | | Обход админ-авторизации | не найден | | SQL-инъекции без авторизации (2 шт.) | **исправлены** | | Пароли в открытом виде в базе | **исправлено** | | XSS в форме регистрации | **исправлено** | | MD5 без соли | остаётся, требует отдельной работы | | Установка кода по HTTP на ноды | источник убран из установщика | | Root-пароль для доступа к нодам | особенность устройства, закрывается файрволом | | Пароль в письме о регистрации | остаётся | **Для коммерческого хостинга с реальными клиентами и платежами эту панель использовать не стоит** — и из-за правового статуса (см. README), и из-за MD5-паролей, и потому что происхождение кода не заслуживает доверия. Для изучения, внутренних задач и аудита — пригодна. --- **Поддерживается и развивается с помощью [REDL.IO](https://redl.io) — Хостинг с искусственным интеллектом.**