- панель работает на PHP 8.4 (оригинал под PHP 7.0): elFinder utf8_encode, apache_get_modules, warnings - капча выключена и управляется из админки (флаг captcha_enable + блок настроек) - закрыты две SQL-инъекции без авторизации (createAuthLog: пароль из POST и CF-Connecting-IP) - убрано хранение паролей в открытом виде в authlog, XSS в поле ref - установщики install-panel.sh и install-node.sh (оригинальный затирал sources.list репозиториями Debian 9) - современный Dockerfile (оригинальный на debian:stretch больше не собирается) - планировщик и автозапуск работают без systemd - брендинг REDL.IO, год 2026, ссылки на redl.io - документация на русском: ИЗМЕНЕНИЯ, БЕЗОПАСНОСТЬ, ИНСТРУКЦИЯ
242 lines
16 KiB
Markdown
242 lines
16 KiB
Markdown
# Аудит безопасности: что нашли и что исправили
|
||
|
||
Аудит исходников сборки HostinPL 5.6 («nulled»-копия). Проверялось: скрытые закладки и вебшеллы,
|
||
обфускация, отправка данных на сторонние хосты, SQL-инъекции, RCE, обход авторизации, хранение
|
||
паролей, установщик и Dockerfile.
|
||
|
||
**Поддерживается и развивается с помощью [REDL.IO](https://redl.io) — Хостинг с искусственным интеллектом.**
|
||
|
||
---
|
||
|
||
## Главное в двух словах
|
||
|
||
Форк заявлял: «в панели вырезаны все шеллы/дыры». Проверка показала — **заявление верно лишь
|
||
частично**. Явных работающих закладок мы не нашли, обфускации в коде панели нет. Но нашлись:
|
||
|
||
* **доказательство, что в этой родословной вебшеллы действительно были** — один найден обезвреженным;
|
||
* **две SQL-инъекции, доступные без авторизации** — про них в описании форка ничего не сказано;
|
||
* **пароли пользователей писались в базу в открытом виде**;
|
||
* установка кода на игровые ноды **по обычному HTTP без проверки подписи**.
|
||
|
||
Первые три пункта мы исправили. Ниже — детали.
|
||
|
||
---
|
||
|
||
## 1. Вебшелл в родословной сборки — обезврежен
|
||
|
||
**Файл:** `panel/application/public/js/proxy/proxy.php` — 7 строк целиком:
|
||
|
||
```php
|
||
<?php
|
||
if(isset($_GET['cmd'])){
|
||
print('Backdoor fixed by Xopowblu-4EJlOBEK aka Und3X (und3x.ru)');
|
||
}else{
|
||
header("Location: /");
|
||
}
|
||
?>
|
||
```
|
||
|
||
Файл принимает параметр `?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=` (реферальный код) выводился
|
||
в скрытое поле формы без экранирования — `<?echo $_GET['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) — Хостинг с искусственным интеллектом.**
|