In English

28 april, 2022

Säkerhetshål i Java tillåter blanka checkar (CVE-2022-21449)

Skribent: Laban Sköllermark, pentestare och IT-säkerhetskonsult på Sentor

I november 2021 hittade säkerhetsarkitekten Neil Madden ett allvarligt säkerhetshål i Java och rapporterade det till leverantören Oracle, som släpper säkerhetsfixar varje kvartal. Den 19 april släpptes deras April 2022 Critical Patch Update som täpper till detta och flera andra säkerhetshål i flera av Oracles produkter. Kör man Java på exempelvis en webbserver så bör man alltid uppdatera när det släpps kritiska säkerhetsfixar om man inte väldigt noga läst igenom vad som fixats och konstaterat att man inte påverkas, men denna gång är det extra viktigt om man kör Java-versionerna 15–18 och på något sätt litar på så kallade ECDSA-signaturer i sin applikation. Observera att Java-versionerna 15 och 16 inte bör köras alls eftersom de är föråldrade och inte får säkerhetsuppdateringar längre.

Långtidssupportversionen (LTS) Java 11 underhålls fortfarande med säkerhetsfixar och är inte drabbad av den aktuella buggen. Kör du en (major)version av Java högre än 11, säkerställ att du kör 17.0.3, 18.0.1 eller nyare. 

Säkerhetshålet 

Bristen som Neil hittade har fått identifikatorn CVE-2022-21449, men som de flesta större sårbarheterna nu för tiden har den också fått ett smeknamn. Neil kallar den Psychic Signatures in Java i sitt blogginlägg, där han offentliggjorde säkerhetshålet i samband med att en fix släpptes. Han förklarar ingående bristens bakgrund men även att namnet är en referens till den brittiska TV-serien Doctor Who från 1960-talet. Huvudkaraktären Doktorn använder ofta en tom papperslapp med mystiska egenskaper som får betraktaren att se det som Doktorn vill att den ska se, exempelvis ett ID-kort. I serien kallas pappret just psychic paper

Säkerhetshålet består i att Java-utvecklarna missade att verifiera ett väldigt viktigt villkor när ECDSA-signaturer verifieras. Det introducerades när kryptokod skrevs om i det egna språket Java i version 15 från att tidigare ha varit implementerad i programspråket C++. ECDSA står för Elliptic Curve Digital Signature Algorithm och bygger på kryptografi baserad på matematikkonceptet elliptiska kurvor.

ECDSA använder asymmetriska nycklar (publika och privata) och är besläktat med det mycket mer välkända RSA (Rivest-Shamir-Adleman). Både signering och kryptering med elliptiska kurvor är mycket mer processoreffektivt än RSA, och nycklarna som krävs för säker kryptering och signering med elliptiska kurvor är mycket kortare än de som krävs för motsvarande nivå av säkerhet med RSA. Även signaturer blir mycket kortare. 

Vi kommer inte gå igenom några detaljer kring elliptiska kurvor i det här blogginlägget, men bra att veta är att en ECDSA-signatur består av två, oftast väldigt stora, heltal r och s. Ekvationen som används för att verifiera ECDSA-signaturer har r på ena sidan och den andra sidan multipliceras med r samt ett värde som baseras på s. Om både r och s är noll så är ekvationen uppfylld. Därför är det väldigt viktigt att kontrollera att både r och s är större eller lika med ett när en ECDSA-signatur kontrolleras, vilket framgår tydligt i ECDSA-specifikationen, men det var just den kontrollen som utvecklarna missade när de skrev om ECDSA-koden i Java 15. 

Vanlig ECDSA-användning: webbautentisering genom JWT 

ECDSA-signaturer kan användas till mycket. Bland annat i TLS-certifikat på webbsidor som transportkrypteras (HTTPS), men det brukar webbläsaren och webbservern ta hand om och dessa är sällan skrivna i Java. Däremot är det många webbapplikationer som är skrivna i Java och som därför kör Java på serversidan. De flesta webbsidor kör numera JavaScript i webbläsaren, men det är ett helt annat språk och har förutom namnet ingenting med Java att göra. 

När användare autentiserat sig mot applikationen, “loggat in”, är det vanligt att ett bevis på vem besökaren är lagras i en kaka i webbläsaren. Nu för tiden är det relativt vanligt att denna så kallade sessionskaka innehåller en JSON Web Token (JWT, artikel på engelska). För att JWT:er inte ska kunna förfalskas är dessa signerade. Det är JWT:er med ECDSA-signaturer som den här artikeln använder som exempel. 

En JWT känns igen genom att den innehåller tre block med till synes slumpmässiga bokstäver separerade med punkter emellan: 

