Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
shiro — CVE-2026-49268 — Analyse et remédiation d'une vulnérabilité de contournement d'authentification par injection LDAP | Kitploit
Outils/GitHubGitHub/sassoftware/shiro
Analyse Statique de Code (SAST)Analyse des VulnérabilitésAnalyse de CodeSécurité WebAuthentificationApprentissage et Éducation
GitHubsassoftware/shiro

shiro

CVE-2026-49268 — Analyse et remédiation d'une vulnérabilité de contournement d'authentification par injection LDAP

Voir le dépôt
1il y a 6 joursPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2026-49268 — Analyse et remédiation d'une vulnérabilité de contournement d'authentification par injection LDAP

Branche1.13-CVE-2026-49268
AuteurJinwoo Hwang (https://JinwooHwang.com)

Cette branche (1.13-CVE-2026-49268) contient une remédiation de sécurité complète d'une vulnérabilité de contournement d'authentification par injection LDAP dans la version 1.13 d'Apache Shiro.

Description officielle NVD

Un attaquant distant peut injecter des caractères spéciaux LDAP dans la construction du Distinguished Name (DN) dans la classe DefaultLdapRealm. L'entrée du nom d'utilisateur fournie par l'utilisateur est directement concaténée dans le modèle de DN LDAP sans aucun échappement des caractères spéciaux RFC 2253. Cela permet à un attaquant de manipuler la structure du DN utilisée pour l'authentification par bind LDAP, contournant potentiellement l'authentification ou usurpant l'identité d'autres utilisateurs. Ce problème affecte toutes les versions d'Apache Shiro jusqu'à la 2.2.0, et la 3.0.0-alpha-1 lors de l'utilisation de DefaultLdapRealm.


1. Aperçu de la vulnérabilité


2. Résumé exécutif

Apache Shiro peut authentifier les utilisateurs auprès d'un annuaire LDAP. Pour ce faire, il transforme un nom d'utilisateur soumis en un Distinguished Name — l'adresse de l'annuaire pour l'entrée de cet utilisateur — en insérant le nom d'utilisateur dans un modèle configuré, par exemple uid={0},ou=users,dc=mycompany,dc=com.

Dans les versions affectées, le nom d'utilisateur est inséré dans ce modèle en tant que texte brut. Une poignée de caractères de ponctuation — le plus important étant la virgule — ne sont pas du texte ordinaire pour un serveur d'annuaire ; ils constituent la syntaxe qui sépare une partie d'une adresse de la suivante. Un nom d'utilisateur contenant ces caractères cesse donc de se comporter comme une valeur à l'intérieur de l'adresse et commence à se comporter comme une partie de l'adresse elle-même.

La conséquence pratique est qu'une personne qui se connecte peut influencer l'entrée d'annuaire contre laquelle Shiro tente de s'authentifier, plutôt que de simplement fournir son propre nom. Au lieu d'être recherchée dans le conteneur prévu par le déploiement, la recherche peut être redirigée ailleurs dans l'annuaire. Selon la façon dont l'annuaire est structuré et ce qu'il autorise, cela peut conduire à s'authentifier avec la mauvaise identité, ou à une authentification réussissant alors qu'elle ne le devrait pas.

Profil de risque. Ce qui augmente le risque : aucun accès ni identifiant préalable n'est requis — l'entrée arrive à la frontière de connexion, qui est accessible à quiconque peut atteindre l'application ; et la classe affectée est la manière standard et documentée de connecter Shiro à LDAP, il ne s'agit donc pas d'une configuration exotique. Ce qui le réduit : le déploiement doit réellement utiliser DefaultLdapRealm (ou JndiLdapRealm) avec un modèle de DN configuré ; les déploiements qui s'authentifient par d'autres moyens, ou qui transmettent un DN complet ou un identifiant non textuel tel qu'un certificat, ne sont pas affectés. Qu'une recherche redirigée produise une authentification utilisable dépend également de la structure et des règles d'accès propres à l'annuaire cible, qui varient selon les sites.

Une seconde conséquence, non liée à la sécurité, mérite d'être notée pour la planification des versions : les utilisateurs dont les noms d'utilisateur légitimes contiennent une barre oblique inverse ou commencent par # ne peuvent actuellement pas se connecter du tout, car l'adresse construite pour eux n'est pas un nom bien formé et est rejetée d'emblée. Les autres ponctuations n'échouent pas d'emblée — elles modifient silencieusement l'entrée à laquelle l'adresse se réfère, ce qui constitue le problème de sécurité ci-dessus. Le même correctif résout les deux.


3. Analyse de la cause racine

3.1 Le défaut

core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java, getUserDn(String), lignes 227–250 sur la base non corrigée :```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:~
Le template est découpé une fois, au moment de la configuration, autour du token `{0}`
(`setUserDnTemplate`, lignes 181–200), en un `prefix` et un `suffix`. Au moment de l'authentification,
le principal est concaténé entre eux. **Aucun encodage n'est appliqué à aucun moment.** Il n'existe
aucun utilitaire d'échappement nulle part dans `org/apache/shiro/realm/ldap/`.

### 3.2 L'invariant supposé mais jamais appliqué

Le code environnant traite le résultat de `getUserDn` comme un Distinguished Name bien formé —
il est transmis directement à `LdapContextFactory.getLdapContext(...)` et de là à JNDI comme
bind DN. Cela n'est valide que si le principal substitué est une *valeur d'attribut unique*.

La concaténation de chaînes ne peut pas garantir cela. La RFC 2253 (et la RFC 4514) réservent
`,` `+` `"` `\` `<` `>` `;` `=`, un `#` en tête, ainsi que les espaces en début et fin comme syntaxe
structurelle au sein d'un DN. Lorsque l'un de ces caractères apparaît dans le principal, il est lu par
l'analyseur comme de la structure, et non comme du contenu. L'invariant — *« le principal occupe exactement une valeur RDN »* —
était supposé par chaque consommateur en aval et appliqué par aucun.

### 3.3 Flux d'exécution, du point d'entrée au défaut```
  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 Effet démontré

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

DN construitRDN analysés
Prévuuid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com4
Référence (non corrigé)uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com5

Le nom gagne un composant. ou=users n'est plus le conteneur adressé — ou=admins est interposé. Vérifié par analyse avec javax.naming.ldap.LdapName, qui est indépendant de toute implémentation d'échappement.

Comportement mesuré de la référence sur l'ensemble réservé (JDK 8, analyse LdapName) :

Deux caractères altèrent la structure du nom, pas seulement son contenu : la virgule, et le point-virgule — que la RFC 1779 accepte comme séparateur de RDN alternatif. + introduit une seconde valeur d'attribut dans le même RDN. Seuls \ et un # en tête rendent le nom non analysable.

3.5 Code adjacent qui est correct, et pourquoi — rayon d'impact

  • userDnTemplate non défini. getUserDn renvoie le principal tel quel. C'est le mode documenté « le principal soumis est le DN » ; l'appelant fournit un DN complet et assume sa validité. Non affecté.
  • Principaux non-String. getLdapPrincipal ne route que les principaux String vers getUserDn ; tout le reste (certificats X.509, jetons personnalisés) passe à travers. Non affecté.
  • Validation de setUserDnTemplate (lignes 181–200) rejette correctement un template nul, vide, ou sans {0}. Elle valide le template fourni par l'opérateur, qui n'a jamais été l'entrée non fiable — c'est donc du code correct qui simplement ne traite pas ce défaut.
  • JndiLdapRealm étend DefaultLdapRealm et ne redéfinit pas getUserDn, il hérite donc du défaut. Confirmé par test (§6.1). Dans le périmètre, et corrigé par le même changement.

3.6 Chemin connexe — ÉVALUÉ, NON AFFECTÉ

AbstractLdapRealm.searchFilter (ligne 89) porte une seconde substitution {0}, par défaut (&(objectClass=*)(userPrincipalName={0})), utilisée sur le chemin d'autorisation. Il n'est pas affecté.

Un balayage de chaque appel search(, de chaque appelant getLdapContext(, et de chaque littéral de chaîne de forme LDAP à travers les sources principales core, support et web a trouvé exactement un usage de searchFilter — ActiveDirectoryRealm.getRoleNamesForUser, ligne 172 :```java Object[] searchArguments = new Object[]{userPrincipalName}; NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);

root@kitploit:~
Il s'agit de la surcharge **paramétrée** `DirContext.search(String, String, Object[], SearchControls)`.
Le nom d'utilisateur est passé comme *argument* de filtre, jamais concaténé dans la chaîne
de filtre. Selon la javadoc de `javax.naming.directory.DirContext` du JDK 8, textuellement :

> « Lorsqu'un argument de filtre de type chaîne est substitué à une variable, le filtre est
> interprété comme si la chaîne était donnée à la place de la variable, tous les caractères
> ayant une signification particulière au sein des filtres (tels que `'*'`) ayant été échappés
> conformément aux règles de la RFC 2254. »

Le fournisseur JNDI effectue l'échappement. La trace historique de cela se trouve dans la
déclaration du champ elle-même.

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

Source : core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java, lignes 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:~
Le nom d'utilisateur parvient au répertoire en tant que `searchArguments[0]`, jamais en tant que texte inséré dans `searchFilter`. Le jeton `{0}` dans le filtre est résolu par le fournisseur JNDI, et non par Shiro.

`ActiveDirectoryRealm` ligne 108 transmet le nom d'utilisateur brut à `getLdapContext(username, password)`. Cette valeur devient une entrée d'environnement JNDI `SECURITY_PRINCIPAL` unique ; elle n'est pas substituée dans un modèle et aucun DN n'est construit à partir de celle-ci, elle est donc en dehors du mécanisme de ce défaut.

**Vérifié empiriquement contre un serveur de répertoire en fonctionnement.** L'échappement JNDI n'a pas été accepté sur parole : il a été observé sur le réseau. Un serveur LDAP in-process (UnboundID `InMemoryDirectoryServer`) a été instrumenté avec un `InMemoryOperationInterceptor` qui enregistre le filtre de chaque requête de recherche qu'il reçoit, et le `searchFilter` par défaut de Shiro a été émis via le même appel JNDI paramétré utilisé par `ActiveDirectoryRealm`, avec un argument forgé :```
filter template : (&(objectClass=*)(userPrincipalName={0}))
argument passed : *)(uid=jsmith
server received : (&(objectClass=*)(userPrincipalName=\2a\29\28uid=jsmith))

Les *, ) et ( sont arrivés au serveur sous la forme \2a, \29 et \28 — échappés conformément à la RFC 2254. Le filtre ne conserve que deux clauses ; l'argument n'a pas pu en ajouter une troisième.

Contrôle, le même modèle avec l'argument inséré en texte brut au lieu d'être passé comme argument de filtre :``` CONTROL, raw splice: (&(objectClass=)(userPrincipalName=)(uid=jsmith)) server received : (&(objectClass=)(userPrincipalName=)(uid=jsmith))

root@kitploit:~
Le contrôle atteint le serveur avec sa structure altérée — une troisième clause ajoutée et le
test `userPrincipalName` réduit à un joker. La forme paramétrée, elle, ne l'est pas. Cela
confirme, par observation plutôt que par contrat, que le chemin `searchFilter` n'est pas
affecté.

---

## 4. Étapes de reproduction pas à pas

Déterministes, à partir d'un checkout propre. Le répertoire de travail est la racine du dépôt tout au long.

### 4.1 Prérequis

| Exigence | Valeur utilisée |
|---|---|
| JDK | **8** — la branche définit `jdk.version` à 1.8. Les résultats ci-dessous ont été produits avec Zulu 1.8.0_432 (arm64). Pointez `JAVA_HOME` vers une installation JDK 8 avant d'exécuter toute commande de cette section : |
| Build | Apache Maven, accès réseau requis (POM parent `org.apache:apache:38`) |
| Référence | `origin/1.13.x` |
| Configuration | `userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com` |
| Serveur d'annuaire | **Non requis pour §4.3-§4.4** — le défaut se situe dans la construction du DN, avant tout appel réseau, donc ces étapes simulent `LdapContextFactory`. §4.5 reproduit en outre l'ensemble du chemin contre un **vrai** serveur LDAP via HTTP. |

Définition de `JAVA_HOME` vers une installation JDK 8 :```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

Confirmez avec mvn -v, qui indique le JDK que Maven utilisera. Les commandes ci-dessous supposent que celui-ci est déjà défini.

4.2 Obtenir la base non corrigée```bash

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

root@kitploit:~
### 4.3 Observer directement le défaut

Il s'agit du déclencheur minimal, indépendant de la compilation de Shiro. Écrivez `Repro.java` dans un répertoire temporaire :```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());
    }
}

