# 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.