Hopp til hovedinnhold

Teknologi / 7 minutter /

Innbrudd!

En av de største redslene jeg har når jeg lager webapplikasjoner, er at noen skal bryte seg inn i applikasjonen og få tilgang til de interne systemene våre. Slike innbrudd kan få store konsekvenser.

I 2017, ble navn, adresser og SSN (tilsvarende det norske fødselsnummeret) for 150 millioner amerikanere stjålet fra Equifax, et kredittvurderingsbyrå i USA. Med slik informasjon, er man på god vei til å kunne stjele identiteten til ofrene. Heldigvis var dette angrepet organisert av Kina. De var antagelig mer interessert i spionasje og rettede angrep mot utvalgte enkeltpersoner, enn å ta opp en lån i tilfeldige amerikaneres navn.

Et annet slikt innbrudd ble utført av Russland mot SolarWinds. I dette angrepet, ble programvaren til SolarWinds endret slik at kundene ble eksponert for videre innbrudd. Når kundelisten til SolarWinds inneholder nesten alle firmaene på Fortune 500-listen i USA, samt offentlige institusjoner som Finansdepartementet og Handelsdepartementet, er det opplagt at angrepet er svært alvorlig.

Men hvor lett er det egentlig å gjennomføre et slikt innbrudd? Hva kan vi som utviklere gjøre for å redusere risikoen?

Ofte krever slike innbrudd flere kodefeil i tillegg til feilkonfigurerte servere. Men i 2020, ble en sårbarhet oppdaget hos Netflix som fikk navnet CVE-2020–9296. Denne er både lett å introdusere og lett å utnytte. Den underliggende feilen er den samme som i Log4Shell (sjekk om du er sårbar), men denne er enklere å demonstrere. Begge to åpner for å kjøre vilkårlig kode på serveren. Altså at klienten leverer kode til serveren som serveren så utfører. Dette kalles en RCE, Remote Code Execution, og er det mest alvorlige sikkerhetsbruddet.

Jeg har laget et veldig enkelt API for postnummersøk ved hjelp av Spring Boot. Basis er en standard Java 17-applikasjon der jeg har lagt til Validation og Spring Web. For å vise sårbarheten, har jeg nedgradert Spring Boot til 2.4.13. Denne ble sluppet i november, 2021. 2.4.x må derfor sies å være en aktiv versjon selv om den ikke lenger er tilgjengelig fra ‘spring initializr’-siden.

API’et presenter en GET mot /postnr som tar inntil fire søkeparametre pn (for postnummer), ps (for poststed), kn (for kommunenummer) og k (for kommunenavn).

Koden er tilgjengelig på GitHub. Det er koden frem til taggen ‘Enkelt-API’ som er relevant hittil.

For å teste API’et, kan serveren startes med

mvn spring-boot:run

I et annet terminalvindu kan man da utføre forskjellige søk

curl -s localhost:8080/postnr?kn=4601 | jq

Dette API-et er enkelt og ryddig, men det er litt lite hjelpsomt. Alle feltene her sendes inn som ‘String’, men både postnr og kommunenr er egentlig numeriske felter. Hva med å legge inn en liten sjekk på det?

...

Java har en egen standard for validering, Bean Validation — JSR 380. Denne gjør det enkelt å validere et felt på en Java Bean ved hjelp av annotasjoner. Vi kan legge på noen annotasjoner på PostnrInfo-klassen og få utført validering av de feltene som sendes inn fra klienten:

Numeric er en annotasjon laget spesielt for dette formålet. JSR 380 krever at slike annotasjoner følger en spesifikk mal, men utover det er ikke selve annotasjonen så vesentlig. Det viktige er at denne annotasjonen brukes på de feltene som skal valideres. Da tar Bean Validator seg av å opprette rett ConstraintValidator, kjøre rett valideringsmetode og samle opp resultatene.

Selve valideringen skjer i NumericValidator som implementerer grensesnittet ConstraintValidator<Numeric, String>.

Her er det to standardiserte metoder. Denne implementasjonen returnerer at alle verdier er ugyldige uansett. Dermed er det mulig å verifisere at all mekanikken rundt validering og excptions henger sammen.

For å trigge valideringen, annoteres PostnrService med @Validated, mens innputt-objektet til search()-metoden annoteres med @Valid.

For å presentere feilmeldingen på en hyggelig måte, fanger ValidationExceptionHandler opp alle exceptions som valideringen kan gi, og transformerer dem til en liste av PostnrError-objekter.

All koden frem til nå finnes i taggen ‘Numeric-Skall’.

...

Selve valideringen går på å sjekke at strengen ‘s’, kun inneholder sifre og gi en passende feilmelding tilbake til brukeren

