Hopp til hovedinnhold
Erik Nilsen
12 min lesingVis som Markdown

Hovedsiden holdt, satellittene gjorde det ikke

  • #pentest
  • #wordpress
  • #security
  • #api-security
  • #third-party

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 ([email protected]). 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.

SubdomeneVertTeknologi
tjallas.no / www203.0.113.96 (DigitalOcean/Cloudways)WordPress 6.9.4 + Elementor, WPML, WP Rocket, WPForms
kurs.tjallas.no198.51.100.185 (DigitalOcean)WordPress 7.0
seokurs.tjallas.no198.51.100.185 (samme boks)WordPress 7.0
nettsideopplaring.tjallas.no203.0.113.160 (DigitalOcean)WordPress 7.0
matomo.tjallas.no198.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:

GET /wp-json/wp/v2/users HTTP/1.1
Host: tjallas.no
HTTP/1.1 401 Unauthorized
 
{"code":"rest_not_logged_in","message":"..."}
GET /?author=1 HTTP/1.1
Host: tjallas.no
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å:

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) 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:
GET /wp-json/wp/v2/users HTTP/1.1
Host: nettsideopplaring.tjallas.no
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"}]}}
]
  1. Bekreft via author-arkivet (den klassiske ?author=N-omveien):
GET /?author=1 HTTP/1.1
Host: nettsideopplaring.tjallas.no
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 ([email protected]). Samme admin-slug larsb gikk igjen på alle tre kurs-nettsteder — samme person, samme gravatar-hash, tre installasjoner som deler credentials i praksis.

  1. Negativ kontroll — samme oppslag mot hovedhosten, som har brukerenumerering slått av:
GET /wp-json/wp/v2/users HTTP/1.1
Host: tjallas.no
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) 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å:

wpscan --url https://seokurs.tjallas.no --api-token <REDACTED> \
  --enumerate vp --random-user-agent --throttle 1500

Utdrag av resultatet (sanert):

[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) 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:
GET /env.js HTTP/1.1
Host: dsp.tjallas.no
HTTP/1.1 200 OK
Content-Type: application/javascript
 
window.__ENV__ = {
  GOOGLE_API_KEY: "<REDACTED>",
  AUTH0_CLIENT_ID: "<REDACTED>",
  AUTH0_DOMAIN: "<REDACTED>.auth0.com",
  AUTH0_AUDIENCE: "<REDACTED>",
  DATADOG_KEY: "<REDACTED>",
  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.
  1. Negativ kontroll / falske positiver. Agenten flagget også config.json, .env og version.jsondsp.tjallas.no som potensielle funn. Verifisering viste at alle returnerte den samme 1672-byte index.html-fallbacken — typisk SPA-ruting, ikke ekte filer:
GET /config.json HTTP/1.1
Host: dsp.tjallas.no
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1672
 
<!doctype html><html lang="en"><head>...<!-- AngularJS sirius SPA --></head>...

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 (<?php-exit) — ingen hemmeligheter lekket. Matomo var for øvrig herdet: vendor/, .git/, plugins/ ga 403, installasjonsmodulen var låst («already installed»), og API krevde auth (getMatomoVersion → access denied). Ikke et funn.

matomo.tjallas.no innlogging eksponert mot internett — det er et reelt, om enn lavt, funn (F5). Versjon var skjult (bra), CSP var til stede. Anbefalingen er IP-begrensning av admin og påtving 2FA.

M365/tenant-lekkasje: getuserrealm for tjallas.no returnerte State=4 NameSpaceType=Unknown — domenet er ikke en managed Azure AD-tenant. Ingen openid-config-eksponering, ingenting å pivotere. Ikke et funn.

Subdomain-takeover: alle NXDOMAIN-subdomener manglet dangling CNAME, og alle SaaS-CNAME-er pekte på levende leverandører. Ingen takeover.

Lavere alvorlighetsgrad — verdt å nevne

Resten av funnene var lavprioritet, men samler seg til et bilde av inkonsekvent herding:

  • F3 — readme.html tilgjengelig (Lav) på alle tre kurs-nettsteder, avslører WordPress 7.0. Hovedsiden blokkerer den. Low
  • 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å. Low
  • 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. Low
  • 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. Low

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 <REDACTED> --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 — 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 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.