New Container, Old Code: Why the Incident Returned After Three Months

Two 1C-Bitrix sites shared the same web shell WebShellOrb after an attacker exploited multiple vulnerabilities in a legacy self-written application called appeal. The initial compromise occurred via an unauthenticated SQL injection in AppealModel::getReportData combined with JWT authentication bypass and hardcoded secrets. Logs were retained for only one month, so investigators could not trace the entry point and incorrectly concluded there was no persistence after restarting the container. The shared filesystem and single system user allowed the shell to be copied to the second site, while password hashes remained extractable through API methods. Even after password changes and container redeployment, the attacker regained access weeks later because the JWT secret, code flaws, and common storage were never addressed. The case demonstrates that redeploying containers without fixing root vulnerabilities in legacy code leaves systems exposed for months.

HabrTranslated from Russian

Read at source

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.

Meridian digestThe week's most consequential security stories, distilled into one briefing.See the digest

More in Vulnerabilities & Exploits

Ginux Rust Kernel Receives Security Fixes in 10 Hours While Linux Patch Takes 69 Days to Reach Mainline
OneDrive and SharePoint URLs Enable Corporate Username Enumeration, Microsoft Calls Risk Low
WordPress 7.1.4 Addresses RCE Risk in Upgrade Process and Multiple Stored XSS Vulnerabilities