IgnatiusHeo

구현단계 보안약점 제거 기준-LDAP 삽입 본문

자격/SW보안약점진단원

구현단계 보안약점 제거 기준-LDAP 삽입

Ignatius Heo 2023. 7. 4. 05:00

작성일: 230704

 

※ 본 게시글은 학습 목적으로 행정안전부·KISA의 소프트웨어 보안약점 진단 가이드, 소프트웨어 개발보안 가이드를 참고하여 작성하였습니다.

 

정리 내용: 소프트웨어 보안약점 진단 가이드(264~271p)

 

구분 - 입력데이터 검증 및 표현
설계단계 - 디렉토리 서비스 조회 및 결과 검증
https://cryptocurrencyclub.tistory.com/91
개요
공격자가 외부 입력으로 의도하지 않은 LDAP(Lightweight Directory Access Protocol) 명령어를 수행할 수 있다.

즉, 웹 응용프로그램이 사용자가 제공한 입력을 올바르게 처리하지 못하면, 공격자가 LDAP 명령문의 구성을 바꿀 수 있다. 이로 인해 프로세스가 명령을 실행한 컴포넌트와 동일한 권한(Permission)을 가지고 동작하게 된다.

외부입력값을 적절한 처리 없이 LDAP 쿼리문이나 결과의 일부로 사용하는 경우, LDAP 쿼리문이 실행될 때 공격자는 LDAP 쿼리문의 내용을 마음대로 변경할 수 있다.

진단 세부사항
(설계단계)
 
 ① LDAP 인증서버로 인증을 구현하는 경우 인증요청을 위해 사용되는 외부입력값은 LDAP 삽입 취약점을 가지지 않도록 필터링해서 사용해야 하며, LDAP 질의문 생성 시 사용되는 입력값과 조회결과에 대한 검증방법(필터링 등)을 설계하고 유효하지 않은 값에 대한 처리방법이 명시되어 있는지 확인한다.
  ㅇ  LDAP 조회필터 생성에 사용되는 입력값을 필터링하는 기능이 설계되어 있거나, 안전한 외부라이브러리를 사용하도록 설계되어 있는지 확인
  ㅇ  LDAP 조회기능 구현시, LDAP필터링을 적용하기 위한 코딩규칙이 개발가이드에 정의되어 있는지 확인
  ㅇ  LDAP 필터구문을 변경할 수 있는 입력값을 사용하여 LDAP필터의 규칙이 변경되는지를 점검하기 위한 테스트계획 수립 여부 확인
    → 구문 변경 가능 입력값: = + < > # ; \

 
보안대책
(구현단계)

