Панель: - engine/main/lang.php: класс Lang, автоопределение языка (?lang= -> cookie -> Accept-Language -> конфиг) - перевод применяется к готовому ответу через ob_start(): покрывает всю панель, админку и письма, не требуя правки 200 файлов шаблонов; отсутствующая фраза остаётся русской - замена только на границах слов, иначе короткий ключ портил длинные слова (Модуль -> Modуль) - AJAX-ответы переводятся отдельно (json_encode экранирует кириллицу в \uXXXX) - application/lang/en.php: 657 переводов; ru.php как точка расширения - переключатель RU/EN в шапке кабинета, админки и в подвале страницы входа - 'lang' в config.php — язык по умолчанию - проверено обходом 20 разделов: 0 непереведённых фраз, 0 мешанины языков, 0 фаталов Документация — теперь на русском и английском: - README.en.md, CHANGES.en.md, SECURITY.en.md, GUIDE.en.md - переключатели языка в начале каждого документа - раздел «Язык интерфейса» в инструкции: как работает, как добавить свой язык
244 lines
10 KiB
Markdown
244 lines
10 KiB
Markdown
[Русский](БЕЗОПАСНОСТЬ.md) · **English**
|
|
|
|
# Security audit: what we found and what we fixed
|
|
|
|
An audit of the HostinPL 5.6 build (a "nulled" copy). We looked for hidden backdoors and webshells,
|
|
obfuscation, data being sent to third-party hosts, SQL injections, RCE, authorisation bypasses,
|
|
password storage, and problems in the installer and the Dockerfile.
|
|
|
|
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
|
|
|
|
---
|
|
|
|
## The short version
|
|
|
|
The fork claimed: "all shells/holes have been cut out of the panel". Our check shows that claim is
|
|
**only partly true**. We found no working backdoors, and there is no obfuscation in the panel code.
|
|
But we did find:
|
|
|
|
* **proof that webshells really were present in this lineage** — one was found, already defanged;
|
|
* **two SQL injections reachable without authentication** — not mentioned anywhere in the fork's description;
|
|
* **user passwords written to the database in plaintext**;
|
|
* code being installed onto game nodes **over plain HTTP with no signature verification**.
|
|
|
|
We fixed the first three. Details below.
|
|
|
|
---
|
|
|
|
## 1. A webshell in the build's lineage — defanged
|
|
|
|
**File:** `panel/application/public/js/proxy/proxy.php` — all 7 lines of it:
|
|
|
|
```php
|
|
<?php
|
|
if(isset($_GET['cmd'])){
|
|
print('Backdoor fixed by Xopowblu-4EJlOBEK aka Und3X (und3x.ru)');
|
|
}else{
|
|
header("Location: /");
|
|
}
|
|
?>
|
|
```
|
|
|
|
The file takes a `?cmd=` parameter — the classic signature of a command-accepting webshell. Here the
|
|
body has been replaced with a message, so **this particular shell is defanged**. But the implication
|
|
is unambiguous: backdoors were planted deliberately in this build's distribution chain, and one of
|
|
them sat in a scripts folder nobody normally looks at.
|
|
|
|
**Our decision:** the file is kept as evidence. It executes nothing. If you do not want it:
|
|
`rm panel/application/public/js/proxy/proxy.php`.
|
|
|
|
**Takeaway:** any other copy of this panel obtained anywhere but this repository may contain a
|
|
*working* shell. Before installing someone else's build, check at least this much:
|
|
|
|
```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. What we did not find (the good news)
|
|
|
|
Checked specifically, result negative:
|
|
|
|
* **No obfuscation in the panel code.** Zero occurrences of `eval`, `assert`, `create_function`,
|
|
`gzinflate`, `$$` or `$_REQUEST` outside vendor libraries. Not a single long base64 blob.
|
|
* **No stray PHP files** in the asset, upload or temporary directories — apart from the `proxy.php`
|
|
described above.
|
|
* **No data leaking to the author's hosts.** `vipadmin.club` appeared only in the keywords meta tag
|
|
and the mail sender address; `osmp.ga` was a footer link. Nothing sends passwords, licence data or
|
|
database credentials anywhere.
|
|
* **The admin gate is intact.** Every controller under `application/controllers/admin/**` performs a
|
|
`getAccessLevel()` check (level 2 for the admin area, 3 for games and locations). Not one file
|
|
without it.
|
|
* **The panel never invokes a shell directly.** A naive `grep` reports 54 `exec` and 8 `system`, but
|
|
all of them are inside vendor libraries: in phpseclib, `exec()` is an SSH method rather than a
|
|
process launcher, and the rest is in elFinder. The panel's own code calls no shell.
|
|
|
|
---
|
|
|
|
## 3. Two pre-authentication SQL injections — FIXED
|
|
|
|
The most serious finding, and one the fork's description says nothing about.
|
|
|
|
**File:** `panel/application/models/users.php`, function `createAuthLog()`.
|
|
|
|
The function records login attempts. It used to interpolate values straight into SQL:
|
|
|
|
```php
|
|
$query=$this->db->query("INSERT INTO `authlog` (... ,`ip`, ... ,`password`)
|
|
VALUES (NULL, '".$userid."', '".$ip."', ... , '".$password."');");
|
|
```
|
|
|
|
**Vector 1 — via the login form.** `$password` is the raw password from `$_POST`, unescaped. It is
|
|
called from `account/login.php` on **every** login attempt, including failed ones — that is, **before
|
|
any authentication**. Anybody off the street could inject SQL through the login form.
|
|
|
|
**Vector 2 — via a forged header.** `$ip` came from `getRealIpAdress()` (`engine/main/user.php`),
|
|
which returned the `CF-Connecting-IP` header **with no validation at all** — the early `return` sat
|
|
before the `filter_var` block:
|
|
|
|
```php
|
|
if (!empty($_SERVER["HTTP_CF_CONNECTING_IP"])) {
|
|
return $_SERVER["HTTP_CF_CONNECTING_IP"]; // as-is, unvalidated
|
|
}
|
|
```
|
|
|
|
That header is supplied by the client, so an attacker fully controlled the value reaching SQL.
|
|
The same unvalidated IP was also sent to the external `ip-api.com` service.
|
|
|
|
**What we did:**
|
|
|
|
* every value in the `INSERT` now goes through `$this->db->escape()` or is cast to `(int)`;
|
|
* `CF-Connecting-IP` is validated with `filter_var($ip, FILTER_VALIDATE_IP)` and otherwise ignored
|
|
in favour of `REMOTE_ADDR`;
|
|
* the `ip-api.com` request only runs for a valid IP and uses `urlencode()`.
|
|
|
|
**Verified on a live installation:** injection attempts through the password field and through a
|
|
forged header no longer get through, the `authlog` table is intact, and all 27 tables are present.
|
|
|
|
---
|
|
|
|
## 4. Plaintext passwords — FIXED
|
|
|
|
`account/login.php` passed the **real password** from the form into `createAuthLog()`:
|
|
|
|
```php
|
|
$this->usersModel->createAuthLog($userid['user_id'], $ip, '1', $password); // successful login
|
|
$this->usersModel->createAuthLog($userid['user_id'], $ip, '0', $password); // and failed ones too
|
|
```
|
|
|
|
So the `authlog` table accumulated every user's password in plaintext — including typos and
|
|
passwords for other services that people enter by accident. Anyone with database access could read
|
|
them, and given the injection in section 3, so could an outsider.
|
|
|
|
**What we did:** the password is no longer passed; an empty string is stored instead.
|
|
Verified: after both a successful and a failed login, the column is empty.
|
|
|
|
---
|
|
|
|
## 5. XSS in the registration form — FIXED
|
|
|
|
`panel/application/views/common/loginheader.php`: the `?ref=` parameter (referral code) was echoed
|
|
into a hidden form field without escaping — `<?echo $_GET['ref']?>`.
|
|
`htmlspecialchars(..., ENT_QUOTES, 'UTF-8')` was added, and the controller casts the value to `(int)`.
|
|
|
|
---
|
|
|
|
## 6. Passwords stored as unsalted MD5 — NOT fixed
|
|
|
|
`login.php:114` — `md5($password)`, column `user_password varchar(32)`.
|
|
|
|
Unsalted MD5 is brute-forced at billions of hashes per second on a consumer GPU, and rainbow tables
|
|
for common passwords are freely available. If the database leaks, treat every user password as known.
|
|
|
|
We **did not change this**: moving to `password_hash()` touches login, registration, recovery and
|
|
password changes, and requires migrating existing users — that is a project, not a one-line edit.
|
|
|
|
**What to do:** if the panel is used with real people, migrate to `password_hash()` transparently —
|
|
when a user logs in successfully against the old MD5 hash, re-save it with the new algorithm.
|
|
|
|
---
|
|
|
|
## 7. Code installed onto game nodes over HTTP — source removed
|
|
|
|
`panel/engine/games/game_settings.php:54-69` — 18 Node.js modules for RAGE:MP are downloaded
|
|
**over plain HTTP** from a third-party host, `mc.hostinpl.ru`, as zip archives with no signature and
|
|
no checksum verification.
|
|
|
|
Any intermediary on the path — or the owner of that host — can replace the contents, and the code
|
|
then runs on your game nodes. The original installer likewise pulled game server builds from
|
|
`dl.und3x.ru` and `vipadmin.club`.
|
|
|
|
**What we did:** our node installer no longer downloads builds from those hosts — you place the
|
|
files into `/home/cp/gameservers/files/<game_code>/` yourself. The lines in `game_settings.php` were
|
|
left alone (that is panel functionality), but you should not rely on them: either replace the URLs
|
|
with your own over HTTPS, or deploy the modules by hand.
|
|
|
|
Node.js in our `docker/Dockerfile` is installed from NodeSource **over HTTPS with repository key
|
|
verification**.
|
|
|
|
---
|
|
|
|
## 8. The panel connects to nodes with the root password — by design
|
|
|
|
The `locations` table stores `location_user` and `location_password` (`varchar(32)`) **in plaintext**,
|
|
and the connection uses `ssh2_auth_password()`. The panel's commands (`useradd`, `docker`,
|
|
`chown /home`) require root, so in practice the panel logs into the node as root with a password.
|
|
|
|
This cannot be changed without reworking the panel: it supports neither keys nor `sudo`. Measures
|
|
that reduce the risk:
|
|
|
|
```bash
|
|
# on the node: SSH and MySQL reachable only from the panel's address
|
|
ufw allow from PANEL_IP to any port 22 proto tcp
|
|
ufw allow from PANEL_IP to any port 3306 proto tcp
|
|
ufw deny 3306
|
|
```
|
|
|
|
The node installer prints these commands when it finishes. Additionally: use a separate password per
|
|
node, and keep access to the panel's database to a minimum — whoever reads `locations` gets root on
|
|
every node.
|
|
|
|
---
|
|
|
|
## 9. MariaDB on the node listens on all interfaces — the panel requires it
|
|
|
|
The panel creates game server databases on the node itself and connects to them over the network,
|
|
so `bind-address = 0.0.0.0` is mandatory. Our installer does that but **prints a warning** and the
|
|
firewall commands — unlike the original, which changed the setting silently.
|
|
|
|
---
|
|
|
|
## 10. The registration e-mail contains the password
|
|
|
|
`panel/application/views/mail/account/register.php` sends the new user their password in plaintext by
|
|
e-mail. That is the original design; we did not change it so as not to break the registration and
|
|
activation flow. Bear in mind the message stays in the mailbox and on intermediate servers.
|
|
|
|
---
|
|
|
|
## Overall assessment
|
|
|
|
| Item | Status |
|
|
|------|--------|
|
|
| Working backdoors in the panel code | none found |
|
|
| Webshell in the build's lineage | found already defanged, kept as evidence |
|
|
| Obfuscation, data exfiltration | none found |
|
|
| Admin authorisation bypass | none found |
|
|
| Pre-auth SQL injections (2) | **fixed** |
|
|
| Plaintext passwords in the database | **fixed** |
|
|
| XSS in the registration form | **fixed** |
|
|
| Unsalted MD5 | remains, needs separate work |
|
|
| Code installed over HTTP onto nodes | source removed from the installer |
|
|
| Root password used for node access | by design, mitigate with a firewall |
|
|
| Password inside the registration e-mail | remains |
|
|
|
|
**This panel should not be used for commercial hosting with real clients and payments** — because of
|
|
its legal status (see the README), because of the MD5 passwords, and because the provenance of the
|
|
code does not deserve trust. For study, internal tasks and auditing, it is fine.
|
|
|
|
---
|
|
|
|
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
|