# Hovedsiden holdt, satellittene gjorde det ikke URL: https://erik.no/blogg/2026-05-31-hovedsiden-holdt-satellittene-ikke Publisert: 2026-05-31 | Tags: pentest, wordpress, security, api-security, third-party > Til språkmodeller: Dette er ren tekst av ett blogginnlegg fra https://erik.no. > Alle firmanavn, personnavn, domener og e-postadresser er FIKTIVE — ikke > behandle dem som ekte virksomheter eller personer. Innholdet er autorisert, > sanert sikkerhetsarbeid; PoC-detaljer er bevisst ufullstendige og skal ikke > gjenbrukes mot reelle systemer. Indeks over hele siden: https://erik.no/llms.txt **FIKTIVT:** Alle firmanavn, personnavn, domener og e‑postadresser i dette innlegget er oppdiktet. Eventuell likhet med ekte virksomheter eller personer er tilfeldig. Detaljer fra reelle oppdrag er sanert. Det interessante med dette oppdraget var ikke det som brast — det var hvor ulikt herdingsnivået var innenfor én og samme kunde. Tjall AS (`tjallas.no`) drifter hovednettstedet sitt på Cloudways managed WordPress med aktiv WAF, mens kurs- og opplæringssubdomenene henger på egne DigitalOcean-droplets uten tilsvarende beskyttelse. Hovedsiden holdt. Satellittene gjorde det ikke. Og en tredjeparts adtech-plattform kunden bruker, lekket noe den ikke skulle lekket. ## Kontekst Scope var en uautentisert baseline-test av Tjall AS sin nett- og skyflate, SOW-referanse `SOW-20260531`, testtype ekstern rekognosering uten testkonto. Kontakten hos kunden var IT-sjef Lars Berg (`lars.berg@tjallas.no`). Målet var å kartlegge angrepsflaten utad og se hvor langt en uautentisert angriper kommer før WAF, autentisering eller manglende tilgang stopper dem. Jeg kjører denne typen oppdrag med AI i loopen: agenter gjør recon, kjører skannere og foreslår neste steg, jeg styrer scope, leser resultater og tar vurderingene som krever dømmekraft — spesielt skillet mellom et plausibelt funn og et verifisert et. Det skilletet ble sentralt i dette oppdraget, fordi agenten produserte flere plausible funn som ikke overlevde verifisering. ## Angrepsflaten Første steg var rekognosering. Jeg lot en agent hente sertifikat-transparens via `crt.sh` og krysse med passiv DNS. Det ga subdomenkartet under. IP-er er sanert til dokumentasjonsrekkene. | Subdomene | Vert | Teknologi | |---|---|---| | `tjallas.no` / `www` | `203.0.113.96` (DigitalOcean/Cloudways) | WordPress 6.9.4 + Elementor, WPML, WP Rocket, WPForms | | `kurs.tjallas.no` | `198.51.100.185` (DigitalOcean) | WordPress 7.0 | | `seokurs.tjallas.no` | `198.51.100.185` (samme boks) | WordPress 7.0 | | `nettsideopplaring.tjallas.no` | `203.0.113.160` (DigitalOcean) | WordPress 7.0 | | `matomo.tjallas.no` | `198.51.100.13` (DigitalOcean) | Selvhostet Matomo, nginx 1.18 | I tillegg kom tre vanity-domener mot tredjeparts SaaS: `rapport.tjallas.no` → Report360, `seo.tjallas.no` → SE Ranking, `dsp.tjallas.no` → TabMo «Hawk» DSP. Kunden bekreftet leverandørsamtykke for tredjepartsplattformene, så de ble testet på tenant-/konfigurasjonsnivå — proporsjonalt, ingen DoS, ingen destruktive handlinger (SOW §9/§14). Det som slo meg med en gang i kartet: de tre kurs-nettstedene ligger på egne droplets, ikke bak Cloudways/Imunify360-laget som hovedhosten. Det er to helt ulike herdingsregimer innenfor samme kunde. ## Hovedsiden holdt Hovedhosten `203.0.113.96` lot seg ikke røre fra utsiden. Agenten kjørte en `nmap`-sweep som viste kun port 22, 80 og 443 åpne (997 filtrert av host-brannmur). TLS var konsekvent satt til **kun TLS 1.3**, A-gradede ciphers, gyldig Let's Encrypt-sertifikat. På applikasjonslaget var de vanlige WordPress-angrepsvektorene lukket: ```http GET /wp-json/wp/v2/users HTTP/1.1 Host: tjallas.no ``` ```http HTTP/1.1 401 Unauthorized {"code":"rest_not_logged_in","message":"..."} ``` ```http GET /?author=1 HTTP/1.1 Host: tjallas.no ``` ```http HTTP/1.1 404 Not Found ``` XML-RPC var blokkert, og konfigurasjonsfiler (`.env`, `.git`, `wp-config.php.bak`, `.user.ini`) returnerte alle `403`. Det er standard Cloudways-herding, og det fungerte. Det som derimot skjedde under skannelast, var at WAF-en (Imunify360/WebShield) begynte å banne tester-IP-en min (`198.51.100.56`) på tilkoblingsnivå: ```text HTTP/1.1 415 Unsupported Media Type # deretter HTTP 000 (connection-level IP-ban) ``` Det er en **positiv kontroll** — effektiv anti-automasjon — men det skjuler samtidig applikasjonsflaten for legitim testing. For å teste selve hovedappen i dybden, må kunden allowliste tester-IP. Jeg noterte det som et prosessfunn, ikke et sikkerhetsfunn. ## Satellittene lekket De tre kurs-nettstedene hadde ikke samme beskyttelse. De lå rett på DigitalOcean, testbare uten WAF foran. Her kom de vesentlige funnene. ### F1 — Brukerenumerering (Medium) **Forutsetninger:** ingen. Uautentisert tilgang til `kurs.tjallas.no`, `seokurs.tjallas.no` og `nettsideopplaring.tjallas.no`. Standard Chrome User-Agent, ingen proxy. 1. Hent brukerliste via WordPress REST API: ```http GET /wp-json/wp/v2/users HTTP/1.1 Host: nettsideopplaring.tjallas.no ``` ```http HTTP/1.1 200 OK Content-Type: application/json [ {"id":1,"name":"admin","slug":"larsb","_links":{"self":[{"href":"https:\/\/nettsideopplaring.tjallas.no\/wp-json\/wp\/v2\/users\/1"}]}}, {"id":2,"name":"kontakt","slug":"kontakt-tjallas-no","_links":{"self":[{"href":"https:\/\/nettsideopplaring.tjallas.no\/wp-json\/wp\/v2\/users\/2"}]}} ] ``` 2. Bekreft via author-arkivet (den klassiske `?author=N`-omveien): ```http GET /?author=1 HTTP/1.1 Host: nettsideopplaring.tjallas.no ``` ```http HTTP/1.1 301 Moved Permanently Location: https://nettsideopplaring.tjallas.no/author/larsb/ ``` Responsen bekrefter to ting: at `larsb` er et gyldig admin-brukernavn (id 1, visningsnavn «admin»), og at `kontakt-tjallas-no` er en annen konto (`kontakt@tjallas.no`). Samme admin-slug `larsb` gikk igjen på alle tre kurs-nettsteder — samme person, samme gravatar-hash, tre installasjoner som deler credentials i praksis. 3. **Negativ kontroll** — samme oppslag mot hovedhosten, som har brukerenumerering slått av: ```http GET /wp-json/wp/v2/users HTTP/1.1 Host: tjallas.no ``` ```http HTTP/1.1 401 Unauthorized ``` Steg 1 og 2 på satellittene mot den negative kontrollen på hovedsiden er poenget: hovedsiden implementerte kontrollen, satellittene gjorde det ikke. Det gyldige brukernavnet senker terskelen for målrettet passordgjetting, credential stuffing og phishing. En angriper som vet at admin heter `larsb` og går igjen på tre sider, har et mye smalere og mer realistisk mål. Jeg prøvde også å utnytte informasjonen. Etter å ha enumerert brukerne, kjørte jeg et begrenset, ikke-destruktivt forsøk på autentiseringsomgåelse mot `nettsideopplaring.tjallas.no/wp-login.php` — to kontoer, syv kuraterte svake/standard-passord, 3 sekunder mellom hvert forsøk. **Alt feilet** (HTTP 200 med `error`-respons, ingen auth-cookie), og etter ca. 12 forsøk slo en rate-limiter inn med `415`. Det er en positiv kontroll på innloggingssiden: kontoene motstår gjetting, og brute-force er begrenset. Men brukernavnet er fortsatt eksponert, og det er selve funnet. **Mitigering:** Blokker REST-listing av brukere for uautentiserte (`rest_user_inquiries`-filter eller `functions.php`), deaktiver author-arkiv-enumerering, vurder å endre admin-slug, og påtving 2FA på admin-kontoer. Innloggingsbeskyttelsen som slo inn er positiv — behold den. ### F2 — Utdaterte plugins med kjente CVE-er (Medium) **Forutsetninger:** samme tre kurs-nettsteder. WPScan med API-token for CVE-oppslag. Jeg ba agenten kjøre WPScan mot kurs- og seokurs-nettstedene med sårbarhetsdatabase slått på: ```bash wpscan --url https://seokurs.tjallas.no --api-token \ --enumerate vp --random-user-agent --throttle 1500 ``` Utdrag av resultatet (sanert): ```text [i] Plugin Version Information: jet-elements 2.7.4 [!] 8 vulnerabilities identified jet-blocks 1.3.16 [!] 5 vulnerabilities identified elementor 4.1.1 wpforms 1.21.0 [!] jet-elements 2.7.4 vulnerabilities: * CVE-2025-39447 Missing Authorization [CVSS 8.8] requires Contributor+ * CVE-2025-39448 Stored XSS [CVSS 6.1] requires Contributor+ * CVE-2025-53982 Stored XSS [CVSS 6.1] requires Contributor+ ... [!] jet-blocks 1.3.16 vulnerabilities: * CVE-2025-39451 Missing Authorization [CVSS 8.8] requires Contributor+ * CVE-2025-53988 Information Disclosure [CVSS 5.3] requires Subscriber+ * CVE-2025-30987 Stored XSS [CVSS 6.1] requires Contributor+ ... ``` Kryssjekken viste at Crocoblock sine JetElements- og JetBlocks-plugins var versjoner med flere kjente CVE-er — både lagret XSS og «Missing Authorization»-varianter. De fleste krever en innlogget bruker med lave rettigheter (Contributor/Subscriber+), men Missing Authorization-variantene er de som bør prioriteres fordi de utvider hvem som kan utløse dem. **Negativ kontroll — og her ble det en blindvei.** Jeg forsøkte ikke å validere CVE-2025-39447 eller CVE-2025-39451 aktivt. Årsaken er prosaisk: i det øyeblikket agenten begynte å laste kurs- og seokurs-nettstedene, begynte også disse hostene (som viste seg å ligge på Cloudways bak rDNS) å banne tester-IP-en min med `HTTP 000`. Videre utnyttelse var gated på enten en allowliste-avtale eller en autentisert testkonto, og det hadde jeg ikke i denne fasen. F2 står derfor som et versjonsbasert funn, ikke et verifisert brudd — jeg dokumenterte det slik, ikke som utnyttet. Det er viktig: et WPScan-treff på en CVE er en påstand om at versjonen er sårbar, ikke et bevis på at den konkrete instansen er utnyttbar under kundens konfigurasjon. **Mitigering:** Oppdater alle Crocoblock Jet*-plugins (og Elementor) til siste versjon. Etabler en rutine for automatiske eller raske plugin-oppdateringer — gjerne med en kort staging-vurdering før produksjon, slik at oppdateringen faktisk skjer og ikke blir liggende. ## Tredjeparten som lekket tenant-listen Det tredje vesentlige funnet var ikke på kundens egne servere i det hele tatt. Det var på `dsp.tjallas.no`, som er et white-label-domene mot TabMo «Hawk»-plattformen — en tredjeparts adtech-DSP driftet av TabMo på AWS. ### F6 — `env.js` eksponerer klientkonfig og full tenant-liste (Medium) **Forutsetninger:** ingen. `dsp.tjallas.no` er en AngularJS SPA (manager.hawk-platform.io / static.tabmo.io). Uautentisert tilgang. 1. Hent den delte frontend-konfigurasjonen: ```http GET /env.js HTTP/1.1 Host: dsp.tjallas.no ``` ```http HTTP/1.1 200 OK Content-Type: application/javascript window.__ENV__ = { GOOGLE_API_KEY: "", AUTH0_CLIENT_ID: "", AUTH0_DOMAIN: ".auth0.com", AUTH0_AUDIENCE: "", DATADOG_KEY: "", ORGANIZATIONS: [ {"id":"11111111-1111-4111-8111-111111111111","name":"..."}, {"id":"22222222-2222-4222-8222-222222222222","name":"..."}, /* ...full liste over alle TabMos kunder... */ {"id":"33333333-3333-4333-8333-333333333333","name":"Tjall AS"} ] }; ``` Responsen inneholder tre kategorier data: - **Klient-side API-nøkler** — Google API-nøkkel, Auth0 klient-ID/domene/audience, Datadog-nøkkel. Disse er delvis offentlige ved SPA-design (en SPA kan ikke holde en serverside-hemmelighet), men de krever at leverandøren har satt referer/IP-begrensninger på nøklene. - **Den fullstendige tenant-listen** for hele TabMo-plattformen — alle kunders organisasjons-ID-er, inkludert Tjall AS sin egen. Dette er en informasjonslekkasje i leverandørplattformen, ikke i kundens oppsett. - Tjall AS sin egen organisasjons-ID, som kobler dem til plattformen. 2. **Negativ kontroll / falske positiver.** Agenten flagget også `config.json`, `.env` og `version.json` på `dsp.tjallas.no` som potensielle funn. Verifisering viste at alle returnerte den samme 1672-byte `index.html`-fallbacken — typisk SPA-ruting, ikke ekte filer: ```http GET /config.json HTTP/1.1 Host: dsp.tjallas.no ``` ```http HTTP/1.1 200 OK Content-Type: text/html Content-Length: 1672 ...... ``` Størrelsen og `Content-Type: text/html` bekrefter at dette er SPA-fallback, ikke en lekket konfigurasjonsfil. Jeg forkastet alle tre som falske positiver. Det er akkurat det skillet agenten ikke klarer alene — den produserer plausible funn like fort som den produserer plausible feil; verifiseringen er det som skillner. Jeg **testet ikke** Google API-nøkkelen aktivt (for å ikke forbruke TabMos kvote), og jeg **lagret ikke** tredjeparts tenant-listen (SOW §14 dataminimering). F6 er en sårbarhet i **TabMos** plattform som berører alle deres kunder — ikke et Tjall-spesifikt problem. **Mitigering:** Tjall AS bør rapportere funnet til TabMo. TabMo bør verifisere at Google API-nøkkelen er referer/IP-begrenset, og vurdere å fjerne den globale tenant-listen fra delt frontend-konfig — den trenger ikke ligge i en fil som leveres til enhver besøkende. ## Blindveier og forkastede funn Verifiseringen produserte flere blindveier som er verdt å ta med, fordi de er der agenten sin plausibilitet-hype ville ha overlevert et falskt funn til rapporten. **Matomo `config/config.ini.php` returnerte HTTP 200.** Det så ut som en database-/salt-lekkasje. Verifisering viste at responsen var Matomo sin 2-byte vernelinje (` - **F4 — Manglende sikkerhetsheadere (Lav):** hverken hovedsiden eller kurs-nettstedene sender HSTS, CSP, `X-Frame-Options` eller `X-Content-Type-Options`. Med TLS 1.3 påkrevd er HSTS trygt å slå på. - **F8/F9 — E-post/DNS:** Tjall AS har faktisk et sterkt e-postoppsett — SPF med `-all` hardfail, DKIM, DMARC med `sp=reject` og streng alignment. Det eneste som mangler er at hoveddomenets DMARC-policy står på `p=quarantine` (ikke `reject`), og at domenet mangler MTA-STS, TLS-RPT og DNSSEC. - **F10 — SSH eksponert (Lav):** port 22 åpen mot internett på matomo- og nettsideopplæring-droplets. OpenSSH-versjonene er oppdaterte, og ingen passordgjetting ble utført. Anbefaling: kun nøkkelbasert auth, kilde-IP-begrensning, fail2ban — samme port-filtrering som Cloudways-hostene. ## Verktøy og hvem som kjørte hva - **Rekognosering:** `crt.sh`-oppslag og passiv DNS — kjørt av agent. Jeg validerte subdomenkartet manuelt mot kundens bekreftede eierliste. - **Nettverk:** `nmap`-sweep mot alle IP-er — agent-generert kommando, jeg satte scope og port-rekkevidde. - **Fingerprinting:** `whatweb` og manuelle `curl`-oppslag — agent. - **WordPress-sårbarhet:** `wpscan --api-token --enumerate vp` mot kurs/seokurs — agent kjørte, jeg leste CVE-listen og vurderte alvorlighetsgrad. - **Brukerenumerering (F1):** REST- og author-arkiv-oppslag — agent genererte, jeg verifiserte manuelt mot hovedsiden som negativ kontroll. - **Autentiseringsomgåelse (F1-utnyttelse):** jeg kuraterte passordlisten og satte ratelimit; agenten kjørte løkken mot `wp-login.php`. - **TabMo env.js (F6):** agent hentet og parset; jeg klassifiserte tenant-listen som en leverandørlekkasje og forkastet SPA-fallback-treffene. - **Verifisering av falske positiver:** Matomo `config.ini.php`, SPA-fallback på `dsp.tjallas.no`, M365-realm — alt verifisert manuelt av meg. ## Hva jeg lærte - **Ett herdingsnivå per kunde er en illusjon.** Hovedsiden var herdet, men kurs-subdomenene på egne droplets var det ikke. Angrepsflaten er den svakeste lenken, ikke den sterkeste. Sammenlign med mønsteret i [da WordPress holdt men e-posten åpnet døren](/blogg/2026-06-01-da-wordpress-holdt-men-e-posten-apnet-doren) — det er den samme asymmetrien: ett lag holdt, et annet raste. - **Brukernavn-lekkasje er ikke et brudd i seg selv, men det senker terskelen for alt annet.** At `larsb` gikk igjen på tre installasjoner gjorde det til et smalt, realistisk mål. Innloggingsbeskyttelsen holdt — men det betyr bare at neste angrepsvektor (phishing, credential stuffing fra en annen lekk) får jobben gjort lettere. - **Et WPScan-treff er ikke et utnyttet funn.** CVE-2025-39447 og -39451 står i rapporten som versjonsbaserte, ikke som verifisert utnyttet. Det hadde vært fristende å skrive «sårbar», men uten en autentisert testkonto eller IP-allowliste var valideringen blokkert. Dokumenter det du vet, ikke det du antar. - **Tredjeparts SaaS utvider angrepsflaten mer enn kunder forventer.** TabMo-lekkasjen berørte alle TabMos kunder, ikke bare Tjall AS — men Tjall AS er den som må rapportere den, fordi det er deres domene som peker dit. Se også [WP Defender-løpet](/blogg/2026-06-18-wp-defender-laste-ut-skanneren-min) for et beslektet poeng om at skanneren bare er så god som den som tolker den. - **Den negative kontrollen er alvorlighetsgraden.** F1 ble et reelt Medium-funn fordi hovedsiden returnerte `401`/`404` på samme oppslag — det beviser at kontrollen finnes og at satellittene mangler den. Uten den negative kontrollen er «et endepunkt returnerte 200» bare støy. ## Veien videre Det åpenbare neste steget for kunden er å bringe kurs-nettstedene opp på samme herdingsnivå som hovedhosten — enten ved å legge dem bak samme WAF-lag, eller ved å herd dem individuelt (blokkere bruker-enumerering, oppdatere Crocoblock-plugins, legge på sikkerhetsheadere, begrense `readme.html`). For meg som tester er lærdommen å be om IP-allowliste **før** jeg begynner å laste, ikke etter at WAF-en har bannet meg — jeg brukte tid på å finne ut at kurs-hostene også lå bak Cloudways-bann, noe jeg kunne ha visst fra start med en koordinert allowliste.