Exécutez-le :```bash javac Repro.java && java Repro

root@kitploit:~
Sortie observée :```
benign : uid=jsmith,ou=users,dc=mycompany,dc=com  -> RDNs=4
crafted: uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com  -> RDNs=5

Le nombre de composants passe de 4 à 5. La valeur soumise est devenue une structure de nom.

4.4 Reproduire via l'API propre de Shiro

Depuis la racine du dépôt sur la base non corrigée, appliquez les tests de la §6.3 et exécutez :```bash mvn -B clean verify

root@kitploit:~
La compilation s'arrête à `Apache Shiro :: Core` avec les tests de validation qui échouent — voir §6.1 pour la sortie complète capturée :```
DefaultLdapRealmTest   Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest      Tests run: 16, Failures: 4, Errors: 0, Skipped: 0

4.5 Reproduction de bout en bout via HTTP contre un serveur d'annuaire en production

Reproduit à travers l'intégralité de la pile — une véritable requête HTTP, un véritable conteneur de servlets, le propre FormAuthenticationFilter de Shiro, et un véritable serveur LDAP. Rien n'est simulé à aucun niveau.

Fixture d'annuaire — un conteneur privilégié imbriqué, une disposition courante dans le monde réel :``` 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 Build affecté

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

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

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

root@kitploit:~
**Bind DN reçu par l'annuaire**```
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com

