BLOG

Gebrekkige toegangscontrole: een slimme zwembadcontroller overnemen

Een verbonden zwembad is nog steeds een webapplicatie met een motor eraan. Bij een eerder onderzoek was het doel een slim zwembadsysteem. Een controller stond in de technische ruimte. Een mobiele applicatie draaide op de telefoon van de klant. Een cloudportaal liet installateurs elk verkocht systeem beheren. Het inloggen werkte. Het probleem was alles daarna. Het platform controleerde of u was ingelogd, maar bijna nooit wat u mocht doen.

Eén regel staat boven alles. Test alleen een systeem dat u mag testen, binnen een afgesproken opdracht. De stappen hieronder zetten een echt apparaat in beweging en veranderen een echte chemische instelling. De toestemming is wat een onderzoek onderscheidt van sabotage.

Het systeem op één pagina

Vier onderdelen telden. Een cloudportaal voor beheerders en installateurs. Een mobiele applicatie met twee rollen: installateur en eindgebruiker. Een kleine Linux-controller in de technische ruimte. En de fysieke apparatuur die de controller aanstuurt: een warmtepomp, een zoutelektrolyse, een circulatiepomp, verlichting en een uv-lamp. De applicatie en het portaal praten allebei met één backend-API. De controller bereikt de cloud via een message broker. Elke stap daarin viel binnen de opdracht, en de interessante fouten zaten in de API.

Architectuur van het slimme zwembadplatform: apps en cloud links, de controller op locatie en de fysieke apparatuur rechts
De doelomgeving, en het pad dat de gebrekkige controle blootlegde

Authenticatie werd gecontroleerd, autorisatie niet

Voor het portaal betekende “ingelogd” één ding: er stond een token in de browser. De hele controle aan de clientkant was één functie.

// De volledige "auth"-controle aan de clientkant
public isAuthenticated(): boolean {
 return localStorage.getItem('accessToken') !== null;
}

Een willekeurige tekst zoals abcdef bracht u niet ver, want de backend controleerde nog steeds de handtekening van het token. Een echt token wel, en daar ging het mis. Bij een nieuw account stuurde het platform een verificatielink per e-mail. Die bevatte een kortlevend JWT, ongeveer vijftien minuten geldig. Dat verificatietoken werd geaccepteerd als een volwaardig accesstoken. Een account dat de registratie nooit had afgerond, kon er beheerdersendpoints mee aanroepen.

DELETE /api/controllers/inventory HTTP/2
Host: cloud.example.net
Authorization: Bearer eyJhbGci...
 (type: access, exp +15m, uit de e-maillink)
Content-Type: application/json

{"controller_uuid":"1234"}
HTTP/2 200 OK
Content-Type: application/json

{"message":"Controller deleted: 1234"}

Geen beheerdersrol, geen afgerond account, een token dat alleen een e-mailadres moest bevestigen. Toch verwijderde de server een controller.

Authenticatie beantwoordt wie u bent. Autorisatie beantwoordt wat u mag doen. Een platform dat het eerste controleert en het tweede vertrouwt, geeft elk geldig token dezelfde sleutels.

Elk account kon bij elk apparaat

Dezelfde fout liep door in normaal gebruik. De mobiele applicatie benoemde een apparaat met een controller-UUID. De backend vertrouwde die UUID, zonder te controleren of de aanvrager de eigenaar was. De UUID’s waren ook niet geheim. De beheerconsole toonde ze allemaal. Die console stond op het open internet, zonder VPN of IP-filtering ervoor. Door dezelfde ontbrekende controle kon elk ingelogd account de console openen en elke UUID lezen. Zo kon de ene eindgebruiker de apparatuur van de andere aansturen, alleen met diens controller-UUID.

De beheerconsole toonde elke controller op UUID
De openbare beheerconsole, elke controller op UUID

POST /api/device/execute HTTP/2
Host: cloud.example.net
Authorization: Bearer <token van eindgebruiker B>
Content-Type: application/json

{"controller_uuid":"<controller van gebruiker A>",
 "device_id":"PoolLight","operation":"turn_on"}
HTTP/2 200 OK
Content-Type: application/json

{"ack":"turn_on",
 "controller_uuid":"<controller van gebruiker A>",
 "user_id":"<gebruiker A>"}

De response gaf het gebruikers-id van het slachtoffer terug. Dat bevestigde dat de actie op hun account liep, niet op dat van de aanvaller. Dit is autorisatie op objectniveau die volledig ontbreekt: de klassieke IDOR, nu op een fysiek apparaat in plaats van een databaserij.

Van een gewoon account naar fysieke controle

Een lamp aanzetten is hinderlijk. De warmtepomp en de zoutelektrolyse zijn dat niet. Om daarbij te komen was één stap extra nodig, en het platform bood die. Installateurs kunnen tijdelijke toegang tot de controller van een klant aanvragen, voor ondersteuning. De klant hoort die goed te keuren. Die goedkeuring werd nooit afgedwongen. Een aanvaller kon de toegang rechtstreeks toevoegen en kreeg een Access inserted-antwoord. Daarna ruilde hij die in voor een vers accesstoken, gericht op de controller van het slachtoffer, met een privilege-claim. Dat token stuurde het device-execute-endpoint aan.

POST /api/device/execute HTTP/2
Host: cloud.example.net
Authorization: Bearer <privilege-token,
 scope ["privilege"], access_for: gebruiker A>
Content-Type: application/json

{"controller_uuid":"<controller van gebruiker A>",
 "device_id":"HeatPump","operation":"turn_on"}
HTTP/2 200 OK
Content-Type: application/json

