**Русский** · [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
<?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) — Хостинг с искусственным интеллектом.**
