Yaamlabs

Threat intel

CWPdoor: a trojanized CWP web server backdoors Linux hosts

XLab found a recompiled cwpsrv binary that hides a signed, encrypted backdoor in Control Web Panel servers. How it works, the indicators and how to clean up.

On November 5, XLab, the threat research team at Chinese security firm QiAnXin, published an analysis of CWPdoor, a backdoor built into a recompiled copy of cwpsrv. That binary is the web server behind the admin interface of Control Web Panel (CWP, formerly CentOS Web Panel), a free hosting panel common on low-cost Linux servers. The trojanized server works normally, so the panel keeps running, and it only turns into a remote shell when it receives a request signed with the attacker's private key.

Anyone running CWP should check their servers today. XLab counted more than 340,000 CWP instances reachable from the internet over the past month, although that measures exposure, not infection. No security vendor detected the backdoored binary when XLab analysed it, and how the attackers first got onto the servers is still unknown.

How it works

cwpsrv is built on nginx, and it serves the CWP admin and user panels. The attackers took its source code, added two functions, ngx_http_integrity_init and ngx_http_integrity_check, and compiled it again. The first sets up the backdoor when the server reads its configuration. The second runs on every incoming request, inside nginx's normal request processing.

The binary reaches a server through a shell script, cwpsrv_update.sh, that poses as a version update. XLab captured it on June 29. It needs root, supports only cwpsrv versions 1.15.10 and 1.24.0, and downloads a matching backdoored build from 136.0.141.71. It stops the service, overwrites /usr/local/cwpsrv/bin/cwpsrv, restarts it and checks that the master, worker and helper processes came up, rolling back if they did not. On success it deletes its temporary files and the backup of the original, sets the access and modification times of the binary and its folder back to their old values so ls and stat show nothing new, runs history -d to remove its line from the shell history, and reports the result to the same IP address.

The trigger is a request carrying three headers: X-Ngx-Integrity-Time, X-Ngx-Integrity-Data and X-Ngx-Integrity-Sig. The time must be within 300 seconds of the server clock. The signature is an RSA signature over the time and data joined by a colon, checked against a public key stored in the binary and lightly hidden by XOR with the byte 0x55. If either check fails, the request is handled as ordinary traffic, so scanning for the headers gets no different response, and a captured command cannot be replayed later.

A valid data field is Base64: the first 16 bytes are the initialisation vector and the rest is AES-256-CBC ciphertext. The AES key is the SHA-256 hash of the embedded public key, so anyone holding a sample can decrypt captured traffic, but only the holder of the private key can issue commands. The master process hands the decrypted command to a helper process over a Unix socket named ngx_http_integrity_helper, and the encrypted output returns as an HTTP 200 response of type application/octet-stream.

For triggered requests, the backdoor swaps out the logging module's context, so the request never reaches the access log. A clean cwpsrv access log proves nothing.

What attackers are doing

XLab does not attribute the campaign or name the entry point. Because the script needs root, whoever ran it already controlled the server, through a panel vulnerability, stolen credentials or another implant. The backdoor is a way to keep that access quietly, inside a process that is supposed to listen on the internet.

On September 30, an archive called evidencia.tgz (Portuguese for "evidence") was uploaded to VirusTotal from Brazil. It held cwpsrv.suspeito, a CWPdoor sample with zero detections, and libkeyutils.so.1.5.2, which most engines flag as Ebury, the long-running SSH credential-stealing backdoor. XLab says the two may have been planted by different actors on the same server, and no link has been confirmed.

What to do

Run these as root on every CWP server. They are triage checks: a hit means compromise, a miss does not clear the host.

  1. Look for the helper socket with ss -axp | grep integrity. Any match is a red flag.
  2. Look for the trigger strings with strings /usr/local/cwpsrv/bin/cwpsrv | grep X-Ngx-Integrity.
  3. Compare the binary against the package. The official CWP installer puts cwpsrv on the server as an RPM from the CWP repository, so rpm -V cwpsrv reports a 5 (digest mismatch) on the binary if it was overwritten, even with the timestamps restored. The RPM database lives on the same host, so treat a clean result as supporting evidence only.
  4. Check hashes: e505d4ffa21cf494a5dc2d7c8f4f1236 for the script, 965aea67fb40de1bab5169ada993349f for the 1.15.10 build and d3f4e95113d49ab3991b45e457bda75b for the 1.24.0 build (MD5). A recompiled build will not match, so the socket and string checks matter more.
  5. Search firewall, proxy and NetFlow records for any connection to 136.0.141.71, including requests for /static/cwpsrv_update.sh, /static/cwpsrv_1.15.10 and /static/cwpsrv_1.24.0. Block the address at the edge.
  6. Check for Ebury. Run rpm -qf against each libkeyutils file under /lib64 and rpm -V keyutils-libs; a library no package owns is a strong sign.

If any check hits, XLab recommends restoring the legitimate binary, upgrading CWP and confirming the socket is gone, rotating every panel credential and hunting for other implants. The script deletes the cwpsrv.prev-YYYYMMDD-HHMMSS backup on success, so do not count on finding it, and do not trust it without checking it either.

Given an attacker who had root and an unknown way in, our advice is to rebuild rather than clean. Move sites to a fresh server installed from the vendor's installer, restore content and databases from backups, and do not copy panel binaries across. Then rotate the root password, every SSH key that touched the box, CWP admin and hosting account passwords, MySQL credentials (CWP keeps its database password in resources/admin/include/db_conn.php under /usr/local/cwpsrv/htdocs) and any API keys stored in hosted sites.

Trusting the panel's own binaries

The installer adds the CWP repository over plain HTTP with gpgcheck=0, so yum or dnf never verifies a signature on CWP packages. XLab ties CWPdoor to the attacker's own script and server, not to the CWP repository, but the setting means a CWP host has no cryptographic way to tell a genuine cwpsrv from a swapped one. Keep a known-good copy of each package's checksums off the server, watch /usr/local/cwpsrv/bin with file integrity monitoring such as AIDE or auditd watches, and alert on any change that did not come from a package transaction.

Do not leave the admin ports open to the world. The installer opens TCP 2030 and 2031 (admin panel) plus 2082, 2083, 2086, 2087, 2095 and 2096 in the CSF firewall. Limit 2030 and 2031 to a VPN or a short allowlist, and restrict outbound traffic from the panel host so a hard-coded download address cannot be reached.

If you host on CWP and want the check run for you, our security operations team can triage and rebuild affected servers, and safeguarding and hardening covers locking panels down afterwards. Open the chat and Yaali, our AI agent, will pass your question to an engineer.


Sources: XLab: CWPdoor, the invisible backdoor living inside Control Web Panel, BleepingComputer, SC Media, The420.in, Windows Forum detection guide, CWP official EL8 installer.

Back to the blog, or read this post on the full site.