---
title: "Hovedsiden holdt, satellittene gjorde det ikke"
date: 2026-05-31
description: "En uautentisert pentest der hoveddomenet var herdet bak Cloudways WAF, men kurs-subdomenene lekket admin-brukernavn og gikk med utdaterte Crocoblock-plugins — og en tredjeparts adtech-plattform eksponerte hele tenant-listen sin."
tags: ["pentest", "wordpress", "security", "api-security", "third-party"]
url: https://erik.no/blogg/2026-05-31-hovedsiden-holdt-satellittene-ikke
---

<Callout type="info">
  **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.
</Callout>

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) <Severity level="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) <Severity level="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 <REDACTED> \
  --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) <Severity level="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: "<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.

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

<!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. <Severity level="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å.
  <Severity level="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. <Severity level="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.
  <Severity level="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](/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.
