Mega Sekurak Hacking Party w Krakowie! 20.10.2025 r. Bilety -30%
Fortinet FortiWeb Fabric Connector i podatności z lat 90’
Nie wiemy jaką dokładnie wartość wskazywał licznik odliczający dni od ostatniej krytycznej podatności w produktach firmy Fortinet, ale w redakcji, mamy wrażenie, że panowie z WatchTowr trzymają tabliczkę z cyfrą “0” w pogotowiu. Tym razem legendarny SinSinology zaprezentował załatanego i krytycznego n-daya w produkcie FortiWeb Fabric Connector – błąd klasy SQL Injection (w 2025 roku…) w wersji pre-auth (jeszcze raz sprawdzamy kalendarz, ale tam wciąż rok 2025) – czyli na bogato. Podatność otrzymała numer CVE-2025-25257 i jej CVSS (3.1) to 9.8.
Zanim zaczniecie lekturę oryginalnego postu, lub naszego streszczenia, zalecamy aktualizację FortiWeba do wersji 7.6.4 (lub wyższej), 7.4.8 (lub wyższej), 7.2.11 (lub wyższej) oraz 7.0.11 (lub wyższej).
Osoby, które nie korzystają i nigdy nie słyszały o tym produkcie, wspomnimy tylko, że FortiWeb Fabric Connector to hub wbudowany w FortiWeb łączący go z Security Fabric tego producenta. Pozwala to na wymianę informacji takich jak adresy, IoC, stany urządzeń i aktualizację polityk rozwiązań. Same założenia wyglądają dość dobrze. Jednak implementacja pozostawia wiele do życzenia.
Jeśli chodzi o sam sposób odnalezienia podatności tego typu, w momencie gdy istnieje już gotowa do zaaplikowania łatka, to jak zwykle z pomocą przychodzi proces porównywania (ang. diffing) kodu (czasami również w formie zdekompilowanych binariów).
Tym samym badacze znaleźli całkiem sporo wprowadzonych zmian, które odróżniały poprawioną/załataną binarkę /bin/httpsd (czyli plik wykonywalny serwera webowego Fortigate) od podatnego pierwowzoru.

