소프트웨어 개발보안 가이드 분석(2021) : LDAP 삽입

공격자가 외부 입력으로 의도하지 않은 LDAP 명령어를 수행할 수 있다.

즉, 웹 응용프로그램이 사용자가 제공한 입력을 올바르게 처리하지 못하면, 공격자가 LDAP 명령문의 구성을 바꿀 수 있다.

이로 인해 프로세스가 명령을 실행한 컴포넌트와 동일한 권한(Permission)을 가지고 동작하게 된다.

소프트웨어 개발보안가이드(2021), 한국인터넷진흥원

LDAP은 디렉터리 서비스에서 정보를 검색하고 수정하기 위한 프로토콜로, 사용자 인증, 권한 부여, 사용자 정보 검색 등의 용도로 사용됩니다. 이번 포스팅에서는 이러한 LDAP을 이용하여 Injection 공격을 수행하는 LDAP 삽입에 취약한 코드 예제와 이를 방지하기 위한 안전한 코드 작성 방법을 살펴보려고 합니다.

LDAP 삽입이란

LDAP 삽입(LDAP Injection)은 웹 애플리케이션에서 발생할 수 있는 보안 취약점 중 하나로, 사용자로부터 입력 받은 데이터를 적절히 검증하지 않고 LDAP(Lightweight Directory Access Protocol) 쿼리에 삽입하여 발생합니다. 이를 통해 공격자는 LDAP 서버에 대한 무단 접근을 시도하거나 기밀 정보를 탈취할 수 있습니다.

LDAP 삽입의 공격 매커니즘

LDAP 삽입 공격은 주로 사용자 입력값에 직접 LDAP 쿼리를 삽입함으로써 이루어집니다. 보통 로그인 폼 등의 입력란에 항상 참의 결과를 나타내는 LDAP 삽입 코드를 입력하여 LDAP 삽입 공격에 취약한 서버에서는 입력한 사용자 이름이나 비밀번호가 LDAP 쿼리에 그대로 반영되고, 그대로 실행됩니다.

취약한 웹 어플리케이션의 예

다음은 LDAP 삽입 취약점을 가진 취약한 웹 어플리케이션의 간단한 예시입니다.

String username = request.getParameter("username");
String password = request.getParameter("password");
String ldapQuery = "(&(uid=" + username + ")(userPassword=" + password + "))";
  • 위 코드에서는 사용자로부터 입력받은 username과 password를 바로 LDAP 쿼리에 삽입하고 있습니다. 이는 입력란에 대한 유효성 검증을 전혀 하고 있지 않기 때문에 공격자가 악의적인 LDAP 쿼리를 삽입하여 LDAP 서버에 대한 공격을 수행할 수 있는 취약점을 가지고 있습니다.
  • 위 코드를 사용한다면 공격자는 인증 우회, 데이터 노출, DoS 등 공격을 수행할 수 있게 됩니다. 그렇다면 이러한 취약점을 막기 위해서 어떻게 작성해야 안전한 코드가 되는지 알아보겠습니다.

시큐어코딩 적용 방법

  • 입력값 검증
