LDAP – Lightweight Directory Access Protocol
Ik wilde graag een introductie geven in het beveiligen van LDAP en toen realiseerde ik me dat we het nog nooit uitgebreid over het LDAP protocol gehad hebben. LDAP staat voor Lightweight Directory Access Protocol en wordt gebruikt voor diverse toepassingen waaronder door Active Directory. LDAP is als het ware een database maar dan geheel op eigen wijze. Dat wil zeggen dat alvorens je aan de slag gaat met LDAP je wel inzicht moet hebben in het protocol en hoe het functioneert. In deze post dus meer informatie over LDAP. Lezen jullie mee?
Alvorens we LDAP kunnen beveiligen is het zaak om eerst te bekijken hoe LDAP werkt. LDAP is een communicatieprotocol welke de methodes definieert waarmee toegang tot een directoryservice kan worden verkregen. Een directoryservice wordt gebruikt om gegevens op te slaan, te organiseren en te presenteren volgens een key-value type indeling (vergelijkbaar met het register). LDAP geeft vorm aan de manier waarop de gegevens in een directoryservice aan gebruikers moeten worden weergegeven.
Attributen
De data binnen een LDAP-systeem wordt opgeslagen in elementen die we “attributen” noemen. Attributen zijn in feite sleutel / waarde-paren. In tegenstelling tot sommige andere systemen, hebben de sleutels vooraf gedefinieerde namen die worden gedicteerd door de objectClasses die zijn geselecteerd voor invoer. Bovendien moeten de gegevens in een attribuut overeenkomen met het type dat is gedefinieerd in de initiële definitie van het attribuut. Met andere woorden, sleutels (attributen) zijn gedefinieerd en de mogelijke waardes moeten een specifiek type zijn (een string, boolean, integer etc.).
Bijvoorbeeld. Het definiëren / instellen van een e-mailadres binnen LDAP zal er als volgt uitzien waarbij het attribuut “mail” is en de waarde een string met een verplicht apenstaartje. Het attribuut en de value worden van elkaar gescheiden met een dubbele punt:
mail: info@jarnobaselier.nl |
Als de waarde van het attribuut wordt opgevraagd zal de retourstring er als volgt uitzien:
mail=info@jarnobaselier.nl |
Let op: i.p.v. de dubbele punt wordt er nu een “=” teken gebruikt.
Attributen worden gedefinieerd met een specifieke syntax die beschrijft hoe het attribuut kan worden aangeroepen, welke data erin opgeslagen kan worden en welke metadata het attribuut nog meer kan bevatten. Het aanmaken van een voorbeeld attribuut ziet er als volgt uit:
attributetype ( 2.5.4.41 NAME 'HackerTarget' DESC 'Dit attribuut bevat een hacker target' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE) |
De NAME flag beschrijft de naam van het attribuut (deze moet uniek zijn) en de DESC flag beschrijft de omschrijving. EQUALITY en SUBSTR dat dit attribuut gebruikt mag worden binnen zoekopdrachten die gelijk zijn (EQUALITY) of een gegeven substring (SUBSTR) bevatten. In dit geval zijn ze beide niet hoofdlettergevoelig. De SYNTAX beschrijft hoe het attribuut “gevuld” mag worden. SINGLE-VALUE betekend dat het maar 1x mag voorkomen binnen een ENTRY (later meer over entries). 1.3.6.1.4.1.1466.115.121.1.15 is een OID (Object-ID) welke een “Directory String” datatype specificeerd welke is gecodeerd in de UTF-8-vorm van ISO 10646 (een superset van Unicode) met een maximale lengte van 128 tekens. Alle verschillende OID’s worden hier uitgelegd: https://ldapwiki.com/wiki/OID.
objectClass
Nu zegt een attribuut op zichzelf niet zo heel veel. Het is vaak de combinatie aan attributen die een “object” omschrijven. Wanneer je een gebruiker aanmaakt in AD krijg je vele LDAP velden (attributen) retour. Al deze velden zijn onderdeel van de user “objectClass”. Een objectclass is een verzameling van verschillende attributen welke bij elkaar horen. Er bestaan echter verschillende objectClasses.
- Structural – Structurele klassen zijn classes die het belangrijkste type object specificeren dat een item vertegenwoordigt (gebruiker, groep, apparaat, enz.). Structurele klassen kunnen overerven van abstracte en structurele objectklassen, maar niet van auxiliary classes.
- Auxiliary – Dit noemen we ookwel “hulp” classes. Deze worden gebruikt om informatie te verschaffen over aanvullende kenmerken van een attribute. Zo kan de “entry” worden uitgebreid zonder dat deze verandert hoeft te worden. Een voorbeeld hiervan is de “strongAuthenticationUser” objectClass als auxiliary class aan een item kan worden toegevoegd om aan te geven dat de gebruiker die door dat item wordt vertegenwoordigd over een certificaat beschikt welke nuttig kan zijn voor authenticatie. Hulpklassen kunnen erven van abstracte of andere auxiliary klassen, maar niet van structurele klassen.
- Abstract – Abstracte classes zijn classes die een set verplichte en optionele kenmerktypen kunnen specificeren maar die alleen bedoeld zijn om te worden gebruikt als ze worden uitgebreid met andere objectklassen. Abstracte classes kunnen worden uitgebreid met elk type objectklasse (inclusief andere abstracte classes) maar als een item een abstracte objectClass bevat, moet het ook ten minste één niet-abstracte objectClass bevatten die ervan overerft.
Een entry moet minimaal over 1 structurele class beschikken maar kan over 0 of meerdere auxiliary classes beschikken.
Het maken van een abstracte objectClass ziet er als volgt uit:
objectclass ( 2.5.6.6 NAME 'persoon' DESC 'Een Persoon' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) ) |
Hierin wordt de objectClass “person” gemaakt met een aantal attributen waarvan de surname en commonname verplicht zijn en de overige velden zijn optioneel.
Entry
Wanneer gegevens binnen LDAP gevuld worden noemen we dit een “entry” ofwel een “item”. Een entry bestaat meestal uit verschillende attributen (of een objectClass) welke gezamenlijk gebruikt worden om iets te omschrijven. Neem bijvoorbeeld een “user”. Een user bestaat uit meerdere attributen waaronder bijvoorbeeld:
dn: sn=Baselier,ou=contacts,dc=jarnobaselier,dc=nl objectclass: person sn: Baselier cn: Jarno Baselier mail: info@jarnobaselier.nl |
Data Information Trees (DIT)
Voordat verschillende LDAP attributen of een objectClass een entry kunnen vormen moeten deze een relatie hebben met elkaar. Dit doen we middels Data Information Trees (DIT). De DIT’s zorgen ervoor dat een attribuut beschikbaar kan zijn bij verschillende entries maar ze wel kan onderscheiden van elkaar. De relaties van attributen binnen de DIT vormen dus verschillende entries. Op deze manier kan LDAP verschillende soorten informatie op een gestructureerde en efficiënte manier verwerken. Dit ziet er schematisch ongeveer als volgt uit:
Een DIT is vergelijkbaar qua opbouw met de mappenstructuur zoals we die binnen een OS kennen. Op het bovenste item (de root) na heeft ieder item 1 bovenliggend item en kunnen er meerdere onderliggende items zijn. Het pad binnen de DIT kunnen we terugvinden in de Distinguished Name (dn). In het voorgaande voorbeeld (sn=Baselier,ou=contacts,dc=jarnobaselier,dc=nl) kun je zien dat we een sn object aanmaken genaamd “Baselier” binnen de contacts “entry” welke weer een onderliggende entry is van de DIT root “jarnobaselier.nl”.
Distinguished Name
De DN ofwel de “Distinguished Name” is de manier waarop we kunnen verwijzen naar een item binnen de DIT. Vergelijk de DIT met een folderpad. Alleen lezen we een DN van rechts naar links (rechts is de root). Een voorbeeld Distinguished Name is:
CN=HP-Superprinter, OU=printers, OU=machines, DC=jarnobaselier, DC=nl |
De DN kunnen we opdelen in specifieke delen welke relatief zijn aan elkaar. Zo bestaat bovenstaande voorbeeld uit 3 RDN’s (Relative Distinguished Name), namelijk “CN=HP-Superprinter”, “OU=printers, OU=machines” en “DC=jarnobaselier, DC=nl”. Verschillende RDN’s zijn:
- DC = domainComponent
- CN = commonName
- OU = organizationalUnitName
- O = organisationName
- STREET = streetAddress
- L = localityName
- ST = stateOrProviceName
- C = countryName
- uid – userid
Hierboven zie je meteen de LDAP naming convention. Namen zijn aan elkaar geschreven, beginnen met een kleine letter en alle opvolgende woorden zijn geschreven met een hoofdletter.
Een LDAP string bevat altijd een stukje van de DN. In een connection string geven we het gebruikte protocol (LDAP of LDAPS) op en de LDAP server. Vervolgens definiëren we het pad naar het object. Stel voor dat ik een connectie via LDAP op wil zetten naar de “printers” OU uit bovenstaande voorbeeld met LDAP dan ziet de connectiestring er als volgt uit:
ldap://%ldapserver%/OU=printers,OU=machines,DC=jarnobaselier,DC=nl |
Schema
ObjectClasses en attribuutdefinities worden op hun beurt gegroepeerd in een “schema”. In tegenstelling tot traditionele relationele databases zijn schema’s in LDAP gewoon verzamelingen van gerelateerde objectClasses en attributen. Een enkele DIT kan veel verschillende schema’s hebben zodat deze de benodigde items en attributen kan maken en terugvinden. Schema’s bevatten vaak aanvullende kenmerkdefinities en vereisen soms ook kenmerken die in andere schema’s zijn gedefinieerd. De “persoon” objectClass die we hierboven hebben aangemaakt vereist bijvoorbeeld dat de achternaam (sn) wordt ingesteld voor alle entries die de persoon objectClass gebruiken. Als de LDAP server nog niet over het “sn” attribuut beschikt kan een ander schema worden geraadpleegd (gebruikt) om deze informatie toe te voegen.
Dus om het hele concept samen te vatten:
Je hebt entries welke bestaan uit 1 of meerdere attributen welke op hun beurt behoren tot een specifieke objectClass. Attributen en objectClasses worden gegroepeerd in een schema. Dat alles bevind zich in een boomstructuur (de DIT) en wordt genoteerd middels een distinguished name.
Om bovenstaande nog schematisch in te tekenen voor Active Directory:
LDAP Query
Om informatie op te vragen of weg te schrijven wordt een LDAP Query (ofwel LDAP Search Filter) gebruikt. Deze query bestaat uit een specifieke syntax welke in mijn ogen simpeler had gekund. Neem bijvoorbeeld:
(&(objectClass=user)(sAMAccountName=jarnobaselier)(memberof=CN=Administrators,OU=Users,DC=jarnobaselier,DC=nl)) |
Zoals je in bovenstaande voorbeeld ziet (of niet) wordt hier mijn gebruikersaccount opgevraagd. In de basis is een searchstring als volgt opgebouwd:
- () – De hele LDAP Query staat tussen haakjes
- Wildcards – De LDAP Query begint met een wildcard. Alle opgegeven waardes moeten hieraan voldoen. Een & betekend een AND en dus moeten alle waardes voldoen. Met een | (pipe) ofwel een OR moet 1 waarde voldoen.
- objectClass = Geef 1 of meerdere objectClasses op waarbinnen je wilt zoeken. Dit groepeer je tussen haakjes.
- RDN = Geef eventueel optionele RDN waardes op waar naar gezocht moet worden. Dit groepeer je tussen haakjes.
Dus, als we zoeken naar gebruikers die behoren tot de objectClass “user” en “gebruiker” dan ziet de string er als volgt uit:
(&(objectClass=user)(objectClass=gebruiker)) |
En als we willen zoeken naar alle gebruikers die behoren tot de objectClass “user” en een CN hebben met “*hacker*” dan doen we het volgende:
(&(objectClass=user)(cn=*hacker*)) |
We kunnen deze ook combineren natuurlijk:
(&(objectClass=user)(objectClass=gebruiker)(cn=*hacker*)) |
Als we echter willen dat alle gebruikers geretourneerd worden die slechts tot 1 van bovenstaande behoren dan gebruiken we het “pipe” (OR) symbool:
(|(objectClass=user)(objectClass=gebruiker)(cn=*hacker*)) |
We kunnen ook meerdere wildcards combineren. Stel dat we willen dat alle gebruikers geretourneerd worden die lid zijn van beide objectclasses OF van de CN “*hacker*” dan doen we het volgende:
(&(objectClass=user)(objectClass=gebruiker)(|(cn=*hacker*))) |
Het uitroepteken (!) kunnen we gebruiken als NOT operator. Om dus alle groepen te retourneren uit de OU “Breda” en niet uit de OU “Rotterdam” gebruiken we:
(&(objectClass=groep)(&(ou:dn:=Breda)(!(ou:dn:=Rotterdam)))) |
Dit voorbeeld kunnen we uitbreiden. Als we b.v. alle gebruikers willen zien van een groep genaamd “superusers” doen we het volgende:
(&(objectCategory=Person)(sAMAccountName=*)(memberOf=cn=superusers,ou=gebruikers,dc=jarnobaselier,dc=nl)) |
Bovenstaande zoekt echter alleen users die direct lid zijn van deze groep. Om ook de lidmaatschappen van nested groeps te zien moet deze nummerieke string worden toegevoegd “:1.2.840.113556.1.4.1941:”:
(&(objectCategory=Person)(sAMAccountName=*)(memberOf:1.2.840.113556.1.4.1941:=cn=superusers,ou=gebruikers,dc=jarnobaselier,dc=nl)) |
Om gebruikers te zoeken die lid zijn van de groep “superusers” OF “administrators” maar niet van “losers” gebruik je de volgende zoekopdracht:
(&(objectCategory=Person)(sAMAccountName=*)(|(memberOf=cn=superusers, ou=gebruikers,dc=jarnobaselier,dc=nl)(memberOf=cn=administrators, ou=gebruikers,dc=jarnobaselier,dc=nl)(!(memberOf=cn=losers,ou=gebruikers,dc=jarnobaselier,dc=nl)))) |
LDAP Protocol
Het laatste wat interessant is om te begrijpen is hoe LDAP een sessie tot stand brengt. Het opbouwen en uitvoeren van een sessie gaat middels de volgende stappen:
- StartTLS – Wanneer LDAPS gebruikt wordt zal eerst een TLS sessie opgebouwd worden.
- Bind – Authentiseer en specificeer de LDAP protocol versie (meestal v3).
- Search – Zoeken naar entries binnen LDAP.
- Compare – Bekijk of een entry de juiste waardes bevat.
- Add – Voeg een nieuwe entry toe.
- Delete – Verwijder een entry.
- Modifiy – Maak aanpassingen in een bestaande entry.
- Modify DN – Verander de Distinguished Name door een entry te hernoemen of te verplaatsen.
- Abandon – Annuleer een eerdere LDAP aanvraag.
- Extended Operation – Definieer een andere actie
- Unbind – Sluit de connectie
- StopTLS – Breek de TLS sessie af.
LDAP functioneert over poort 389. LDAPS communiceert over poort 636.
Zoals je hierboven zag werken de zoekopdrachten soms met nummers. Zo betekend nummer “1.2.840.113556.1.4.803” het “LDAP_MATCHING_RULE_BIT_AND”. Door op bit 2 te controleren zien we alle attributen die gerepliceerd kunnen worden. Disabled users hebben bit “514”. Op die manier kunnen we b.v. alle gebruikers opvragen die niet disabled zijn:
(objectCategory=person)(objectClass=user)(!useraccountcontrol:1.2.840.113556.1.4.803:=2) |
Alle OID’s zijn terug te vinden op LDAPWiki – https://ldapwiki.com/wiki/.
Zoals al gezegd wordt “normaal” LDAP verkeer onversleuteld over het netwerk gestuurd. Zoals je ziet kunnen er zeer belangrijke gegevens van een organisatie buitgemaakt worden door simpelweg het verkeer te sniffen. Om dit te beveiligen kunnen we LDAP Signing, Channel Binding of LDAPS (Lightweight Directory Access Protocol Over Secure Socket Links) implementeren. Laten we deze opties eens bekijken.
Conclusie
Tot zo ver de LDAP theorie. Hopelijk heb je een klein beetje meer inzicht in LDAP en hoe deze op de achtergrond functioneerd. In de volgende post gaan we dan eindelijk starten met het beveiligen van LDAP. Tot dan!
Als je echt tof bent deel je deze post even op je website of social media, geef je hem een duimpje omhoog of stuur je even een leuk berichtje. Dankjewel!!