Суть проблемы
Консольная команда func.alcohol (обрабатывает клики по UI магазина NPC) регистрируется без какой-либо проверки прав или источника вызова - доступна
любому подключённому игроку через игровую консоль (F1), не только через клик по кнопке в интерфейсе.
Ветка category selling <Category> amount.buy <Y> <Page> <Amount> парсит Amount через Int32.Parse без ограничения диапазона (только
if (Amount == 0) return;) , а метод BuyItem() вообще не проверяет, хватает ли у игрока предмета-цены - снятие валюты (TakeItems) и выдача товара (GiveItem) выполняются безусловно. Проверка достатка (HaveItem) в коде существует, но вызывается только для подсветки кнопки в UI - это чисто визуальная подсказка клиенту, сервер её не перепроверяет при реальном исполнении команды.
Как эксплуатируется
Игрок вручную вводит в консоль:
func.alcohol category selling Exchanger amount.buy 0 0 -1000000
(индекс/страница/категория подбираются под нужный товар в конфиге).
Что происходит внутри TakeItems: при отрицательном Amount итоговый TargetAmount (цена × количество) становится отрицательным. Условие if (use.amount > TargetAmount) из-за этого выполняется всегда (любое положительное число больше отрицательного), и код делает:
use.amount -= TargetAmount; // при TargetAmount < 0 это фактически += |TargetAmount|
То есть вместо списания валюты стек предмета-цены у игрока
раздувается на произвольную величину - достаточно иметь всего 1 штуку нужного предмета в инвентаре.
Даже без отрицательных чисел: если просто вызвать amount.buy с большим Amount, не имея вообще валюты, TakeItems спишет только то, что реально есть (может не хватить или быть 0), но GiveItem всё равно выдаст полный объём товара - оплата не обязательна.
Что в итоге
Полноценный дюп любого предмета, назначенного как "цена" в категориях EquipmentCategory/ExchangerCategory (по дефолтному конфигу - scrap, water.salt/алкоголь), плюс бесплатное получение товаров из магазина (оружие, hazmat и т.д. в Exchanger-категории). Уязвимость эксплуатируется без прав администратора, без взаимодействия с самим NPC — чистый консольный вызов.
Как я это исправил у себя
- В обработчике команды (FuncionalCommad, ветка "selling"): Amount валидируется в диапазон 1..1000 (константа MaxBuyAmount), Y/Page - проверяются на выход за границы фактического списка товаров на странице, до этого возможен был и IndexOutOfRangeException на кривом Y.
- В BuyItem(): перед TakeItems/GiveItem добавлена серверная проверка HaveItem(player, PriceItems.Shortname, PriceItems.Amount * Amount, PriceItems.SkinID) - если у игрока реально нет нужного количества, покупка просто не выполняется.