You are currently seeing the German version of our site. Do you want to switch to the English version?

Switch to English.

Java

Wie man den perfekten Java Supply Chain Angriffsvektor findet und wie er behoben wurde

Vielleicht hat man schon von den Änderungen zur Verarbeitung von Annotation in Java 23 gehört.
Dieser Post behandelt das zugrunde liegende Thema, wie ich es gefunden habe und wie es behoben wurde.

Hintergrund

Um zu verstehen wie Annotations-Verarbeitung funktioniert und warum es überhaupt existiert müssen wir zunächst in unseren DeLorean steigen und ins Jahr 1955 2005 zurückreisen.

Damals gab es also noch keine Build-Tools wie das heutzutags gängige Maven oder Gradle. Java wurde noch von Sun Microsystems entwickelt und die neueste Version war 5.

Java-Annotationen wurden mit Java 5 eingeführt, aber eine API für ihre Verarbeitung zur Build-Zeit gab es nicht und so wurde JSR 269 erschaffen.

Die generelle Idee dahinter scheint darin zu bestehen, die Generierung von (Boilerplate-)Code und ähnliche Operationen zu ermöglichen. Beispiele hierfür, die das Konzept heutzutage verwenden, sind wahrscheinlich Lombok oder Dagger (für die Android-Welt), aber diese werden normalerweise mit speziellen Plugins ausgeführt.
Ich habe ein altes Video von einer Präsentation zu diesem Thema gefunden - ist eventuell ganz interessant um zusätzliche Hintergrundinformationen zu erhalten.

JSR 269 wurde schließlich implementiert und 2006 mit Java 6 ausgeliefert.
Die meisten Details wurden im Laufe der Jahre wahrscheinlich ignoriert, da bessere Build-Tools wie Maven oder Gradle allmählich an Popularität gewannen.

Wie der Hund Compiler meine records fraß…

Zurück in die Gegenwart - naja fast: Bleiben wir an einem Tag Ende 2022 stehen.

Damals haben wir eines unserer größeren Maven-basierten Projekte auf Java 17 aktualisiert und ein paar Klassen in die neuen records konvertiert. Alles wurde ohne Probleme kompiliert, also haben wir das Projekt ausgeliefert.

Ein paar Tage später wurde dann erneut einen record hinzugefügt und beim Versuch diesen zu compilieren gab es auf einmal folgenden Crash:

[ERROR] org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.10.1:compile (default-compile) on project X: Compilation failure
...
Caused by: org.apache.maven.plugin.compiler.CompilationFailureException: Compilation failure
C:\...\ClassUsingARecord.java: Internal compiler error: java.lang.Exception: javax.lang.model.element.UnknownElementException: Unknown element: \"RecordClass\" 
    at org.eclipse.jdt.internal.compiler.apt.dispatch.RoundDispatcher.handleProcessor(RoundDispatcher.java:172)
    at org.apache.maven.plugin.compiler.AbstractCompilerMojo.execute (AbstractCompilerMojo.java:1310)
    at org.apache.maven.plugin.compiler.CompilerMojo.execute (CompilerMojo.java:198)
    ...
    at org.codehaus.classworlds.Launcher.main (Launcher.java:47)

Der Fehler sah ziemlich seltsam aus (ich hatte noch nie eine UnknownElementException vorher gesehen) und nach ein wenig Debugging kam ich zu den folgenden Schlussfolgerungen:

  • Ja, es wurde tatsächlich Java 17 verwendet - das records unterstützt - und nicht irgendeine andere Version.
  • Kollegen hatten das gleiche Problem mit dem gleichen Codestand
  • Das Problem trat bei keinem anderen Java-17-Projekt auf
  • Das Problem trat nur bei einigen wenigen Maven-Modulen auf, die das Schlüsselwort record scheinbar nicht mochten.

Nachdem mir die Ideen ausgingen, warf ich nochmal einen Blick auf den Stacktrace und stellte fest, dass die Ausnahme bei handleProcessor auftrat, was sich - nach einigen Nachforschungen - als ein Annotationsprozessor herausstellte.

