tom@home.htb:~$

Blog o HTB

25 November 2020

Insecure deserialization napříč stacky

Úvod a kontext

Deserializace se často vysvětluje přes jednotlivé technologie: Java serializace, .NET BinaryFormatter, Json.NET, YAML parser nebo nějaký konkrétní gadget chain. To je užitečné pro exploit development, ale méně užitečné pro architekturu a code review. Ve skutečnosti totiž nejde o problém jednoho frameworku. Jde o opakující se chybu důvěry: aplikace převezme od klienta nebo jiné nedůvěryhodné vrstvy objekt, typ nebo celý objektový graf a uvěří, že jeho znovuvytvoření je bezpečné.

Právě proto dává smysl dívat se na insecure deserialization napříč stacky. Na povrchu vypadá jinak v .NET webu, jinak v Java klientovi a jinak v interním remoting endpointu. Ale bezpečnostní jádro je stejné: útočník neovládá jen data, nýbrž i to, jaký objekt aplikace vytvoří a jaké vedlejší efekty při tom vzniknou.

Co se při deserializaci skutečně pokazí

Bezpečná serializace pracuje s prostými daty:

Nebezpečná deserializace začíná ve chvíli, kdy vstup může rozhodovat i o:

Tím se z formátu pro přenos dat stává mechanismus pro spouštění kódu nebo pro vyvolání chování, které vývojář nikdy nezamýšlel jako veřejný vstup.

Proč je problém tak přenositelný mezi technologiemi

Jednotlivé stacky používají jinou syntaxi, ale velmi podobný mentální model:

Rozdíl tedy není v tom, jestli jde o JSON, YAML nebo binární stream. Rozdíl je jen v tom, jak daná platforma dovolí propojit data s typovým systémem a se side-effecty při vytváření objektů.

Tři základní vzory, které se opakují

1. Útočník ovládá typ

Nejnebezpečnější varianta je ta, kde vstup přímo říká, jaký objekt má aplikace vytvořit. V .NET to může být například JSON s polem $type:

{
  "$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework",
  "MethodName": "Start"
}

V jiném stacku se stejná logika projeví jinak:

Jakmile vstup určuje typ, je potřeba velmi přísně hlídat, které typy jsou vůbec přípustné. Bez toho si aplikace sama staví objekt z nedůvěryhodného materiálu.

2. Útočník neovládá jen data, ale i vedlejší efekty

Mnoho nebezpečných gadgetů není nebezpečných proto, že “obsahují shell”. Jsou nebezpečné proto, že jejich vytvoření nebo následná práce s nimi:

To je důležité i pro review. Není nutné hledat jen explicitní exec(). Stačí najít objektový graf, který po znovuvytvoření vede k nečekané akci.

3. Server věří, že vstup poslal “náš klient”

To je častý a podceňovaný motiv. U interních klientů, desktopových aplikací nebo remoting endpointů se často předpokládá:

Jenže veřejně dostupný klient lze stáhnout, dekompilovat a znovu implementovat. Jakmile server slepě přijímá serializovaný objekt jen proto, že očekává “našeho klienta”, je to stejný problém jako u webového API bez validace vstupu.

Jak se to projevilo v různých případech

V jednom .NET webu se nebezpečná deserializace schovala do cookie OAuth2. Frontend ji chápal jako bearer token, backend ji ale zpracovával jako objekt a dovolil přes $type vytvořit ObjectDataProvider, který spustil proces na serveru. Tady je dobře vidět, že problém neleží v cookie sama o sobě. Leží v tom, že server přijal klientský JSON jako autoritativní popis objektu.

Jinde šlo o interní .NET remoting endpoint. Ten už ze své podstaty nepřenášel jen data, ale serializované objekty. Dekompilace klienta odhalila endpoint, debug credentials i to, že služba slepě přijímá deserializovaný vstup. Výsledek nebyl “jen bug v remotingu”. Výsledek byl celý produkční debug kanál postavený na důvěře k objektům od klienta.

V Java/Tomcat prostředí zase parser YAML nepůsobil jako klasická serializace. Praktický efekt byl ale stejný: uživatelský vstup vedl k materializaci nečekaného typu, který pak stáhl a aktivoval další kód. Formát je jiný, ale bezpečnostní chyba zůstává totožná: data určují typ a tím i chování.

Další případ stál na tom, že vlastní Java klient komunikoval se serverem přes serializované objekty. Nejdřív bylo potřeba klient rozchodit, dekompilovat a pochopit protokol. Teprve potom vyšlo najevo, že server-side funkce pro změnu hesla slepě zpracovává serializovaný objekt ClientCredential. To je velmi čistý příklad toho, že insecure deserialization není jen webový problém. Je to problém celého návrhu klient/server důvěry.

Jak takový problém poznat při review

Při code review a architekturní analýze se vyplatí hledat několik konkrétních signálů.

Typ rozhoduje vstup

Server přijímá bohatý objekt tam, kde by stačila data

Pokud API ve skutečnosti nepotřebuje nic víc než:

a přesto přijímá nebo rekonstruuje komplexní objekty, je to silný varovný signál.

Důvěra stojí na původu klienta, ne na validaci

To všechno jsou slabé předpoklady. Jakmile je klient nebo binárka dostupná, je jen otázka času, kdy ji někdo rozebere.

Co je důležitější než gadget chain

U deserializace je snadné sklouznout k seznamu gadgetů a payloadů. Pro obranu je ale důležitější rozlišit tři úrovně rizika:

  1. přijímám jen prostá data,
  2. přijímám polymorfní data, ale s pevným allowlistem typů,
  3. přijímám volně specifikovaný objekt nebo celý objektový graf.

Teprve třetí úroveň bývá skutečně katastrofická. V tu chvíli aplikace už neparsuje vstup. Ona z něj rekonstruuje programové chování.

Obrana a bezpečnější návrh

1. Přenášet data, ne objekty

Nejbezpečnější řešení je architektonické: veřejné a interní API má přijímat jednoduché datové struktury, ne serializované objekty s typovou informací.

2. Vypnout nebo omezit polymorfní typy

Pokud platforma umí deserializovat podle typu ze vstupu, je to potřeba chápat jako vysoce rizikovou funkci. Většina aplikací ji pro běžný provoz vůbec nepotřebuje.

3. Opustit nebezpečné legacy mechanismy

U .NET to znamená nepoužívat staré serializer/remoting mechanizmy pro nedůvěryhodná data. U jiných stacků to znamená totéž v jejich vlastním slovníku: nepoužívat loader nebo parser v režimu, který materializuje libovolné objekty.

4. Klientský původ nebrat jako bezpečnostní záruku

To, že zprávu vytváří “náš klient”, není bezpečnostní kontrola. Jakmile lze klienta stáhnout nebo dekompilovat, musí být server připravený na ručně vytvářený vstup.

5. Oddělit deserializační hranici od citlivých akcí

Proces, který obnovuje objekty nebo bohaté dokumenty, nemá běžet v kontextu, kde může:

Shrnutí klíčových poznatků

Co si odnést do praxe

tags: deserialization - java - dotnet - serialization