W bardzo szybki sposób wytypowano winowajcę SQL Injection – funkcję get_fabric_user_by_token
. Pomijając szczegóły analizy (polecamy zapoznać się z postem) zacytujemy tylko podatną część kodu:
[...]
if ( !v1 )
{
**// VULN
snprintf(s, 0x400u, "select id from fabric_user.user_table where token='%s'", a1);**
[...]
Listing 1. Fragment podatnej funkcji (źródło)
W tym momencie istnienie podatności SQL Injection powinno być jasne, argument a1 wklejany jest bezpośrednio w zapytanie do bazy danych. Jak można się domyślić, to parametr ten jest kontrolowany przez nieuwierzytelnionego użytkownika. Analiza odwołań krzyżowych (ang. cross-references lub xrefs) pozwoliła ustalić, jakie dane trafiają do tego zapytania.

Okazało się, że parametr trafiający bezpośrednio do zapytania SQL pochodzi z zapytania HTTP, a konkretnie z nagłówka Authorization: Bearer <token>
, który przyjmuje 128 bajtowy token.
__int64 __fastcall fabric_access_check(__int64 a1)
{
__int64 v1; // rdi
__int64 v2; // rax
_OWORD v4[8]; // [rsp+0h] [rbp-A0h] BYREF
char v5; // [rsp+80h] [rbp-20h]
unsigned __int64 v6; // [rsp+88h] [rbp-18h]
v1 = *(_QWORD *)(a1 + 248);
v6 = __readfsqword(0x28u);
v5 = 0;
memset(v4, 0, sizeof(v4));
v3 = apr_table_get(v1, "Authorization"); // [1]
if ( (unsigned int)__isoc23_sscanf(v2, "Bearer %128s", v4) != 1 ) // [2]
return 0;
v5 = 0;
if ( (unsigned int)fabric_user_db_init()
|| (unsigned int)refresh_fabric_user()
|| (unsigned int)get_fabric_user_by_token((const char *)v4) ) // [3]
{
return 0;
}
else
{
return 2 * (unsigned int)((unsigned int)update_fabric_user_expire_time_by_token((const char *)v4) == 0);
}
}
Listing 2. Fragment kodu, który przetwarza przychodzące zapytanie i wykorzystuje podatną funkcję (źródło)
Programiści C/C++ (tak wiemy, że te języki już nie są do siebie podobne) zauważą od razu, że wykorzystywana jest funkcja __isoc23_sscanf, która zakończy przetwarzanie ciągu po napotkaniu pierwszej spacji. Niemożliwe jest więc wysłanie “klasycznego” fragmentu zapytania SQL, ponieważ zostanie ono ucięte na pierwszej spacji, o czym przekonali się badacze.

Jednak nie jest to problem nie do obejścia i każdy, kto chociaż raz czytał materiał o SQL Injection i popularnych metodach obejścia filtrów wie, że spacje nie są potrzebne do wykonania ataku. Wystarczy skorzystać ze składni /**/, która określa komentarze. Tym samym, przekształcone zapytanie do postaci AAAAAA'/**/or/**/sleep(5)--/**/-'
w rzeczywistości pozwoliło potwierdzić wykonanie ataku.

Unauth SQLi to “miły” dodatek do pentestingowego raportu, jednak w przypadku badania bezpieczeństwa produktów informatycznych (zwłaszcza tych związanych z bezpieczeństwem), warto zastanowić się nad maksymalizacją skutków takiego ataku. W tym przypadku mowa oczywiście o “świętym Graalu”, czyli zdalnym wykonaniu kodu (ang. remote code execution – RCE), oczywiście bez uwierzytelnienia.
Aby osiągnąć możliwość wykonania dowolnego kodu na docelowym systemie, badacze postanowili wykorzystać dyrektywę INTO OUTFILE z MYSQLa. Tutaj warto nadmienić, że proces bazy danych działa… tak zgadliście, w kontekście użytkownika root, więc wykorzystując SQL Injection, atakujący może zapisywać pliki jako root w systemie plików urządzenia (jeszcze raz zerkamy na kalendarz). Diabeł jak zwykle tkwi jednak w szczegółach, INTO OUTFILE nie pozwala dopisywać/modyfikować czy nadpisywać istniejących plików. Do tego nie pozwala na odpowiednie ustawienie atrybutów tworzonego pliku (czyli nie da się zapalić bitu execute). To trochę utrudnia i ogranicza wektory ataków pozwalające na osiągnięcie RCE, ale to wciąż nic straconego.
Analiza systemu plików wskazała, że wewnątrz katalogu /migadmin/cgi-bin, znajdują się skrypty Pythona, które po odwiedzeniu odpowiedniego adresu zostaną wykonane przez serwer WWW zgodnie z mechanizmem Common Gateway Interface. Jednak ścieżka, musi być “zarejestrowana”, więc po prostu dodanie swojego skryptu i następnie nawigacja do URL z jego nazwą, nie zakończy się sukcesem.
I tutaj wykorzystano stary ale bardzo kreatywny trick, dla którego opisujemy tę podatność, ponieważ wierzymy, że część z naszych Czytelników, w swojej karierze, może trafić na taką sytuację. Warto wtedy znać rozwiązanie uciążliwego problemu. Problem został dobrze opisany przez SonarSource – eksploitacja wykorzystuje mechanizm Pythona – site-specific configuration hooks. W skrócie jest to wbudowana cecha Pythona, która pozwala rozszerzyć listę potencjalnych ścieżek, w których mogą znajdować się importowane moduły. Wystarczy stworzyć plik o rozszerzeniu .pth i dowolnej nazwie w lokalizacji .local/lib/pythonX.Y/site-packages/ w katalogu domowym użytkownika. Wymagana jest więc możliwość tworzenia plików w konkretnej lokalizacji z konkretnym rozszerzeniem. Warunki te spełnia INTO OUTFILE.
Teraz najważniejsze, jeśli plik ten rozpoczyna się od import{spacja} albo import{tabulator}, to podany moduł zostanie zaimportowany i wykonany. Stąd droga do unauth RCE jest już znana. Atakujący tworzy plik .pth w katalogu site-packages, wykonuje skrypt /cgi-bin/ml-draw.py, Apache oraz Python wykonają resztę.
Trochę inną technikę obrał inny badacz, @0x_shaq, który odkrył tę podatność w lutym 2025.

Wykorzystał on możliwość “nadpisania” modułu, przez stworzenie pliku /var/log/lib/python3.10/matplotlib.py. Przy pomocy tego “gadżetu”, wywołanie skryptu przez cgi również doprowadzi do zdalnego wykonania kodu.
Kuriozum sytuacji, w której się znajdujemy opisując takie podatności najlepiej oddaje niżej przedstawiony mem, którym @0x_shaq okrasił swój writeup:

Oczywiście skoro podatność doczekała się już łatki, to opublikowane zostały PoC (PoC1 oraz PoC2) – zachęcamy do własnej analizy i wyciągania wniosków.
Na zakończenie chcielibyśmy pogratulować zarówno 0x_shaq oraz SinSinology, przy lekturze writeupów bawiliśmy się przednio. Polecamy też zapoznać się z memami, które rozrzucone są po całym Twitterze/X’ie.
~Black Hat Logan