Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
shiro — CVE-2026-49268 — LDAP 인젝션 인증 우회 취약점 분석 및 완화 | Kitploit
도구/GitHubGitHub/sassoftware/shiro
Static Code Analysis (SAST)Vulnerability AnalysisCode AnalysisWeb SecurityAuthenticationLearning & Education
GitHubsassoftware/shiro

shiro

CVE-2026-49268 — LDAP 인젝션 인증 우회 취약점 분석 및 완화

저장소 보기
16일 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-49268 — LDAP 인젝션 인증 우회 취약점 분석 및 해결

브랜치1.13-CVE-2026-49268
작성자Jinwoo Hwang (https://JinwooHwang.com)

이 브랜치(1.13-CVE-2026-49268)는 Apache Shiro 1.13 릴리스의 LDAP 인젝션 인증 우회 취약점에 대한 포괄적인 보안 해결책을 포함하고 있습니다.

공식 NVD 설명

원격 공격자가 DefaultLdapRealm 클래스의 Distinguished Name(DN) 구성에 LDAP 특수 문자를 주입할 수 있습니다. 사용자가 제공한 사용자 이름 입력이 RFC 2253 특수 문자에 대한 이스케이프 처리 없이 LDAP DN 템플릿에 직접 연결됩니다. 이로 인해 공격자가 LDAP 바인드 인증에 사용되는 DN 구조를 조작할 수 있으며, 잠재적으로 인증을 우회하거나 다른 사용자를 사칭할 수 있습니다. 이 문제는 DefaultLdapRealm을 사용할 때 모든 Apache Shiro 버전(2.2.0까지)과 3.0.0-alpha-1에 영향을 미칩니다.


1. 취약점 개요


2. 요약

Apache Shiro는 LDAP 디렉터리에 대해 사용자를 인증할 수 있습니다. 이를 위해 제출된 사용자 이름을 Distinguished Name(해당 사용자 항목에 대한 디렉터리의 주소)으로 변환하는데, 사용자 이름을 구성된 템플릿에 그대로 넣습니다. 예를 들어 uid={0},ou=users,dc=mycompany,dc=com과 같습니다.

영향받는 버전에서는 사용자 이름이 원시 텍스트로 해당 템플릿에 붙여넣어집니다. 몇 가지 구두점 문자 — 가장 중요한 것은 쉼표 — 는 디렉터리 서버에게 일반 텍스트가 아니라, 주소의 한 부분을 다음 부분과 구분하는 구문입니다. 따라서 이러한 문자를 포함한 사용자 이름은 주소 내부의 값처럼 동작하지 않고 주소 자체의 일부처럼 동작하기 시작합니다.

실질적인 결과는 로그인하는 사람이 자신의 이름을 제공하는 것뿐만 아니라 Shiro가 어느 디렉터리 항목에 대해 인증을 시도할지에 영향을 줄 수 있다는 것입니다. 배포에서 의도한 컨테이너에서 조회되는 대신, 조회가 디렉터리의 다른 곳으로 리디렉션될 수 있습니다. 디렉터리의 구성 방식과 허용 범위에 따라, 이는 잘못된 신원으로 인증되거나, 인증이 성공해서는 안 되는 상황에서 성공하는 결과로 이어질 수 있습니다.

위험 프로필. 위험을 높이는 요소: 사전 접근 권한이나 자격 증명이 필요하지 않습니다 — 입력이 로그인 경계에 도달하며, 이는 애플리케이션에 접근할 수 있는 누구나 도달할 수 있습니다; 그리고 영향받는 클래스는 Shiro를 LDAP에 연결하는 표준적이고 문서화된 방식이므로, 이는 특이한 구성이 아닙니다. 위험을 낮추는 요소: 배포에서 실제로 DefaultLdapRealm(또는 JndiLdapRealm)을 구성된 DN 템플릿과 함께 사용해야 합니다; 다른 방식으로 인증하거나, 전체 DN 또는 인증서와 같은 비텍스트 자격 증명을 전달하는 배포는 영향을 받지 않습니다. 리디렉션된 조회가 사용 가능한 인증을 산출하는지 여부는 대상 디렉터리 자체의 구성과 접근 규칙에 따라 달라지며, 이는 사이트마다 다릅니다.

릴리스 계획을 위해 주목할 만한 두 번째의 비보안적 결과가 있습니다: 정당한 사용자 이름에 백슬래시가 포함되거나 #으로 시작하는 사용자는 현재 전혀 로그인할 수 없습니다. 그들을 위해 생성된 주소가 올바른 형식의 이름이 아니어서 즉시 거부되기 때문입니다. 다른 구두점은 즉시 실패하지 않습니다 — 주소가 참조하는 항목을 조용히 변경하며, 이것이 위의 보안 문제입니다. 동일한 수정이 두 가지 모두를 해결합니다.


3. 근본 원인 분석

3.1 결함

core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java, getUserDn(String), 수정되지 않은 기준 버전의 227–250행:```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:~
템플릿은 구성 시점에 `{0}` 토큰을 기준으로 한 번 분할되어
(`setUserDnTemplate`, 181–200행) `prefix`와 `suffix`로 나뉜다. 인증 시점에는
principal이 그 사이에 연결된다. **어느 시점에도 인코딩은 적용되지 않는다.** `org/apache/shiro/realm/ldap/` 어디에도
이스케이프 헬퍼는 존재하지 않는다.

### 3.2 가정되었지만 결코 강제되지 않은 불변식

주변 코드는 `getUserDn`의 결과를 잘 구성된 Distinguished Name으로 취급한다 —
그것은 곧바로 `LdapContextFactory.getLdapContext(...)`에 전달되고, 거기서부터 JNDI에
바인드 DN으로 전달된다. 이는 대체된 principal이 *단일 속성 값*일 때만 타당하다.

문자열 연결로는 그것을 강제할 수 없다. RFC 2253(그리고 RFC 4514)은
`,` `+` `"` `\` `<` `>` `;` `=`, 선행 `#`, 그리고 선행/후행 공백을 DN 내의 구조적
구문으로 규정한다. 이들 중 어느 것이든 principal에 나타나면 파서는 그것을 내용이 아니라
구조로 읽는다. *"principal은 정확히 하나의 RDN 값을 차지한다"* 라는 불변식은
모든 하위 소비자에 의해 가정되었으나 어느 곳에서도 강제되지 않았다.

### 3.3 실행 흐름, 진입점에서 결함까지```
  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 입증된 효과

템플릿 uid={0},ou=users,dc=mycompany,dc=com, principal jsmith,ou=admins:

생성된 DN파싱된 RDN
의도된 결과uid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com4
기준선 (수정 전)uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com5

이름에 구성 요소가 추가된다. ou=users는 더 이상 대상이 되는 컨테이너가 아니며 — ou=admins가 그 사이에 삽입된다. 이는 어떤 이스케이프 구현과도 독립적인 javax.naming.ldap.LdapName으로 파싱하여 검증되었다.

예약 문자 집합에 대한 기준선의 측정된 동작 (JDK 8, LdapName 파싱):

두 문자가 이름의 내용뿐 아니라 구조를 변경한다: 쉼표, 그리고 RFC 1779가 대체 RDN 구분자로 허용하는 세미콜론이다. +는 동일한 RDN에 두 번째 속성 값을 도입한다. \와 선행 #만이 이름을 파싱 불가능하게 만든다.

3.5 올바른 인접 코드와 그 이유 — 영향 범위

  • userDnTemplate 미설정. getUserDn은 principal을 그대로 반환한다. 이는 문서화된 "제출된 principal이 곧 DN" 모드이며, 호출자가 완전한 DN을 제공하고 그 정확성에 대한 책임을 진다. 영향을 받지 않음.
  • 비-String principal. getLdapPrincipal은 String principal만 getUserDn으로 라우팅하며, 그 외의 것(X.509 인증서, 사용자 정의 토큰)은 그대로 통과한다. 영향을 받지 않음.
  • setUserDnTemplate 검증 (181–200행)은 null, 공백, 또는 {0}이 없는 템플릿을 올바르게 거부한다. 이는 운영자가 제공한 템플릿을 검증하는데, 그것은 결코 신뢰할 수 없는 입력이 아니었다 — 따라서 이는 올바른 코드이며 단지 이 결함을 다루지 않을 뿐이다.
  • **JndiLdapRealm**은 DefaultLdapRealm을 확장하며 getUserDn을 재정의하지 않으므로 결함을 상속한다. 테스트로 확인됨(§6.1). 범위 내에 있으며, 동일한 변경으로 수정된다.

3.6 관련 경로 — 평가됨, 영향 없음

AbstractLdapRealm.searchFilter (89행)는 두 번째 {0} 치환을 가지며, 기본값은 (&(objectClass=*)(userPrincipalName={0}))이고 인가 경로에서 사용된다. 이는 영향을 받지 않는다.

모든 search( 호출, 모든 getLdapContext( 호출자, 그리고 core, support, web 메인 소스 전반의 모든 LDAP 형태 문자열 리터럴을 훑어본 결과 searchFilter의 사용은 정확히 한 곳 — ActiveDirectoryRealm.getRoleNamesForUser, 172행 — 에서 발견되었다:```java Object[] searchArguments = new Object[]{userPrincipalName}; NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);

root@kitploit:~
This is the **parameterized** `DirContext.search(String, String, Object[], SearchControls)`
overload. The username is passed as a filter *argument*, never concatenated into the filter
string. Per the JDK 8 `javax.naming.directory.DirContext` javadoc, verbatim:

> "When a string-valued filter argument is substituted for a variable, the filter is
> interpreted as if the string were given in place of the variable, with any characters
> having special significance within filters (such as `'*'`) having been escaped according
> to the rules of RFC 2254."

The JNDI provider performs the escaping. The historical record of this is in the field
declaration itself.

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

출처: core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java, 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:~
사용자 이름은 `searchArguments[0]`로 디렉터리에 도달하며, `searchFilter`에 텍스트로 삽입되는 방식이
아니다. 필터의 `{0}` 토큰은 Shiro가 아니라 JNDI 프로바이더에 의해 해석된다.

`ActiveDirectoryRealm` 108번째 줄은 원시 사용자 이름을
`getLdapContext(username, password)`에 전달한다. 그 값은 단일 JNDI
`SECURITY_PRINCIPAL` 환경 항목이 되며, 템플릿에 대입되지 않고 그로부터 DN이
구성되지도 않으므로 이 결함의 메커니즘 범위 밖에 있다.

**실제 디렉터리 서버를 대상으로 경험적으로 검증되었다.** JNDI 이스케이프는
신뢰에 기반해 받아들여진 것이 아니라, 통신 회선 상에서 관찰되었다. 인프로세스 LDAP 서버
(UnboundID `InMemoryDirectoryServer`)에 수신하는 모든 검색 요청의 필터를 기록하는
`InMemoryOperationInterceptor`를 계측하고, Shiro의 기본 `searchFilter`를
`ActiveDirectoryRealm`에서 사용하는 것과 동일한 매개변수화된 JNDI 호출을 통해
조작된 인수와 함께 발행했다:```
filter template : (&(objectClass=*)(userPrincipalName={0}))
argument passed : *)(uid=jsmith
server received : (&(objectClass=*)(userPrincipalName=\2a\29\28uid=jsmith))

*, ) 및 (는 서버에 \2a, \29, \28로 도착했습니다 — RFC 2254에 따라 이스케이프된 것입니다. 필터는 정확히 두 개의 절만 유지하며, 인수는 세 번째 절을 추가할 수 없었습니다.

대조군으로, 인수를 필터 인수로 전달하는 대신 원시 텍스트로 삽입한 동일한 템플릿:``` CONTROL, raw splice: (&(objectClass=)(userPrincipalName=)(uid=jsmith)) server received : (&(objectClass=)(userPrincipalName=)(uid=jsmith))

root@kitploit:~
제어는 구조가 변경된 상태로 서버에 도달합니다 — 세 번째 절이 추가되고
`userPrincipalName` 테스트가 와일드카드로 축소됩니다. 매개변수화된 형태는 그렇지 않습니다. 이는 계약이 아닌 관찰을 통해 `searchFilter` 경로가
영향을 받지 않음을 확인시켜 줍니다.

---

## 4. 단계별 재현 절차

깨끗한 체크아웃에서 결정론적으로 진행합니다. 작업 디렉터리는 전체에 걸쳐 저장소 루트입니다.

### 4.1 사전 요구 사항

| 요구 사항 | 사용된 값 |
|---|---|
| JDK | **8** — 브랜치는 `jdk.version`을 1.8로 설정합니다. 아래 결과는 Zulu 1.8.0_432 (arm64)로 생성되었습니다. 이 섹션의 명령을 실행하기 전에 `JAVA_HOME`을 JDK 8 설치 위치로 지정하십시오: |
| 빌드 | Apache Maven, 네트워크 접근 필요 (부모 POM `org.apache:apache:38`) |
| 기준 | `origin/1.13.x` |
| 구성 | `userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com` |
| 디렉터리 서버 | **§4.3-§4.4에는 필요하지 않음** — 결함은 네트워크 호출 이전의 DN 구성에 있으므로 해당 단계에서는 `LdapContextFactory`를 모킹합니다. §4.5는 추가로 HTTP를 통해 **실제** LDAP 서버에 대해 전체 경로를 재현합니다. |

`JAVA_HOME`을 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

mvn -v로 확인하세요. 이 명령은 Maven이 사용할 JDK를 보고합니다. 아래 명령은 이것이 이미 설정되어 있다고 가정합니다.

4.2 수정되지 않은 기준 버전 확보```bash

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

root@kitploit:~
### 4.3 결함을 직접 관찰하기

이것은 Shiro의 빌드와 무관한 최소 트리거입니다. `Repro.java`를 임시 디렉터리에 작성하세요:```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());
    }
}

실행하세요:```bash javac Repro.java && java Repro

root@kitploit:~
관찰된 출력:```
benign : uid=jsmith,ou=users,dc=mycompany,dc=com  -> RDNs=4
crafted: uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com  -> RDNs=5

컴포넌트 개수가 4에서 5로 변경됩니다. 제출된 값이 name 구조가 되었습니다.

4.4 Shiro 자체 API를 통한 재현

수정되지 않은 기준 버전의 저장소 루트에서 §6.3의 테스트를 적용하고 다음을 실행합니다:```bash mvn -B clean verify

root@kitploit:~
빌드는 `Apache Shiro :: Core`에서 증명 테스트가 실패하며 중단됩니다 — 전체 캡처 출력은 §6.1을 참조하십시오:```
DefaultLdapRealmTest   Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest      Tests run: 16, Failures: 4, Errors: 0, Skipped: 0

4.5 라이브 디렉터리 서버를 대상으로 한 HTTP를 통한 종단 간 재현

전체 스택을 통해 재현됨 — 실제 HTTP 요청, 실제 서블릿 컨테이너, Shiro 자체의 FormAuthenticationFilter, 그리고 실제 LDAP 서버. 어떤 계층에서도 모킹되지 않음.

디렉터리 픽스처 — 중첩된 권한 있는 컨테이너, 일반적인 실제 배포 구조:``` 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 영향을 받는 빌드

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

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

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

root@kitploit:~
**디렉터리가 수신한 Bind DN**```
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com

302는 FormAuthenticationFilter의 성공 리다이렉트입니다: 요청이 인증되었습니다. DN은 템플릿이 네 개를 정의하는데 다섯 개의 컴포넌트를 가지며, 도달한 항목은 ou=admins 아래의 권한 있는 항목입니다.

대조 — 일반 계정의 비밀번호를 사용한 동일한 사용자 이름```http POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded

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

root@kitploit:~
## 2.3.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.2.```http
HTTP/1.1 200 OK

거부됨. 이것이 우연이 아니라 사칭을 입증하는 근거이다: 조작된 사용자 이름은 권한 있는 계정의 비밀번호로 인증되고 일반 사용자의 비밀번호로는 실패하므로, 검사되는 자격 증명은 템플릿이 지정하는 것과 다른 디렉터리 항목에 속한다.

4.5.2 수정된 빌드 — 동일한 요청

조작된 사용자 이름에 대해 디렉터리가 수신한 Bind DN:``` 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:~
일반 로그인은 변경되지 않습니다 — 행 1은 두 빌드 모두에서 동일하게 인증되며,
디렉터리는 `uid=jsmith,ou=users,dc=mycompany,dc=com`을 수신합니다.

두 실행 모두 동일한 하네스와 동일한 픽스처를 사용했으며, 클래스패스 상의 `shiro-core`만
달랐습니다. 실제로 로드된 클래스는 각 실행마다 `-verbose:class`로 확인했습니다.

---

## 5. Remediation Details & Remediation Code

### 5.1 Primary fix

치환 전에 principal을 단일 DN 속성 값으로 인코딩하며, JDK 자체의 RFC 2253 인코더인
`javax.naming.ldap.Rdn.escapeValue`를 사용합니다. 이는 JDK가 이름을 구성할 때 사용하는
것과 동일한 메커니즘이므로, 그 출력은 이를 소비하는 파서와 구조적으로 일관됩니다.

**File:** `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);
         }

헝크가 강제하는 것. Rdn.escapeValue는 RFC 2253 예약 문자 집합 (\ , = + < > # ; ")과 선행 및 후행 공백을 백슬래시로 이스케이프합니다. 그 후, 대체된 텍스트는 오직 하나의 RDN 값만 차지할 수 있습니다. 파서는 이스케이프된 문자를 내용으로 읽으므로, 결과의 컴포넌트 수는 템플릿에 의해 고정되며 프린시펄의 영향을 받을 수 없습니다. StringBuilder 용량 힌트는 인코딩된 길이로 갱신됩니다 — 이는 크기 조정 세부 사항이지 동작상의 세부 사항이 아닙니다.

배치. 인코딩은 구성되지 않은 템플릿 사례에 대한 조기 반환 이후에 위치합니다. 이는 의도적입니다. 해당 모드에서는 호출자가 완전한 DN을 제공하며, 이를 인코딩하면 DN이 손상되기 때문입니다. 두 분기 모두 기존 계약을 유지합니다.

5.2 정당한 호출자에 대한 동작 변경

이 수정은 예약 문자를 포함하는 모든 프린시펄에 대해 생성되는 DN 문자열을 변경합니다. 명확히 밝힐 가치가 있는 세 가지 결과:

  1. 이전에 깨졌던 로그인이 이제 작동합니다. 백슬래시를 포함하거나 #으로 시작하는 사용자 이름은 올바른 형식의 이름이 아닌 DN을 생성했으므로, 바인드를 시도조차 할 수 없었습니다. 이제 유효한 DN으로 인코딩되어 바인드를 시도합니다. 사이트에서는 이전에 인증할 수 없었던 계정이 인증을 시작하는 것을 볼 수 있습니다 — 수정이지만, 가시적인 변경입니다. ,, ;, + 또는 =를 포함하는 사용자 이름은 이전에 거부되지 않았습니다. 이들은 잘못된 항목으로 해석되었고, 이제 의도된 항목으로 해석됩니다.
  2. 디렉터리로 전송되는 바인드 DN이 영향을 받는 사용자 이름에 대해 달라집니다. 디렉터리 측 로그, 감사 추적, 그리고 리터럴 DN 문자열을 매칭하는 모든 로그 스크래핑은 이스케이프된 형식 (uid=jsmith\,ou\=admins,...)을 보게 됩니다. 구성 변경은 필요하지 않습니다.
  3. 사용자 이름 필드에서 다중 컴포넌트 DN을 구성하기 위해 이전 동작에 의존했던 배포는 중단됩니다. 이는 지원되는 구성이 아닙니다 — 템플릿은 구조를 정의하기 위해 존재합니다 — 하지만 이것이 유일한 마이그레이션 위험이며, 이는 이 수정이 제거하려고 존재하는 바로 그 동작입니다.

getUserDnTemplate()은 getUserDn("{0}")으로 구현됩니다. {와 }는 RFC 2253에서 예약되어 있지 않으므로, 접근자의 반환 값은 변경되지 않습니다. 수정 없이 통과하는 기존의 testUserDnTemplate에 의해 확인되었습니다.


6. 검증 및 테스트

6.1 이전 — 기준선, 수정되지 않음

브랜치 1.13-CVE-2026-49268, JDK Zulu 1.8.0_432. 명령은 §4.4와 같음.``` DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0

root@kitploit:~
실패 출력 캡처 — 이것이 증거이며, 수정이 적용되면 복구할 수 없다:```
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는 베이스라인에서 통과했으며, 이는 의도된 대로입니다 — 이는 과잉 수정에 대한 가드이지, 증명 테스트가 아닙니다.

6.2 이후 — 수정 사항이 적용된 상태

동일한 명령, 동일한 JDK:``` DefaultLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0

root@kitploit:~
이제 네 가지 증명 테스트가 두 클래스 모두에서 통과합니다. 수정 사항을 적용한 상태에서 §4.3을 다시 실행하면
조작된 principal에 대해 `RDNs=4`가 나옵니다.

### 6.3 추가된 테스트

**파일:** `core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java`
(+124줄, 삭제 0줄). `JndiLdapRealmTest extends DefaultLdapRealmTest`이므로 아래의 모든
테스트는 **두** realm 모두에 대해 실행됩니다 — 5개 메서드에서 10회 실행.

| 테스트 | 검증 내용 | 기준선 |
|---|---|---|
| `testGetUserDnLeavesOrdinaryPrincipalUnchanged` | 일반 principal이 정확히 예상된 DN, 4개의 RDN, 값이 그대로 유지된 채로 나온다. 과잉 이스케이프를 방지한다. | **통과** (가드) |
| `testGetUserDnPreservesTemplateStructure` | `jsmith,ou=admins`에 대해 DN이 여전히 4개의 컴포넌트를 가지며 principal이 하나의 속성 값으로 유지된다. | 실패 |
| `testGetUserDnPreservesTemplateStructureForReservedCharacters` | 위와 동일하되, 10개의 예약 문자 principal 모두에 대해 수행하며, DN이 올바른 형식의 이름이 아니면 테스트를 실패시킨다. | 실패 |
| `testGetUserDnEncodesSubstitutedValue` | 치환된 값이 제출된 principal로 왕복 변환되도록 인코딩된다. | 실패 |
| `testUserDnTemplateSubstitutionPreservesStructure` | `getAuthenticationInfo`를 통한 종단 간 검증: `LdapContextFactory`에 전달된 DN이 템플릿의 구조를 유지한다. | 실패 |

**설계 참고.** 구조적 검증은 예상되는 이스케이프된 문자열을 단정하는 대신
`javax.naming.ldap.LdapName`으로 결과를 파싱하여 컴포넌트 개수와 왕복 변환된 리프 값을
비교한다. 따라서 테스트는 실제로 요구되는 속성을 검증하며 `Rdn.escapeValue`를 구현으로
전제하지 않는다 — 대안적인 올바른 인코더도 여전히 통과할 것이다.

명령:```bash
mvn -B clean verify

6.4 회귀 — 전체 빌드

전체 리액터 빌드가 플래그 없이, 스킵 없이 통과합니다:``` mvn -B clean verify

root@kitploit:~
**BUILD SUCCESS — 전체 리액터에서 907개 테스트, 0개 실패, 0개 오류, 3개 건너뜀.**
이번 실행에서는 모든 게이트가 활성화됩니다: 단위 테스트(surefire), 통합 테스트(failsafe),
Apache RAT 라이선스 감사, maven-enforcer, 그리고 japicmp.

| 게이트 | 결과 |
|---|---|
| 단위 테스트, 전체 리액터 | **907개 실행, 0개 실패, 0개 오류, 3개 건너뜀** |
| `core` 모듈 단독 | **321개 실행, 0개 실패, 0개 오류, 0개 건너뜀** |
| LDAP 테스트 (`DefaultLdapRealmTest` + `JndiLdapRealmTest`) | **32개 실행, 0개 실패** |
| 통합 테스트 (failsafe) | 전체 리액터에서 실행됨, 실패 없음 |
| Apache RAT 라이선스 감사 | 미승인: 0, 알 수 없음: 0 |
| maven-enforcer | 위반 없음 |
| japicmp | 보고된 비호환성 없음 |

3개의 건너뜀은 이 변경과 관련 없는 모듈에 이미 존재하던 `@Ignore`입니다; `core` — 이번에
유일하게 수정된 모듈 — 는 아무것도 건너뛰지 않습니다.

## 참고 자료

- CVE 레코드 (MITRE API): `https://cveawg.mitre.org/api/cve/CVE-2026-49268`
- 업스트림 공지: `https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s`
- Apache Shiro 보안 보고서: `https://shiro.apache.org/security-reports.html`
- RFC 2253 / RFC 4514 — Distinguished Names의 LDAP 문자열 표현
- RFC 4515 — LDAP 검색 필터 문자열 표현

## 연락처

보안 취약점 연구 및 수정 작성자: Jinwoo Hwang ([https://JinwooHwang.com](https://jinwoohwang.com/))
도구 다운로드
필드값
CVECVE-2026-49268 — Apache Shiro: DefaultLdapRealm의 LDAP DN 인젝션
CWE IDCWE-90 — LDAP 쿼리에 사용되는 특수 요소의 부적절한 중화('LDAP 인젝션')
CVSS v4.08.8 HIGH — 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 CRITICAL — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
영향받는 버전org.apache.shiro:shiro-core 0부터 2.2.0까지(포함); 3.0.0-alpha-0부터 3.0.0-alpha-1까지(포함)
업스트림에서 수정된 버전2.2.1 및 3.0.0-alpha-2
참조https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s
Principal기준선 결과효과
jsmith,ou=admins파싱됨, 5 RDN구조 변경됨 — 컨테이너가 삽입됨
jsmith;ou=admins파싱됨, 5 RDN구조 변경됨 — RFC 1779에서 ;는 RDN 구분자임
jsmith+uid=admin파싱됨, 4 RDN다중값 RDN이 됨; uid가 두 번째 값을 얻음
jsmith=admin파싱됨, 4 RDN값 손상됨
quo"te, angle<br>ackets, leadingSpace, trailingSpace 파싱됨, 4 RDN값 손상됨
back\slashIllegalArgumentException형식 오류 — 바인드를 시도할 수 없음
#leadingNumberSignIllegalArgumentException형식 오류 — 바인드를 시도할 수 없음
계층구성 요소
HTTP 클라이언트HttpURLConnection, 원시 폼 POST
서블릿 컨테이너내장 Jetty 9.4.58.v20250814
보안 필터ShiroFilter + EnvironmentLoaderListener, authc (FormAuthenticationFilter)
RealmDefaultLdapRealm, userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
디렉터리UnboundID InMemoryDirectoryServer, 모든 바인드 DN을 기록하도록 계측됨
요청취약수정됨
username=jsmith&password=userpass302 인증됨302 인증됨
username=jsmith%2Cou%3Dadmins&password=adminpass302 인증됨200 거부됨
username=jsmith%2Cou%3Dadmins&password=userpass200 거부됨200 거부됨