DN(Distinguished Name)과 필터에 사용되는 사용자 입력값에는 특수문자가 포함되지 않도록 특수 문자를 제거한다. 만약 특수문자를 사용해야 하는 경우에는 특수문자( = + < > # ; \ 등)가 실행 명령이 아닌 일반문자로 인식되도록 처리한다.

진단방법
(구현단계)

① LDAP 조회 쿼리가 실행됨을 확인하고,

② LDAP 조회문의 필터에 사용되는 변수가 외부 입력값인지 확인한 후,

③ 해당 변수에 대한 필터링 모듈이 존재하는지 확인한다.

필터링 모듈이 존재하거나 관련 프레임워크에서 적절히 조치할 경우엔 안전한 것으로 판정한다.

 

다. 코드예제


ㅇ 분석

1: private void searchRecord(String userSN, String userPassword) throws
NamingException {
2: Hashtable<String, String> env = new Hashtable<String, String>();
3: env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
4: try {
5: DirContext dctx = new InitialDirContext(env);
6: SearchControls sc = new SearchControls();
7: String[] attributeFilter = { "cn", "mail" };
8: sc.setReturningAttributes(attributeFilter);
9: sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
10: String base = "dc=example,dc=com";
//userSN과 userPassword 값에 LDAP필터를 조작할 수 있는 공격 문자열에 대한 검증이 없어 안전하지
않다.
11: String filter = "(&(sn=" + userSN + ")(userPassword=" + userPassword + "))";
12: NamingEnumeration<?> results = dctx.search(base, filter, sc);
13: while (results.hasMore()) {
14: SearchResult sr = (SearchResult) results.next();
15: Attributes attrs = sr.getAttributes();
16: Attribute attr = attrs.get("cn");
17: ......
18: }
19: dctx.close();
20: } catch (NamingException e) { … }
21:}

ㅇ 설명

1. r11에서 userSN, userPassword를 필터링 안하고 그대로 filter에 삽입함. filter에 넣기 전에 검증해야함.

 

 

ㅇ 수정

1: private void searchRecord(String userSN, String userPassword) throws NamingException {
2: Hashtable<String, String> env = new Hashtable<String, String>();
3: env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
4: try {
5: DirContext dctx = new InitialDirContext(env);
6: SearchControls sc = new SearchControls();
7: String[] attributeFilter = {"cn", "mail" };
8: sc.setReturningAttributes(attributeFilter);
9: sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
10: String base = "dc=example,dc=com";
// userSN과 userPassword 값에서 LDAP 필터를 조작할 수 있는 문자열을 제거하고 사용
11: if (!userSN.matches("[\\w\\s]*") || !userPassword.matches("[\\w]*")) {
12: throw new IllegalArgumentException("Invalid input");
13: }
14: String filter = "(&(sn=" + userSN + ")(userPassword=" + userPassword + "))";
15: NamingEnumeration<?> results = dctx.search(base, filter, sc);
16: while (results.hasMore()) {
17: SearchResult sr = (SearchResult) results.next();
18: Attributes attrs = sr.getAttributes();
19: Attribute attr = attrs.get("cn");
20: ......
21: }
22: dctx.close();
23: } catch (NamingException e) { … }
24:}

ㅇ 설명

1. filter 삽입 전 구문 변경 가능 입력값 필터링 수행

w = word(특수문자를 포함하지 않는 문자 및 숫자임. _ 포함 ), s = space 임

!userSN.matches("[\\w\\s]*") =  단어문자, 공백문자로만 구성되어 있는지 확인

!userPassword.matches("[\\w]*") = 단어문자로만 구성되는지 확인

 


ㅇ 분석

1: static void SearchRecord(string userSN, string userPW)
2: {
3: try {
4: DirectoryEntry oDE;
5: oDE = new DirectoryEntry(GetStrPath());
// 인증을 하지않은 익명 바인딩으로 LDAP 쿼리를 실행
6: foreach(DirectoryEntry objChildDE om oDE.Children) {
7: ...
8: }
9: } catch (NamingException e) { ... }
10:}

ㅇ 내용

1. 디렉토리 엔트리 객체 생성 시 GetStrPath뒤가 비어있는 걸 보면 익명바인딩임. 왜 입력값 받아놓고 권한설정 + 확인안함?

 

ㅇ 수정

1: static void SearchRecord(string userSN, string userPW)
2: {
3: try {
4: DirectoryEntry oDE;
5: oDE = new DirectoryEntry(GetStrPath(), userSN, userPW);
// userSN과 userPW 로 인증 후에 LDAP 쿼리를 실행
6: foreach(DirectoryEntry objChildDE om oDE.Children) {
7: ...
8: }
9: } catch (NamingException e) { ... }

ㅇ 내용

1. 유저 인증정보 삽입

 


ㅇ 분석

 

1: void LDAPInjection() {
2: char *filter = getenv(“Filter”);
3: int error_code;
4: LDAP *ld = NULL;
5: LDAPMessage *result;
// 외부에서 불러온 filter를 검증 없이 사용
6: error_code = ldap_search_ext_s(ld, FIND_DN, LDAP_SCOPE_BASE, filter,
NULL, 0, NULL, NULL, LDAP_NO_LIMIT, LDAP_NO_LIMIT, &result);
7: }

ㅇ 내용

1. r2 getenv filter값에 대한 검증 없이

2. r6의 LDAP 쿼리에 그대로 적용해서 사용하고 있음

 

ㅇ 수정

1: void LDAPInjection() {
2: char *filter = getenv(“Filter”);
3: int error_code;
4: int i;
5: LDAP *ld = NULL;
6: LDAPMessage *result;
// 정보를 알고 싶은 사용자의 이름을 고정 값으로 사용
7: for(i = 0; *(filter + i) != 0; i++) {
// 공격 가능한 문자열 검사
8: switch(*(filter + i)) {
9: case ‘*’:
10: case ‘(’:
11: case ‘)’:
12: …
13: return;
14:}
15:}
16:error_code = ldap_search_ext_s(ld, FIND_DN, LDAP_SCOPE_BASE, filter,
NULL, 0, NULL, NULL, LDAP_NO_LIMIT, LDAP_NO_LIMIT, &result);

ㅇ 내용

1. 문자열 검사 실행

 

 

 

 

 

 

 

 

 

 

 

 

 

 

끝.