PHP давно перестал быть языком исключительно для небольших сайтов. На нем работают банковские кабинеты, CRM, интернет-магазины, корпоративные порталы и API. При этом одна из самых старых особенностей языка до сих пор регулярно приводит к появлению критических уязвимостей — автоматическое приведение типов.
Для разработчика это выглядит как удобная возможность сравнить строку с числом без лишнего преобразования. Для пентестера — как шанс обойти проверку пароля, HMAC, токена или логики авторизации. Именно поэтому задания на PHP Type Juggling регулярно встречаются в CTF, лабораториях PortSwigger и старых версиях реальных приложений.
В этой статье разберем, как работает приведение типов в PHP, почему оператор == опасен, что такое magic hash и какие признаки Type Juggling стоит искать во время тестирования безопасности.
В конце материала можно перейти к практическому заданию из бесплатного курса «Профессия Белый Хакер» и проверить, как небезопасная десериализация работает в учебном приложении.
Почему Type Juggling вообще существует
PHP относится к языкам с динамической типизацией. Переменная не имеет жестко закрепленного типа, а сам интерпретатор постоянно пытается привести данные к наиболее подходящему виду.
Например:
$a = "10";
$b = 10;
var_dump($a == $b); // true
На первый взгляд это выглядит вполне логично. Строка содержит число, значит их можно сравнить.
Проблемы начинаются тогда, когда в сравнении участвуют значения, которые не являются числами, но PHP все равно пытается интерпретировать их как числовые.
Именно здесь появляются неожиданные результаты, которые способны полностью изменить логику приложения.
Два оператора сравнения — две разные модели безопасности
В PHP существуют два основных оператора сравнения.
Первый — нестрогий:
==
Второй — строгий:
===
Разница кажется небольшой, но именно она определяет, будет ли приложение уязвимым.
При использовании === сравниваются одновременно:
- тип;
- значение.
"10" === 10
Результат:
false
Потому что один операнд — строка, второй — целое число.
Оператор == работает иначе. Перед сравнением PHP пытается привести оба значения к одному типу.
"10" == 10
Результат:
true
Само по себе это не является ошибкой. Опасность появляется, когда разработчик использует == при проверке:
- паролей;
- токенов;
- HMAC;
- идентификаторов;
- результатов криптографических функций.
Практически все известные атаки Type Juggling начинаются именно отсюда.
Самые опасные преобразования
Чтобы понимать дальнейшие примеры, достаточно запомнить несколько правил.
Если PHP видит строку, похожую на число, он попытается превратить ее в число.
Например:
"15" == 15
вернет
true
Если строка начинается с цифры, интерпретатор использует числовую часть.
"123admin" == 123
Результат:
true
В старых версиях PHP ситуация была еще интереснее.
"0admin" == 0
возвращало
true
То же самое происходило и со строками без цифр.
"admin" == 0
В PHP 7 результатом также было
true
Именно такое поведение годами становилось причиной обходов различных проверок. Начиная с PHP 8 часть подобных сравнений была изменена, однако старые приложения продолжают использовать предыдущие версии языка, поэтому подобные конструкции до сих пор регулярно встречаются во время пентестов.
Scientific notation — самый известный трюк
Самая знаменитая особенность Type Juggling связана с научной записью числа.
PHP воспринимает строку
1e3
как число
1000
Поэтому сравнение
"1e3" == 1000
вернет
true
Но гораздо интереснее выглядит другой случай.
"0e12345"
Для человека это обычная строка.
Для PHP — число
0 × 10^12345
То есть обычный ноль.
Соответственно,
"0e12345" == 0
будет истинным.
На этом построен один из самых известных классов атак — magic hash.
Что такое Magic Hash
Представим, что приложение проверяет пароль так:
if (md5($password) == $stored_hash) {
login();
}
Разработчик считает, что сравнивает две строки.
Но оператор == считает иначе.
Если обе строки выглядят как научная запись числа, PHP преобразует их в числа.
Например:
0e462097431906509019562988736854
и
0e830400451993494058024219903391
после преобразования становятся обычным нулем.
Поэтому
"0e462097431906509019562988736854" ==
"0e830400451993494058024219903391"
вернет
true
Хотя сами строки совершенно разные.
Подобные значения называют magic hash — хешами, начинающимися с 0e, после которых идут только цифры.
Если приложение использует слабое сравнение, подобрать второй такой хеш иногда оказывается достаточно для обхода проверки.
Именно поэтому во многих старых CTF первая мысль при виде конструкции
if (md5($input) == $hash)
— попробовать найти magic hash.
Почему это работало годами
На первый взгляд кажется, что проблема очевидна. Почему же она существовала столько лет?
Причина проста.
Type Juggling — не баг PHP.
Это особенность языка.
В большинстве случаев она действительно удобна и позволяет писать меньше кода. Проблема возникает тогда, когда разработчик использует автоматическое приведение типов в местах, где требуется криптографическая точность.
Особенно часто это встречалось в старых проектах, написанных во времена PHP 5 и ранних версий PHP 7, когда использование == считалось практически нормой.
Позже ситуация изменилась. В PHP 8 разработчики языка пересмотрели правила сравнения строк и чисел, сделав многие неочевидные преобразования невозможными. Однако это не означает, что Type Juggling исчез. Если приложение работает на старой версии PHP или использует небезопасную логику сравнения внутри собственного кода, риск остается вполне реальным.
Что должен искать пентестер
Во время тестирования не стоит пытаться перебирать magic hash вслепую.
Гораздо полезнее сначала определить, существуют ли вообще условия для эксплуатации.
В первую очередь внимание стоит обратить на:
- использование оператора == вместо ===;
- сравнение результатов md5(), sha1(), hash() или hash_hmac();
- проверку токенов и цифровых подписей;
- старые проекты на PHP 5.x или PHP 7.x;
- нестандартные механизмы аутентификации;
- проверки, где пользователь может влиять на тип входных данных.
Нередко одного взгляда на исходный код достаточно, чтобы понять: приложение потенциально уязвимо к Type Juggling.
Во второй части мы перейдем от теории к эксплуатации. Разберем, как использовать массивы вместо строк, почему некоторые функции возвращают NULL, каким образом это позволяет обходить проверки HMAC и как подобные ошибки выглядят в реальных CTF и во время аудита веб-приложений.
Как массив превращается в обход аутентификации
Magic Hash — далеко не единственный способ эксплуатации Type Juggling. На практике пентестеры гораздо чаще сталкиваются с другой особенностью PHP: возможностью передать массив вместо строки.
На первый взгляд это кажется невозможным. Если приложение ожидает строку, откуда возьмется массив?
Ответ кроется в механизме обработки HTTP-параметров.
Если отправить запрос вида:
?token[]=123
то PHP автоматически сформирует массив:
$_GET['token'] = ['123'];
То же самое работает для POST-параметров:
username=admin
password[]=123
и даже для Cookie:
Cookie: session_data[]=test
После обработки запроса в $_COOKIE окажется уже массив. Многие разработчики забывают об этой особенности и предполагают, что пользователь всегда передаст строку. Именно здесь начинается самое интересное.
Когда функции возвращают NULL вместо ошибки
Ранние версии PHP отличались довольно мягким отношением к ошибкам.
Многие встроенные функции при получении параметра неправильного типа не завершали выполнение программы. Вместо этого они выводили предупреждение (Warning) и возвращали NULL.
Одной из таких функций долгое время была hash_hmac().
Предположим, приложение проверяет подпись следующим образом:
if (hash_hmac('sha256', $_COOKIE['session'], $key) == $_COOKIE['hmac']) {
// пользователь авторизован
}
Все выглядит правильно.
Но если вместо строки передать массив:
Cookie: session[]=admin
Cookie: hmac=
в старых версиях PHP произойдет следующее:
- hash_hmac() получит массив вместо строки.
- Функция выдаст Warning.
- Вернет NULL.
- Затем выполнится слабое сравнение.
Фактически получится:
NULL == ""
Для оператора == такое сравнение оказывается истинным.
В результате проверка подписи завершается успешно, хотя никакой корректный HMAC пользователь не предоставил. Именно такой сценарий нередко встречается в учебных лабораториях и хорошо демонстрирует, почему опасно одновременно использовать слабое сравнение и не проверять типы входных данных.
Почему PHP 8 решил не все проблемы
После появления PHP 8 разработчики значительно пересмотрели правила нестрогого сравнения.
Например, конструкция
"admin" == 0
теперь возвращает false, тогда как в PHP 7 результатом было true.
Это устранило большое количество неожиданных сравнений строк и чисел.
Однако важно понимать одну вещь.
Type Juggling не исчез.
Во-первых, огромное количество корпоративных приложений до сих пор работает на PHP 7.4 и более ранних версиях.
Во-вторых, многие уязвимости возникают вовсе не из-за сравнения строки с числом, а из-за некорректной обработки NULL, массивов или результатов криптографических функций.
Поэтому при тестировании безопасности всегда необходимо учитывать используемую версию PHP.
Как искать Type Juggling во время пентеста
Если у вас есть исходный код, задача значительно упрощается.
В первую очередь стоит искать:
- оператор ==;
- оператор !=;
- вызовы md5(), sha1(), hash(), hash_hmac();
- проверки токенов;
- сравнение пользовательских данных с вычисленным хешем;
- проверки Cookie;
- нестандартную реализацию авторизации.
Если доступен только веб-интерфейс, полезно попробовать изменить тип входных данных.
Например:
- заменить строку массивом;
- отправить несколько параметров с одинаковым именем;
- использовать JSON-массив вместо строки в API;
- проверить обработку пустых значений;
- обратить внимание на Warning в ответах сервера.
Иногда одно изменение типа параметра позволяет пройти значительно дальше по сценарию авторизации.
Типичные признаки CTF-задачи
После нескольких задач подобные уязвимости начинают узнаваться практически сразу.
Наиболее распространенные признаки:
- приложение написано на PHP;
- используется md5() или sha1();
- в коде присутствует ==;
- сравниваются HMAC или токены;
- параметр можно передать в виде массива;
- сервер работает на старой версии PHP.
Во многих CTF этого уже достаточно, чтобы понять направление дальнейшей эксплуатации.
Например, увидев конструкцию
if ($_POST['password'] == md5($secret))
стоит задуматься не только о подборе значения, но и о том, как повлиять на тип сравниваемых данных.
Как защититься
Хорошая новость заключается в том, что большинство подобных уязвимостей устраняется достаточно простыми правилами.
Минимальный набор рекомендаций выглядит так:
- всегда использовать === и !== вместо == и !=;
- проверять тип пользовательских данных до выполнения бизнес-логики;
- использовать hash_equals() для сравнения криптографических значений;
- не полагаться на автоматическое приведение типов;
- обновлять приложения до современных версий PHP;
- явно отклонять массивы там, где ожидается строка;
- использовать строгий режим анализа кода и статические анализаторы.
Отдельно стоит упомянуть функцию in_array(). По умолчанию она также использует нестрогое сравнение.
Вместо
in_array($value, $array)
следует использовать
in_array($value, $array, true)
Третий параметр включает строгое сравнение и устраняет целый класс логических ошибок.
Что важно запомнить
Type Juggling — хороший пример того, как удобная особенность языка превращается в проблему безопасности.
Само по себе автоматическое приведение типов не является уязвимостью. Уязвимость появляется тогда, когда разработчик использует его при проверке данных, от которых зависит безопасность приложения: паролей, HMAC, токенов или результатов криптографических функций.
Для пентестера эта тема важна по двум причинам. Во-первых, подобные ошибки до сих пор встречаются в легаси-приложениях и регулярно появляются в CTF. Во-вторых, они учат смотреть не только на значения параметров, но и на их типы. Иногда достаточно передать массив вместо строки или воспользоваться особенностями нестрогого сравнения, чтобы полностью изменить логику работы приложения.
Именно поэтому при анализе PHP-кода стоит воспринимать каждый оператор == как потенциальную точку внимания. В большинстве случаев он окажется безобидным. Но если рядом выполняется проверка аутентификации, подписи или хеша, стоит разобраться подробнее — возможно, перед вами именно та логическая ошибка, которая приведет к успешной эксплуатации.
Проверьте себя на практике
Прочитать про PHP Type Juggling недостаточно. Чтобы уверенно находить такие ошибки во время пентеста, важно самому пройти весь путь: проанализировать приложение, выдвинуть гипотезу, проверить ее и добиться эксплуатации.
Для этого подойдет задача «У Джамшута» на платформе ONE TASK.
По сюжету ваш знакомый Джамшут, владелец местной шаурмичной, предлагает необычную сделку: если найдете уязвимость в его веб-приложении, получите скидку на всю продукцию на целый год.
Задача построена так, чтобы участник самостоятельно обнаружил логическую ошибку и применил знания о PHP Type Juggling на практике. Это хороший способ закрепить материал статьи и понять, как подобные уязвимости выглядят не в искусственном примере из документации, а в приложении, максимально приближенном к реальному.
Чтобы получить доступ к заданию, зарегистрируйтесь на бесплатный курс «Профессия Белый Хакер» и откройте раздел ONE TASK.
Разбирайте задачи вместе с экспертом
Если решить задачу с первого раза не получилось — это нормально. Именно поэтому для участников бесплатного курса «Профессия Белый Хакер» регулярно проводят открытые разборы задач ONE TASK.
На вебинарах эксперт не просто показывает готовый ответ, а последовательно объясняет ход исследования: на какие детали обратить внимание, как проверять гипотезы, где обычно допускают ошибки и почему некоторые направления поиска оказываются тупиковыми.
Такой формат помогает понять логику работы пентестера, а не просто запомнить конкретный эксплойт. Со временем именно этот навык позволяет быстрее находить уязвимости и увереннее решать более сложные CTF и реальные задачи по анализу защищенности.
Следите за анонсами вебинаров, регистрируйтесь на бесплатный курс «Профессия Белый Хакер» и используйте ONE TASK как регулярную практику для развития навыков offensive security.