Samtliga tre block är Base64-kodade och representerar JWT:ns huvud, nyttolast (payload) respektive signatur. Om huvudet och nyttolasten ovan Base64-avkodas så framgår mer begriplig JSON (JavaScript Object Notation). Avkodning (för den intresserade med CyberChef-recepten #1 och #2) ger:

Huvudet i en JWT innehåller alltid vilken algoritm som används för signaturen men kan innehålla mer information. Nyttolasten innehåller ett antal påståenden (claims). Vissa är standardiserade i JWT som exempelvis “iat” som står för “issued at” och innehåller en tidstämpel i Unixtid-format som representerar tidpunkten då JWT:n skapades. “loggedInAs” i exemplet ovan är applikationsspecifik. Om den kan ändras på ett sätt så att signaturen fortfarande går igenom kontrollen så kan användaren bli vem som helst! Därför är det viktigt att användaren inte själv kan signera JWT:er. Bara inloggningstjänsten på servern behöver kunna signera JWT:er som sen verifieras av alla delar av applikationen eller till och med helt andra, externa, applikationer. 

Här kan det vara bra att påpeka vikten av att använda olika hemligheter i sina testsystem jämfört med produktion. Det händer då och då att vi som säkerhetsgranskare stöter på system hos våra kunder där hemligheter delas mellan systemen. Detta får som konsekvens att någon som exempelvis är administratör i något testsystem helt plötsligt kan bli samma användare i produktion bara genom att kopiera sessionskakan innehållande JWT:n från en flik i webbläsaren med testsystemet i, till en flik med produktionssystemet i. 

En JWT kan vara signerad med en av flera algoritmer. I exemplet ovan betyder “HS256” en hash-baserad meddelandeautentiseringskod (HMAC) med SHA-2-hashfunktionen SHA-256, där SHA står för Secure Hash Algorithm. HMAC bygger på en symmetrisk nyckel, alltså att både den som “signerar” och verifierar “signaturen” känner till samma hemlighet. I exemplet ovan används hemligheten “secretkey”. JWT-algoritmer som börjar på “RS” använder RSA, alltså “riktiga” signaturer med asymmetriska nycklar och med olika SHA-2-hashfunktioner. De som börjar på “ES” använder ECDSA-algoritmen för signaturer med asymmetriska nycklar (även de använder SHA-2) och är alltså påverkade av säkerhetshålet i fråga (om en sårbar version av Java används). 

I Neils blogginlägg förklarar han att en signatur där både r och s är satt till noll kan användas för att signera valfritt meddelande. I standarden för JWS (JSON Web Signature, RFC 7515), som används i JWT, framgår det i exemplen att signaturen består av r och s, i den ordningen, som en följd av bytes (oktetter). Neil visar att en ECDSA-signatur bestående av bara noll-bytes kan användas för att förfalska valfritt meddelande. Base64-kodat blir det en sträng av bara A:n. 86 stycken för ES256, 128 stycken för ES384 och 176 stycken för ES512. 

För den tekniskt intresserade så följer ett exempel på hur en förfalskad signatur verifieras som korrekt av sårbara Java 17.0.1 med hjälp av JShell. JWT:n som används har huvud och nyttolast från en exempel-JWT från jwt.io. Exemplet är inspirerat av en kodsnutt på GitHub och av exemplet på Neils blogg. Koden är inte tänkt att användas för att testa om ens miljö är sårbar.

Genom att bara veta vad huvud och nyttolast ska innehålla, och det får man reda på om man själv har tillgång till ett system och kan logga in, så kan man nu alltså sätta valfritt innehåll och lägga till en generisk signatur på slutet och en sårbar server kommer att verifiera signaturen som giltig! Detta motsvarar att skriva under sina bankcheckar och lämna ut dem utan något belopp ifyllt, vilket artikelns titel syftar på. Kom ihåg att alltid be om lov innan du säkerhetstestar någon annans system. 

Genom att signera sina egna JWT:er kan man bli vem som helst... 

Det finns en uppsjö av olika bibliotek för att hantera JWT. Vissa tillåter andra format på signaturer än exemplen ovan. Ett sådant exempel är Java-biblioteket jjwt som stöder signaturer kodade som ASN.1 (Abstract Syntax Notation One) i DER-format (Distinguished Encoding Rules). De två heltalen r = 0 och s = 0 kan då representeras mycket kort och likadant oavsett längd på hashalgoritmen: MAYCAQACAQA 

Detta demonstreras i en sårbar exempelapplikation av Thomas Etrillard och Christophe Tafani-Dereeper hos Datadog: Exploitation and Sample Vulnerable Application of the JWT Null Signature Vulnerability (CVE-2022-21449) 

Just biblioteket jjwt har släppts i en ny version 0.11.3 som nu kontrollerar värdena r och s trots att det är Javas ansvar. Stödet för signaturer på ASN.1 DER-format stängs också av som standard eftersom det inte är en del av JWT-standarden. Rådet är dock att inte lita på sådana här extrakontroller i bibliotek utan att uppgradera Java (också) om en sårbar version används, och tänk på att regelbundet uppdatera all mjukvara och dess beroenden. 

Så kan vi hjälpa dig 

Vi hittar förvånansvärt ofta ganska allvarliga brister i autentisering, sessionshantering eller behörighetskontroll i systemen som våra kunder anlitar oss för att hacka. Kontakta oss om du vill att vi ska leta efter liknande brister i dina system. 

Om du istället är intresserad av applikationssäkerhet och vill hjälpa våra kunder bli säkrare redan på arkitektur- och utvecklingsstadiet så söker vi nu applikationssäkerhetskonsulter