금융권 LDAP Injection 실전 시나리오

가이드 49 제1장-10: LDAP 삽입 | 위험도: 5 (매우 높음)

🔴 공격자 (임직원 인증 시스템 공격)

LDAP Injection 준비 중...

https://intranet.xxbank.com/login

시나리오를 시작하세요

공격 진행 단계

시나리오를 시작하세요...

실전 공격 시나리오

취약한 코드 (LDAP Injection)

문제 원인
// Java - 취약한 LDAP 쿼리

@Service
public class LdapAuthService {
    
    @Autowired
    private LdapTemplate ldapTemplate;
    
    public User authenticate(String username, String password) {
        // ❌ 사용자 입력을 직접 연결!
        String filter = "(uid=" + username + ")";
        
        // ❌ LDAP Injection 취약!
        List users = ldapTemplate.search(
            "ou=people,dc=xxbank,dc=com",
            filter,
            new UserAttributesMapper()
        );
        
        if (users.isEmpty()) {
            throw new AuthenticationException("User not found");
        }
        
        User user = users.get(0);
        
        // ❌ 비밀번호 검증도 우회 가능
        if (user.getPassword().equals(password)) {
            return user;
        }
        
        throw new AuthenticationException("Invalid password");
    }
}

// Python LDAP - 취약한 코드

import ldap

def ldap_authenticate(username, password):
    # ❌ 입력값 검증 없음!
    ldap_filter = f"(uid={username})"
    
    conn = ldap.initialize('ldap://ldap.xxbank.com')
    
    # ❌ LDAP Injection 취약
    result = conn.search_s(
        'ou=people,dc=xxbank,dc=com',
        ldap.SCOPE_SUBTREE,
        ldap_filter
    )
    
    if not result:
        raise Exception("User not found")
    
    dn, attrs = result[0]
    
    # ❌ 비밀번호 검증
    try:
        conn.simple_bind_s(dn, password)
        return attrs
    except:
        raise Exception("Authentication failed")

// Node.js - 취약한 LDAP

const ldap = require('ldapjs');

function authenticateUser(username, password) {
    const client = ldap.createClient({
        url: 'ldap://ldap.xxbank.com'
    });
    
    // ❌ 필터에 직접 입력!
    const filter = `(uid=${username})`;
    
    const opts = {
        filter: filter,
        scope: 'sub'
    };
    
    // ❌ LDAP Injection 가능
    client.search(
        'ou=people,dc=xxbank,dc=com',
        opts,
        (err, res) => {
            res.on('searchEntry', (entry) => {
                // 사용자 정보 반환
            });
        }
    );
}

// 공격 예시

// 인증 우회
username: admin)(&(password=*))
filter: (uid=admin)(&(password=*)))
결과: 관리자 계정 인증 성공!

// 전체 조회
username: *
filter: (uid=*)
결과: 모든 사용자 정보 노출!

// 속성 조작
username: *)(|(uid=*
filter: (uid=*)(|(uid=*))
결과: OR 조건으로 모든 사용자 조회

🟢 LDAP 서버 (Active Directory)

임직원 인증 및 정보 관리

[LDAP] Waiting for connection...
LDAP 서버 상태: RUNNING

안전한 코드 (LDAP Injection 방어)

해결책
// Java - 안전한 LDAP 쿼리

@Service
public class SecureLdapAuthService {
    
    @Autowired
    private LdapTemplate ldapTemplate;
    
    public User authenticate(String username, String password) {
        
        // ✅ 입력값 검증
        if (!isValidUsername(username)) {
            throw new IllegalArgumentException(
                "Invalid username format"
            );
        }
        
        // ✅ 파라미터 바인딩 사용
        LdapQuery query = LdapQueryBuilder
            .query()
            .base("ou=people,dc=xxbank,dc=com")
            .where("uid").is(username);  // 자동 이스케이프
        
        // ✅ 안전한 검색
        List users = ldapTemplate.search(
            query,
            new UserAttributesMapper()
        );
        
        if (users.isEmpty()) {
            // 일반적인 에러 메시지
            throw new AuthenticationException(
                "인증에 실패했습니다."
            );
        }
        
        User user = users.get(0);
        
        // ✅ LDAP bind로 비밀번호 검증
        try {
            ldapTemplate.authenticate(
                query,
                password
            );
            return user;
        } catch (Exception e) {
            throw new AuthenticationException(
                "인증에 실패했습니다."
            );
        }
    }
    
    // ✅ 입력값 검증 함수
    private boolean isValidUsername(String username) {
        if (username == null || username.isEmpty()) {
            return false;
        }
        
        // 영문, 숫자, 언더스코어만 허용
        return username.matches("^[a-zA-Z0-9_]{3,20}$");
    }
}

// Spring Security LDAP - 권장 방법

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) 
        throws Exception {
        
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            // ✅ Spring Security LDAP 사용
            .ldapAuthentication()
                .userDnPatterns("uid={0},ou=people")
                .contextSource()
                    .url("ldap://ldap.xxbank.com:389/dc=xxbank,dc=com")
                    .and()
                .passwordCompare()
                    .passwordAttribute("userPassword");
        
        return http.build();
    }
}

