Two sites running on 1C-Bitrix — a self-written application named appeal and a newer portal — both hosted the web shell WebShellOrb (WSO). The first signs appeared in Nginx logs showing POST requests to /assets/images/access.php followed by GET requests that referenced the shell itself.
Because logs were kept for only one month, no scanning activity was visible. Investigators assumed the compromise predated the log window and decided to check 1C-Bitrix agents for suspicious code. Finding none, they concluded there was no persistence and restarted the container.
Three months later the attacker returned. Analysis revealed that appeal had never been migrated to the CMS and still contained the original vulnerable code. The infrastructure used a shared filesystem and a single system user, so the shell copied itself to the second site. Multiple shells were later uploaded for redundancy.
The attack chain relied on four specific weaknesses:
- SQL injection in AppealModel::getReportData() inside AppealModel.php; user input reached the query without parameterization when $isSkipArgs was set to true.
- JWT authentication bypass in Identity.php that skipped token validation for login and logout routes.
- Password hash leakage through User.php; getRow(5) always returned the record with ID 5, exposing hashes via the API.
- Hardcoded JWT secret stored in plain text in the configuration file.
After the container restart, administrators changed passwords but left the JWT secret unchanged. Because Identity.php did not enforce token checks on certain routes, previously issued tokens remained valid. The shared volume preserved the web shells, and the unchanged code continued to leak credentials.
The incident only ended after the development team parameterized all SQL queries, removed the skipArgs flag, enforced proper authorization checks, moved the JWT secret to environment variables, removed the password field from API responses, and separated the two sites onto distinct users and filesystems.