Koden her sjekker om det er noe å validere og sjekker så at alle bokstavene er sifre. Hvis noe er feil, stoppes den medfølgende valideringshåndteringen, slik at brukeren kan få en litt mer tilpasset feilmelding. Enkel og grei og hyggelig kode.

Denne kan igjen kjøres opp med

mvn spring-boot:run

Kall med ugyldige verdier vil da gi en pen feilmelding.

curl -s ‘localhost:8080/postnr?kn=Bergen&pn=Paradis’ | jq

Men vi har fått betraktelig mer enn vi ba om her.

curl er et ganske avansert program. Det kan bl.a. gjøre urlencoding av innputt. Dette kan komme nyttig med. For eksempel så er

identisk med GET-kallet over, men der vi får curl til å urlencode parametrene. Dette spiller ikke så stor rolle akkurat her, men hvis vi kjører

får vi et særdeles interessant resultat:

Vi har da vitterlig ikke skrevet ‘bbc’ i innputten noe sted. Derimot ser det ut som om vi har kjørt Java-koden

“abc”.replace(“a”, “b”)

Og det er faktisk det som har skjedd. Serveren har kjørt den koden vi sendte den som innputt.

JSR 380 versjon 1.1 la til at buildConstraintViolationWithTemplate() nå kan benytte ‘unified expression language’ for templaten. Tanken er at man kan legge på annotasjoner som for eksempel

@Size(min=5, max=15, message=”Nøkkelen må ha \\{{min}\\} til \\{{max}\\} tegn”)

og så få meldingen ‘Nøkkel må ha 5 til 15 tegn’. Problemet, eller kanskje heller fordelen for denne artikkelen, er at EL er et utrolig kraftig språk laget for JSP 2.1. Å kunne lage templater som kan formatere datoer og tall kan være fint, men EL gir tilgang til hele maskinen.

Koden vi kjørte er altså ikke egentlig Java, men et svært lignende skript-språk kalt ‘unified expression language’.

For å terminere den kjørende serverapplikasjonen kan vi kjøre følgende.

Vi kan også dumpe alle miljøvariablene. I en 12-faktor-app, kan man være heldig å få mye nyttig informasjon om databaser, brukere og passord derfra.

Å lagre passord i miljøvariabler kan karakteriseres som en konfigurasjonsfeil. Men slik 12-faktor app er beskrevet, er det ikke usannsynlig at gjøres ofte.

...

I neste del, ser vi på hvordan vi kan få full remote shell via denne sårbarheten.

...

Feilen som ble gjort i denne koden, var at innputt fra klienten, ukritisk ble brukt i kallet til buildConstraintViolationWithTemplate(). Denne kategorien feil kalles Server-side template injection. Dette er en svært enkel feil å gjøre. Men det er lite i koden som tilsier at JVM’en vil prøve å kjøre kode levert fra klienten her.

Egentlig er feilen i spesifikasjonen til Java Bean Validation 1.1. Å inkludere hele EL her, er en helt unødvendig risiko. Men når spesifikasjonen er sånn, så blir alle måter å rette det på, spesifikke til den implementasjonen som brukes. For Hibernate, vil den beste måten å fikse dette på, være å oppgradere Hibernate Validator til 6.2 eller 7.x. Disse versjonene gir ikke tilgang til å kalle metoder i EL før det eksplisitt slåes på ved et kall til enableExpressionLanguage() på en HibernateContext. Denne utvidelsen av standarden er ikke være portabelt til f.x. Apache sin implementasjon. Koden i NumericValidator.isValid() kan da se slik ut:

Linje 4 og 5 vil fungere også i Hibernate Validator 6.1 slik at vi unngår feilen. Linje 6 må med i versjoner 6.2.0 og senere. Uten denne vil ikke meldingen inneholder innputt-verdien vises i det hele tatt:

...

En rekke verktøy kan hjelpe til med å avdekke at sårbare avhengigheter er inkludert i koden vår.

OWASP har en Maven Plugin, som kan kjøre ved hvert bygg.

Knust glass i dør
Knust glass i dør

Denne er knyttet til ‘verify’, så den kjører samtidig som integrasjonstestene

mvn verify

OWASP sin dependency-check plug-in er gratis, men oppdager bare sårbare avhengigheter når applikasjonen bygges. Sårbarheter som oppdages etter at applikasjonen er satt i produksjon, forblir ukjente.

Snyk laster opp listen av avhengigheter, og sjekker dem jevnlig. Dermed får du en epost når nye sårbarheter oppdages. Snyk kan også kobles rett på offentlig tilgjengelige repoer som GitHub eller skyløsningen til BitBucket. De tilbyr også noe gratis.

Nexus IQ, Whitesource og Black Duck tilbyr tilsvarende løsninger.

Merkelig nok fant hverken OWASP eller Snyk denne spesifikke sårbarheten, selv om de fant en rekke andre.

...

Har også skrevet en oppfølger - en del to.