A mai alkalmazások biztonsága egy pen-tester szemével
Mi is az a betörési teszt, és miért fizet valaki ilyesmiért? A biztonsági betörési teszt (angolul security penetration test) célja egy rendszer sebezhetőségeinek felmérése gyakorlatias megközelítést alkalmazva, a valódi támadók (angolul black-hat hacker) eszköztárának felhasználásával. A betörési tesztet végző szakemberek (angolul pen-tester, white-hat hacker, ethical hacker) alapvetően ugyanúgy fő elfoglaltságként a sebezhetőségek felderítésével és kihasználásuk mikéntjével foglalkoznak, mint a számítógépes bűnözők.
Mindkét térfélen rengeteg időt és energiát fektetnek a támadási technikák kifejlesztésére és demonstrálására vagy éppen tényleges végrehajtására. Az alapvető különbség a célokban rejlik, míg az egyik esetben a rendszer hibáinak kijavítására és a biztonság megerősítésére, a másikban jogosulatlan haszonszerzésre, lopásra és károkozásra kell gondolni.
Egy jó biztonsági szakértőnek mindenekelőtt érteni kell az elterjedt programozási nyelvek, alkalmazásplatformok (.NET, Java), adatbázis-kezelő rendszerek és operációs rendszerek biztonsági vonatkozásaihoz, továbbá az ismert sebezhetőségek kihasználásának módjaihoz és hacker eszközök használatához. Szükséges továbbá egy kis csavar, ami megkülönbözteti az elvileg hasonlóan jól képzett rendszergazdáktól és fejlesztőktől, ez pedig a szemléletmódban rejlik. Tudnia kell ugyanis a bűnözők fejével gondolkodni. A tapasztalatok szerint erre a főállású fejlesztők és rendszergazdák csak nagyon kis hányada képes.
A gyakorlatban a “készen” átadott rendszerek szinte kivétel nélkül tartalmaznak súlyos biztonsági hibákat, amiket egy későbbi biztonsági betörési teszt a felszínre hoz. A betörési tesztek célja az ilyen hiányosságok felderítése és a biztonsági szakértők javaslatai alapján a kijavítása. Mi történik, ha ezeket nem javítják ki? Lehet hogy hosszú ideig semmi.
Aki olvas híreket mostanában, annak látnia kell viszont: a tendenciák a játszma eldurvulására utalnak. A „rosszfiúk” valóban léteznek, és a minket is körülvevő világ az ő játszóterük is. Ha valaki az utóbbi időkben történt komoly biztonsági incidensekről szeretne tájékozódni, a következő javasolt keresőszavak alapján szemezgethet: Sony, EMC/RSA Inc., Lockheed-Martin, L3 Communications, Anonymous vs HBGary, Stuxnet, GhostNet, Operation Aurora. Hazai példaként említhetjük az Ozeki nevű magyar fejlesztőcég informatikai “kirámolását”. A felsorolt példák részletezésétől eltekintenék, mert kiváló és kevésbé kiváló cikkek tucatjai születtek már róluk a külföldi és a hazai sajtóban egyaránt. A statisztikákat kedvelők számára érdekes olvasmány lehet a témában a Verizon által évente publikált „Data Breach Investigations Report”.
Ennyi bemelegítés után térjünk a lényegre, vagyis mit kell tudni az alkalmazások biztonságáról. Ebben a cikkben az alkalmazás szintű betörési tesztekkel kapcsolatos saját tapasztalatok alapján szeretnék pár gondolatot megosztani. Az információs infrastruktúra (hálózati eszközök, szerver operációs rendszerek, alkalmazásszerver platformok, adatbázis-kezelők) biztonsági tesztelését ez a cikk nem tárgyalja. Hogy éppen black-box vagy white-box megközelítést alkalmazunk, azt természetesen előre egyeztetjük az ügyféllel aszerint, milyen alkalmazást vizsgálunk. Az interneten elérhető publikus vagy zárt hálózatban elérhető belső alkalmazások esetében más-más fenyegetettségekre és támadókra (angolul threat agent) kell felkészülni.
A többfelhasználós alkalmazások eléréséhez manapság klienseket szokás használni, ami lehet vékony kliens (tipikusan web böngésző) vagy vastag kliens (Java, .NET, vagy esetleg natív kódú futtatható program). A szerver oldalon állhat közvetlenül egy adatbázis-kezelő (ekkor beszélünk kétrétegű alkalmazás architektúráról) vagy egy alkalmazás-szerver, és mögötte az adatbázis-kezelő (ezt nevezik 3 vagy többrétegű architektúrának). A mobiltelefonon futó (nem a mobilos web browserekre optimalizált, hanem a klienset is magában foglaló) alkalmazások logikailag a kétrétegű architektúra alá sorolhatóak.
Biztonsági szempontból hatalmas különbség van a két megközelítés között. A kétrétegű alkalmazások a gyakorlatban biztonsági szempontból eleve problémás helyzetből indulnak. Az alkalmazásokat a felhasználók általában különböző jogosultságokkal érik el. Csak a példa kedvéért léteznek rögzítő felhasználók, akik tranzakciókat rögzíthetnek (pl. pénzátutalás, részvény vétel/eladás) és jóváhagyó felhasználók, akik az alájuk rendelt felhasználók tranzakcióit jóváhagyják (a pénz ténylegesen elutalódik a forrás számláról a célszámlára). Léteznie kell még legalább egy felhasználónak, aki a többi felhasználót létrehozza/törli és a jogosultságaikat állítgatja. A helyzet tovább bonyolódik: vegyük hozzá az előbbiekhez, hogy egyes felhasználók csak egyes ügyfelek/vállalatok adataihoz férhetnek, és kizárólag a hozzájuk tartozó számlák/értékpapírok felett rendelkezhetnek.
Ebből már látható, hogy a jogosultságok kezelésére (autorizáció) funkció szinten (melyik felhasználó mely műveleteket hajthatja végre) és adat szinten (mely felhasználó mely ügyfelek adatait olvashatja/módosíthatja) egyaránt oda kell figyelni.
Az alkalmazások biztonsági tesztelésénél alkalmazható a statikus megközelítés, itt alapvetően az alkalmazás forráskódjából kiindulva próbáljuk a hibákat megtalálni. Ez történhet automatizált eszközökkel, melyek a tipikus programozási hibákat tudják beazonosítani. Így kiszűrhető például az SQL-utasítások összerakása felhasználói bemenetről származó sztringeket használó kódrészlet, ami SQL-injekció sebezhetőségekhez vezethet.
Szintén kiszűrhetőek így a nem biztonságos függvény/metódushívások a C vagy C++ programok forráskódjából, melyek integer/buffer overflow jellegű sebezhetőségeket eredményeznek. Ilyen automatizált eszköz nagyon sok létezik akár ingyenesen, akár megvásárolható termékként. Az egyik gyakorlati probléma a használatukkal kapcsolatban, hogy rengeteg figyelmeztetést generálhatnak. Ezeken egy hozzáértő szakembernek egyesével végig kell menni és a nagy számú hamis riasztásból kiszűrni a lényegeseket.
Léteznek olyan programozási hibák, amiket a jelenlegi statikus kódellenőrző eszközök nem képesek kiszűrni (logikai hibák, információszivárgás, race-condition, back-door, magic number jellegű konstansok használata, privilégiumszintek nem megfelelő használata, kriptográfia nem megfelelő alkalmazása). A forráskódot emberi erővel átnézni biztonsági hibákat keresve drága, időigényes és ritkán alkalmazott módszer (jellemzően több hetes speciális szakértelmet igénylő munka az alkalmazás összetettségétől is függően).
A dinamikus megközelítés lényege, hogy az alkalmazást működés közben vizsgáljuk biztonsági hibákat keresve. Szintén léteznek automatizált eszközök (angolul: application security/vulnerability scanner) webes alkalmazások biztonsági tesztelésére, hatékonyságukról és összehasonlításukról az interneten részletes elemzéseket találhatunk. A gyakorlati tapasztalatok alapján elmondható róluk: a leggyakoribb biztonsági hibák (Cross-Site Scripting, SQL Injection, Directory Traversal, Forceful Browsing) jelentős részét különböző hatékonysággal képesek megtalálni, jellemzően számos hamis riasztást generálva.
Megjegyezzük, hogy az összes „alap” biztonsági hibához léteznek olyan körmönfontabb esetek, mikor a humán intelligenciával ellentétben egy automata nem képes a problémát detektálni. A fenti hibák részletes leírása meghaladja jelen cikk kereteit, az érdeklődők hasznos információt találhatnak az OWASP (Open Web Application Security Project http://www.owasp.org) honlapján. Érdekes olvasmányok lehetnek még leggyakoribb biztonsági hibákat taglaló elemzések, mint például az évente publikált „OWASP Top 10” eredményhirdetése.
Végül még mindig maradnak olyan biztonsági hibák, melyeket a legfejlettebb, automatikus sérülékenységellenőrző eszközök sem tudnak megtalálni: jogosultságkezelési rendszer (authorization system) kikerülése, session-kezelés hibái, több lépésből álló folyamatokban nem azonnal jelentkező hibák.
Ez utóbbit pár példával talán érthetőbbé tudjuk tenni.
Például egy pénzügyi tranzakciókat is lehetővé tevő rendszerben egy pénz/értékpapír ügylet feladása rendszerint többlépcsős, általában külön jóváhagyási lépést is tartalmazó folyamat. Ha például a folyamat sokadik lépésénél (értsd: sokadik kitöltendő HTML form a webes alkalmazásban) egy Man in The Middle Proxy (pl. Paros) alkalmazásával a HTTP-kérést elfogva (a böngésző és a szerver közé ékelődve) kicseréljük a cél és a forrás számlaszámokat, vajon az alkalmazás szerver része észreveszi-e a turpisságot? Ha nem, jogosulatlanul tudunk utalni más számlájáról a sajátunkra, vagy más nevében tudunk értékpapírokat venni/eladni.
Az elküldött tranzakciók részletezésénél a lekérdező (hovatovább a jóváhagyó) funkciónak küldött HTTP GET/POST kérésben a sajátunktól eltérő tranzakcióazonosítót elküldve az alkalmazás megmutatja/jóváhagyja-e a máshoz tartozó tranzakciót? Ha nincsenek ilyen jogosultságellenőrzések az alkalmazásba építve, szabadon turkálhatunk mások pénzügyeiben.
Gyakorlati tapasztalataimra hivatkozva kijelenthetem, az évek során eddig tesztelt rendszerek majd mindegyikében sikerült hasonló hibákat azonosítani. Jellemző tévképzet a fejlesztők fejében, hogy ha egy menüpontot nem rajzolunk ki a felhasználó elé rakott GUI-ra, vagy egy adatot nem listázunk ki, mert ahhoz a felhasználónak egyébként nem lenne jogosultsága, akkor az adott dolgot biztonságilag jól megvédtük. Ez sajnos nem igaz: az elrejtett funkció URL-jére a megfelelő kérést ettől függetlenül elküldhetjük a szerver felé, ha az nem ellenőriz jogosultságot, és az „elrejtett” adatot az alkalmazás memóriaterületéről közvetlenül kiolvashatjuk vagy ott tetszés szerint módosíthatjuk. Az alkalmazandó ökölszabályok: minden alkalmazáslogikához tartozó döntést a szerver oldalon kell meghozni; az alkalmazás működése során a klienstől származó minden adat eredete megbízhatatlannak tekintendő.
A problémák egyébként már a bejelentkezésnél elkezdődnek. Kétrétegű alkalmazásoknál tipikus, hogy a kliens egy darab adatbázisszintű felhasználó és jelszó birtokában azonosítja magát az adatbázis-kezelő felé. Az alkalmazásfelhasználó ezt a jelszót normál esetben nem látja, ő az adatbázisban tárolt (jó esetben titkosított) alkalmazás jelszavát adja meg bejelentkezéskor. Az alkalmazásfelhasználó az alkalmazáshoz tartozó összes funkciót és adatot elérheti, attól függetlenül, ki ül a klienst futtató számítógép előtt.
Az alkalmazás szintű azonosítás (autentikáció) egy kliensoldalon lejátszódó ellenőrzés és döntés eredménye. Minden kliens oldalon történő döntés és onnét származó adat szigorúan megbízhatatlannak tekinthető. A kliensből kinyerhető az adatbázis-felhasználó jelszava, az azonosítást megvalósító, alacsony szintű utasítások debuggerrel megtalálhatóak, és működésük módosítható. Ha az alkalmazás rossz kezekbe kerül, a támadó bármit megtehet, amire az alkalmazás felhasználónak van jogosultsága. A gyakorlati tapasztalatok szerint ezt pár napos munka után a gyakorlatban is demonstrálható az egyébként először jellemzően kétkedő ügyfelek számára.
Rövid ismertető következik a biztonsági teszteléshez leggyakrabban használt eszközökről:
MITM (Man in The Middle) proxy: A kliens és szerver közé beékelődésre szolgáló program, jellemzően lehetővé teszi az éppen elfogott kérés/válasz módosítását és naplózását továbbküldés előtt. A beékelődést általában a hálózati kommunikáció vagy rendszerhívások eltérítésével érik el. A fejlettebbek alkalmasak az SSL-titkosítás mellett kliensoldali tanúsítványok kezelésére is (ennek nagy jelentősége lehet, de itt helyszűke miatt ezt nem részletezem).
Debugger: A programok futás közbeni nyomkövetésére alkalmas eszközök. A program működése közben elemezhetjük és módosíthatjuk annak viselkedését a megfelelő memóriaterületeken található adatok vagy alacsony szintű utasítások módosításával. A natív kódú alkalmazásokhoz, Javás alkalmazásokhoz és a .NET platformra írt alkalmazásokhoz egyaránt találhatunk jól használható megoldásokat. A legfejlettebbek képesek arra, hogy scriptek alkalmazásával automatikusan és futásidőben módosítják egy program működését: például egy töréspont elérésekor adott feltételek teljesülése esetén adatokat vagy utasításokat „ütnek át” közvetlenül a számítógép memóriájában. A lehetőségek szinte határtalanok, könyvespolcokat lehetne megtölteni elemzésükkel.
Rendszermonitorozó eszközök: Egy futó alkalmazás működését valós időben monitorozzák. Például rögzítik a kiválasztott operációs rendszer processz hálózati kommunikációra, fájlműveletekre, Windows registry írás/olvasás műveletekre vagy szálak indítására/leállítására irányuló aktivitását. Alkalmazásukkal hasznos információkat nyerhetünk egy ismeretlen forráskódú és komplex alkalmazásról annak működésével kapcsolatban.
Fuzzer: Egy alkalmazás adott funkcióinak (hálózati szolgáltatás, interfész metódushívások, adatfájl betöltés) hibáit nagy mennyiségű különböző véletlenszerű mintákat tartalmazó bemenet átadásával és az alkalmazás működésének figyelésével próbálja felderíteni. A használható eredmény érdekében érdemes a használt formátum (leírómezők, hosszmezők) részleteit ismerni, és ennek megfelelően értelmes, vagy szándékosan elrontott bemenetekkel próbálkozni. Alkalmas adatbeolvasást és saját protokollra jellemző üzenetformátumok feldolgozását végző kódrészletek tesztelésére.
A témának külön szakirodalma van, például natív kódú alkalmazásokban integer/buffer overflow típusú hibák felfedezésére szokták bevetni jól használható eredményeket produkálva. Az olyan komplex, de széles körben elterjedt alkalmazások, mint a Microsoft Outlook, irodai alkalmazások (pl. Word, Excel, Adobe Reader) és webböngészők biztonsági hibáinak jelentős részét valószínűleg ezzel a módszerrel találják napjainkban.
Az alábbiakban következzen pár, a trivialitásokon túlmutató hiba bemutatása, amiket tapasztalataim szerint az egyébként jól képzett, jó munkát végző, fejlett keretrendszereket alkalmazó és a biztonságra odafigyelő fejlesztők is rendszeresen elkövetnek.
Megfelelő bemeneti szűrés hiánya: Vegyük a következő helyzetet. Az adott alkalmazás más alkalmazásokkal is kommunikál a már régóta használt alkalmazáshoz később „hozzábarkácsolt” interfészeken keresztül. Az interfészeken utazó adatok formátuma nem teljesen azonos módon értelmezett a két oldalon. Az egyik alkalmazás adatot tölt át a másikból, például egy felhasználótól származó bemenet által meghatározott szűrés/keresési feltétel alapján. Az egyik oldalon sincs megfelelő bemenetszűrés megvalósítva, attól félve, nehogy a kommunikációba belenyúlva működési hiba keletkezhessen, vagy azt gondolva: biztosan megoldották már ezt a másik oldalon. A félreértés eredményeként egyes mezőkben olyan speciális karaktereket tudunk bevinni, melyek végül közvetlenül megváltoztatják a háttérben lefutó SQL-kifejezés működését, és egy támadó saját SQL-parancsot futtathat le a háttérben.
Session-kezelés hibái: Az adott vastag klienst alkalmazó alkalmazás az éppen belépett felhasználókat egy egyedi és véletlenszerű érték (session cookie) segítségével azonosítja. A kliens a bejelentkezés során a szervertől kapott válaszban található értékek alapján jeleníti meg a felhasználó jogosultságai alapján a számára elérhető funkciók menüpontjait. Képzeljük el, hogy ezt a választ a bejelentkezéskor azelőtt módosítjuk, hogy a kliens megkapta volna, például a felhasználó szerepkörét magasabb jogosultságokat biztosító szerepkörre módosítjuk, vagy amúgy nem elérhető menüpontokat bekapcsolunk. Egy támadó így alacsony jogosultságú felhasználó birtokában magas jogosultságú funkciókat érhet el (pl. felhasználók adminisztrációja, bizalmas üzleti adatok elérése).
Jogosultságellenőrzési (autorizációs) hiba: Képzeljünk el egy webes alkalmazást, ami ügyfelek (pl. hitelesek, biztosítottak) kezelésére szolgál. A működés során az ügymenethez hozzátartozik, hogy az adott igény elbírálásához igazolásokat kell tárolni (pl. beszkennelt hivatalos iratok) és az igény sikeres elbírálása esetén szerződés keletkezik (pl. hitelről, biztosításról).
A tárolt dokumentumokat mondjuk állományként a fájlrendszerben vagy egy adatbázis táblában BLOB (Binary Large Object) típusú értékként tárolja az alkalmazás. Az adott tárolt dokumentumot egy numerikus értékkel azonosítja az alkalmazás, és mindenhol ez alapján hivatkozik rá. Jellemzően ez a numerikus érték új dokumentum keletkezésekor pontosan egyel nő az eddig utolsóként tárolt azonosítójához képest. A dokumentum letöltésekor/megnézésekor elküldött kérésben az azonosítót átírva például egyel kisebbre, máris hozzáférhetünk a más felhasználóhoz tartozó dokumentumhoz, mert az alkalmazás nem ellenőrzi, hogy a kérő felhasználónak tényleg lenne-e joga a kért dokumentum megtekintéséhez.
Konklúzióként elmondhatjuk, hogy az alkalmazások biztonságossá tételét nem bízhatjuk kizárólag a fejlesztőkre. A megfelelő biztonsági teszteléshez sajátos gondolkodásmód, speciális szaktudás és tapasztalat szükséges. A megfelelő képességek megszerzése és fenntartása teljes embert kívánó feladat, ezért a legtöbb cég házon belül nem képes megfelelő kompetencia létrehozására és megtartására. A megfelelő belső erőforrásokkal rendelkező legnagyobb fejlesztőcégek (pl. Google, Microsoft) is pénzbeli jutalommal honorálják a külsős biztonsági tesztelőket.
Ha valaki egy módszertan jellegű áttekintést szeretne kapni a témáról, javasolt az Open Source Testing Methodology (OSSTM) és az OWASP Testing Guide tanulmányozása.
A biztonságra törekvést és biztonsági tesztelést komolyabb fejlesztéseknél javasolt már a projekt korai fázisaiba beépíteni, erre specializálódott szakértőket igénybe véve. Ezzel elkerülhető az az effektus, amikor egy új alkalmazás verzió élesbe állítása a biztonsági hibák kijavítása miatt csúszik.
Szerző: Major Marcell
A szerző
- Marcell jelenleg a Deloitte IT-biztonsági csapatának tagja. Több mint 5 éves tapasztalattal rendelkezik betörési tesztek és egyéb informatikai biztonsági vizsgálatok terén. Tanulmányait a szoftverfejlesztésre és az informatikai biztonságra fóku- szálva végezte. Tapasztalatokat szerzett az alkalmazások biztonsági tesztelése, a reverse engineering, a kriptográfiai algoritmusok és protokollok megvalósítása terén. Az előadók névsorában rendszeresen megtalálhatjuk hazai és külföldi hacker konferenciákon.