CWPdoor CWP Backdoor: Detection, Hunting and IOCs – ThreatDefence
Skip to content
Security advisories/QiAnXin XLab
HighActive

CWPdoor CWP Backdoor: Detection, Hunting and IOCs

Anton Guzhevskiy
Anton Guzhevskiy · Principal Product Researcher
Published November 6, 2026 · Updated November 7, 2026

CWPdoor is a backdoored build of cwpsrv, the web server behind Control Web Panel. It runs signed commands as root, hides them from the access log, and antivirus misses it.

TL;DR

Check every CWP server for the ngx_http_integrity_helper socket and treat a hit as full root compromise.

November 7, 2026: CWP 1.30.3 is now the current version, eight more CWP CVEs published on November 2 are listed under hardening, and VirusTotal now flags the 1.15.10 build and the installer.
November 6, 2026: First published, with indicators from the XLab report and our VirusTotal check of the published hashes and download server.
At a glance
SeverityHigh
StatusActive
ExploitedConfirmed victim in the wild: a backdoored cwpsrv was uploaded to VirusTotal from Brazil on September 30, 2026. Installer first captured June 29, 2026. Initial access method unknown
ProductControl Web Panel (CWP, formerly CentOS Web Panel) panel web server cwpsrv 1.15.10 (EL7) and 1.24.0 (EL8, EL9)
CVEsNone assigned
Fixed inNo patch: this is malware planted after root access, not a CWP flaw. Check cwpsrv, rebuild or reinstall, rotate credentials, update CWP to 1.30.3 or later

Nothing about CWPdoor needs a vulnerability in CWP itself. The attacker already has root on the server, runs a script named cwpsrv_update.sh that poses as a version update, and swaps the panel’s web server binary for one rebuilt from source with a backdoor added. XLab does not know how the attackers get root. CWP has a history of unauthenticated remote code execution flaws that are in CISA’s Known Exploited Vulnerabilities catalog, so unpatched panels are the obvious suspect, but no source links CWPdoor to a specific CVE.

What makes it dangerous is how little it shows. The backdoored cwpsrv serves the panel exactly like the real one, opens no outbound connection while idle, and suppresses logging for the attacker’s requests. File timestamps are put back after the swap, so ls -l shows the original date. The hunts below rely on what the attacker cannot hide: a Unix socket, strings in the binary, the file’s change time, the processes it spawns, and the download from the attacker’s server.

How CWPdoor gets onto a server

XLab captured the installer on June 29, 2026. It needs root and does the following:

  • Checks the cwpsrv version. It supports only 1.15.10 and 1.24.0. CWP’s changelog shows those are the panel web server versions on EL7 (1.15.10) and on EL8 and EL9 (1.24.0) before CWP 1.29.
  • Downloads the matching build from 136.0.141.71 and confirms its version.
  • Backs up the real binary as cwpsrv.prev-<date>-<time> in /usr/local/cwpsrv/bin.
  • Stops cwpsrv, overwrites /usr/local/cwpsrv/bin/cwpsrv and restarts it, then checks that the master, worker and helper processes are up. If they are not, it rolls back.
  • Cleans up after a successful swap. It deletes the temporary files and the backup.
  • Hides the change. Whether or not the swap worked, it restores the access and modification times of the binary and of its folder, and runs history -d to remove the session’s commands from shell history.
  • Reports the result back to 136.0.141.71.

A second file uploaded to VirusTotal from Brazil on September 30, 2026, an archive named evidencia.tgz (“evidence” in Portuguese), held a CWPdoor build named cwpsrv.suspeito next to libkeyutils.so.1.5.2, a library used by the Ebury SSH backdoor. XLab says the two may have been planted by different attackers. Either way, a server with one should be checked for the other.

How the backdoor works

cwpsrv is CWP’s customized Nginx. The attackers added two functions to its source:

  • ngx_http_integrity_init runs when the configuration loads. It creates a Unix domain socket named ngx_http_integrity_helper and starts a helper child process that will run commands.
  • ngx_http_integrity_check runs at the start of every HTTP request. It looks for three headers:
  • X-Ngx-Integrity-Time, a timestamp that must be within 300 seconds of the server clock
  • X-Ngx-Integrity-Data, the command, encrypted with AES-256-CBC and Base64-encoded
  • X-Ngx-Integrity-Sig, an RSA signature over the time and data values, checked against a public key embedded in the binary