public static void main(String[] args) {
        // 사용자로부터 입력 받기
        String username = args[0];
        String password = args[1];

        // LDAP 연결 설정
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://ldap.example.com");

        // 사용자 입력을 이스케이프하여 안전한 LDAP 쿼리 생성
  1     String escapedUsername = escapeLDAPSearchFilter(username);
  2     String escapedPassword = escapeLDAPSearchFilter(password);
  3     String ldapQuery = "(&(uid=" + escapedUsername + ")(userPassword=" + escapedPassword + "))";
    }

    public static String escapeLDAPSearchFilter(String filter) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < filter.length(); i++) {
            char curChar = filter.charAt(i);
            switch (curChar) {
                case '\\':
                    sb.append("\\5c");
                    break;
                case '*':
                    sb.append("\\2a");
                    break;
                case '(':
                    sb.append("\\28");
                    break;
                case ')':
                    sb.append("\\29");
                    break;
                case '\u0000':
                    sb.append("\\00");
                    break;
                default:
                    sb.append(curChar);
            }
        }
        return sb.toString();
    }
  • escapeLDAPSearchFilter 메서드를 이용하여 특수 문자를 이스케이프하여 입력값을 안전하게 처리해 줍니다.
  • 1, 2 : 사용자 입력 값(Id, password)을 이스케이프 처리합니다.
  • 3 : 서버에 저장되 있는 uid와 이스케이프 처리된 ID(escapedUsername), userPassword와 이스케이프 처리된 password(escapePassword)를 각각 비교하여 모두 일치하는지 검증합니다.
  • 결과적으로 사용자 입력값에 대한 유효성 검증을 수행한 후 서버에 저장되어 있는 uid와 userPasswrod를 비교하는 사용자 인증을 수행합니다.

  • 매개변수화된 쿼리 ≈ Prepared Statement
    public static void main(String[] args) {
        // 사용자로부터 입력 받기

        // LDAP 연결 설정

        // LDAP 컨텍스트 생성
  1     DirContext ctx = new InitialDirContext(env);

        // 매개변수화된 LDAP 쿼리 생성
  2     String ldapQuery = "(&(uid={0})(userPassword={1}))";
  3     Object[] queryArgs = {username, password};
        SearchControls controls = new SearchControls();
        controls.setSearchScope(SearchControls.SUBTREE_SCOPE);

        // LDAP 쿼리 실행
  4     NamingEnumeration<SearchResult> results = ctx.search("", ldapQuery, queryArgs, controls);

        // 결과 처리
        while (results.hasMore()) {
            SearchResult searchResult = results.next();
        }
    }
  • "매개변수화된 쿼리"는 Prepared Statement와 비슷한 개념입니다. Prepared Statement는 주로 관계형 데이터베이스에서 사용되는 용어지만, 비슷한 개념이 LDAP 쿼리에도 적용될 수 있습니다.
  • 1 : 설정된 연결 속성을 사용하여 초기 LDAP 컨텍스트를 생성합니다.
  • 2,3 : 매개변수화된 LDAP 쿼리를 생성합니다. 여기서 {0}, {1}가 username, password를 대체할 자리 표시자 입니다.
  • 4 : LDAP 쿼리를 실행하고 결과를 검색합니다. 실행된 쿼리에 매개변수가 바인딩되어 실행됩니다. 여기서는 빈 문자열("")을 기준으로 검색을 수행하며, LDAP 쿼리, 매개변수, 검색 범위 및 검색 옵션을 통해 결과를 검색합니다.
  • 결과적으로 사용자가 입력한 username과 password를 LDAP 쿼리에 직접 삽입하는 대신, 매개변수화된 방식을 사용하여 쿼리를 안전하게 실행합니다. 또한 LDAP 서버에 연결하기 위한 속성들을 안전하게 설정하고, 로그인 실패 시 적절한 오류 처리를 수행할 수 있는 이점도 있습니다.

 

LDAP 삽입 공격을 방지하기 위한 시큐어코딩 적용 방법으로는 다음과 같은 방법들이 있습니다.

  1. 입력값 검증: 모든 사용자 입력값에 대해 적절한 검증을 수행하여 LDAP 쿼리에 삽입될 수 있는 특수문자를 필터링합니다.
  2. 매개변수화된 쿼리 사용: 사용자 입력을 쿼리에 직접 삽입하는 대신, 매개변수화된 쿼리를 사용하여 안전하게 쿼리를 실행합니다.
  3. 최소한의 권한 부여: LDAP 서버에 접근하는 계정에는 최소한의 권한만을 부여하여 불필요한 데이터에 대한 접근을 제한합니다.
  4. 보안 강화된 라이브러리 사용: LDAP 쿼리를 실행하는 데 사용되는 라이브러리가 보안 강화된 것을 사용하여 취약점을 최소화합니다.
  5. 로그 모니터링 : 시스템 로그를 주기적으로 모니터링하여 LDAP 삽입 공격과 같은 보안 이벤트를 식별하고 대응합니다.

  • 이전 소프트웨어 개발보안 가이드 분석(2021) : XML 삽입
  • 다음 소프트웨어 개발보안 가이드 분석(2021) : 크로스사이트 요청 위조