LDAP Injection and Mitigation


RV here…

The Lightweight Directory Access Protocol (LDAP) API provides a mechanism for connecting to, searching, and modifying internet directories. A LDAP (Lightweight Directory Access Protocol) injection attack exploits vulnerabilities in input validation to run arbitrary LDAP statements against information directories. It can occur when your application uses input to construct dynamic LDAP statements to access directory services. Using the LDAP injection attack, the attacker can execute arbitrary statements against the directory services.

In .NET System.DirectoryServices provides multiple classes to search and retrieve information from LDAP stores such as Active Directory. The following code illustrates the vulnerable code.

   1: public string GetUserFullName(string loginName)
   2: {
   3:     //Root entry for contoso.com global catalog
   4:     DirectoryEntry root = new 
   5:         DirectoryEntry("GC://DC=contoso,DC=com");
   6:  
   7:     //Instantiating directory searcher object
   8:     DirectorySearcher searcher = new DirectorySearcher();
   9:     root.AuthenticationType = AuthenticationTypes.Secure;
  10:     searcher.SearchRoot = root;
  11:  
  12:     //Searching the directory for the specified windows login name
  13:     searcher.Filter = string.Format("(sAMAccountName={1})", 
  14:         loginName);
  15:     SearchResult result = searcher.FindOne();
  16:     //Retrieving user common name
  17:     string userName = result.Properties["cn"][0].ToString();
  18:     root.Close();
  19:     //Returning the user name
  20:     return userName;
  21: }

Line 13 in the above code dynamically constructs the LDAP search query which allows the user to completely control the LDAP query. In order to mitigate the above vulnerability input needs to be validated or encoded. Input validation is always the best approach but it still does not make the query valid if input allows one of the special characters in LDAP search query. RFC 4515 : Lightweight Directory Access Protocol (LDAP): String Representation of Search Filters provides detailed information on how the search filters should be formed and what characters need to be escaped or encoded. In this case proper character encoding mitigates the LDAP Injection Vulnerability. The simple character encoding requires each octet of the character to be escaped is replaced by a backslash and two hex digits. In essence a left parenthesis ( can be escaped as \28 and right parenthesis as \29 respectively. The following table shows more complex encoding examples.

Input Encoded Input
(o=Parens R Us (for all your parenthetical needs)) (o=Parens R Us \28for all your parenthetical needs\29)
(cn=***) (cn=*\2A*)
(filename=C:\MyFile) (filename=C:\5cMyFile)
(bin=NULLNULLNULLEOT) (bin=\00\00\00\04)
(sn=Lučić) (sn=Lu\c4\8di\c4\87)

To fix the above vulnerable code user input needs to be properly encoded. The next version of Web Protection Library (WPL) will include encoding method for LDAP search strings which will perform above mentioned hex encoding. Similar to Anti-XSS Library, LDAP encoding uses white-list which includes non English character sets such as CJK, Indic Languages etc. Keep checking the blog for more posts on new features in WPL.

Thanks

RV

Comments (2)

  1. sfaust says:

    The encoding depends on how the value is used.

    When the value is used for a DN, the following characters needs to be escaped using the

    &

    !

    |

    =

    <

    >

    ,

    +

    "

    ;

    When the value is used as part of the filter, the following needs to be encoded using {ASCII}

    ( => 28

    ) => 29

    => 5c

    * => 2a

    / => 2f

    NULL =>

    Code implementation

    using System;

    using System.Collections.Generic;

    using System.Text;

    namespace LdapValidation

    {

       public class LdapCanonicaliztion

       {

           /// <summary>

           /// Characters that must be escaped in an LDAP filter path

           /// WARNING: Always keep ‘\’ at the very beginning to avoid recursive replacements

           /// </summary>

           private static char[] ldapFilterEscapeSequence = new char[] { ‘\’, ‘*’, ‘(‘, ‘)’, ‘’, ‘/’ };

           /// <summary>

           /// Mapping strings of the LDAP filter escape sequence characters

           /// </summary>

           private static string[] ldapFilterEscapeSequenceCharacter = new string[] { "\5c", "\2a", "\28", "\29", "\00", "\2f" };

           /// <summary>

           /// Characters that must be escaped in an LDAP DN path

           /// </summary>

           private static char[] ldapDnEscapeSequence = new char[] { ‘\’, ‘,’, ‘+’, ‘"’, ‘<‘, ‘>’,’;’ };

           /// <summary>

           /// Canonicalize a ldap filter string by inserting LDAP escape sequences.

           /// </summary>

           /// <param name="userInput">User input string to canonicalize</param>

           /// <returns>Canonicalized user input so it can be used in LDAP filter</returns>

           public static string CanonicalizeStringForLdapFilter(string userInput)

           {

               if (String.IsNullOrEmpty(userInput))

               {

                   return userInput;

               }

               string name = (string)userInput.Clone();

               for (int charIndex = 0; charIndex < ldapFilterEscapeSequence.Length; ++charIndex)

               {

                   int index = name.IndexOf(ldapFilterEscapeSequence[charIndex]);

                   if (index != -1)

                   {

                       name = name.Replace(new String(ldapFilterEscapeSequence[charIndex], 1), ldapFilterEscapeSequenceCharacter[charIndex]);

                   }

               }

               return name;

           }

           /// <summary>

           /// Canonicalize a ldap dn string by inserting LDAP escape sequences.

           /// </summary>

           /// <param name="userInput">User input string to canonicalize</param>

           /// <returns>Canonicalized user input so it can be used in LDAP filter</returns>

           public static string CanonicalizeStringForLdapDN(string userInput)

           {

               if (String.IsNullOrEmpty(userInput))

               {

                   return userInput;

               }

               string name = (string)userInput.Clone();

               for (int charIndex = 0; charIndex < ldapDnEscapeSequence.Length; ++charIndex)

               {

                   int index = name.IndexOf(ldapDnEscapeSequence[charIndex]);

                   if (index != -1)

                   {

                       name = name.Replace(new string(ldapDnEscapeSequence[charIndex], 1), @"" + ldapDnEscapeSequence[charIndex] );

                   }

               }

               return name;

           }

           /// <summary>

           /// Ensure that a user provided string can be plugged into an LDAP search filter

           /// such that there is no risk of an LDAP injection attack.

           /// </summary>

           /// <param name="userInput">String value to check.</param>

           /// <returns>True if value is valid or null, false otherwise.</returns>

           public static bool IsUserGivenStringPluggableIntoLdapSearchFilter(string userInput)

           {

               if (string.IsNullOrEmpty(userInput))

               {

                   return true;

               }

               if (userInput.IndexOfAny(ldapDnEscapeSequence) != -1)

               {

                   return false;

               }

               return true;

           }

           /// <summary>

           /// Ensure that a user provided string can be plugged into an LDAP DN

           /// such that there is no risk of an LDAP injection attack.

           /// </summary>

           /// <param name="userInput">String value to check.</param>

           /// <returns>True if value is valid or null, false otherwise.</returns>

           public static bool IsUserGivenStringPluggableIntoLdapDN(string userInput)

           {

               if (string.IsNullOrEmpty(userInput))

               {

                   return true;

               }

               if (userInput.IndexOfAny(ldapFilterEscapeSequence) != -1)

               {

                   return false;

               }

               return true;

           }

       }

    }

  2. anilkr says:

    Currently our encoding function is for values in the query. Encoding for values beyond the mentioned characters in XX worked fine with our tests. We tested using sAMAccountName, Street, OU, CN, etc. We also handle unicode characters in XXXX format based on the existing whitelist from Anti-XSS library.