If the signature is valid, the command goes to the helper over the socket, and the output comes back AES-encrypted in an HTTP 200 response with content type application/octet-stream. The backdoor swaps out the logging context for that request, so it never reaches the cwpsrv access log. A request without a valid signature is served as normal traffic, so scanning a server from outside cannot tell you whether it is infected.

Only the holder of the private key can use the backdoor. There is no public tool to trigger it, and nobody else can take over an infected server through it.

Who is affected

  • Any CWP server where an attacker has had root. That includes the free CWP7 and CWPpro, since both use cwpsrv. No source separates the editions.
  • EL7 servers on cwpsrv 1.15.10 and EL8 or EL9 servers on cwpsrv 1.24.0. CWP 1.29, released October 31, 2026, moved cwpsrv to Nginx 1.30.6, which the known installer does not support. That does not clean a server infected before the update, and the attackers can build a new version.
  • Hosting providers and resellers. XLab counted more than 340,000 CWP panels exposed to the internet in the past month. That is the pool of potential targets, not an infection count. No infection count has been published.

CWP panels listen on ports 2030, 2031, 2086 and 2087 (admin) and 2082 and 2083 (user) by default, with 2304 for the API.

Checking a CWP server

Run these as root on every CWP server. A hit on any of the first three means the server is infected.

# 1. The backdoor's Unix socket
ss -axp | grep -i integrity

# 2. The trigger header names inside the binary
strings /usr/local/cwpsrv/bin/cwpsrv | grep -i 'X-Ngx-Integrity'
grep -c 'ngx_http_integrity' /usr/local/cwpsrv/bin/cwpsrv

# 3. Known sample hashes (MD5 is all XLab published)
md5sum /usr/local/cwpsrv/bin/cwpsrv
#   965aea67fb40de1bab5169ada993349f  backdoored 1.15.10
#   d3f4e95113d49ab3991b45e457bda75b  backdoored 1.24.0

# 4. Version and leftovers of the installer
/usr/local/cwpsrv/bin/cwpsrv -v
ls -la /usr/local/cwpsrv/bin/ | grep -E 'cwpsrv\.prev-|cwpsrv_'
find / -xdev \( -name 'cwpsrv_update.sh' -o -name 'cwpsrv_1.15.10' -o -name 'cwpsrv_1.24.0' \) 2>/dev/null

# 5. Change time, which the installer cannot reset
stat -c 'modify=%y change=%z %n' /usr/local/cwpsrv/bin/cwpsrv

# 6. Children of cwpsrv that are not normal Nginx workers
ps -eo pid,ppid,user,lstart,args --forest | grep -B1 -A3 '[c]wpsrv'

Notes on reading the results:

  • Change time beats modification time. The installer resets the modification time, but Linux sets the change time (%z) itself whenever a file is replaced, and touch cannot set it back. A change time that is newer than the modification time, and does not line up with a CWP update you ran, deserves a closer look. A CWP update that replaced cwpsrv will also move it, so compare it with your update history in /var/log/yum.log or dnf history.
  • A cwpsrv.prev-* file means the installer ran. It deletes the backup only after a successful swap, so a leftover backup can point to a failed or interrupted run, or to a run that stopped before cleanup. Check the live binary either way.
  • No hit does not prove the server is clean. The attackers had root and can change names and strings in a new build. If you have reason to suspect the server, compare the binary with a clean copy from a fresh CWP install of the same version.

Threat hunting queries

The queries below are examples written in KQL for Microsoft Defender for Endpoint on Linux and Microsoft Sentinel. Adapt them to your own platform, tables and field names, and test them before you rely on them. Run them back to June 1, 2026 or as far as retention reaches, since the installer was in use by June 29.

Hunt 1: connections to the download server

let cwpdoor_ip = "136.0.141.71";
union isfuzzy=true
  (DeviceNetworkEvents
   | where Timestamp > datetime(2026-06-01)
   | where RemoteIP == cwpdoor_ip
   | project Timestamp, Source = "MDE", DeviceName, RemoteIP, RemotePort, RemoteUrl,
             InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessAccountName),
  (CommonSecurityLog
   | where TimeGenerated > datetime(2026-06-01)
   | where DestinationIP == cwpdoor_ip or SourceIP == cwpdoor_ip
   | project Timestamp = TimeGenerated, Source = DeviceVendor, DeviceName = SourceHostName,
             RemoteIP = DestinationIP, RemotePort = DestinationPort, RemoteUrl = RequestURL,
             InitiatingProcessFileName = "", InitiatingProcessCommandLine = "", InitiatingProcessAccountName = SourceUserName)
