Каждый знает про SQL-инъекции (SQL injection). Это как знаменитость в мире ИБ. Но кроме них еще существует множество других разновидностей инъекций, которые могут лишь позавидовать популярности SQL-инъекций. И это не совсем справедливо. Устраним же эту несправедливость и поговорим о LDAP-инъекции (LDAP injection).

English version is here.

Что такое LDAP?

LDAP означает Lightweight Directory Access Protocol. Это сетевой протокол прикладного уровня для доступа к так называемой службе каталогов. Протокол является не текстовым (как, например, HTTP), а бинарным. LDAP протокол обычно используется поверх TCP/IP, но лучше еще использовать в рамках TLS соединения. За подробностями о LDAP протоколе стоит обратиться к RFC 2251.

Если вкратце, то LDAP сервер содержит базу данных, где данные хранятся в виде “атрибут = значение”.  LDAP описывает как эти данные могут быть получены и изменены.

На текущей момент известно некоторое количество ПО, которое поддерживает LDAP, например:

  • OpenLDAP
  • Java содержит в себе LDAP-клиент, который доступен через JNDI.

Что такое LDAP-инъекция?

LDAP-инъекция (LDAP injection) это способ нападения, который позволяет злобному злоумышленнику коварно модифицировать LDAP-запрос, который использует приложение для обращения к LDAP-хранилищу. Достигается это злодейство путем отправки приложению хитроумно подготовленных данных, которые содержат части LDAP-запроса. В результате беспринципный злоумышленник может отправлять LDAP-хранилищу всевозможные запросы, порой даже абсолютно любые, которые приложение само никогда бы не отправило. Это в свою очередь может привести к несанкционированному чтению или даже изменению всякой важной информации, которая может храниться в LDAP-хранилище. Обычно LDAP-инъекции возникают, потому что приложение не проверяет входные данные от пользователя, который может оказаться злобным злоумышленником, и наивно использует их для построения LDAP-запроса к базе данных.

Пример LDAP-инъекции

Давайте на минуточку представим, что у нас есть LDAP-сервер, который надежно спрятан в нашей внутренней сети, которую героически защищает от враждебного внешнего мира наша огненная стена, которая больше известна, как firewall. Пусть этот LDAP-сервер хранит данные пользователей, такие как адреса, пароли, явки. Еще представим, что у нас есть некое приложение, которое использует данные из нашего LDAP-хранилища для аутентификации пользователей. Другими словами, для предоставления доступа наше приложение просит пользователя ввести имя и пароль, которые оно потом сверяет с данными, которые хранятся на нашем LDAP-сервере.

В целях демонстрации мы можем запустить локальный LDAP-сервер с помощью Ldaptor (он еще нуждается в Twisted, который можно установить с помощью pip). Разработчики Ldaptor любезно предоставили пример LDAP-сервера, который я благодарно позаимствовал:

Вышеприведенный сервер может быть запущен с помощью команды python ldapserver.py

А вот и пример уязвимого приложения на Java:

Наше бедное приложение спрашивает у пользователя его имя и пароль, которые пользователь предоставляет через параметры командной строки. Далее наше милое приложение отправляет LDAP-запрос к нашему LDAP-хранилищу, чтобы получить соответствующую запись о нашем пользователе. Если такая запись была найдена, то счастливый пользователь получает доступ, который ознаменовывается появлением сообщения “Access granted”. Приведем пример того, как это приложение работает, когда пользователь вводит правильные и неправильные пароли:

$ javac -d classes LDAPLogin.java 
$ java -classpath classes LDAPLogin bob secret
LDAP query: (&(uid=bob)(userPassword=secret))
Access granted
$ java -classpath classes LDAPLogin bob wrong
LDAP query: (&(uid=bob)(userPassword=wrong))
Access denied