Also deaktivieren wir also einfach alle Annotationsprozessoren und alles funktioniert wieder? Oder?

Gute Idee… Es gibt nur ein Problem:
Es sind keine Annotationsprozessoren vorhanden - zu mindestens wurden keine in der Konfiguration von maven-compiler-plugin angegeben.

Woher kam also der Annotationsprozessor und warum wurde er zur Buildzeit ausgeführt?

Wie sich herausstellte: In einem vorgelagerten Maven-Module - das sich innerhalb aller Module befand, die das Problem hatten - war hibernate-validator-annotation-processor als Abhängigkeit definiert.
Diese veraltete Library war jahrelang unbenutzt und wurde wahrscheinlich beim Aufräumen vergessen.
Die Library enthält einen Annotationsprozessor: ConstraintValidationProcessor und dieser Prozessor konnte keinen Java 17 Code verarbeiten.

Das Problem wurde durch das Entfernen der veralteten Library behoben, aber dass ein Annotationsprozessor irgendwie automatisch geladen und während der Buildzeit ausgeführt wird, ohne dass es jemand bemerkt, hat mich ein wenig verwirrt...

Down the rabbit hole

Am nächsten Tag recherchierte ich etwas weiter und entdeckte im Grunde alles, was bereits im Abschnitt „Hintergrund“ oben steht.

Naja nicht alles, denn tief in den JavaDocs fand sich folgendes:
> Unless annotation processing is disabled with the -proc:none option, the compiler searches for any annotation processors that are available. 
> The search path can be specified with the -processorpath option. If no path is specified, then the user class path is used. 
> Processors are located by means of service provider-configuration files named META-INF/services/javax.annotation.processing ... Java Docs

Jetzt machte das vorher beschriebene Verhalten auch Sinn (es war eine Java Service Loader-Datei innerhalb der Abhängigkeit/Library in Form von META-INF/services/javax.annotation.processing.Processor), aber die Beschreibung war immer noch etwas eigenartig da ich keine andere Programmiersprache/Compiler kenne der standardmäßig Code ausführt anstelle ihn zu kompilieren.

Der perfekte Angriffsvektor

Gleichzeitig begannen bei mir die Alarmglocken zu läuten - kurze Zeit vorher gab es eine Reihe von Supply-Chain Angriffe auf diverse NPM Pakete - und dies war im Grunde ein sehr ähnlicher Angriffsvektor:

Man kann sich nun folgendes Angriffszenario vorstellen:

  • Ein Projekt benutzt Library L
  • Automatische Abhängigkeitsupdates sind aktiviert. Zum Beispiel in der Form von Renovate oder Dependabot
  • Die durch die Updates erstellen PullRequests werden automatisch durch eine CI kompiliert
  • Jetzt gehen wir davon aus das Library L Opfer eines Supply-Chain Angriffs wird. Ein bösartiger Annotationsprozessor + Service Loader Datei wird in die Library eingefügt.
  • Eine neue bösartige Version von Library L wird veröffentlicht.
  • Das Abhängigkeitsupdate-Tool erkennt die neue Version und erstellt einen PullRequest.
  • Die CI - die nur den PR compiliert - wird möglicherweise während der Kompilierung kompromittiert.
    • Dies kann missbraucht werden um Secrets während des Builds zu stehlen, z.B. Zugriffsschlüssel für Paketmanager/Registries.
    • Wenn der Buildprozess nicht isoliert ist kann eine Menge mehr Schaden verursacht werden.
  • Ein Entwickler der das Update manuell prüft kann kompromittiert werden sobald er die Entwicklungsumgebung startet
    • Die Kompromittierung kann vermutlich nicht von einer (Datei-) Signaturen basierten Anti-Virus Software erkannt werden, da der Angriff durch den Compiler hindurch ausgeführt wird