| order by Timestamp desc

Any hit from a CWP server is a strong lead. The address belongs to a hosting provider, so check what else ran on the server around that time.

Hunt 2: the installer and its downloads

DeviceProcessEvents
| where Timestamp > datetime(2026-06-01)
| where ProcessCommandLine has_any ("cwpsrv_update.sh", "cwpsrv_1.15.10", "cwpsrv_1.24.0", "136.0.141.71")
   or (FileName in~ ("curl", "wget") and ProcessCommandLine has "/static/cwpsrv")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
          InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessParentFileName
| order by Timestamp desc

history -d removes the commands from bash history, not from EDR or auditd records.

Hunt 3: the cwpsrv binary replaced outside a CWP update

The installer’s timestamp reset hides the change from ls, not from file events.

DeviceFileEvents
| where Timestamp > datetime(2026-06-01)
| where FolderPath startswith "/usr/local/cwpsrv/bin/"
| where FileName == "cwpsrv" or FileName startswith "cwpsrv.prev-" or FileName startswith "cwpsrv_"
| where ActionType in ("FileCreated", "FileModified", "FileRenamed", "FileDeleted")
| extend KnownBad = MD5 in ("965aea67fb40de1bab5169ada993349f", "d3f4e95113d49ab3991b45e457bda75b")
| project Timestamp, DeviceName, ActionType, FolderPath, FileName, MD5, SHA256, KnownBad,
          InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessParentFileName
| order by KnownBad desc, Timestamp desc

Expected noise: CWP updates replace cwpsrv, usually from yum, dnf, rpm or CWP’s own update script under /usr/local/cwpsrv or /scripts. A replacement started from bash running a script in /tmp, /root or /dev/shm, or a cwpsrv.prev- file, is the pattern to chase.

Hunt 4: shells and tools started by cwpsrv

The helper is a child of the cwpsrv master and runs commands for the attacker, so the commands it starts appear under cwpsrv. Baseline what cwpsrv normally starts on your servers before you judge the results.

DeviceProcessEvents
| where Timestamp > datetime(2026-06-01)
| where InitiatingProcessFileName == "cwpsrv" or InitiatingProcessParentFileName == "cwpsrv"
| where FileName in~ ("sh", "bash", "dash", "python", "python3", "perl", "curl", "wget", "nc", "ncat", "socat",
                      "id", "whoami", "uname", "cat", "base64", "chmod", "crontab", "useradd", "passwd")
| summarize Count = count(), First = min(Timestamp), Last = max(Timestamp),
            Commands = make_set(ProcessCommandLine, 20) by DeviceName, FileName, InitiatingProcessCommandLine
| order by Count asc

Expected noise: some CWP builds or add-ons may start helper scripts from cwpsrv. Baseline the results per server. Rare commands, reconnaissance such as id and uname, and anything that downloads or decodes are what to chase.

Hunt 5: panel ports reachable from the internet

Internet-reachable panels are where both the original root access and the backdoor’s trigger requests come from.

CommonSecurityLog
| where TimeGenerated > ago(30d)
| where DestinationPort in (2030, 2031, 2082, 2083, 2086, 2087, 2304)
| where DeviceAction in~ ("allow", "accept", "permit", "allowed", "accepted")
| where not(ipv4_is_private(SourceIP))
| summarize Connections = count(), Sources = dcount(SourceIP), SampleSources = make_set(SourceIP, 10),
            First = min(TimeGenerated), Last = max(TimeGenerated) by DestinationIP, DestinationPort
| order by Sources desc

Restrict these ports to known admin addresses or a VPN. For servers whose panel must stay public, a sudden source address that talks only to the admin port and receives application/octet-stream responses, seen by a TLS-terminating proxy or WAF but missing from the cwpsrv access log, matches the backdoor’s pattern.

Hunt 6: the Ebury library found next to CWPdoor

DeviceFileEvents
| where Timestamp > datetime(2026-06-01)
| where FileName startswith "libkeyutils.so"
| where ActionType in ("FileCreated", "FileModified", "FileRenamed")
| where InitiatingProcessFileName !in~ ("yum", "dnf", "rpm", "packagekitd", "apt", "dpkg")
| project Timestamp, DeviceName, ActionType, FolderPath, FileName, SHA256,
          InitiatingProcessFileName, InitiatingProcessCommandLine