// Python - 안전한 LDAP

import ldap
import ldap.filter

def secure_ldap_authenticate(username, password):
    
    # ✅ 입력값 검증
    if not is_valid_username(username):
        raise ValueError("Invalid username format")
    
    # ✅ LDAP 필터 이스케이프
    escaped_username = ldap.filter.escape_filter_chars(
        username
    )
    
    ldap_filter = f"(uid={escaped_username})"
    
    conn = ldap.initialize('ldap://ldap.xxbank.com')
    
    # ✅ 안전한 검색
    result = conn.search_s(
        'ou=people,dc=xxbank,dc=com',
        ldap.SCOPE_SUBTREE,
        ldap_filter,
        attrlist=['uid', 'cn', 'mail']  # 필요한 속성만
    )
    
    if not result:
        raise Exception("인증에 실패했습니다.")
    
    dn, attrs = result[0]
    
    # ✅ LDAP bind로 검증
    try:
        conn.simple_bind_s(dn, password)
        return {
            'uid': attrs.get('uid', [b''])[0].decode(),
            'name': attrs.get('cn', [b''])[0].decode(),
            'email': attrs.get('mail', [b''])[0].decode()
        }
    except ldap.INVALID_CREDENTIALS:
        raise Exception("인증에 실패했습니다.")
    finally:
        conn.unbind()

def is_valid_username(username):
    # 영문, 숫자, 언더스코어만 허용
    import re
    pattern = r'^[a-zA-Z0-9_]{3,20}$'
    return bool(re.match(pattern, username))

// Node.js - 안전한 LDAP

const ldap = require('ldapjs');

function secureLdapAuth(username, password, callback) {
    
    // ✅ 입력값 검증
    if (!isValidUsername(username)) {
        return callback(new Error('Invalid username'));
    }
    
    const client = ldap.createClient({
        url: 'ldap://ldap.xxbank.com'
    });
    
    // ✅ LDAP 이스케이프
    const escapedUsername = ldap.filters
        .escape(username);
    
    // ✅ 필터 객체 사용 (권장)
    const filter = new ldap.EqualityFilter({
        attribute: 'uid',
        value: escapedUsername
    });
    
    const opts = {
        filter: filter,
        scope: 'sub',
        // 필요한 속성만 요청
        attributes: ['uid', 'cn', 'mail']
    };
    
    client.search(
        'ou=people,dc=xxbank,dc=com',
        opts,
        (err, res) => {
            if (err) {
                return callback(err);
            }
            
            res.on('searchEntry', (entry) => {
                const dn = entry.objectName;
                
                // ✅ bind로 비밀번호 검증
                client.bind(dn, password, (bindErr) => {
                    if (bindErr) {
                        return callback(
                            new Error('인증 실패')
                        );
                    }
                    
                    callback(null, entry.object);
                });
            });
            
            res.on('error', (err) => {
                callback(err);
            });
        }
    );
}

function isValidUsername(username) {
    // 영문, 숫자, 언더스코어만
    return /^[a-zA-Z0-9_]{3,20}$/.test(username);
}

// LDAP 이스케이프 필수 문자

// 특수 문자 이스케이프
* → \2a
( → \28
) → \29
\ → \5c
NUL → \00
/ → \2f

// 예시: admin*) → admin\2a\29

// 방어 체크리스트

✅ 입력값 검증 (화이트리스트)
✅ LDAP 필터 이스케이프
✅ 파라미터 바인딩 사용
✅ 최소 권한 원칙
✅ 에러 메시지 일반화
✅ 로깅 및 모니터링
✅ 계정 잠금 정책
✅ TLS/SSL 암호화