Двуязычный интерфейс (RU/EN) с автоопределением + документация на двух языках
Панель: - 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 - переключатели языка в начале каждого документа - раздел «Язык интерфейса» в инструкции: как работает, как добавить свой язык
This commit is contained in:
+243
@@ -0,0 +1,243 @@
|
||||
[Русский](БЕЗОПАСНОСТЬ.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.**
|
||||
Reference in New Issue
Block a user