{"ack":"turn_on",
 "device":{"device_id":"HeatPump",
 "operation_params":{
 "port":"/dev/ttyS0","baud_rate":9600,
 "command":["0x01,0x06,0x00,0x64,0x00,0x28,0xC8,0x0B"]}}}

Dat laatste veld is waar het om draait. Het cloudverzoek stopt niet bij de cloud. De response liet zien hoe het een seriecommando op laag niveau voor de zwembadhardware werd: een poort, een baudrate en een ruwe byte-instructie. Wij hebben de controller niet van binnen onderzocht. Het frame heeft de vorm van een Modbus RTU-schrijfactie naar één register. Dat betekent: functiecode 0x06, een register en een waarde, dan een controlewaarde van twee bytes. Dat is het soort serieprotocol dat deze actuatoren gebruiken. Hoe dan ook is de betekenis gelijk. Een JSON-aanroep vanaf de telefoon van een vreemde werd een fysieke instructie over een seriële lijn naar een motor.

De applicatie zelf liet dit niet toe. De bediening hield de temperatuur en de pH binnen veilige grenzen. Maar die grens zat alleen in de front-end. Opnieuw verstuurd via Burp Suite, met een waarde buiten bereik, bereikte hetzelfde verzoek de backend. Die sloeg de waarde op zonder controle op grenzen. Vanaf een account dat niets hiervan bezat, ging de streefwaarde van een testwarmtepomp naar 100 graden Celsius. De zoutelektrolyse ging naar een pH van bijna 1. Dat zijn veiligheidsextremen, geen comfortstanden. Een softwarefout in de autorisatie werd een fysiek veiligheidsprobleem.

Warmtepomp op 100 graden Celsius, en het actielog van het apparaat dat de wijziging vastlegt
De warmtepomp gedwongen naar 100 °C, en het actielog van het apparaat

De rest van het beeld

De autorisatiefouten waren het hoofdpunt. De lokale webapplicatie van de controller voegde er meer aan toe. Het endpoint voor apparaatstatus bouwde SQL op door strings aan elkaar te plakken. Eén quote in een apparaatnaam gaf een 500. Een UNION-query las daarna de opgeslagen controller-UUID en de accountnaam zo terug.

POST /local/device/status HTTP/1.1
Host: 192.0.2.61
Content-Type: application/json

{"device_id":"PoolLight'"}
HTTP/1.1 500 Internal Server Error
Server: nginx
Content-Type: text/html

<h1>Internal Server Error</h1>

Op zichzelf was geen van deze fouten de ingang. Gecombineerd verbreden ze die. De lokale webapplicatie van de controller sprak gewoon HTTP, met het bearer-token leesbaar in het verkeer. Iedereen op hetzelfde netwerk kon een geldig token onderscheppen. Omdat autorisatie nooit werd afgedwongen, was dat token alleen al genoeg om de apparatuur aan te sturen, zonder account. De bouwkant maakte van één apparaat alle apparaten. Privésleutels, inloggegevens voor database en message broker, en encryptiesleutels stonden in de broncode en de CI. De mobiele applicatie werd zonder afscherming uitgeleverd, met een sleutel hard in de code. Haal de applicatie uit elkaar, haal de geheimen eruit, en u bereikt de broker en de database rechtstreeks. Een gebrekkige toegangscontrole beheerst één zwembad. De gelekte inloggegevens beheersen elk zwembad.

Wat verdedigers hieruit kunnen halen

De oplossingen zijn niet ingewikkeld. Het zijn de controles die dit platform oversloeg.

  • Dwing autorisatie af bij elk verzoek, op de server, voor zowel de actie als het specifieke object. Controleer dat de aanvrager die ene controller bezit of mag bedienen, voordat u iets aanraakt. Vertrouw nooit een identificatie die de client aanlevert.
  • Houd verificatietokens los van accesstokens. Een link die een e-mailadres bevestigt, mag geen sessie worden; geef die het kleinste bereik en de kortste levensduur.
  • Zie het bestaan van een token niet als bewijs. Controleer het token, en controleer daarna de rechten.
  • Parameteriseer elke query.
  • Zet ook TLS op het interne netwerk, want “intern” is geen vertrouwensgrens.
  • Houd geheimen uit de broncode en de CI, en versleutel tokens in rust op het apparaat.
  • Behandel een autorisatiecontrole als een veiligheidsmaatregel bij alles wat een motor beweegt of een chemische stof doseert, want dat is wat het is.

Damp boven een verwarmd buitenzwembad in de schemering
Wat een gat in de autorisatie kan bereiken

Tot slot

De les is niet het zwembad. De les is dat toegangscontrole het hele spel is zodra authenticatie is opgelost. En dat op een verbonden apparaat de schade fysiek is. Een inlogpagina die werkt, kan nog altijd voor een platform staan dat elk account elk apparaat laat aansturen. Zodra het apparaat water verwarmt en chemie toevoegt, is die kloof geen IT-risico meer, maar een veiligheidsrisico.

Hoe Pentrox ingebedde en verbonden apparaten van begin tot eind test, leest u op de dienstpagina hardwarebeveiliging testen.

De applicatie- en consolebeelden in dit artikel zijn getrouwe reconstructies. Klantherkenbare details zijn gewijzigd of verwijderd om de vertrouwelijkheid van het onderzoek te beschermen.

Deel dit artikel LinkedIn X

Klaar om uw omgeving te beveiligen?

Plan een vrijblijvend intakegesprek om uw assessment af te stemmen. Pentrox identificeert kwetsbaarheden in uw applicaties, infrastructuur en cloudomgevingen voordat aanvallers dat doen.

Plan een intakegesprek