On the host, rpm -V keyutils-libs reports a changed libkeyutils file, and ls -la /lib64/libkeyutils* showing libkeyutils.so.1.5.2 or a file size far from the packaged one warrants a full Ebury check.

Responding to an infected server

  1. Isolate the server and keep evidence. Copy /usr/local/cwpsrv/bin/cwpsrv, any cwpsrv.prev-* file, /usr/local/cwpsrv/logs/ and the system logs before changing anything. Take a memory capture if you can.
  2. Rebuild if you can. The attacker had root through an unknown entry point, and a backdoored panel binary may not be the only implant. A fresh operating system and CWP install, with accounts restored from backups that predate the compromise, is the safest outcome.
  3. If you must clean in place, replace cwpsrv with a known-good binary of the same version, or update CWP to 1.30.3 or later so the panel web server is replaced, then restart it and confirm that ss -axp | grep integrity and the strings check return nothing. XLab suggests restoring the cwpsrv.prev-* backup, but a successful install deletes it.
  4. Rotate every credential on the server. That means the root password and SSH keys, the CWP admin, reseller and user passwords, CWP API keys, MySQL root and database passwords, mailbox passwords, and any cloud or registrar keys stored on the server. Hosted customers’ passwords are exposed too.
  5. Look for the rest. Check for Ebury (libkeyutils), new root SSH keys, cron jobs, systemd units, new users, web shells in hosted sites and outbound connections you cannot explain.
  6. Find the way in. Review the CWP version history against CWP’s KEV-listed flaws and the panel’s login and API logs for the weeks before the swap.

Hardening CWP servers

  • Update CWP to 1.30.3 or later. Version 1.30.1 (November 3, 2026) tightened API key checks, login links and many panel modules, and made CWP’s own updates download only over verified HTTPS. Version 1.30.3 (November 4) adds a daily security audit that looks for cryptominers disguised as system processes and root curl | bash runs, and warns when API port 2304 is open to every IP but unused. Neither release mentions cwpsrv or CWPdoor. CVE-2025-48703 (file manager command injection, fixed in 0.9.8.1205) and CVE-2022-44877 (login command injection, fixed in 0.9.8.1147) are both in CISA’s KEV catalog. Ten more CWP CVEs were published on November 2, 2026. The most serious affect every version before 1.29: CVE-2026-104080 (unauthenticated command injection as root through the loopback API valid_token route), CVE-2026-77877 (missing authentication on root-level loopback API routes), CVE-2026-77878 (command injection in the restoredatabase API) and CVE-2026-77881 (blind SQL injection). CVE-2026-73695, CVE-2026-73697, CVE-2026-77876, CVE-2026-77879 and CVE-2026-77880 are fixed in 1.16, and CVE-2026-73696 in 1.12. None is in CISA’s KEV catalog, and no source links them to CWPdoor, but any of them is a route to root on an old panel.
  • Close the panel ports to the internet. Allow 2030, 2031, 2082, 2083, 2086, 2087 and 2304 only from admin addresses or a VPN.
  • Watch the binary. Add /usr/local/cwpsrv/bin/ to file integrity monitoring or auditd watch rules (-w /usr/local/cwpsrv/bin/ -p wa -k cwpsrv_bin), so a replacement raises an alert regardless of timestamps.
  • Log in front of the panel. A reverse proxy or WAF in front of the panel ports keeps a request record that the backdoor cannot suppress.
  • Run EDR on hosting servers. Process and file events are what catch this backdoor, and panel servers are often left without them.

Indicators of compromise

Source: QiAnXin XLab report (November 5, 2026), marked [X]; ThreatDefence VirusTotal check (November 6, 2026), marked [TD]. XLab published MD5 hashes only.

