Перед тем как приручать ошибки, я бы рекомендовал изучить каждый вид и отдельно обратить внимание на самых ярких представителей.
Чтобы ни одна ошибка не ушла незамеченной потребуется включить отслеживание всех ошибок с помощью функции error_reporting(), а с помощью директивы display_errors включить их отображение:
Фатальные ошибки
Самый грозный вид ошибок — фатальные, они могут возникнуть как при компиляции, так и при работе парсера или PHP-скрипта, выполнение скрипта при этом прерывается.
E_PARSE
Это ошибка появляется, когда вы допускаете грубую ошибку синтаксиса и интерпретатор PHP не понимает, что вы от него хотите, например если не закрыли фигурную или круглую скобочку:
Или написали на непонятном языке:
Лишние скобочки тоже встречаются, и не так важно круглые либо фигурные:
Отмечу один важный момент — код файла, в котором вы допустили parse error не будет выполнен, следовательно, если вы попытаетесь включить отображение ошибок в том же файле, где возникла ошибка парсера то это не сработает:
E_ERROR
Это ошибка появляется, когда PHP понял что вы хотите, но сделать сие не получилось ввиду ряда причин. Эта ошибка так же прерывает выполнение скрипта, при этом код до появления ошибки сработает:
Не был найден подключаемый файл:
Было брошено исключение (что это за зверь, расскажу немного погодя), но не было обработано:
При попытке вызвать несуществующий метод класса:
Отсутствия свободной памяти (больше, чем прописано в директиве memory_limit) или ещё чего-нить подобного:
Рекурсивный вызов функции. В данном примере он закончился на 256-ой итерации, ибо так прописано в настройках xdebug (да, данная ошибка может проявиться в таком виде только при включении xdebug расширения):
Не фатальные
Данный вид не прерывает выполнение скрипта, но именно их обычно находит тестировщик. Именно такие ошибки доставляют больше всего хлопот начинающим разработчикам.
E_WARNING
Бывает, если используешь неправильный тип аргументов при вызове функций:
Их очень много, и перечислять все не имеет смысла…
E_NOTICE
Это самые распространенные ошибки, мало того, есть любители отключать вывод ошибок и клепают их целыми днями. Возникают при целом ряде тривиальных ошибок.
Когда обращаются к неопределенной переменной:
Когда обращаются к несуществующему элементу массива:
Когда обращаются к несуществующей константе:
Когда не конвертируют типы данных:
Для избежания подобных ошибок — будьте внимательней, и если вам IDE подсказывает о чём-то — не игнорируйте её:
E_STRICT
E_DEPRECATED
Так PHP будет ругаться, если вы используете устаревшие функции (т. е. те, что помечены как deprecated, и в следующем мажорном релизе их не будет):
В моём редакторе подобные функции будут зачёркнуты:
Пользовательские
Этот вид, которые «разводит» сам разработчик кода, я уже давно их не встречал, и не рекомендую вам ими злоупотреблять:
Теперь, когда вы познакомились с большинством видов и типов ошибок, пора озвучить небольшое пояснение по работе директивы display_errors :
Приручение
Для работы с ошибками в PHP существует 3 функции:
-
— устанавливает обработчик для ошибок, которые не обрывают работу скрипта (т. е. для не фатальных ошибок) — получает информацию о последней ошибке — регистрирует обработчик который будет запущен при завершении работы скрипта. Данная функция не относится непосредственно к обработчикам ошибок, но зачастую используется именно для этого
С обработчиком, который написан выше есть одна существенная проблема — он не ловит фатальные ошибки, и при таких ошибках вместо сайта пользователи увидят лишь пустую страницу, либо, что ещё хуже, сообщение об ошибке. Дабы не допустить подобного сценария следует воспользоваться функцией register_shutdown_function() и с её помощью зарегистрировать функцию, которая всегда будет выполняться по окончанию работы скрипта:
Данная функция будет срабатывать всегда!
Но вернёмся к ошибкам, для отслеживания появления в коде ошибки воспользуемся функцией error_get_last(), с её помощью можно получить информацию о последней выявленной ошибке, а поскольку фатальные ошибки прерывают выполнение кода, то они всегда будут выполнять роль «последних»:
О прожорливости
Проведём простой тест, и выясним — сколько драгоценных ресурсов кушает самая тривиальная ошибка:
В результате запуска данного скрипта у меня получился вот такой результат:
Теперь добавим ошибку в цикле:
Результат ожидаемо хуже, и на порядок (даже на два порядка!):
Вывод однозначен — ошибки в коде приводят к лишней прожорливости скриптов — так что во время разработки и тестирования приложения включайте отображение всех ошибок!
Где собака зарыта
В PHP есть спец символ «@» — оператор подавления ошибок, его используют дабы не писать обработку ошибок, а положится на корректное поведение PHP в случае чего:
Исключения
Исключения — исключительные событие в PHP, в отличии от ошибок не просто констатируют наличие проблемы, а требуют от программиста дополнительных действий по обработке каждого конкретного случая.
К примеру, скрипт должен сохранить какие-то данные в кеш файл, если что-то пошло не так (нет доступа на запись, нет места на диске), генерируется исключение соответствующего типа, а в обработчике исключений принимается решение — сохранить в другое место или сообщить пользователю о проблеме.
В каких случаях стоит применять исключения:
Соответственно ловить данные исключения будем примерно так:
В данном примере приведен очень простой сценарий обработки исключений, когда у нас любая исключительная ситуация обрабатывается на один манер. Но зачастую, различные исключения требуют различного подхода к обработке, и тогда следует использовать коды исключений и задать иерархию исключений в приложении:
Теперь, если использовать эти исключения то можно получить следующий код:
Важно помнить, что Exception — это прежде всего исключительное событие, иными словами исключение из правил. Не нужно использовать их для обработки очевидных ошибок, к примеру, для валидации введённых пользователем данных (хотя тут не всё так однозначно). При этом обработчик исключений должен быть написан в том месте, где он будет способен его обработать. К примеру, обработчик для исключений вызванных недоступностью файла для записи должен быть в методе, который отвечает за выбор файла или методе его вызывающем, для того что бы он имел возможность выбрать другой файл или другую директорию.
Чтобы избежать подобной ситуации следует использовать функцию set_exception_handler() и установить обработчик для исключений, которые брошены вне блока try-catch и не были обработаны. После вызова такого обработчика выполнение скрипта будет остановлено:
Ещё расскажу про конструкцию с использованием блока finally — этот блок будет выполнен вне зависимости от того, было выброшено исключение или нет:
Для понимания того, что это нам даёт приведу следующий пример использования блока finally :
Т. е. запомните — блок finally будет выполнен даже в том случае, если вы в блоке catch пробрасываете исключение выше (собственно именно так он и задумывался).
Для вводной статьи информации в самый раз, кто жаждет ещё подробностей, то вы их найдёте в статье Исключительный код
PHP7 — всё не так, как было раньше
Так, вот вы сейчас всю информацию выше усвоили и теперь я буду грузить вас нововведениями в PHP7, т. е. я буду рассказывать о том, с чем вы будете сталкиваться работая над современным PHP проектом. Ранее я вам рассказывал и показывал на примерах какой костыль нужно соорудить, чтобы отлавливать критические ошибки, так вот — в PHP7 это решили исправить, но? как обычно? завязались на обратную совместимость кода, и получили хоть и универсальное решение, но оно далеко от идеала. А теперь по пунктам об изменениях:
Сложно? Теперь на примерах, возьмём те, что были выше и слегка модернизируем:
В результате ошибку поймаем и выведем:
И чуть-чуть деталей:
TypeError — для ошибок, когда тип аргументов функции не совпадает с передаваемым типом:
ArithmeticError — могут возникнуть при математических операциях, к примеру когда результат вычисления превышает лимит выделенный для целого числа:
AssertionError — редкий зверь, появляется когда условие заданное в assert() не выполняется:
Полный список предопределённых исключений вы найдёте в официальном мануале, там же иерархия SPL исключений.
Единообразие
— Там ошибки, тут исключения, а можно это всё как-то до кучи сгрести?
Да запросто, у нас же есть set_error_handler() и никто нам не запретит внутри оного обработчика бросить исключение:
Но данный подход с PHP7 избыточен, со всем теперь справляется Throwable :
Отладка
Иногда, для отладки кода, нужно отследить что происходило с переменной или объектом на определённом этапе, для этих целей есть функция debug_backtrace() и debug_print_backtrace() которые вернут историю вызовов функций/методов в обратном порядке:
В результате выполнения функции debug_print_backtrace() будет выведен список вызовов приведших нас к данной точке:
Assert
Первый случай — это когда вам надо написать TODO прямо в коде, да так, чтобы точно не забыть реализовать заданный функционал:
В результате выполнения данного кода получим E_WARNING :
PHP7 можно переключить в режим exception, и вместо ошибки будет всегда появляться исключение AssertionError :
При необходимости, можно выбрасывать произвольное исключение:
Второй вариант использования — это создание некоего подобия TDD, но помните — это лишь подобие. Хотя, если сильно постараться, то можно получить забавный результат, который поможет в тестировании вашего кода:
Третий вариант — некое подобие на контрактное программирование, когда вы описали правила использования своей библиотеки, но хотите точно убедится, что вас поняли правильно, и в случае чего сразу указать разработчику на ошибку (я вот даже не уверен, что правильно его понимаю, но пример кода вполне рабочий):
Если у вас есть живой опыт использования assert() — поделитесь со мной, буду благодарен. И да, вот вам ещё занимательно чтива по этой теме — PHP Assertions, с таким же вопросом в конце
Роковые ошибки. Как искать логические уязвимости в веб-приложениях
Эта статья — конспект вебинара по веб‑уязвимостям, который я проводил для читателей «Хакера». В конце будет приведена его запись, где ты сможешь увидеть то, что не попало в текстовую версию. Если тебе интересны такие темы и ты бы хотел разобраться в них глубже, записывайся на мой курс по безопасности веб‑приложений!
Сразу предупрежу, что большинство задач, которые мы сегодня разберем, будут на языке PHP. Оно неспроста — подавляющее большинство сайтов и сервисов в интернете написаны именно на нем. Так что, несмотря на подпорченную репутацию этого языка, тебе придется разбираться в нем, чтобы хакерствовать серьезно. Сейчас же, в рамках этого занятия, знание PHP не является необходимостью, но, конечно, будет очень серьезным подспорьем.
Впрочем, большинство уязвимостей не привязаны к конкретному языку или стеку технологий, так что, узнав их на примере PHP, ты легко сможешь эксплуатировать подобные баги и в ASP. NET, и в каком‑нибудь Node. JS.
А еще предупрежу, что задачки, которые мы сегодня разберем, не совсем начального уровня и совсем уж «валенкам» тут делать нечего — сначала стоит почитать матчасть и хоть немного представлять, с чем хочешь иметь дело. Если же ты можешь отличить HTTP от XML и у тебя не возникает вопросов вида «а что за доллары в коде?», то добро пожаловать!
warning
Ни автор курса, ни редакция «Хакера» не несут ответственности за твои действия. Применение материалов этой статьи против любой системы без разрешения ее владельца преследуется по закону.
Сегодня мы разберем несколько задач, которые я решал сам в рамках тренировки. Возможно, они покажутся тебе сложными, но не пугайся — всегда есть возможность отточить свои навыки на сайтах правительств специализированных сайтах для хакеров. Я сейчас говорю о HackTheBox и Root-me, которыми пользуюсь сам и всячески советую другим. Две из сегодняшних задач взяты именно оттуда.
Задача 1
Сначала я приведу код, с которым мы сейчас будем работать.
По сути, тут всего три строки кода. Казалось бы, где тут может закрасться уязвимость?
Чтобы это понять, давай разберем алгоритм, который здесь реализован. Вообще, при аудите кода стоит уметь читать его построчно. Тогда проще понять, что именно может пойти не так.
В конце очищенное имя файла подставляется в путь и загружается файл с этим именем. Ничего плохого.
Итак, что может пойти не по плану?
Как ты уже, конечно, догадался — проблема в функции очистки ввода (которая preg_replace ). Давай обратимся к первой попавшейся шпаргалке по регулярным выражениям.
Шпаргалка
Тут прямо написан ответ, как обойти защиту (подсказка: ищи справа).
Видишь точку? А шапочку ( ^ )? Та строка читается как «если в начале строки находится любое количество любых символов, кроме переноса строки, и это заканчивается слешем, удалить соответствующую часть строки».
Ключевое тут «кроме переноса строки». Если в начале строки будет перенос строки — регулярка не отработает и введенная строка попадет в include( ) без фильтрации.
Результат
Задача 2
Это задачка с root-me, где ты, возможно, уже видел ее. Но мы все равно рассмотрим ее подробнее — она относится к реалистичным, и шансы встретить что‑то подобное в жизни немаленькие.
В задании нам дается простой файлообменник и просят получить доступ к панели админа.
Интерфейс файлообменника
Интерфейс крайне прост: есть кнопка загрузки файла на сервер и просмотр загруженных файлов по прямым ссылкам. Забегая вперед, скажу, что грузить скрипты на PHP, bash и прочие — бесполезно, проверки реализованы верно и ошибка в другом месте.
Обрати внимание на нижнюю часть страницы, а точнее — на фразу «frequent backups: this opensource script is launched every 5 minutes for saving your files». И приведена ссылка на скрипт, вызываемый каждые пять минут в системе.
Давай глянем на него пристальнее:
Казалось бы — что тут такого? На параметры ты влиять не можешь, а мантру призыва tar вообще знаешь как свои пять пальцев. А проблема в самой мантре: тут она написана не полностью. Точнее, не в том виде, как ее увидит сам tar.
Что делает звездочка? Вместо нее bash подставит имена всех файлов в текущей папке. Вроде ничего криминального.
А давай обратимся к мануалу на Tar, который нам любезно предоставлен вместе с условием задачи.
Интересности в Tar
Теперь вспомним про звездочку: вместо нее шелл (bash) подставит список всех файлов в текущей папке, при этом они могут иметь любые имена. В том числе такие, которые будут восприняты архиватором как специальные параметры.
Просто заголовок и команда копирования админской панели в текущую папку. Естественно, тут мог быть реверс‑шелл или еще что‑то, но для решения конкретно этой задачи такая «тяжелая артиллерия» не нужна.
Теперь дожидаемся выполнения нашего шелла — и увидим в окне файлообменника файл админ‑панели в виде простого текста. Осталось только открыть его и найти там пароль!
Пароль в чистом виде
Задача 3
Тут у нас плагин для WordPress, который позволяет запись аудио и видео.
Я не буду просить тебя найти уязвимость, а сразу покажу ее.
Уязвимое место
Как видно из строк 247–251 на скриншоте, не предусмотрено никаких проверок на тип или содержимое файла — это просто классическая загрузка!
Есть, правда, ограничение: файл грузится в стандартную директорию WordPress ( / wordpress/ wp-content/ uploads/< YEAR>/ < MONTH>). Это значит, что листинг содержимого нам по умолчанию недоступен. А в строке 247 генерируется случайный идентификатор, который подставляется в начало имени файла, то есть обратиться к / wordpress/ wp-content/ uploads/ 2021/ 01/ shell. php уже не выйдет. Непорядок!
Получает уникальный идентификатор с префиксом, основанный на текущем времени в микросекундах.
<…>
Внимание. Эта функция не гарантирует получения уникального значения. Большинство операционных систем синхронизирует время с NTP либо его аналогами, так что системное время постоянно меняется. Следовательно, возможна ситуация, когда эта функция вернет неуникальный идентификатор для процесса/потока. <…>
Так как PHP — проект открытый, мы можем подсмотреть исходники функций стандартной библиотеки. Открываем исходник uniqid( ) на GitHub, переходим к строке 76 и наблюдаем следующее:
Что тут происходит? А то, что возвращаемое значение зависит исключительно от текущего времени, которое в рамках одной планеты вполне предсказуемо.
Хоть выходная последовательность и выглядит случайной, она таковой не является. Чтобы не быть голословным, вот пример имени файла, сгенерированного таким алгоритмом:
Полученное значение легко можно конвертировать обратно в дату и время его генерации:
Конечно, брутить все 13 символов — вши заедят, но у нас есть способ получше: мы можем пробрутить варианты на основе времени загрузки плюс‑минус полсекунды, чтобы нивелировать разбежки часов на клиенте и сервере. А можно просто поверить, что часы у обоих хостов точные, а значит, можно проверить не миллион вариантов (1 секунду), а только варианты, возможные между временем отправки запроса и временем получения ответа. На шустром канале это будет порядка 300–700 мс, что не так и много.
Конечно, не все реальные кейсы требуют глубоких познаний в PHP или другом серверном языке. Многие ошибки можно найти, даже не открывая код — с помощью автоматических сканеров. Подробнее о них — в нашей статье об автоматическом взломе. Они здорово помогают, так что не грех иметь парочку под рукой для экспресс‑анализа!
Я набросал простой скрипт на Python для демонстрации такой возможности. Его код представлен ниже:
Нам нужно запустить его несколько раз, чтобы подобрать минимальное время между отправкой запроса и получением ответа — это позволит уменьшить время перебора.
Также нужно помнить, что разбежки все же могут быть, и чисто на всякий случай стоит проверить, насколько локальное время соответствует времени на сервере. Частенько оно возвращается сервером в заголовке Last-Modified и позволяет понять, какую величину коррекции внести в свои расчеты.
Как бы еще оптимизировать перебор?
Ну, во‑первых, питон сам по себе очень медленный и, конечно, не смог бы выполнить соединение, передачу заголовков, отправку файла и прочие мелкие накладные расходы в тот же момент. А интерпретатор PHP на стороне сервера едва ли моментально проверит права, запустит скрипт, отработает служебные функции и дойдет до собственно уязвимого места. Тут можно накинуть эдак тысяч сто микросекунд без малейших потерь.
Во‑вторых, выполнение uniqid( ) очевидно происходит не в самом конце функции. Еще нужно время на обработку загруженного файла, запись ответа (заголовков), отправку этого всего по сети и на обработку ответа интерпретатором Python. Тут тоже можно порядка 100 000 микросекунд вычесть.
Вот так на ровном месте мы сократили перебор на 200 000 запросов. Много это или мало? В моем случае это сократило количество запросов еще примерно на треть.
Осталось порядка 500 000 вариантов, которые можно перебрать в пределах часа или даже меньше — у меня это заняло минут 15.
Теперь давай напишем еще один скрипт, который и будет искать наш шелл с использованием этого алгоритма:
Вот и всё: запускаешь, через некоторое время получаешь путь, и хост захвачен!
Наверняка у тебя возник вопрос, нельзя ли как‑то еще усовершенствовать этот перебор, потому что 500 тысяч вариантов — это все равно как‑то многовато? Можно, но такого значимого ускорения, как раньше, уже не будет. Суть в том, что можно идти не от начала промежутка времени к концу, а от середины к краям. По опыту, это работает несколько быстрее.
Другой способ
Задача 4
Последняя на сегодня задачка — тоже с root-me и тоже из категории реалистичных, но заметно посложнее. Сервис Web TV — новейшая французская разработка в сфере интернет‑телевидения. Но нас интересует не новая дешевая трагедия, а админка.
Главная страница Web TV. Простите за мой французский
Только — вот незадача — Gobuster никаких признаков админки не обнаружил. Придется изучать, что нам доступно. А доступен логин (там форма авторизации) и ссылка на неработающий эфир.
Попробуем залогиниться и перехватить запрос на авторизацию с помощью Burp.
Буква З в слове «реальность» означает «защищенность»
Запрос отправляем в Repeater (повторитель). Пусть пока там полежит.
Взглянем еще разок на форму логина. Какие мысли тебя посещают, когда ты видишь форму для авторизации? Конечно, SQL-инъекция! А давай ткнем туда кавычку. Написали. Отправляем. Хм, ничего не поменялось. А как вообще узнать, что что‑то поменялось? Смотри на заголовок Content-Length в ответе: в нашем случае там приходит ровно 2079 байт, если инъекции не было, и, очевидно, придет сильно другой результат в противном случае. Я попробовал еще немного, и инъекция так просто не выявилась, так что давай поищем в другом месте, а потом вернемся к этому запросу.
Я попробовал перейти на страницу / page_index и получил ошибку как на скрине ниже.
Ошибка интерпретатора
/?action=../../index. php
Видно не все, но если открыть ответ в Burp или даже просто просмотреть код страницы браузером — открывается полный исходник. Вот тебе и directory traversal налицо.
Результат обхода каталога
Помнишь, мы не могли найти путь к админке? А на скриншоте он есть: именно на него будет редирект, когда скрипт проверит логин и пароль.
Давай, не отходя от кассы, сразу и его прочитаем — вдруг там что‑нибудь интересное есть.
https://m. habr. com/ru/post/440744/
https://xakep. ru/2021/01/12/websec-errors/