I deleted the backdoor. Sixty seconds later, a new webshell appeared on a site I’d already cleaned. Then another on a different site. Then three more.
The attacker was still inside the account — watching us clean, and re-deploying from a foothold we hadn’t found yet. What started as a routine shared-hosting cleanup turned into a real-time adversarial engagement across 55 WordPress installations.
This is the story of how a compiled reverse shell, 14 obfuscated webshells, SEO spam cloaking, and an attacker who refused to leave quietly forced a two-wave remediation — and how we caught every move in the access logs.
The Battlefield: 55 Sites, One Account
The client ran a shared hosting account with 55 WordPress installations under a single cPanel user. That’s 55 separate document roots, 55 sets of plugins and themes, 55 wp-config.php files — all owned by the same Linux user. From an attacker’s perspective, compromising one site means having write access to all 55.
The initial report mentioned malware detections on a few sites. The security product had flagged some files but couldn’t keep up with the reinfection rate. That detail — “can’t keep up” — should have been the first red flag that someone was actively working inside the account.
Layer 1: The In-Memory Reverse Shell
The first thing I check on any engagement is running processes. Most investigators start with files. But files can be deleted after execution — processes can’t hide from ps.
$ ps aux | grep -v "lsphp\|cpanel\|dovecot"
USER PID STARTED CMD
hosting 3685785 Jul 23 04:12:00 x64_nc
A process called x64_nc, running since the previous day. Not a system binary. Not a cPanel service. Not a PHP process. A quick check of its network connections confirmed the worst:
$ ss -tnp | grep 3685785
ESTAB 0 0 10.x.x.x:random 217.154.53.116:443
ESTAB 0 0 10.x.x.x:random 217.154.53.116:443
ESTAB 0 0 10.x.x.x:random 217.154.53.116:443
Three active connections to a single C2 server on port 443. This was Global Socket (gsocket) — a tool that creates encrypted peer-to-peer TCP tunnels, typically over port 443 to blend in with HTTPS traffic. The binary had already been deleted from disk, but it was running in memory, maintaining persistent shell access to the server regardless of any file-level cleanup.
This is the critical difference between a webshell and a reverse shell: you can delete every PHP file on the server and the attacker still has a terminal. The process survives until it’s explicitly killed or the server reboots.
Layer 2: The wp2shell Dropper
The gsocket binary didn’t deploy itself. It was launched by a PHP dropper — a technique known as wp2shell. The dropper files were 5.6–5.8 MB each, disguised with names like termianal.php (note the typo — a consistent fingerprint across this actor’s campaigns), profil.php, and style.php.
Five copies were spread across different sites:
site1.com/wp-admin/termianal.php 5,876,765 bytes
site2.com/wp-admin/profil.php 5,624,754 bytes
site3.com/cgi-bin/termianal.php 5,624,754 bytes
site4.com/style.php 5,624,754 bytes
site5.com/.well-known/acme-challenge/
termianal.php 5,624,754 bytes
The placement strategy is worth noting. wp-admin/ is a legitimate WordPress directory that most scanners treat with light scrutiny. cgi-bin/ is almost always empty on modern hosting. .well-known/acme-challenge/ is the Let’s Encrypt certificate renewal path — a directory that exists on nearly every server, is rarely monitored, and is often excluded from security scans because it’s “infrastructure.”
The dropper mechanism uses putenv() to set LD_PRELOAD, pointing to a shared library (x64.so) that intercepts system calls, then launches the compiled gsocket binary with connection parameters stored in a .env-gs file. Once the binary is running in memory, the PHP dropper’s job is done — it could be deleted and the reverse shell would persist.
Layer 3: The Vathan Webshell Army
While the gsocket binary provided persistent shell access, the attacker also deployed a fleet of obfuscated PHP webshells for web-based access. All 14 copies were identical: 35,819 bytes, Base64 + gzuncompress obfuscation, identifying itself as “Vathan Vs Everybody” when decoded.
The webshell provided two primary functions: a filesystem browser (?path=/home/user/) and a command execution interface (?action=command). It’s a full-featured attack platform accessible from any browser.
The naming convention was deliberately chosen to blend in with WordPress core files:
site1.com/wp-admin/moderationis.php
site2.com/wp-signin.php
site3.com/wp-loading.php
site4.com/wp-track.php
site5.com/wp-seting.php
site6.com/wp-links-opm.php
site7.com/wp-admin/revisionis.php
site8.com/wp-admin/edit-tagss.php
None of these are real WordPress core files, but they’re close enough to survive a quick visual scan. wp-signin.php looks like it could be a login page. wp-links-opm.php is one character off from the legitimate wp-links-opml.php. moderationis.php sounds administrative. The attacker was betting that a human reviewer would see these in a file listing and skip past them.
Other copies were buried deeper:
site9.com/wp-content/plugins/index.php (replaced the
legitimate placeholder)
site10.com/wp-includes/css/buttons.min.php (PHP in a CSS dir)
site11.com/.well-known/acme-challenge/
antimo.php (certificate path)
Replacing the wp-content/plugins/index.php placeholder (normally a 28-byte “Silence is golden” file) with a 35 KB webshell is clever. WordPress won’t break — the file isn’t functionally required — but any plugin directory listing will show index.php as expected. The size difference is the only tell.
Layer 4: SEO Cloaking and Spam
The attacker’s monetization strategy was SEO spam — specifically Indonesian gambling keywords (QRIS234). On four sites, the legitimate WordPress index.php was replaced with a 953-byte cloaking script:
$ua = strtolower($_SERVER['HTTP_USER_AGENT']);
$ref = strtolower($_SERVER['HTTP_REFERER'] ?? '');
// If Googlebot or Indonesian referral → serve gambling spam
if (preg_match('/(googlebot|bingbot|yandex|slurp)/i', $ua)
|| strpos($ref, '.co.id') !== false
|| strpos($ref, 'qris') !== false) {
include('home.php'); // 500KB+ gambling doorway page
exit;
}
// Everyone else → serve the real site
include('index_old.php'); // original WP index.php, renamed
The cloaking is simple but effective. Regular visitors see the real site. Search engine crawlers see gambling spam. The site owner has no idea their domain is ranking for casino keywords in Indonesian search results — because they never see the spam pages in their own browser.
The attacker even saved the original index.php as index_old.php before replacing it — a courtesy that ensured the legitimate site kept working while the spam was served to bots.
Additionally, .htaccess files were injected with bot-redirect rules marked with Indonesian-language comments (# BOT SHORT-CIRCUIT / PASTI DI PALING ATAS), funneling crawler traffic to dedicated doorway pages up to 1.1 MB each.
The Turning Point: Wave 1 Cleanup
At this point, the picture seemed clear: kill the process, remove the webshells, clean the SEO spam, verify core integrity. Standard procedure. I killed the gsocket process, severed all three C2 connections, and began systematically removing malware across the 10 affected sites I’d identified.
# Kill in-memory reverse shell
$ kill -9 3685785
# Verify C2 connections severed
$ ss -tnp | grep 217.154.53.116
(empty)
# Remove wp2shell droppers
$ rm -f site1.com/wp-admin/termianal.php
$ rm -f site2.com/wp-admin/profil.php
...
# Remove Vathan webshells
$ rm -f site1.com/wp-admin/moderationis.php
...
# Restore clean WordPress index.php on affected sites
$ echo "<?php ... require __DIR__ . '/wp-blog-header.php';" \
> site3.com/index.php
...
Wave 1 was clean. I ran a full sweep: no Vathan files, no suspicious processes, no C2 connections. The account looked clear.
Then I checked the access logs.
Caught in the Act
This is the part that doesn’t happen in most incident write-ups, because most attackers are long gone by the time an analyst connects to the server. This one wasn’t.
103.22.182.235 - - [24/Jul/2026:05:28:14 -0400]
"GET /wp-links-opm.php?path=/home/ HTTP/1.1" 200 4521
103.22.182.235 - - [24/Jul/2026:05:31:47 -0400]
"POST /wp-links-opm.php?action=command HTTP/1.1" 200 89
103.22.182.235 - - [24/Jul/2026:05:33:22 -0400]
"POST /wp-links-opm.php?action=command HTTP/1.1" 200 89
103.22.182.235 - - [24/Jul/2026:05:43:11 -0400]
"POST /wp-links-opm.php?action=command HTTP/1.1" 200 89
Someone from IP 103.22.182.235 was actively using a Vathan webshell that I hadn’t found yet. They were browsing the filesystem with ?path=, then executing commands with ?action=command to drop new webshells onto sites I’d already cleaned.
The webshell they were using — wp-links-opm.php — was on a site I hadn’t included in my initial scope. I’d found 10 affected domains in Wave 1. The attacker was operating from an 11th.
The access log timeline told the full story:
- 05:28–05:43 — Initial deployment wave. The attacker dropped 13 new Vathan webshells across 8 sites using the
?action=commandinterface on their surviving foothold. Each command was a Base64-encoded PHP payload piped throughbase64 -dand written to a target path. - (several hours gap) — Our Wave 1 cleanup runs. We remove everything we’ve found. Account looks clean.
- 11:28 — The attacker returns. Checks their webshell — it’s still there (we hadn’t found this one). Starts browsing the filesystem again.
- 11:31–11:39 — Re-deploys malware on sites we cleaned. Drops new webshells with different filenames. Re-injects the
.htaccessbot-redirect rules. - 11:39:41 — Last recorded activity. We’d found and removed their remaining foothold. No further access.
The attacker’s persistence strategy was straightforward: as long as one webshell survived anywhere in the account, they could re-deploy everything. And on a 55-site account, finding every single copy requires scanning every document root, every plugin directory, every wp-admin folder, every wp-includes subdirectory, every .well-known path. Miss one, and you’re back to square one.
Wave 2: The Complete Sweep
Once I realized the attacker was active, the approach changed entirely. This was no longer a cleanup — it was a pursuit. Every minute spent investigating was a minute the attacker could use to establish new footholds.
I expanded the search to all 55 WordPress installations simultaneously:
# Find ALL 35,819-byte PHP files (Vathan signature)
$ find /home/user/ -name "*.php" -size 35819c
/home/user/site11.com/wp-admin/eraser.php
/home/user/site12.com/wp-admin/post-news.php
/home/user/site12.com/wp-loading.php
/home/user/site13.com/wp-admin/revisionis.php
/home/user/site13.com/wp-links-opm.php ← ACTIVE ENTRY POINT
/home/user/site14.com/wp-track.php
/home/user/site15.com/.well-known/acme-challenge/antimo.php
/home/user/site15.com/wp-seting.php
/home/user/site16.com/wp-admin/edit-tagss.php
/home/user/site16.com/wp-signin.php
/home/user/site17.com/wp-admin/adminaktif.php
/home/user/site17.com/wp-content/plugins/index.php
/home/user/site17.com/wp-includes/css/buttons.min.php
Thirteen new webshells. All the same hash. All deployed between 05:28 and 05:43 from the surviving foothold. The scope had grown from 10 to 15 affected domains.
I backed up every file for evidence, then removed all 13 in a single pass. Restored the legitimate plugins/index.php placeholder. Cleaned the re-injected .htaccess files. Ran a comprehensive sweep again — non-core PHP in webroots, suspicious filenames, recently modified files, processes, network connections.
Zero hits. The attacker’s last recorded request was at 11:39:41. There was no further activity.
WordPress Core Integrity: The Silent Check That Matters
After eradicating the obvious malware, I ran WordPress core integrity verification across all 55 installations. This step is often skipped on large accounts because it’s time-consuming — and that’s exactly why attackers target core files.
$ for dir in /home/user/*/; do
[ -f "$dir/wp-config.php" ] || continue
wp core verify-checksums --path="$dir" 2>&1
done
All 55 installations came back clean. The attacker hadn’t modified any WordPress core files — they relied entirely on rogue files and .htaccess injections. This is notable because many campaigns in the same attack family do inject into wp-blog-header.php, wp-settings.php, or wp-config.php. This actor chose not to, perhaps to avoid triggering file integrity monitoring that was already running on the server.
Why Shared Hosting Is a Force Multiplier
This case illustrates a fundamental problem with shared hosting architecture. All 55 WordPress installations ran under a single Linux user. PHP processes for any site could read and write files for every other site. A webshell on site13.com had exactly the same filesystem permissions as one on site1.com.
The attacker exploited this deliberately. Their deployment pattern showed a clear strategy:
- Establish initial access on one site (likely through a vulnerable plugin or stolen credentials)
- Deploy a compiled reverse shell for persistent, file-independent access
- Scatter webshells across multiple sites as fallback entry points
- Monetize with SEO spam on a subset of sites
- If discovered, re-deploy from surviving footholds on other sites
This is not a sophisticated attack chain. It’s a volume play. The attacker doesn’t need to be stealthy when they have 55 potential hiding spots and can re-deploy everything from any single surviving webshell in under 15 minutes.
The countermeasure is straightforward in theory: filesystem isolation between sites (CageFS, mod_ruid2, PHP-FPM pools with separate users). In practice, many shared hosting configurations still run all sites under a single user for simplicity — and attackers know it.
Detection Indicators
For defenders investigating similar compromises, here are the indicators that surfaced in this case:
Process-Level
- Non-standard binary names in process list (
x64_nc,gs-netcat,gs-pipe) - Outbound connections on port 443 from non-web processes
- Processes with deleted binary (
/proc/PID/exeshows(deleted)) LD_PRELOADenvironment variable set in/proc/PID/environ
Filesystem
- PHP files in
.well-known/acme-challenge/ - PHP files in
wp-includes/css/or other non-PHP directories - Core-looking filenames with typos (
termianal,moderationis,wp-seting) index.phpfiles significantly larger than 405 bytes (WP standard)index_old.phpalongside a modifiedindex.php- Files exactly 35,819 bytes with
gzuncompress+ Base64 encoding - Multi-megabyte PHP files in
wp-admin/orcgi-bin/(dropper binaries)
Access Log Patterns
- Requests to non-core PHP files with
?path=or?action=commandparameters - POST requests to webroot PHP files that aren’t WordPress core
- Sequential requests from the same IP hitting multiple sites in the same account
- Indonesian-language
.htaccesscomments (PASTI DI PALING ATAS)
Network
- C2 IP:
217.154.53.116(port 443) - Attacker IP observed:
103.22.182.235
Lessons From a Live Fight
Most malware cleanups are archaeological — the attacker came, planted their tools, and left. You’re reading cold evidence and reconstructing what happened. This one was different. The attacker was present, active, and adapting in near-real-time.
Three things made the difference in this engagement:
1. Process inspection before file scanning. Starting with ps and ss revealed the in-memory reverse shell immediately. If I’d started with file scanning (the typical approach), I would have found and deleted the webshells while the attacker maintained shell access through the gsocket tunnel — and they would have re-deployed everything the moment I finished.
2. Access log review after each remediation wave. The Wave 2 reinfection was only caught because I checked the logs after the cleanup, not just before. The attacker’s activity was visible in the access logs within minutes of each deployment. On a large account, this means grepping every site’s access log for the attacker’s IP, not just the sites you’ve already identified as compromised.
3. Full-account scope, not site-by-site. The initial temptation on a 55-site account is to triage: find the worst-hit sites, clean those, move on. But when the attacker has cross-site write access, every uncleaned site is a potential re-entry point. The only safe approach is treating the entire account as one compromise and scanning everything before declaring victory.
The attacker’s last request hit the server at 11:39:41. After that — silence. Every subsequent sweep came back clean. Every process check showed nothing. Every network connection was legitimate. The fight was over.
Until the next one.
About this case: This article documents a real incident. All domain names, usernames, and identifying details have been changed or omitted. The attacker IPs (103.22.182.235 and 217.154.53.116) are reported as-is for threat intelligence purposes. Malware samples from this case have been submitted for signature creation.