[Русский](БЕЗОПАСНОСТЬ.md) · **English**

# HostinPL 5.6 · 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.**