Type Indicator Context
MD5 e505d4ffa21cf494a5dc2d7c8f4f1236 cwpsrv_update.sh installer [X]. 7 engines on VirusTotal on November 7 [TD]
MD5 965aea67fb40de1bab5169ada993349f Backdoored cwpsrv 1.15.10 for EL7 [X]. No detections on November 6, 8 engines on November 7 [TD]
MD5 d3f4e95113d49ab3991b45e457bda75b Backdoored cwpsrv 1.24.0 for EL8 and EL9 [X]. Not on VirusTotal [TD]
IP 136.0.141[.]71 Download server and result reporting [X]. Hosted by Evoxt, located in the Netherlands [TD]
URL hxxp://136.0.141[.]71/static/cwpsrv_update.sh Installer [X]
URL hxxp://136.0.141[.]71/static/cwpsrv_1.15.10 Backdoored binary [X]
URL hxxp://136.0.141[.]71/static/cwpsrv_1.24.0 Backdoored binary [X]
File path /usr/local/cwpsrv/bin/cwpsrv Replaced binary [X]
File name cwpsrv.prev-<YYYYMMDD-HHMMSS> in /usr/local/cwpsrv/bin Installer’s backup of the real binary [X]
File name cwpsrv_update.sh, cwpsrv.suspeito, evidencia.tgz Installer, and names in the Brazil upload [X]
File name libkeyutils.so.1.5.2 Ebury library found in the same archive [X]
Unix socket ngx_http_integrity_helper Backdoor command channel [X]
HTTP headers X-Ngx-Integrity-Time, X-Ngx-Integrity-Data, X-Ngx-Integrity-Sig Trigger request [X]
Function names ngx_http_integrity_init, ngx_http_integrity_check Symbols left in the stripped binary [X]
HTTP response 200, Content-Type: application/octet-stream on a panel port Encrypted command output [X]
Embedded key RSA-2048 public key beginning MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwf9x0uLMxBK0AaCJuBsD, stored XOR-encoded with 0x55 Signature check; full key in the XLab report [X]

Timeline

  • June 29, 2026: XLab’s threat system captures cwpsrv_update.sh.
  • September 30, 2026: evidencia.tgz, holding a CWPdoor build and an Ebury library, is uploaded to VirusTotal from Brazil. The CWPdoor file has no detections.
  • October 31, 2026: CWP 1.29 moves cwpsrv to Nginx 1.30.6. Its changelog does not mention CWPdoor.
  • November 3, 2026: CWP 1.30.1 ships as a security update.
  • November 5, 2026: XLab publishes its CWPdoor analysis.
  • November 4, 2026: CWP 1.30.3 ships with a daily security audit for disguised cryptominers.
  • November 6, 2026: the 1.15.10 sample still has no detections on VirusTotal, and the 1.24.0 sample is not there. No statement from CWP.
  • November 7, 2026: VirusTotal flags the 1.15.10 build with 8 engines and the installer with 7. The 1.24.0 sample is still not there, and CWP has still made no statement.

Frequently Asked Questions

Is CWPdoor a vulnerability in Control Web Panel? No. It is malware an attacker installs after gaining root on the server. It has no CVE, and no CWP update fixes it. How the attackers get root is not known.

Can CWPdoor be found by scanning a server from the internet? No. Requests without the attacker’s signature are served normally, so an infected panel looks the same as a clean one. Check on the host for the socket, the strings in the binary and the processes.

Does updating CWP remove CWPdoor? Updating to CWP 1.29 or later replaces cwpsrv with Nginx 1.30.6, which should overwrite the backdoored binary, but CWP has not said so. Verify with the checks above afterward. An update does not undo the root compromise that came first.

Why are there no access log entries for the attacker’s commands? The backdoor removes the logging context from the requests it handles, so cwpsrv never writes them. A proxy, WAF or network sensor in front of the panel still sees them.

Is CWPdoor related to Ebury? Unclear. Both were in the same archive from one victim in Brazil, and XLab says different attackers may have planted them. Check any CWPdoor server for Ebury and the reverse.

How many servers are infected? No number has been published. XLab counted more than 340,000 CWP panels exposed to the internet, which is the number of possible targets.

Do the antivirus engines detect it? Not reliably. The 1.15.10 build had no detections on VirusTotal on November 6, 2026, and only 8 engines flagged it a day later. File integrity checks and EDR process and file events are more dependable.

How ThreatDefence is responding

ThreatDefence has added the CWPdoor indicators to our threat intelligence. Where we hold Linux endpoint, firewall and network telemetry, our SOC is:

  • running Hunts 1 to 4 back to June 2026 for customers with CWP or other hosting panels
  • alerting on changes to /usr/local/cwpsrv/bin/ and on shells started by cwpsrv
  • flagging CWP panel ports reachable from the internet

If you run CWP servers for your business or your customers and want help checking them or with an incident, contact us. Read more about our endpoint threat visibility and network detection and response.

Sources

Need help checking your environment for this issue? Talk to the ThreatDefence SOC.Talk to Our SOC

Talk to Our SOC

Questions about this advisory or help checking your environment.

    I'm an MSP, MSSP, or reseller

    By submitting this form, you accept the terms in our privacy policy.