Nedávno vyšla práce týmů ze Stanford University a University of Texas nazvaná The most dangerous code in the world: validating SSL certificates in non-browser software, která potvrzuje, že problém se správnou implementací SSL je rozšířenější než jenom občasná chyba osamělé aplikace:
- V mnoha aplikacích je použito SSL/TLS přes různé knihovny, ale kvůli šíleným API a slabé dokumentaci je těžké to implementovat správně.
- Někteří autoři si „pomohli“ tak, že jednoduše validaci řetězce certifikátů vypli, aby jim to neházelo tu špatnou „certificate is not trusted exception“ (viz příklady ze StackOverflow na konci práce).
- Knihovny často nevalidují DNS jména v certifikátu (Common Name – CN, Subject Alternative Name – SAN) a požadují to po autorovi aplikace – příklad OpenSSL; cURL nebo NSS to naopak implementuje.
- Výsledkem toho všeho je, že existuje mnoho aplikací a platebních bran, které s nadšením akceptují jakýkoli man-in-the-middle útok s jakýmkoli, třeba i self-signed certifikátem nebo certifikátem vydaným na úplně jiné DNS jméno.
Jednou z těchto chyb pořád trpí na Androidu i Opera Mini, a to mají vývojáři Opery mnohem víc zkušeností než náhodný jiný vývojář, který dostal v zadání „tady má být HTTPS“. Opera Mobile zahlásí varování o certifikátu.
Shodou okolností ve stejném čase trochu situaci napravují iSEC Partners, kteří napsali pěkný komentovaný návod s příklady, jak dělat v OpenSSL právě validaci řetězců a jména (PDF). Neradujme se ale předčasně, i v tomto návodu je vynecháno validování tzv. wildcard jmen s hvězdičkou jako *.example.com a speciální případy jako vícero CN. Nicméně jsou alespoň tyto limitace zmíněny.
Hlouběji do vnitřností SSL knihoven
Vraťme se ale k původnímu „the most dangerous code“. Jednou zmiňovanou nástrahou v podobě neočekávaného API je parametr CURLOPT_SSL_VERIFYHOST u cURL. Ten je enum s možnými hodnotami {0,1,2}, kdežto parametr s podobným významem CURLOPT_SSL_VERIFYPEER je boolean (přijímá hodnoty 0,1) – na tom některé zmiňované aplikace pohořely. I když cURL má ve výchozím stavu oba parametry nastaveny správně a i správně uvedeny v dokumentaci (na rozdíl od OpenSSL), neopatrné nastavení CURLOPT_SSL_VERIFYHOST na „true“ bude znamenat nastavení na jedničku – což vypne validaci DNS jména v Common Name certifikátu. Výsledkem bude, že se přijme libovolný řetězec certifikátů k nějaké důvěryhodné CA bez kontroly, k jakému serveru jsme připojeni.
Co v textu zmiňováno není a je možná ještě horší nástraha: některé chování a parametry cURL závisí na tom, s kterou SSL/TLS knihovnou byla slinkována. Možnosti jsou OpenSSL, GnuTLS a NSS. Chování závisí i na operačním systému. Jedním příkladem je CURLOPT_CAPATH, kde se při použití linkování cURL s OpenSSL vyžaduje, aby se s OpenSSL utilitou c_rehash vygenerovaly symlinky k certifikátům CA. Pokud někdo použije PyCURL (pythoní binding na cURL), nemůže v zásadě tušit, s kterou SSL knihovnou je cURL linkována, pokud neví předem, v jakých prostředích a operačních systémech aplikace poběží (až na nějaké statické linkování apod).
Samotné SSL implementace se liší v různých drobnostech – GnuTLS typicky bývá striktnější vůči RFC a standardům, například odmítne řetězec certifikátů ve špatném pořadí. NSS a OpenSSL si v tichosti přeuspořádají certifikáty a zakrývají tím chybu konfigurace serveru. Samo o sobě přeuspořádání certifikátů není díra, ale zanáší další „nedeterministické“ a těžko debugovatelné chování, které vede pak třeba k tzv. transvalidním certifikátům.
Myslím že důvod, proč něco takového vzniklo je, že nejprve to někdo v produktu X naimplementoval jako „fíčuru“, na ostatní vývojáře produktů Y a Z začali tlačit uživatelé a/nebo marketing „vždyť v X to funguje!“ Vývojáři Y a Z to nejspíš po dlouhém vysvětlování, že chyba není u nich, vzdali a prostě to naimplementovali stejně. Nehledal jsem, co byl ten „první X“, ale v zásadě analogická historie nastala u generátorů náhodných čísel v různých routerech.
Vyskytují se i absurdní bizarnosti, například cURL s NSS občas hodí HTTP 400 místo očekávaného HTTP 200… ale jenom velmi vzácně, závisí to na pořadí serverů, ke kterým se připojujete. Debugoval jsem to dlouho, ke skutečné příčině jsem se ještě nedopátral, zatím se zdá že cURL+NSS se nemá rádo s Varnish cache. Po výměně NSS za GnuTLS nebo OpenSSL problém s HTTP 400 „záhadně“ zmizí.
Programátorova kobyla chodí nenaprogramována
Zde si rýpnu i do autorů původního článku, že dělají jednu z těch samých lehce přehlídnutelných chyb – odkazování na zastaralé RFC. Může se to zdát jako drobnost, ale dělá to mnoho lidí a už znám důvod – podívejte se na rozdíl mezi textem RFC 2527 a druhým textem RFC 2527. Vtip je v tom, že v HTML verzi je hned na začátku uvedeno, že RFC je „obsolete“ i s odkazem na aktuální verzi. Ale googlením typicky člověk narazí na tu textovou verzi, kde to uvedeno není (textovou verzi mají autoři práce uvedenou i v referencích). Chtěl jsem tím poukázat, jak těžké je napsat aplikaci využívající SSL správně a neudělat drobnou chybu, ať už různé frameworky tvrdí cokoliv jiného.
Taky tvrdí, že různé implementace SSL knihoven nesprávně nakládají s rozšířením certificate policies. Ale hned v dalším odstavci říkají, že X.509 rozšíření jsou mimo rozsah práce a možná se k nim v budoucnu vrátí. V předkladu z „akademičtiny“ to značí „my taky nevíme, jak to má být správně, a možná už žádná další práce nebude“.
Certificate policies jsem si vybral záměrně, je to snad nejvíc na hlavu postavené X.509 rozšíření, pohleďte jenom na jeho ASN.1 syntax (znalost ASN.1 nebo gramatik není nutná aby člověk poznal, jaký je to hnus). Přimíchejte do toho ještě X.509 rozšíření jako policy mapping a policy constraints a migréna na pár měsíců je zaručena. Jediné štěstí, že se tyhle tři rozšíření vyskytují jenom velmi výjimečně.
Certificate policies se totiž můžou chovat „kvantově“: mějme certifikát s rozšířením certificate policies, které je označené jako critical. Je klidně možné, že ze dvou TLS klientů jeden daný certifikát nepřijme a druhý ano – přitom u obou to může být korektní chování. Viděl jsem to i naživo u Chrome a Firefoxu, jednalo se o korejskou CA (certifikáty mezitím obměnili, ale mám je uložené). Trik je v tom, že rozšíření je označeno jako „critical“. To značí, že pokud mu klient rozumí, pak se podle něj musí chovat a pokud mu nerozumí, musí ohlásit chybu validace. Jenže policy, podle které se má prohlížeč nebo TLS klient rozhodnout, je označeno přes OID (object identifier – klidně si OID představte jako binární řetězec). Různých OID pro policy může být téměř libovolně mnoho, CA si je můžou vymýšlet podle potřeby a není v silách nikoho to implementovat.
Jak z toho ven – „útočit“ na vlastní aplikace s MitM
Tolik teorie. „Automagicky“ se to za den nespraví, flamewarů o TLS bylo dost, ale existuje pár způsobů, jak se vyhnout největším botám. V principu je to analogie „unit testy by měly testovat nejen to, že aplikace dělá to co má, ale taky že nedělá to, co nemá“. S nějakými certificate policies se vůbec nebudeme trápit, ani čtením zdrojáků SSL knihoven. Potřebujeme se především vyhnout dvěma případům:
- Aplikace nesmí přijmout evidentně špatný certifikát, jako třeba libovolný self-signed certifikát.
- Aplikace nesmí přijmout řetězec certifikátů, který je vystaven na jiné DNS jméno.
Nástroje
Několik nástrojů, které lze k testům použít:
- Fiddler – je to HTTPS MitM proxy, oblíbená u pentesterů a reverzerů, jedinou nevýhodou je závislost na .NET. Možná by ji šlo rozchodit pod Mono, ale nezkoušel jsem. Vypadá ale nejvíc uživatelsky přívětivá.
- DNS MitM přes Unbound, Bind nebo Dnsmasq – manuálně v testovacím zařízení nastavíme resolver na vlastní rekurzivní DNS server, který nám bude směrovat A/AAAA záznamy, kam chceme.
- Apache, Nginx, „OpenSSL s_server“ – libovolný HTTPS server, na který si můžeme směrovat provoz z MitM.
- IPtables – tabulka NAT, řetězec PREROUTING, cíl DNAT – budeme přesměrovávat IP adresu někam jinam (na náš HTTPS server).
- Ettercap – umí ARP poisoning i DNS spoofing A záznamů.
Příklad s Unboundem
V unbound.conf
existuje direktiva local-zone a local-data, která nám umožňuje přesměrovávat DNS záznamy selektivně, příklad rovnou z distribučního unbound.conf (stačí z configu odkomentovat pro vyzkoušení):
# You can redirect a domain to a fixed address with local-zone: "example.com" redirect local-data: "example.com A 192.0.2.3"
Když už víme, jak si aplikaci přesměrovat, zkusíme si ji nejprve přesměrovat na vlastní HTTPS server a podíváme se, co to udělá. Pak si ji zkusíme přesměrovat různě po internetu na servery, které mají sice validní řetězec certifikátů, ale pro naši aplikaci nesprávné DNS jméno.
Na závěr: trocha historie s basic constraints
Rozšíření basic constraints uvádím už jen na okraj – pokud má aplikace zapnutou validaci řetězce certifikátů, aplikační vývojář se o nějaké basic constraints vůbec nemá starat. Ale podmínka je nevypínat validaci. Co se stane, když SSL knihovna nekontroluje basic constraints, bylo možné vidět v roce 2002 u Internet Exploreru a v roce 2011 u iPhone: chyba knihovny umožnovala serverovým certifikátem podepisovat další certifikáty.