가이드 49 제1장-10: LDAP 삽입 | 위험도: 5 (매우 높음)
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 조건으로 모든 사용자 조회
임직원 인증 및 정보 관리
// 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 암호화