Проблема этого простого приложения заключается в том, что оно ветрено подставляет имя и пароль, которые предоставляет ему пользователь, прямо в LDAP-запрос. Такое халатное поведение приложения дает хитрому злоумышленнику возможность модифицировать запрос, которые отправляется к LDAP-хранилищу. В свою очередь это может быть использовано, например, для обхода аутентификации. Если коварный злоумышленник не знает пароля Боба, то он может попросту использовать строку bob)(|(uid=bob в качестве имени пользователя и строку wrong) в качестве пароля, что в результате позволит ему получить долгожданный доступ.

$ java -classpath classes LDAPLogin "bob)(|(uid=bob" "wrong)"
LDAP query: (&(uid=bob)(|(uid=bob)(userPassword=wrong)))
Access granted

Можно видеть, что приложение отправило запрос (&(uid=bob)(|(uid=bob)(userPassword=wrong))) к LDAP-хранилищу. LDAP использует в запросах к базе данных так называемую “польскую нотацию”. Этот запрос означает “(uid == bob) and (uid=bob or userPassword=wrong)”. Это логическое выражение всегда истинно для записи Боба, поэтому запрос всегда возвращает запись Боба, если даже был подсунут неправильный пароль. В результате злобный злоумышленник может обойти аутентификацию.

Настоящий exploit для LDAP-инъекции может зависеть от разных вещей, например:

  • LDAP-запроса, который используется приложением
  • Версии LDAP-сервера
  • Логики работы приложения

В природе можно наблюдать три основных разновидностей LDAP-запросов:

  • Запрос без логических операторов. Выглядят они как (field=value). Если даже мы можем поместить что угодно вместо value, то мы все равно не сможем использовать никакие логические операторы, потому что записи в Польской нотации требуют, чтобы логический оператор был вначале. Все что можно сделать, это попробовать использовать какую-то уязвимость в самом LDAP-сервере, если она может быть вызвана каким-то значением value. Например, это может быть переполнение буфера, если конечно сервер ему подвержен.
  • Оператор AND находится вначале поискового запроса. Выглядит это как (&(field1=value1)(field2=value2)). Пример подобной ситуации был рассмотрен выше.
  • Оператор OR находится вначале поискового запроса. Выглядит это как
  • (|(field1=value1)(field2=value2)).

Для успеха предприятия очень хорошо и полезно знать, какой тип запроса используется приложением.

Приложение также может использовать LDAP-запросы для обновления данных в LDAP-хранилище. LDAP использует LDAP Data Interchange Format (LDIF) для обновления данных в хранилище. LDIF представляет собой список команд для добавления, удаления или изменения записей в базе данных. Это попросту набор записей, одна запись - одна команда. ldapserver.py, который приведен выше, содержит пример данных в формате LDIF, который добавляет три записи в базу данных. Если хитрый злоумышленник может внедрить свои данные в LDIF запрос, он может использовать CRLF символы для разделения команд. В результате, злобный злоумышленник потенциально может, например, добавить любые записи в базу данных. Это чем-то похоже на HTTP request splitting.

Версия LDAP-сервера тоже может иметь значение, потому что разные версии LDAP-серверов могут иметь разные особенности в обработке запросов. Иногда коварный злоумышленник может использовать LDAP-инъекцию таким образом, что LDAP-запрос включает в себя несколько поисковых запросов. Если LDAP-сервер использует только первый запрос и игнорирует остальные, вместо возвращения ошибки, то это может помочь в осуществлении атаки. К сожалению, синтаксис LDAP-запросов не допускает комментирования окончания запроса, как это можно сделать в SQL.

LDAP-инъекции вслепую (Blind LDAP injections)

Blind LDAP-инъекции чем-то похожи на blind SQL-инъекции. Приложение может быть уязвимо к LDAP-инъекции, но может так случиться, что вредное приложение не будет отображать все поля записей. Такое печальное обстоятельство не позволит злобному злоумышленнику просто прочитать содержимое LDAP-хранилища. Но если приложение каким-то образом дает знать, что отправленный запрос с данными от злоумышленника завершился успешно или нет, то такое поведение может позволить хитрому злоумышленнику задавать серверу так называемые да/нет вопросы. В результате беспринципный злоумышленник может осуществить эффективный перебор и извлечь ценные данные из базы данных.

Представим, что у нас есть такое приложение на Java. Оно спрашивает у пользователя его идентификатор для поиска записи пользователя в базе данных. Если запись была найдена, то приложение печатает номер телефона пользователя. Если не найдена, то приложение просто сообщает, что ничего не было найдено. Приложение может использовать LDAP-сервер выше. А вот и оно само.

А вот и пример штатного использования приложения:

$ javac -d classes LDAPInfo.java 
$ java -cp classes LDAPInfo bob
LDAP query: (&(uid=bob)(objectClass=person))
Phone: telephoneNumber: 555-9999
$ java -cp classes LDAPInfo boba
LDAP query: (&(uid=boba)(objectClass=person))
Nobody found!

Кто-то может заметить, что LDAPInfo подвержен LDAP-инъекции. Хотя приложение лишь печатает номер телефона, коварный злоумышленник все равно может извлечь ценные данные из других полей. Например, злобный злоумышленник может спросить у сервера, начинается ли поле userPassword в записи Боба с буквы ‘a’:

$ java -cp classes LDAPInfo "bob)(userPassword=a*"
LDAP query: (&(uid=bob)(userPassword=a*)(objectClass=person))
Nobody found!

Если передать строку bob)(userPassword=a* в качестве имени пользователя, то это приведет к появлению сообщения ‘Nobody found!’. Приложение построило запрос (&(uid=bob)(userPassword=a*)(objectClass=person)) , который ничего не вернул, потому что поле userPassword из записи Боба не начинается с буквы ‘a’. Теперь коварный злоумышленник может перебрать первую букву пароля Боба:

$ java -cp classes LDAPInfo "bob)(userPassword=a*"
LDAP query: (&(uid=bob)(userPassword=a*)(objectClass=person))
Nobody found!
$ java -cp classes LDAPInfo "bob)(userPassword=b*"
LDAP query: (&(uid=bob)(userPassword=b*)(objectClass=person))
Nobody found!
$ java -cp classes LDAPInfo "bob)(userPassword=c*"
LDAP query: (&(uid=bob)(userPassword=c*)(objectClass=person))
Nobody found!
[...]
$ java -cp classes LDAPInfo "bob)(userPassword=s*"
LDAP query: (&(uid=bob)(userPassword=s*)(objectClass=person))
Phone: telephoneNumber: 555-9999

Как только злобный злоумышленник попробует букву ‘s’, то приложение вернет номер телефона Боба. Это означает, что пароль Боба начинается с буквы ‘s’. Дальше хитрый злоумышленник начинает перебирать вторую букву пароля Боба подставляя строки вида bob)(userPassword=sa*, bob)(userPassword=sb*", "bob)(userPassword=sc*  и так далее. Это позволяет реализовать эффективный перебор, чтобы получить пароль Боба.

Вот пример реализации такого перебора:

Как предотвратить LDAP-инъекции

Ничего революционного перечислено не будет. Проверка входных данных спасут гиганта мысли и отца русской демократии. Приложениям следует экранировать все специальные символы в данных, которые приходят от пользователей. OWASP любезно предоставляет статью об этом (смотри ниже), которая может быть применена не только к Web-приложениям.

Дополнительно мерой может быть отключение индексирования полей, которые могут содержать важную информацию такую как пароли. Например, если поле userPassword не индексировано, то запрос, который содержит (userPassword=a*) , приведет к ошибке:

$ ldapsearch -h ldap.server m -x -b "dc=test,dc=com" "(&(uid=test)(userPassword=a*))"
....
# search result
search: 2
result: 53 Server is unwilling to perform
text: Function Not Implemented, search filter attribute userpassword is not indexed/cataloged

# numResponses: 1

Но коварный злоумышленник все еще может попробовать извлечь данные из других индексированных полей.

Ссылки: