Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
shiro — CVE-2026-49268 — Analyse und Behebung einer LDAP-Injection-Authentifizierungsumgehungs-Schwachstelle | Kitploit
Tools/GitHubGitHub/sassoftware/shiro
Statische Code-Analyse (SAST)SchwachstellenanalyseCode-AnalyseWebsicherheitAuthentifizierungLernen & Bildung
GitHubsassoftware/shiro

shiro

CVE-2026-49268 — Analyse und Behebung einer LDAP-Injection-Authentifizierungsumgehungs-Schwachstelle

Repository anzeigen
1vor 6 TagenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

CVE-2026-49268 — Analyse und Behebung einer LDAP-Injection-Authentifizierungsumgehung

Branch1.13-CVE-2026-49268
AutorJinwoo Hwang (https://JinwooHwang.com)

Dieser Branch (1.13-CVE-2026-49268) enthält eine umfassende Sicherheitsbehebung einer LDAP-Injection-Authentifizierungsumgehung in der Apache Shiro 1.13-Version.

Offizielle NVD-Beschreibung

Ein entfernter Angreifer kann LDAP-Sonderzeichen in die Konstruktion des Distinguished Name (DN) in der Klasse DefaultLdapRealm einschleusen. Vom Benutzer bereitgestellte Benutzernamen-Eingaben werden direkt in die LDAP-DN- Vorlage eingefügt, ohne dass RFC 2253-Sonderzeichen maskiert werden. Dies ermöglicht es einem Angreifer, die DN-Struktur zu manipulieren, die für die LDAP-Bind-Authentifizierung verwendet wird, wodurch möglicherweise die Authentifizierung umgangen oder die Identität anderer Benutzer angenommen werden kann. Dieses Problem betrifft alle Apache Shiro-Versionen bis 2.2.0 und 3.0.0-alpha-1, wenn DefaultLdapRealm verwendet wird.


1. Übersicht der Schwachstelle


2. Zusammenfassung für die Geschäftsleitung

Apache Shiro kann Benutzer gegenüber einem LDAP-Verzeichnis authentifizieren. Dazu wandelt es einen übermittelten Benutzernamen in einen Distinguished Name um — die Adresse des Verzeichnisses für den Eintrag dieses Benutzers —, indem der Benutzername in eine konfigurierte Vorlage eingesetzt wird, zum Beispiel uid={0},ou=users,dc=mycompany,dc=com.

In den betroffenen Versionen wird der Benutzername als reiner Text in diese Vorlage eingefügt. Eine Handvoll Satzzeichen — allen voran das Komma — sind für einen Verzeichnisserver keine gewöhnlichen Textzeichen; sie sind die Syntax, die einen Teil einer Adresse vom nächsten trennt. Ein Benutzername, der diese Zeichen enthält, verhält sich daher nicht mehr wie ein Wert innerhalb der Adresse, sondern beginnt, sich wie ein Teil der Adresse selbst zu verhalten.

Die praktische Konsequenz ist, dass eine sich anmeldende Person beeinflussen kann, gegen welchen Verzeichniseintrag Shiro zu authentifizieren versucht, anstatt nur ihren eigenen Namen anzugeben. Statt in dem Container nachgeschlagen zu werden, den die Bereitstellung vorgesehen hat, kann die Suche an eine andere Stelle im Verzeichnis umgeleitet werden. Je nachdem, wie das Verzeichnis aufgebaut ist und was es zulässt, kann dies dazu führen, dass man sich als die falsche Identität authentifiziert oder dass die Authentifizierung erfolgreich ist, obwohl sie es nicht sein sollte.

Risikoprofil. Was das Risiko erhöht: Es sind keine vorherigen Zugriffsrechte oder Anmeldedaten erforderlich — die Eingabe erreicht die Anmeldegrenze, die für jeden erreichbar ist, der die Anwendung erreichen kann; und die betroffene Klasse ist die standardmäßige, dokumentierte Art, Shiro mit LDAP zu verbinden, sodass dies keine exotische Konfiguration ist. Was es verringert: Die Bereitstellung muss tatsächlich DefaultLdapRealm (oder JndiLdapRealm) mit einer konfigurierten DN-Vorlage verwenden; Bereitstellungen, die sich auf andere Weise authentifizieren oder die einen vollständigen DN oder eine nicht-textuelle Anmeldeinformation wie ein Zertifikat übergeben, sind nicht betroffen. Ob eine umgeleitete Suche eine nutzbare Authentifizierung ergibt, hängt auch vom eigenen Aufbau und den Zugriffsregeln des Zielverzeichnisses ab, die von Standort zu Standort variieren.

Eine zweite, nicht sicherheitsrelevante Konsequenz ist für die Release-Planung erwähnenswert: Benutzer, deren legitime Benutzernamen einen Backslash enthalten oder mit # beginnen, können sich derzeit überhaupt nicht anmelden, weil die für sie erstellte Adresse kein wohlgeformter Name ist und rundheraus abgelehnt wird. Andere Satzzeichen führen nicht zu einem direkten Fehlschlag — sie ändern stillschweigend, auf welchen Eintrag die Adresse verweist, was das obige Sicherheitsproblem ist. Dieselbe Korrektur behebt beides.


3. Ursachenanalyse

3.1 Der Defekt

core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java, getUserDn(String), Zeilen 227–250 auf der ungepatchten Basisversion:```java protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException { if (!StringUtils.hasText(principal)) { throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction."); } String prefix = getUserDnPrefix(); String suffix = getUserDnSuffix(); if (prefix == null && suffix == null) { log.debug("userDnTemplate property has not been configured, indicating the submitted " + "AuthenticationToken's principal is the same as the User DN. Returning the method argument " + "as is."); return principal; }

root@kitploit:~
int prefixLength = prefix != null ? prefix.length() : 0;
int suffixLength = suffix != null ? suffix.length() : 0;
StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
if (prefixLength > 0) {
    sb.append(prefix);
}
sb.append(principal);            // <-- inserted verbatim
if (suffixLength > 0) {
    sb.append(suffix);
}
return sb.toString();

}

root@kitploit:~
Das Template wird zur Konfigurationszeit einmal um das `{0}`-Token herum aufgeteilt
(`setUserDnTemplate`, Zeilen 181–200), in ein `prefix` und ein `suffix`. Zum Authentifizierungszeitpunkt
wird der Principal dazwischen konkateniert. **Zu keinem Zeitpunkt wird eine Kodierung angewendet.** Es gibt
keinen Escaping-Helfer irgendwo in `org/apache/shiro/realm/ldap/`.

### 3.2 Die Invariante, die angenommen, aber nie durchgesetzt wurde

Der umgebende Code behandelt das Ergebnis von `getUserDn` als wohlgeformten Distinguished Name —
er wird direkt an `LdapContextFactory.getLdapContext(...)` und von dort an JNDI als
Bind-DN übergeben. Das ist nur dann korrekt, wenn der substituierte Principal ein *einzelner Attributwert* ist.

String-Konkatenation kann das nicht durchsetzen. RFC 2253 (und RFC 4514) reservieren
`,` `+` `"` `\` `<` `>` `;` `=`, ein führendes `#` sowie führenden/nachfolgenden Leerraum als strukturelle
Syntax innerhalb eines DN. Wenn eines davon im Principal auftritt, wird es vom Parser als
Struktur gelesen, nicht als Inhalt. Die Invariante — *„der Principal belegt genau einen RDN-Wert"* —
wurde von jedem nachgelagerten Konsumenten angenommen und von keinem durchgesetzt.

### 3.3 Ausführungsfluss, Einstiegspunkt bis zum Defekt```
  submitted credentials (username, password)
        │
        ▼
  DefaultLdapRealm.doGetAuthenticationInfo(AuthenticationToken)        [line 292]
        │
        ▼
  DefaultLdapRealm.getLdapPrincipal(AuthenticationToken)               [line 338]
        │   principal instanceof String ?
        │       ├── no  ──► return principal unchanged   ── NOT AFFECTED (e.g. X.509)
        │       └── yes ──┐
        ▼                 │
  DefaultLdapRealm.getUserDn(String)                                   [line 227]
        │   prefix == null && suffix == null ?
        │       ├── yes ──► return principal unchanged   ── NOT AFFECTED ("principal IS the DN")
        │       └── no  ──┐
        ▼                 │
  prefix + principal + suffix          ◄── DEFECT: unencoded concatenation  [line 246]
        │
        ▼
  LdapContextFactory.getLdapContext(userDn, credentials)
        │
        ▼
  JNDI bind against the directory using the constructed DN

3.4 Nachgewiesene Wirkung

Template uid={0},ou=users,dc=mycompany,dc=com, Principal jsmith,ou=admins:

Konstruierter DNGeparste RDNs
Beabsichtigtuid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com4
Baseline (ungefixt)uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com5

Der Name erhält eine zusätzliche Komponente. ou=users ist nicht mehr der adressierte Container — ou=admins wird dazwischengeschoben. Verifiziert durch Parsen mit javax.naming.ldap.LdapName, das unabhängig von jeder Escaping-Implementierung ist.

Gemessenes Verhalten der Baseline über die reservierte Menge (JDK 8, LdapName-Parse):

Zwei Zeichen verändern die Struktur des Namens, nicht nur seinen Inhalt: das Komma und das Semikolon — welches RFC 1779 als alternativen RDN-Trenner akzeptiert. + führt einen zweiten Attributwert in denselben RDN ein. Nur \ und ein führendes # machen den Namen unparsebar.

3.5 Angrenzender Code, der korrekt ist, und warum — Blast Radius

  • userDnTemplate nicht gesetzt. getUserDn gibt den Principal unverändert zurück. Dies ist der dokumentierte Modus „der übermittelte Principal ist der DN"; der Aufrufer liefert einen vollständigen DN und ist für dessen Korrektheit verantwortlich. Nicht betroffen.
  • Nicht-String-Principals. getLdapPrincipal leitet nur String-Principals in getUserDn; alles andere (X.509-Zertifikate, benutzerdefinierte Tokens) wird durchgereicht. Nicht betroffen.
  • setUserDnTemplate-Validierung (Zeilen 181–200) lehnt korrekt ein null-, leeres oder {0}-loses Template ab. Sie validiert das vom Betreiber bereitgestellte Template, welches nie die nicht vertrauenswürdige Eingabe war — es ist also korrekter Code, der diesen Defekt schlicht nicht adressiert.
  • JndiLdapRealm erweitert DefaultLdapRealm und überschreibt getUserDn nicht, erbt also den Defekt. Bestätigt durch Test (§6.1). Im Geltungsbereich, und durch dieselbe Änderung behoben.

3.6 Verwandter Pfad — BEWERTET, NICHT BETROFFEN

AbstractLdapRealm.searchFilter (Zeile 89) enthält eine zweite {0}-Substitution, standardmäßig (&(objectClass=*)(userPrincipalName={0})), verwendet auf dem Autorisierungs-Pfad. Er ist nicht betroffen.

Ein Durchlauf über jeden search(-Aufruf, jeden getLdapContext(-Aufrufer und jedes LDAP-förmige String-Literal über die Hauptquellen von core, support und web fand genau eine Verwendung von searchFilter — ActiveDirectoryRealm.getRoleNamesForUser, Zeile 172:```java Object[] searchArguments = new Object[]{userPrincipalName}; NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);

root@kitploit:~
Dies ist die **parametrisierte** `DirContext.search(String, String, Object[], SearchControls)`-
Überladung. Der Benutzername wird als Filter-*Argument* übergeben, niemals in die Filter-
Zeichenkette konkateniert. Gemäß dem JDK 8 `javax.naming.directory.DirContext`-Javadoc, wörtlich:

> "Wenn ein zeichenkettenwertiges Filterargument für eine Variable eingesetzt wird, wird der Filter
> so interpretiert, als wäre die Zeichenkette anstelle der Variable angegeben worden, wobei alle
> Zeichen, die innerhalb von Filtern eine besondere Bedeutung haben (wie `'*'`), gemäß
> den Regeln von RFC 2254 maskiert wurden."

Der JNDI-Provider führt die Maskierung durch. Der historische Beleg hierfür findet sich in der
Felddeklaration selbst.

**Quelle: `core/src/main/java/org/apache/shiro/realm/ldap/AbstractLdapRealm.java`, Zeilen 88–89**```java
    //SHIRO-115 - prevent potential code injection:
    protected String searchFilter = "(&(objectClass=*)(userPrincipalName={0}))";

Quelle: core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java, Zeilen 158–172```java protected Set getRoleNamesForUser(String username, LdapContext ldapContext) throws NamingException { Set roleNames; roleNames = new LinkedHashSet();

root@kitploit:~
    SearchControls searchCtls = new SearchControls();
    searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    String userPrincipalName = username;
    if (principalSuffix != null && !userPrincipalName.toLowerCase(Locale.ROOT).endsWith(principalSuffix.toLowerCase(Locale.ROOT))) {
        userPrincipalName += principalSuffix;
    }

    Object[] searchArguments = new Object[]{userPrincipalName};

    NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
root@kitploit:~
Der Benutzername erreicht das Verzeichnis als `searchArguments[0]`, niemals als Text, der in
`searchFilter` eingefügt wird. Das `{0}`-Token im Filter wird vom JNDI-Provider aufgelöst, nicht von
Shiro.

`ActiveDirectoryRealm` Zeile 108 übergibt den rohen Benutzernamen an
`getLdapContext(username, password)`. Dieser Wert wird zu einem einzelnen JNDI-
`SECURITY_PRINCIPAL`-Umgebungseintrag; er wird nicht in eine Vorlage eingesetzt und es wird kein DN
daraus konstruiert, sodass er außerhalb des Mechanismus dieses Defekts liegt.

**Empirisch gegen einen Live-Verzeichnisserver verifiziert.** Das JNDI-Escaping wurde nicht
auf Vertrauen hin angenommen: es wurde auf der Leitung beobachtet. Ein In-Process-LDAP-Server
(UnboundID `InMemoryDirectoryServer`) wurde mit einem
`InMemoryOperationInterceptor` instrumentiert, der den Filter jeder empfangenen Suchanfrage aufzeichnet,
und Shiros Standard-`searchFilter` wurde über denselben parametrisierten JNDI-Aufruf ausgegeben, der
von `ActiveDirectoryRealm` verwendet wird, mit einem präparierten Argument:```
filter template : (&(objectClass=*)(userPrincipalName={0}))
argument passed : *)(uid=jsmith
server received : (&(objectClass=*)(userPrincipalName=\2a\29\28uid=jsmith))

Das *, ) und ( erreichten den Server als \2a, \29 und \28 — gemäß RFC 2254 escaped. Der Filter behält genau zwei Klauseln bei; das Argument konnte keine dritte hinzufügen.

Zur Kontrolle: dasselbe Template, wobei das Argument als Rohtext eingefügt wird, anstatt als Filterargument übergeben zu werden:``` CONTROL, raw splice: (&(objectClass=)(userPrincipalName=)(uid=jsmith)) server received : (&(objectClass=)(userPrincipalName=)(uid=jsmith))

root@kitploit:~
Die Kontrolle erreicht den Server mit veränderter Struktur — eine dritte Klausel hinzugefügt und der `userPrincipalName`-Test auf einen Wildcard reduziert. Die parametrisierte Form nicht. Dies bestätigt, durch Beobachtung statt durch Vertrag, dass der `searchFilter`-Pfad nicht betroffen ist.

---

## 4. Schritt-für-Schritt-Reproduktionsschritte

Deterministisch, aus einem sauberen Checkout. Das Arbeitsverzeichnis ist durchgehend das Repository-Stammverzeichnis.

### 4.1 Voraussetzungen

| Anforderung | Verwendeter Wert |
|---|---|
| JDK | **8** — der Branch setzt `jdk.version` 1.8. Die untenstehenden Ergebnisse wurden mit Zulu 1.8.0_432 (arm64) erzeugt. Richten Sie `JAVA_HOME` auf eine JDK-8-Installation, bevor Sie einen Befehl in diesem Abschnitt ausführen: |
| Build | Apache Maven, Netzwerkzugriff erforderlich (Parent-POM `org.apache:apache:38`) |
| Baseline | `origin/1.13.x` |
| Konfiguration | `userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com` |
| Verzeichnisserver | **Nicht erforderlich für §4.3-§4.4** — der Defekt liegt in der DN-Konstruktion, vor jedem Netzwerkaufruf, daher mocken diese Schritte `LdapContextFactory`. §4.5 reproduziert zusätzlich den gesamten Pfad gegen einen **echten** LDAP-Server über HTTP. |

`JAVA_HOME` auf eine JDK-8-Installation setzen:```bash
# macOS
export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

# Linux (path varies by distribution and vendor)
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

# Windows (cmd)
set JAVA_HOME=C:\Program Files\Zulu\zulu-8

Mit mvn -v bestätigen, was den JDK meldet, den Maven verwenden wird. Die folgenden Befehle gehen davon aus, dass dies bereits festgelegt ist.

4.2 Die ungefixte Baseline beziehen```bash

git clone https://github.com/apache/shiro.git cd shiro git checkout -b repro origin/1.13.x

root@kitploit:~
### 4.3 Den Defekt direkt beobachten

Dies ist der minimale Auslöser, unabhängig von Shiros Build. Schreiben Sie `Repro.java` in ein
Scratch-Verzeichnis:```java
import javax.naming.ldap.LdapName;

public class Repro {
    // reproduces DefaultLdapRealm.getUserDn line-for-line
    static String getUserDn(String prefix, String principal, String suffix) {
        StringBuilder sb = new StringBuilder(prefix.length() + principal.length() + suffix.length());
        sb.append(prefix);
        sb.append(principal);
        sb.append(suffix);
        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        String prefix = "uid=";
        String suffix = ",ou=users,dc=mycompany,dc=com";

        String benign = getUserDn(prefix, "jsmith", suffix);
        System.out.println("benign : " + benign + "  -> RDNs=" + new LdapName(benign).size());

        String crafted = getUserDn(prefix, "jsmith,ou=admins", suffix);
        System.out.println("crafted: " + crafted + "  -> RDNs=" + new LdapName(crafted).size());
    }
}

Führen Sie es aus:```bash javac Repro.java && java Repro

root@kitploit:~
Beobachtete Ausgabe:```
benign : uid=jsmith,ou=users,dc=mycompany,dc=com  -> RDNs=4
crafted: uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com  -> RDNs=5

Die Komponentenanzahl ändert sich von 4 auf 5. Der übermittelte Wert ist zu einer Namensstruktur geworden.

4.4 Reproduktion über Shiros eigene API

Wenden Sie vom Repository-Stammverzeichnis auf der ungepatchten Baseline die Tests aus §6.3 an und führen Sie aus:```bash mvn -B clean verify

root@kitploit:~
Der Build stoppt bei `Apache Shiro :: Core`, wobei die beweisenden Tests fehlschlagen — siehe §6.1 für die
vollständige aufgezeichnete Ausgabe:```
DefaultLdapRealmTest   Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest      Tests run: 16, Failures: 4, Errors: 0, Skipped: 0

4.5 End-to-End-Reproduktion über HTTP gegen einen Live-Verzeichnisserver

Reproduziert über den gesamten Stack — eine echte HTTP-Anfrage, ein echter Servlet-Container, Shiros eigener FormAuthenticationFilter und ein echter LDAP-Server. Auf keiner Ebene wird etwas gemockt.

Verzeichnis-Fixture — ein verschachtelter privilegierter Container, ein gängiges reales Layout:``` dc=mycompany,dc=com └── ou=users ├── uid=jsmith userPassword: userpass (ordinary account) └── ou=admins └── uid=jsmith userPassword: adminpass (privileged account)

root@kitploit:~
#### 4.5.1 Betroffener Build

**Anfrage**```http
POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded

username=jsmith%2Cou%3Dadmins&password=adminpass

Antwort```http HTTP/1.1 302 Found Location: http://127.0.0.1/;jsessionid=node0qu3v2n612vy71alsuyir2bgzz1.node0

root@kitploit:~
**Bind-DN, den das Verzeichnis empfangen hat**```
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com

Die 302 ist der Erfolgs-Redirect des FormAuthenticationFilter: Die Anfrage wurde authentifiziert. Der DN trägt fünf Komponenten, wo das Template vier definiert, und der erreichte Eintrag ist der privilegierte unter ou=admins.

Kontrolle — derselbe Benutzername mit dem Passwort des gewöhnlichen Kontos```http POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded

username=jsmith%2Cou%3Dadmins&password=userpass

root@kitploit:~
| `--no-ssl` | Deaktiviert die SSL-Überprüfung |
| `--no-redirect` | Deaktiviert die Weiterleitung |
| `--timeout` | Timeout in Sekunden |
| `--retries` | Anzahl der Wiederholungsversuche |
| `--proxy` | Proxy-URL |
| `--user-agent` | Benutzerdefinierter User-Agent |
| `--headers` | Benutzerdefinierte Header |
| `--cookies` | Benutzerdefinierte Cookies |
| `--data` | POST-Daten |
| `--json` | JSON-Daten |
| `--form` | Formulardaten |
| `--output` | Ausgabedatei |
| `--verbose` | Ausführliche Ausgabe |
| `--silent` | Stille Ausgabe |
| `--debug` | Debug-Ausgabe |
| `--version` | Versionsinformationen |
| `--help` | Hilfeinformationen |```http
HTTP/1.1 200 OK

Abgelehnt. Dies ist es, was eine Identitätsanmaßung statt eines Zufalls belegt: Der konstruierte Benutzername authentifiziert sich mit dem Passwort des privilegierten Kontos und schlägt mit dem des gewöhnlichen Benutzers fehl, sodass die geprüften Anmeldedaten zu einem anderen Verzeichniseintrag gehören als dem, den die Vorlage adressiert.

4.5.2 Behobener Build — identische Anfragen

Vom Verzeichnis empfangener Bind-DN für den konstruierten Benutzernamen:``` affected : uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com (5 components) remediated : uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com (4 components)

root@kitploit:~
Normale Anmeldungen bleiben unverändert — Zeile 1 authentifiziert sich auf beiden Builds identisch, wobei das
Verzeichnis `uid=jsmith,ou=users,dc=mycompany,dc=com` erhält.

Beide Läufe verwendeten dieselbe Testumgebung und dasselbe Fixture; nur `shiro-core` im Classpath
unterschied sich. Die tatsächlich geladene Klasse wurde mit `-verbose:class` für jeden Lauf bestätigt.

---

## 5. Details zur Behebung & Behebungscode

### 5.1 Primäre Korrektur

Kodiere den Principal als einzelnen DN-Attributwert vor der Substitution, unter Verwendung des
JDK-eigenen RFC 2253-Encoders, `javax.naming.ldap.Rdn.escapeValue`. Dies ist derselbe Mechanismus, den
das JDK verwendet, um Namen zu erstellen, sodass seine Ausgabe konstruktionsbedingt mit dem Parser konsistent ist, der sie verarbeitet.

**Datei:** `core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java````diff
@@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
 import javax.naming.AuthenticationNotSupportedException;
 import javax.naming.NamingException;
 import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.Rdn;
 
 /**
  * An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's
@@ -239,11 +240,14 @@ public class DefaultLdapRealm extends AuthorizingRealm {
 
         int prefixLength = prefix != null ? prefix.length() : 0;
         int suffixLength = suffix != null ? suffix.length() : 0;
-        StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
+        //the principal is a single attribute value within the resulting name, so it is encoded
+        //to keep any characters that are significant in a Distinguished Name within that value:
+        String value = Rdn.escapeValue(principal);
+        StringBuilder sb = new StringBuilder(prefixLength + value.length() + suffixLength);
         if (prefixLength > 0) {
             sb.append(prefix);
         }
-        sb.append(principal);
+        sb.append(value);
         if (suffixLength > 0) {
             sb.append(suffix);
         }

Was der Hunk erzwingt. Rdn.escapeValue maskiert mit Backslash die in RFC 2253 reservierte Menge (\ , = + < > # ; ") sowie führenden und nachfolgenden Whitespace. Danach kann der ersetzte Text nur noch einen einzigen RDN-Wert belegen: Der Parser liest die maskierten Zeichen als Inhalt, sodass die Komponentenanzahl des Ergebnisses durch das Template festgelegt ist und nicht vom Principal beeinflusst werden kann. Der Kapazitätshinweis des StringBuilder wird auf die kodierte Länge aktualisiert — ein Größen- detail, kein verhaltensrelevantes.

Platzierung. Die Kodierung sitzt nach dem frühen Return für den Fall des nicht konfigurierten Templates. Das ist beabsichtigt: In diesem Modus liefert der Aufrufer einen vollständigen DN, und dessen Kodierung würde ihn beschädigen. Beide Zweige behalten ihre bestehenden Verträge.

5.2 Verhaltensänderung für legitime Aufrufer

Dieser Fix ändert den DN-String, der für jeden Principal erzeugt wird, der ein reserviertes Zeichen enthält. Drei Konsequenzen, die klar benannt werden sollten:

  1. Zuvor fehlerhafte Logins funktionieren jetzt. Benutzernamen, die einen Backslash enthalten oder mit # beginnen, erzeugten einen DN, der kein wohlgeformter Name war, sodass überhaupt kein Bind versucht werden konnte. Sie werden jetzt zu einem gültigen DN kodiert und es wird ein Bind versucht. Sites können sehen, dass sich Konten authentifizieren, die dies zuvor nicht konnten — ein Fix, aber eine sichtbare Änderung. Benutzernamen, die ,, ;, + oder = enthalten, wurden zuvor nicht abgelehnt; sie wurden zum falschen Eintrag aufgelöst und werden jetzt zum beabsichtigten aufgelöst.
  2. An das Verzeichnis gesendete Bind-DNs unterscheiden sich für betroffene Benutzernamen. Verzeichnisseitige Logs, Audit-Trails und jedes Log-Scraping, das auf literale DN-Strings matcht, werden maskierte Formen sehen (uid=jsmith\,ou\=admins,...). Es ist keine Konfigurationsänderung erforderlich.
  3. Deployments, die sich auf das alte Verhalten verlassen haben, um mehrkomponentige DNs aus dem Benutzernamenfeld zu konstruieren, würden brechen. Dies ist keine unterstützte Konfiguration — das Template existiert, um Struktur zu definieren —, aber es ist die einzige Migrationsgefahr, und es ist dasselbe Verhalten, das der Fix beseitigen soll.

getUserDnTemplate() ist als getUserDn("{0}") implementiert. { und } sind in RFC 2253 nicht reserviert, sodass der Rückgabewert des Accessors unverändert bleibt. Bestätigt durch den bereits vorhandenen testUserDnTemplate, der unverändert besteht.


6. Verifikation & Testen

6.1 Vorher — Baseline, ungefixt

Branch 1.13-CVE-2026-49268, JDK Zulu 1.8.0_432. Befehl wie in §4.4.``` DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0

root@kitploit:~
Erfasste Fehlerausgabe — dies ist der Beweis, und er ist nicht wiederherstellbar, sobald die Korrektur vorgenommen wurde:```
testGetUserDnPreservesTemplateStructure:238
  User DN gained or lost components relative to the template:
  uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
  expected:<4> but was:<5>

testGetUserDnPreservesTemplateStructureForReservedCharacters:262
  Component count changed for principal [jsmith,ou=admins]:
  uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
  expected:<4> but was:<5>

testGetUserDnEncodesSubstitutedValue:280
  expected:<uid=jsmith[\,ou\]=admins,ou=users,dc=...>
   but was:<uid=jsmith[,ou]=admins,ou=users,dc=...>

testUserDnTemplateSubstitutionPreservesStructure:300
  Unexpected method call
    LdapContextFactory.getLdapContext("uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com", ...)
  expected:
    LdapContextFactory.getLdapContext("uid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com", ...)
    expected: 1, actual: 0

testGetUserDnLeavesOrdinaryPrincipalUnchanged hat auf der Baseline bestanden, wie beabsichtigt — es ist eine Absicherung gegen Überkorrektur, kein beweisender Test.

6.2 Danach — mit angewendetem Fix

Gleicher Befehl, gleiches JDK:``` DefaultLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0

root@kitploit:~
Alle vier Beweistests bestehen nun auf beiden Klassen. Ein erneutes Ausführen von §4.3 mit dem Fix liefert `RDNs=4` für den präparierten Principal.

### 6.3 Hinzugefügte Tests

**Datei:** `core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java`
(+124 Zeilen, 0 Löschungen). Da `JndiLdapRealmTest extends DefaultLdapRealmTest`, wird jeder
untenstehende Test gegen **beide** Realms ausgeführt — 10 Ausführungen aus 5 Methoden.

| Test | Prüft | Baseline |
|---|---|---|
| `testGetUserDnLeavesOrdinaryPrincipalUnchanged` | Ein gewöhnlicher Principal liefert den exakt erwarteten DN, 4 RDNs, Wert intakt. Schützt vor übermäßigem Escaping. | **bestanden** (Guard) |
| `testGetUserDnPreservesTemplateStructure` | Für `jsmith,ou=admins` hat der DN weiterhin 4 Komponenten und der Principal überlebt als ein Attributwert. | fehlgeschlagen |
| `testGetUserDnPreservesTemplateStructureForReservedCharacters` | Dasselbe, über alle 10 Principals mit reservierten Zeichen; lässt den Test ebenfalls fehlschlagen, wenn der DN kein wohlgeformter Name ist. | fehlgeschlagen |
| `testGetUserDnEncodesSubstitutedValue` | Der substituierte Wert wird so kodiert, dass er zum eingereichten Principal zurückgeführt wird. | fehlgeschlagen |
| `testUserDnTemplateSubstitutionPreservesStructure` | End-to-End durch `getAuthenticationInfo`: Der an `LdapContextFactory` übergebene DN behält die Struktur der Vorlage bei. | fehlgeschlagen |

**Design-Hinweis.** Die strukturellen Assertions parsen das Ergebnis mit `javax.naming.ldap.LdapName`
und vergleichen Komponentenanzahl und den zurückgeführten Blattwert, anstatt einen
erwarteten escaped String zu prüfen. Die Tests verifizieren daher die tatsächlich erforderliche
Eigenschaft und setzen `Rdn.escapeValue` nicht als Implementierung voraus — ein alternativer
korrekter Encoder würde ebenfalls bestehen.

Befehl:```bash
mvn -B clean verify

6.4 Regression — vollständiger Build

Der vollständige Reactor-Build läuft mit keinen Flags und keinen Überspringungen durch:``` mvn -B clean verify

root@kitploit:~
**BUILD SUCCESS — 907 Tests, 0 Fehler, 0 Errors, 3 übersprungen, über den gesamten Reactor.**
Jedes Gate ist in diesem Lauf aktiv: Unit-Tests (surefire), Integrationstests (failsafe),
das Apache-RAT-Lizenzaudit, maven-enforcer und japicmp.

| Gate | Ergebnis |
|---|---|
| Unit-Tests, gesamter Reactor | **907 ausgeführt, 0 Fehler, 0 Errors, 3 übersprungen** |
| nur `core`-Modul | **321 ausgeführt, 0 Fehler, 0 Errors, 0 übersprungen** |
| LDAP-Tests (`DefaultLdapRealmTest` + `JndiLdapRealmTest`) | **32 ausgeführt, 0 Fehler** |
| Integrationstests (failsafe) | über den Reactor ausgeführt, keine Fehler |
| Apache-RAT-Lizenzaudit | Nicht genehmigt: 0, unbekannt: 0 |
| maven-enforcer | keine Verstöße |
| japicmp | keine Inkompatibilität gemeldet |

Die 3 Übersprungenen sind bereits vorhandene `@Ignore`s in Modulen, die nichts mit dieser Änderung zu tun haben; `core` — das
einzige berührte Modul — überspringt nichts.

## Referenzen

- CVE-Eintrag (MITRE API): `https://cveawg.mitre.org/api/cve/CVE-2026-49268`
- Upstream-Ankündigung: `https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s`
- Apache Shiro Sicherheitsberichte: `https://shiro.apache.org/security-reports.html`
- RFC 2253 / RFC 4514 — LDAP-String-Repräsentation von Distinguished Names
- RFC 4515 — LDAP-Suchfilter-String-Repräsentation

## Kontakt

Autor für Sicherheitslücken-Forschung und -Behebung: Jinwoo Hwang ([https://JinwooHwang.com](https://jinwoohwang.com/))
Tool herunterladen
FeldWert
CVECVE-2026-49268 — Apache Shiro: LDAP-DN-Injection in DefaultLdapRealm
CWE-IDCWE-90 — Unsachgemäße Neutralisierung von Sonderelementen in einer LDAP-Abfrage ('LDAP-Injection')
CVSS v4.08.8 HOCH — CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/S:P/AU:Y/R:A/RE:L/U:Red
CVSS v3.19.1 KRITISCH — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Betroffene Versionenorg.apache.shiro:shiro-core 0 bis einschließlich 2.2.0; 3.0.0-alpha-0 bis einschließlich 3.0.0-alpha-1
Upstream behoben in2.2.1 und 3.0.0-alpha-2
Referenzhttps://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s
PrincipalBaseline-ErgebnisWirkung
jsmith,ou=adminsparst, 5 RDNsStruktur verändert — ein Container wird dazwischengeschoben
jsmith;ou=adminsparst, 5 RDNsStruktur verändert — ; ist ein RDN-Trenner gemäß RFC 1779
jsmith+uid=adminparst, 4 RDNswird zu einem mehrwertigen RDN; uid erhält einen zweiten Wert
jsmith=adminparst, 4 RDNsWert korrumpiert
quo"te, angle<br>ackets, leadingSpace, trailingSpace parsen, 4 RDNsWert korrumpiert
back\slashIllegalArgumentExceptionfehlerhaft — Bind kann nicht versucht werden
#leadingNumberSignIllegalArgumentExceptionfehlerhaft — Bind kann nicht versucht werden
EbeneKomponente
HTTP-ClientHttpURLConnection, roher Formular-POST
Servlet-ContainerEmbedded Jetty 9.4.58.v20250814
SicherheitsfilterShiroFilter + EnvironmentLoaderListener, authc (FormAuthenticationFilter)
RealmDefaultLdapRealm, userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
VerzeichnisUnboundID InMemoryDirectoryServer, instrumentiert zur Aufzeichnung jedes Bind-DN
AnfrageBetroffenBehoben
username=jsmith&password=userpass302 authentifiziert302 authentifiziert
username=jsmith%2Cou%3Dadmins&password=adminpass302 authentifiziert200 abgelehnt
username=jsmith%2Cou%3Dadmins&password=userpass200 abgelehnt200 abgelehnt