Bevezetés
Egy penetrációs teszt során XXE-sérülékenységet találtam egy webalkalmazás XML-fájlfeltöltési funkciójában. A kezdeti próbálkozások minden alkalommal ugyanazzal az általános hibaüzenettel zárultak, ezért elsőre nem látszott, mi történik a háttérben.
A fordulatot az alkalmazás saját XML-exportja és egy másik funkció, az auditnapló vizsgálata hozta. Egy megfelelően módosított fájl feltöltésével elértem, hogy a szerverről beolvasott fájl tartalma az auditnapló egyik hibaüzenetében jelenjen meg.
A feltárt sérülékenységgel ebben a környezetben fájltartalmak és könyvtárnevek is kiolvashatók voltak, ami lehetővé tette a fájlrendszer feltérképezését. Az alábbiakban a felfedezés folyamatát és a /etc/passwd fájl kiolvasásával igazolt kihasználást mutatom be.
Mit jelent az XXE?
Az XML egyik lehetősége, hogy úgynevezett entitásokat definiálhatunk benne. Ezek névvel ellátott tartalmak, amelyekre a dokumentum más részeiben hivatkozhatunk. A feldolgozó a hivatkozás helyére beillesztheti az entitáshoz tartozó értéket.
A külső entitás tartalma egy hivatkozott forrásból származik. Ez lehet például egy hálózati címen elérhető erőforrás vagy a szerver egyik helyi fájlja. A „külső” tehát nem feltétlenül internetes forrást jelent: az adat ugyanazon a szerveren is lehet, az XML-dokumentumon kívül.
XXE-ről — XML External Entity injectionről — akkor beszélünk, amikor egy támadó az általa megadott XML-en keresztül visszaélhet a külső entitások feloldásával. Egy nem megfelelően beállított XML-feldolgozót így rávehet helyi fájlok beolvasására vagy hálózati kérések indítására. Az elérhető adatok köre az alkalmazás jogosultságaitól és a feldolgozó működésétől is függ.
A következmény lehet érzékeny fájlok kiszivárgása, belső szolgáltatások elérése vagy a belső hálózat feltérképezése. Speciális környezetben, további feltételek mellett akár kódfuttatás is kialakulhat. Az XML-feldolgozás emellett szolgáltatásmegtagadáshoz vezető erőforrás-kimerítési támadásoknak is ki lehet téve.
Az első próbálkozások
A projekt során kifejezetten XXE-hibát keresve kezdtem vizsgálni az XML-fájlfeltöltési funkciót. A teszt korai szakaszában azonban nem kaptam használható visszajelzést: a különböző próbálkozásokra a szerver mindig ugyanazt az egyszerű hibaüzenetet adta vissza.
Egy időre félretettem a funkciót, és folytattam az alkalmazás többi részének vizsgálatát.
A projekt vége felé, a riportolási szakaszban visszatértem hozzá, és feltöltöttem egy klasszikus „Billion Laughs” XML-fájlt. Ennél a támadásnál egymásra épülő entitások ismétlik meg az előző szint tartalmát. A feldolgozás során az eredetileg kis bemenet nagyon nagy szöveggé bővülhet, jelentős memória- és processzorhasználatot okozva.
A feltöltés után a weboldal 10–15 percen keresztül elérhetetlenné vált.
Ez arra utalt, hogy az XML-feldolgozás erőforráskezelését érdemes tovább vizsgálni. A kiesés ugyanakkor önmagában még nem igazolta a külső entitások feloldását: a Billion Laughs belső entitásokkal is működhet, külső fájl vagy hálózati erőforrás beolvasása nélkül.
Ezután azt vizsgáltam tovább, hogy a funkción keresztül szerveroldali adatok is kiolvashatók-e.
Az áttörést az alkalmazás saját XML-exportja hozta
A sikerhez meg kellett értenem, milyen adatokat vár az importáló funkció.
Az internetről elérhető, előre elkészített XXE-mintákkal nem kaptam értelmezhető eredményt. Kiderült viszont, hogy az alkalmazás auditnaplója — a műveleteket és hibákat rögzítő felülete — részletesebb hibaüzenetet jelenít meg, ha az XML szerkezete megfelel az elvárásoknak, de valamelyik feldolgozott mező hibás értéket tartalmaz.
A projekthez nem állt rendelkezésre importálási mintafájl. Ezért az alkalmazás adatvizualizációs felületéről exportáltam egy kevés adatot tartalmazó XML-t. Ez megadta azt a szerkezetet, amelyet az alkalmazás már képes volt érdemben feldolgozni.
Ezt a fájlt módosítottam úgy, hogy az egyik feldolgozandó mező értékét a szerver egy helyi fájljából származó tartalom adja. A cél az volt, hogy az így keletkező hibás mezőérték bekerüljön az auditnaplóba.
Hogyan került a fájl tartalma az auditnaplóba?
Az XXE-vel végzett fájlolvasás egyszerű példáiban a kiolvasott adat közvetlenül a feltöltésre adott válaszban jelenik meg. Ennél az alkalmazásnál a közvetlen válasz továbbra is általános hibaüzenet maradt. A részletes visszajelzést az auditnapló biztosította.
A módosított XML dokumentumtípus-deklarációjának, vagyis a DOCTYPE résznek a belsejében a következő külső entitást definiáltam:
<!ENTITY example SYSTEM "file:///etc/passwd">
Ez az example nevű entitást a szerver /etc/passwd fájljához kapcsolja. Ha a feldolgozó engedélyezi a feloldást, az entitásra történő hivatkozás helyére a fájlból beolvasott tartalom kerülhet.
Ezután az XML egyik olyan adatmezőjében, amelyet az importáló funkció mindenképpen feldolgozott, a következő hivatkozást helyeztem el:
&example;
A folyamat így állt össze:
- Az alkalmazás megkezdte a megfelelő szerkezetű XML feldolgozását.
- Az XML-feldolgozó feloldotta az
examplekülső entitást, és beolvasta a hivatkozott fájlt. - A fájl tartalma bekerült a kiválasztott adatmező értékébe.
- Ez az érték nem felelt meg a mezőre vonatkozó elvárásoknak, ezért az alkalmazás hibát jelzett.
- A hibaüzenet a problémás értéket is tartalmazta, így a
/etc/passwdtartalma megjelent az auditnaplóban.
Az adat tehát az XML-importon keresztül jutott be a feldolgozásba, és az auditnapló felületén vált kiolvashatóvá.
A /etc/passwd a rendszer helyi felhasználói fiókjairól tartalmaz adatokat, például fiókneveket, azonosítókat és saját könyvtárak elérési útját. Modern Linux-rendszereken a jelszóhash-ek jellemzően külön, az /etc/shadow fájlban találhatók.
A bemutatott eredmény így konkrétan igazolta az XXE-n keresztüli helyi fájlolvasást. A sérülékenység jelentősége az volt, hogy az alkalmazás olyan szerveroldali adatot tett hozzáférhetővé, amelyet az importáló funkciónak nem lett volna szabad kiadnia.
Mit tanultam az esetből?
A megfelelő bemeneti szerkezet döntő lehet
Ebben az esetben az alkalmazás saját exportja segített eljutni ahhoz a feldolgozási ponthoz, ahol a hiba láthatóvá vált.
Egy általános tesztfájl elutasítása önmagában nem mutatja meg, hogy az alkalmazás biztonságosan kezeli-e a külső entitásokat. Előfordulhat, hogy a bemenet már egy korábbi ellenőrzésen elakad. Ezért a teszteléshez használt XML-t érdemes az adott funkció által ténylegesen várt adatszerkezethez igazítani.
A visszajelzés másik funkcióban is megjelenhet
A feltöltés válasza csak a folyamat egyik része volt. Az auditnapló olyan részletet tett láthatóvá, amely a közvetlen válaszból hiányzott.
Az eset számomra azt mutatta meg, hogy a kapcsolódó funkciókat együtt kell vizsgálni. Az import eredménye, a feldolgozási hibák és a naplóbejegyzések együttesen adhatnak képet arról, mi történt a háttérben.
A külső hívás hasznos ellenőrzés lehet
Ha a szerver képes kifelé kapcsolódni, a külső entitások feloldása ellenőrizhető egy olyan hivatkozással, amely egy általunk felügyelt tesztkiszolgálóra mutat. A teszthez egyértelműen hozzárendelhető hálózati kérés visszajelzést adhat a külső erőforrás eléréséről.
A visszahívás elmaradása azonban nem zárja ki az XXE-t. A hálózati kapcsolatot például tűzfal is blokkolhatja, miközben a helyi fájlok beolvasása továbbra is lehetséges. Ilyenkor az alkalmazás válaszai, hibajelzései és a hozzáférhető naplók vizsgálata adhat további támpontot.
A belső entitások vizsgálatának korlátai
Az eset után felmerült bennem, hogy egy kisebb, kevésbé erőforrás-igényes „XML bomb” előzetes jelzésként használható lehet, különösen akkor, ha a kifelé irányuló kapcsolatok tiltottak.
Ezt a következtetést azonban pontosítani kell: a belső entitások kibontása és a külső entitások feloldása külön képesség. Az egyik működése nem bizonyítja automatikusan a másikat.
Az alábbi rövid példa a belső entitások egymásra épülő kibontását szemlélteti:
<!DOCTYPE attack [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<attack>&lol3;</attack>
A példában nincs külső fájlra vagy hálózati címre mutató hivatkozás, ezért a sikeres feldolgozás önmagában nem igazol XXE-sérülékenységet. Ez egy szemléltető XML; egy konkrét importáló funkció vizsgálatához annak elvárt szerkezetét is figyelembe kell venni.
Az entitások kezelésének alapvető ellenőrzéséhez egyszerű, kis méretű entitáshelyettesítés is elegendő lehet. A terhelést növelő próbák külön vizsgálatot jelentenek; egy rövidebb „XML bomb” sem tekinthető automatikusan biztonságos tartalékmegoldásnak.
Az & karakter kezelése csak egy részlet
Az &example; típusú entitáshivatkozások miatt az & karakter kezelése is befolyásolhatja a teszt eredményét. Egy elutasított bemenetből azonban nem lehet rögtön megállapítani, hogy ezt a karaktert szűrik, a DOCTYPE használatát tiltják, vagy más ellenőrzés állítja meg a feldolgozást.
A karaktertiltás önmagában nem tekinthető teljes XXE-védelemnek. A védekezést az XML-feldolgozó beállításaira és a külső erőforrások elérésének szabályozására kell építeni.
A teszt időzítése mellett a környezet is számít
A leírt próbálkozás a projekt végén történt, amikor egy kiesés már kevésbé akadályozta volna a tesztelés többi részét. Ez azonban az ügyfél rendszerének működésére gyakorolt kockázatot nem szüntette meg.
A szolgáltatást terhelő vizsgálatokat ezért előzetesen egyeztetett időpontban, meghatározott keretek között, lehetőleg nem éles környezetben érdemes végezni. A cél az, hogy a sérülékenység igazolása a szükséges legkisebb hatással járjon.
Védekezési lehetőségek
Az XML-feldolgozó biztonságos beállítása
Az elsődleges védekezés a külső entitások feloldásának és a külső DTD-k betöltésének letiltása. Ha az alkalmazásnak nincs szüksége dokumentumtípus-definíciókra, a DOCTYPE deklarációkat is célszerű elutasítani.
Java-környezetben például megfelelő DOM- és SAX-feldolgozóknál a disallow-doctype-decl beállítás használható a DOCTYPE tiltására. A XMLConstants.FEATURE_SECURE_PROCESSING hatása a feldolgozótól és a konfigurációtól függ, ezért nem kezelhető minden környezetben az XXE-védelem egyetlen kapcsolójaként. A külső hozzáférések szabályozását és a feldolgozási korlátokat is ellenőrizni kell.
PHP-ban a libxml_disable_entity_loader(true) régebbi megoldás; a függvény PHP 8.0 óta elavult. A libxml 2.9-től az entitáshelyettesítés alapértelmezésben le van tiltva, de a használt feldolgozási opciók ezt befolyásolhatják. A megfelelő védelemhez ezért a tényleges PHP- és libxml-verziót, valamint az alkalmazás beállításait együtt kell vizsgálni.
Sémaellenőrzés és engedélyezett erőforrások
A sémaellenőrzés hasznos a feltöltött XML szerkezetének és adattartalmának ellenőrzésére, de önmagában nem tiltja le a külső entitások feloldását.
Ha üzleti okból szükség van külső erőforrásokra, azok elérését szűk engedélyezési listához kell kötni, lehetőleg ellenőrzött helyi másolatok használatával. A sémaellenőrzés és más XML-feldolgozási lépések külső hozzáféréseit is szabályozni kell.
A feltöltött fájlok típusát és tartalmát szerveroldalon is ellenőrizni kell; a kliensoldali ellenőrzés kiegészítő intézkedés.
Frissítések, erőforráskorlátok és jogosultságok
Az XML-feldolgozó könyvtárakat naprakészen kell tartani. A külső hozzáférések tiltása mellett a feldolgozható bemenet méretére, az entitáskibontásra és az erőforrás-felhasználásra vonatkozó korlátokra is szükség lehet a szolgáltatásmegtagadási kockázatok csökkentéséhez.
A legkisebb jogosultság elve szintén fontos: az alkalmazást futtató folyamat csak azokhoz a fájlokhoz és hálózati erőforrásokhoz férjen hozzá, amelyek a működéséhez szükségesek. Ez egy sikeres kihasználás lehetséges hatását is korlátozza.
A hibaüzenetek és az auditnapló védelme
Ebben az esetben az auditnapló tette hozzáférhetővé a beolvasott adatot, ezért a javításnak a naplózást is érintenie kell.
Felül kell vizsgálni, milyen mezőértékek kerülnek a hibaüzenetekbe, milyen érzékeny adatok jelenhetnek meg a naplóban, és kik férhetnek hozzá ezekhez a bejegyzésekhez. A naplózásnak segítenie kell a hibakeresést anélkül, hogy további adatkiadási lehetőséget teremtene.
Következtetés
Az eset legfontosabb tanulsága, hogy az általános hibaüzenetből nem lehet megállapítani, mi történt a háttérben.
A sérülékenység feltárásához meg kellett értenem az alkalmazás által elvárt XML-szerkezetet, és követnem kellett a feldolgozás eredményét az auditnaplóig. Az alkalmazás saját exportja biztosította a megfelelő bemenetet, a napló pedig láthatóvá tette a szerverről beolvasott adatot.
A hatékony teszteléshez ezért a feltöltést, az adatfeldolgozást, a hibaüzeneteket és a kapcsolódó felületeket együtt érdemes vizsgálni. A javítás során ugyanezt a teljes folyamatot kell áttekinteni: az XML-feldolgozó beállításaitól az alkalmazás jogosultságain át a naplóban megjelenő adatokig.