Le 302 est la redirection de succès de FormAuthenticationFilter : la requête authentifiée. Le DN porte cinq composants là où le template en définit quatre, et l'entrée atteinte est celle privilégiée sous ou=admins.

Contrôle — le même nom d'utilisateur avec le mot de passe du compte ordinaire```http POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded

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

root@kitploit:~
| `-s` | `--server` | `string` | URL du serveur cible (par défaut : `http://localhost:8080`) |
| `-t` | `--token` | `string` | Jeton d'authentification pour le serveur |
| `-o` | `--output` | `string` | Chemin du fichier de sortie pour les résultats |
| `-f` | `--format` | `string` | Format de sortie : `json`, `yaml`, `table` (par défaut : `table`) |
| `-v` | `--verbose` | `bool` | Activer la sortie détaillée |
| `-q` | `--quiet` | `bool` | Supprimer toute la sortie non essentielle |
| `-c` | `--config` | `string` | Chemin du fichier de configuration |
| `-n` | `--no-color` | `bool` | Désactiver la sortie colorée |
| `-h` | `--help` | `bool` | Afficher le message d'aide |

### Exemples

```bash
# Exécuter un scan de base
scanner scan --target example.com

# Exécuter un scan avec un fichier de configuration
scanner scan --config /path/to/config.yaml

# Exporter les résultats au format JSON
scanner scan --target example.com --format json --output results.json

# Exécuter avec une sortie détaillée
scanner scan --target example.com --verbose

Configuration

Le scanner peut être configuré à l'aide d'un fichier de configuration YAML :

root@kitploit:~
server:
  url: "http://localhost:8080"
  token: "your-token-here"
  timeout: 30s

scan:
  target: "example.com"
  ports: "1-1000"
  threads: 10
  timeout: 5s

output:
  format: "json"
  file: "results.json"
  verbose: false

Variables d'environnement

Codes de sortie

Dépannage

Le scanner ne se connecte pas au serveur

Vérifiez que le serveur est en cours d'exécution et que l'URL est correcte :

root@kitploit:~
curl -v http://localhost:8080/health

Erreur d'authentification

Assurez-vous que votre jeton est valide et qu'il dispose des autorisations nécessaires :

root@kitploit:~
scanner auth verify --token your-token-here

Le scan prend trop de temps

Réduisez le nombre de threads ou augmentez le délai d'attente :

root@kitploit:~
scanner scan --target example.com --threads 5 --timeout 10s

Contribution

Les contributions sont les bienvenues ! Veuillez consulter le fichier CONTRIBUTING.md pour plus de détails.

Licence

Ce projet est sous licence MIT - voir le fichier LICENSE pour plus de détails.```http HTTP/1.1 200 OK