Folgende Punkte machen das ganze ziemlich gefährlich:

  • Versteckte Ausführung
    • Es gibt keinerlei Indikator vom Compiler das nun ein Annotationsprozesor ausgeführt wird (vor Java 21)
      • Die meisten Entwickler kennen diese Verhalten wahrscheinlich nicht (keiner meiner Kollegen mit jahrelanger Java-Erfahrung kannte dieses Verhalten)
    • Da der Angriff zu Buildzeit ausgeführt wird, gibt es normalerweise keinerlei Spuren in den final gebauten Dateien
    • Eine bösartiger Prozessor kann perfekt zwischen Legitimen versteckt werden
    • Ohne ein manuelles Review jeder Abhängigkeit/Bibliothek (JAR-Dateien) ist es extrem unwahrscheinlich das irgendjemand oder irgendetwas jemals den Angriff bemerkt - vor allem in großen Projekten mit hunderten Abhängigkeiten
  • Die Ausführung passiert nahezu sofort
    • Der Annotationsprozessor wird während der Kompilierung ausgeführt - was in Entwicklungsumgebungen und CIs ständig passiert

We didn't start the fire... - Wie bekommen wir es gelöscht?

Schauen wir uns zunächst einmal an, was davon betroffen ist:

Hierzu habe ich einen dummy “bösartigen” Annotationsprozessor erstellt, der eine URL öffnet und den Buildprozess abbricht und diesen gegen unterschiedliche Dinge getestet:

  • Compiler
    • Java's standard compiler javac
    • Eclipse compiler ecj
  • Build Tools die diese Compiler benutzen
    • Maven's maven-compiler-plugin
    • Gradle ist vermutlich nicht betroffen, da es die Java Toolchain benutzt und Annotationsprozessoren explizit deklariert werden müssen
  • Entwicklungsumgebungen
    • Die Auswirkungen auf IntelliJ IDEA variieren je nach Build Tool.
      Führt man ein Maven build mit der obigen Demo aus crasht der Buildprozess.
    • Soweit mir bekannt ist führt Eclipse standardmäßig keine Annotationprozessoren aus.
      Sobald man diese jedoch aktiviert crasht die komplette Entwicklungsumgebung da der Buildprozess nicht isoliert ist.
Kurzfristige Fixes und Workarounds

Zum Glück gibt es ein Argument das Annotationsprozessoren währen der Kompilierung abschaltet: 
-proc:none

Dieses einfach in den Compiler Argumenten mit hinzufügen und alles sollte so wie vorher funktionieren.

Beispiel für Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>-proc:none</arg>
        </compilerArgs>
    </configuration>
</plugin>

Falls man trotzdem einen Annotationsprozessor ausführen muss:
Explizit den Annotationsprozessor in einen zusätzlichen Compile-Schritt ausführen.

Beispiel für Maven mit dem maven-processor-plugin:

<plugin>
   <groupId>org.bsc.maven</groupId>
   <artifactId>maven-processor-plugin</artifactId>
   <executions>
       <execution>
           <id>process</id>
           <goals>
               <goal>process</goal>
           </goals>
           <phase>generate-sources</phase>
           <configuration>
               <processors>
                   <processor>org.hibernate.processor.HibernateProcessor</processor>
               </processors>
           </configuration>
       </execution>
   </executions>
</plugin>

Für Entwicklungsumgebungen kann man Annotationsprozessoren üblicherweise in den Einstellungen de-/aktivieren.
IntelliJ IDEA übernimmt z.B. die Einstellungen automatisch aus Maven Projekten.

Globale Problembehebung

Parallel zum Ausrollen der obigen Korrekturen wurde auch ein JDK bug report erstellt, der nach einiger Zeit als erhalten markiert wurde.

Das folgende ist seitdem passiert:

Issue BeschreibungJava Version
JDK-8310061Wenn implizites Annotationsverarbeiten erkannt wurde wird eine Notiz vom Compiler ausgegeben21+
JDK-8308245Aus Paritätsgründen wurde die -proc:full-Option in ältere Java LTS Versionen zurückportiert8, 11, 17
JDK-8321319Annotationsverarbeiten standardmäßig deaktiviert23+

Geschrieben von Blex

Kontakt