root@kitploit:~
Rejeté. C'est ce qui établit une usurpation d'identité plutôt qu'une coïncidence : le nom d'utilisateur forgé s'authentifie avec le mot de passe du **compte privilégié** et échoue avec celui de l'utilisateur ordinaire, donc les identifiants vérifiés appartiennent à une entrée d'annuaire différente de celle adressée par le template.

#### 4.5.2 Build corrigé — requêtes identiques

| Requête | Affecté | Corrigé |
|---|---|---|
| `username=jsmith&password=userpass` | `302` authentifié | `302` authentifié |
| `username=jsmith%2Cou%3Dadmins&password=adminpass` | **`302` authentifié** | **`200` rejeté** |
| `username=jsmith%2Cou%3Dadmins&password=userpass` | `200` rejeté | `200` rejeté |

Bind DN reçu par l'annuaire pour le nom d'utilisateur forgé :```
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)

Les connexions ordinaires sont inchangées — la ligne 1 s'authentifie de manière identique sur les deux versions, l'annuaire recevant uid=jsmith,ou=users,dc=mycompany,dc=com.

Les deux exécutions ont utilisé le même harnais et le même fixture ; seul shiro-core sur le classpath différait. La classe réellement chargée a été confirmée avec -verbose:class pour chaque exécution.


5. Détails de remédiation et code de remédiation

5.1 Correctif principal

Encoder le principal comme une valeur d'attribut DN unique avant substitution, en utilisant l'encodeur RFC 2253 du JDK lui-même, javax.naming.ldap.Rdn.escapeValue. C'est le même mécanisme que le JDK utilise pour construire les noms, donc sa sortie est par construction cohérente avec l'analyseur qui la consomme.

Fichier : `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 {

    root@kitploit:~
     int prefixLength = prefix != null ? prefix.length() : 0;
     int suffixLength = suffix != null ? suffix.length() : 0;
    
  • root@kitploit:~
       StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
    
  • root@kitploit:~
       //the principal is a single attribute value within the resulting name, so it is encoded
    
  • root@kitploit:~
       //to keep any characters that are significant in a Distinguished Name within that value:
    
  • root@kitploit:~
       String value = Rdn.escapeValue(principal);
    
  • root@kitploit:~
       StringBuilder sb = new StringBuilder(prefixLength + value.length() + suffixLength);
       if (prefixLength > 0) {
           sb.append(prefix);
       }
    
  • root@kitploit:~
       sb.append(principal);
    
  • root@kitploit:~
       sb.append(value);
       if (suffixLength > 0) {
           sb.append(suffix);
       }
    
root@kitploit:~
**Ce que le hunk impose.** `Rdn.escapeValue` échappe par barre oblique inverse l'ensemble réservé de la RFC 2253
(`\ , = + < > # ; "`) ainsi que les espaces en début et en fin. Après cela, le texte substitué
ne peut plus jamais occuper qu'une seule valeur RDN : le parseur lit les caractères échappés comme du contenu, donc
le nombre de composants du résultat est fixé par le template et ne peut pas être influencé par le
principal. L'indication de capacité du `StringBuilder` est mise à jour avec la longueur encodée — un détail de dimensionnement,
pas un détail comportemental.

**Placement.** L'encodage se situe *après* le retour anticipé pour le cas de template non configuré.
C'est délibéré : dans ce mode, l'appelant fournit un DN complet et l'encoder
le corromprait. Les deux branches conservent leurs contrats existants.

### 5.2 Changement de comportement pour les appelants légitimes

Ce correctif **modifie la chaîne DN** produite pour tout principal contenant un caractère
réservé. Trois conséquences qu'il vaut la peine d'énoncer clairement :

1. **Les connexions auparavant cassées fonctionnent désormais.** Les noms d'utilisateur contenant une barre oblique inverse ou commençant
   par `#` produisaient un DN qui n'était pas un nom bien formé, donc aucune tentative de bind ne pouvait être effectuée du
   tout. Ils s'encodent désormais en un DN valide et tenteront un bind. Les sites peuvent voir des comptes commencer
   à s'authentifier alors qu'ils ne le pouvaient pas auparavant — un correctif, mais un changement visible. Les noms d'utilisateur
   contenant `,`, `;`, `+` ou `=` n'étaient pas rejetés auparavant ; ils se résolvaient vers la mauvaise
   entrée, et se résolvent désormais vers celle prévue.
2. **Les DN de bind envoyés à l'annuaire diffèrent** pour les noms d'utilisateur concernés. Les journaux côté annuaire,
   les pistes d'audit, et toute extraction de journaux qui correspond à des chaînes DN littérales verront des formes
   échappées (`uid=jsmith\,ou\=admins,...`). Aucun changement de configuration n'est requis.
3. **Les déploiements qui s'appuyaient sur l'ancien comportement pour construire des DN multi-composants à partir du
   champ nom d'utilisateur se casseraient.** Ce n'est pas une configuration prise en charge — le template
   existe pour définir la structure — mais c'est le seul risque de migration, et c'est le même
   comportement que le correctif existe pour supprimer.

`getUserDnTemplate()` est implémenté comme `getUserDn("{0}")`. `{` et `}` ne sont pas réservés dans
la RFC 2253, donc la valeur de retour de l'accesseur est inchangée. Confirmé par le
`testUserDnTemplate` préexistant, qui passe sans modification.

---

## 6. Vérification et tests

### 6.1 Avant — référence, non corrigé

Branche `1.13-CVE-2026-49268`, JDK Zulu 1.8.0_432. Commande comme au §4.4.```
DefaultLdapRealmTest   Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest      Tests run: 16, Failures: 4, Errors: 0, Skipped: 0

Sortie d'échec capturée — c'est la preuve, et elle n'est pas récupérable une fois le correctif en place :``` 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

root@kitploit:~
`testGetUserDnLeavesOrdinaryPrincipalUnchanged` **a réussi sur la base de référence**, comme prévu — il
s'agit d'une protection contre la sur-correction, pas d'un test de validation.

### 6.2 Après — avec le correctif appliqué

Même commande, même JDK :```
DefaultLdapRealmTest   Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
JndiLdapRealmTest      Tests run: 16, Failures: 0, Errors: 0, Skipped: 0

Les quatre tests de vérification passent désormais sur les deux classes. Réexécuter §4.3 avec le correctif en place donne RDNs=4 pour le principal forgé.

6.3 Tests ajoutés

Fichier : core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java (+124 lignes, 0 suppressions). Comme JndiLdapRealmTest extends DefaultLdapRealmTest, chaque test ci-dessous s'exécute contre les deux realms — 10 exécutions à partir de 5 méthodes.

Note de conception. Les assertions structurelles analysent le résultat avec javax.naming.ldap.LdapName et comparent le nombre de composants et la valeur feuille après aller-retour, plutôt que d'affirmer une chaîne échappée attendue. Les tests vérifient donc la propriété réellement requise et ne présupposent pas Rdn.escapeValue comme implémentation — un encodeur correct alternatif passerait également.

Commande :```bash mvn -B clean verify

root@kitploit:~
### 6.4 Régression — build complet

Le build complet du reactor passe avec **aucun flag et aucun skip** :```
mvn -B clean verify

BUILD SUCCESS — 907 tests, 0 failures, 0 errors, 3 skipped, across the full reactor. Chaque garde-fou est actif dans cette exécution : tests unitaires (surefire), tests d'intégration (failsafe), l'audit de licence Apache RAT, maven-enforcer et japicmp.

Les 3 tests ignorés sont des @Ignore préexistants dans des modules sans rapport avec cette modification ; core — le seul module touché — n'ignore rien.

Références

  • Enregistrement CVE (API MITRE) : https://cveawg.mitre.org/api/cve/CVE-2026-49268
  • Annonce amont : https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s
  • Rapports de sécurité Apache Shiro : https://shiro.apache.org/security-reports.html
  • RFC 2253 / RFC 4514 — Représentation sous forme de chaîne LDAP des Distinguished Names
  • RFC 4515 — Représentation sous forme de chaîne des filtres de recherche LDAP

Contact

Auteur de la recherche et de la remédiation des vulnérabilités de sécurité : Jinwoo Hwang (https://JinwooHwang.com)

Télécharger l’outil
ChampValeur
CVECVE-2026-49268 — Apache Shiro : injection de DN LDAP dans DefaultLdapRealm
CWE IDCWE-90 — Neutralisation incorrecte des éléments spéciaux utilisés dans une requête LDAP (« injection LDAP »)
CVSS v4.08.8 ÉLEVÉ — 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 CRITIQUE — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Versions affectéesorg.apache.shiro:shiro-core 0 à 2.2.0 inclus ; 3.0.0-alpha-0 à 3.0.0-alpha-1 inclus
Corrigé en amont dans2.2.1 et 3.0.0-alpha-2
Référencehttps://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s
PrincipalRésultat de référenceEffet
jsmith,ou=adminss'analyse, 5 RDNstructure altérée — un conteneur est interposé
jsmith;ou=adminss'analyse, 5 RDNstructure altérée — ; est un séparateur de RDN selon la RFC 1779
jsmith+uid=admins'analyse, 4 RDNdevient un RDN multi-valué ; uid gagne une seconde valeur
jsmith=admins'analyse, 4 RDNvaleur corrompue
quo"te, angle<br>ackets, leadingSpace, trailingSpace s'analysent, 4 RDNvaleur corrompue
back\slashIllegalArgumentExceptionmalformé — la liaison ne peut pas être tentée
#leadingNumberSignIllegalArgumentExceptionmalformé — la liaison ne peut pas être tentée
CoucheComposant
Client HTTPHttpURLConnection, POST de formulaire brut
Conteneur de servletsJetty 9.4.58.v20250814 embarqué
Filtre de sécuritéShiroFilter + EnvironmentLoaderListener, authc (FormAuthenticationFilter)
RealmDefaultLdapRealm, userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
AnnuaireUnboundID InMemoryDirectoryServer, instrumenté pour enregistrer chaque DN de bind
VariableDescription
SCANNER_SERVER_URLURL du serveur cible
SCANNER_TOKENJeton d'authentification
SCANNER_CONFIGChemin du fichier de configuration
SCANNER_OUTPUT_FORMATFormat de sortie par défaut
SCANNER_VERBOSEActiver la sortie détaillée
CodeDescription
0Succès
1Erreur générale
2Mauvaise utilisation de la ligne de commande
3Erreur de configuration
4Erreur de connexion au serveur
5Erreur d'authentification
TestVérifieRéférence
testGetUserDnLeavesOrdinaryPrincipalUnchangedUn principal ordinaire produit exactement le DN attendu, 4 RDNs, valeur intacte. Protège contre le sur-échappement.réussi (garde)
testGetUserDnPreservesTemplateStructurePour jsmith,ou=admins le DN a toujours 4 composants et le principal survit comme une seule valeur d'attribut.échoué
testGetUserDnPreservesTemplateStructureForReservedCharactersIdem, sur les 10 principaux à caractères réservés ; fait aussi échouer le test si le DN n'est pas un nom bien formé.échoué
testGetUserDnEncodesSubstitutedValueLa valeur substituée est encodée de telle sorte qu'elle effectue un aller-retour vers le principal soumis.échoué
testUserDnTemplateSubstitutionPreservesStructureDe bout en bout via getAuthenticationInfo : le DN transmis à LdapContextFactory conserve la structure du template.échoué
Garde-fouRésultat
Tests unitaires, réacteur complet907 exécutés, 0 échecs, 0 erreurs, 3 ignorés
Module core seul321 exécutés, 0 échecs, 0 erreurs, 0 ignorés
Tests LDAP (DefaultLdapRealmTest + JndiLdapRealmTest)32 exécutés, 0 échecs
Tests d'intégration (failsafe)exécutés sur l'ensemble du réacteur, aucun échec
Audit de licence Apache RATNon approuvés : 0, inconnus : 0
maven-enforceraucune violation
japicmpaucune incompatibilité signalée