Git

Внимание! Статья преследует чисто образовательные цели. Автор не одобряет и не поощряет хакерство, кроме его разрешённого этического применения (white hat).

Большинство разработчиков по всему миру в тот или иной момент жизни пересекались с Git. Более того, многие работают с ним каждый день. 

Несмотря на то, что диапазон применения Git можно с лёгкостью сузить до всего нескольких заученных команд вроде git add, git commit и git push, в действительности за кулисами происходит многое, о чём мы в большинстве случаев даже не задумываемся.

Тем не менее некоторое понимание происходящего за кадром может оказаться очень полезным, и я постараюсь поведать вам об этом в забавной форме: научив использовать слабые места безопасности Git, чтобы вы могли лучше защитить свои собственные сайты

Помните, что эти навыки ни в коем случае нельзя применять против чужих сайтов, не имея на то явного разрешения. В противном случае вы можете навлечь на себя уголовную ответственность. Я же нисколько не отвечаю за то, каким образом вы решите применить полученные из этой статьи знания. 

Крошечный веб-сервер

Если вы хотите пропустить создание файла и прочие детали, то можете просто скопировать код отсюда (при помощи Git :D) и перейти к разделу “Это вообще взлом?”.

Уязвимость, которую мы будем здесь рассматривать, подразумевает, что ваш репозиторий Git или его содержимое открыты для внешнего доступа. На сегодня это актуально для очень многих сайтов, причём в большинстве случаев именно PHP-серверов (удивлены?), поэтому давайте организуем себе такой сервер. 

Не спешите уходить! Писать много PHP-кода нам не придётся, хотя установить этот язык на рабочую машину всё же потребуется. Проверьте, не установлен ли PHP, командой php-v. До написания этого руководства я вообще не работал с PHP и могу вас заверить, что трудностей у меня не возникло. Итак, теперь откройте терминал и создайте директорию Git, добавив в неё следующее:

$ mkdir exposed-git

$ cd exposed-git

$ git init

$ echo "<h1>I will be hacked soon!</h1>" > index.php

Для запуска простого сервера введите $ php -S localhost:8000. А теперь убедитесь, что всё работает, открыв адрес localhost:8000 в браузере. Веб-сервер мы организовали.

Без коммита нету Git’а

Теперь пора снабдить наш сервер нужными компонентами. В корневой директории выполните $ touch config.php. Затем с помощью предпочтительного редактора добавьте в config.php следующее:

<?php

    if(!isset($_SERVER['HTTP_REFERER'])){
        header('location: ../error.php');
        exit;
    }

    define('DB_USER', 'admin');
    define('DB_PASSWORD', 'password123');
?>

Таким образом мы создали макет файла конфигурации с учётными данными БД и добавили “механизм безопасности”, не дающий пользователям получить к нему доступ. Попытка обратиться к localhost:8000/config.php вызовет ошибку страницы, которую мы пропишем ниже:

$ echo "<h1>You really thought you could hack me?</h1>" > error.php

Тут вы можете подумать: “Но ведь никто не хранит параметры доступа к БД в виде простого текста”. На что я отвечу: “Вот официальное руководство WordPress по части настройки файла wp-config.php. Да, всё и впрямь настолько плохо”. Теперь, когда файлы нашего сервера готовы, пора перейти к коммиту. Выполните:

$ git add .$ git commit -m "first commit"

На этом настройка завершена, пора переходить к взлому.

Приступаем

Давайте запустим сервер и попробуем кое-что сделать. Снова выполним команду $ php -S localhost:8000. Убедитесь, что ваш сервер работает в течение всего этого урока, а остальные процессы запускайте в отдельных окнах терминала.

Сначала попробуем обратиться к файлу config.php по адресу localhost:8000/config.php. При этом у вас должно появиться сообщение об ошибке, которое мы настроили ранее. 

Теперь давайте посмотрим, можем ли мы обратиться к скрытой директории .git/, поскольку именно этому и посвящена текущая статья: localhost:8000/.git/. Здесь с PHP 7.3.9 я получаю следующую страницу:

PHP по умолчанию блокирует доступ к директориям. Хотя на деле так бывает не всегда. Некоторые сайты полностью открывают свою директорию .git, и выглядит это примерно так:

Но ведь если мы не имеем доступа ко всей директории, то не можем получить доступ и к её содержимому? Что ж, давайте попробуем. 

На картинке выше мы видим элементы, присущие каждой директории Git. HEAD выглядит особенно интересно (config тоже интересен, но на этот раз мы его трогать не станем). Может быть нам удастся получить доступ к нему? Обращаемся к localhost:8000/.git/HEAD и вуаля! Что-то получилось. Выглядит не особо интересно, но всё же это путь, и мы можем его также попробовать. 

localhost:8000/.git/refs/heads/master

Мы получили хэш! Что бы это могло быть? Давайте подумаем о GitHub.

Здесь у нас скриншот репозитория для интерпретатора PHP. На нём мы видим сообщающий о последнем коммите баннер, который сопровождается частью хэша. Может мы нашли хэш коммита? Учитывая, что Git услужливо хранит для нас коммиты, можем ли мы найти один из них по его хэшу?

Глядя на структуру директории, можно предположить, что коммит хранится в .git/objects/. Хорошее предположение  —  проверим его.

localhost:8000/.git/objects/f452d4085347400afa8751aae3a5184d73113628

Не забудьте заменить хэш выше на полученный вами из localhost:8000/.git/refs/heads/master.

Ничего не найдено. Значит можно либо всё закрыть и забыть, либо побольше узнать о Git и попробовать ещё раз. Откройте любую свою директорию, использующую Git, и загляните в .git/objects/.

Если эта директория отличается повышенной активностью, то objects/ может выглядеть так:

Множество поддиректорий с двухсимвольными шестнадцатеричными именами. Значит искомый нами коммит не просто расположен в objects/. Похоже, он находится внутри поддиректории, чьё имя представлено двумя первыми знаками его хэша. Тогда давайте попробуем его запросить (используйте ваш хэш вместо моего):

localhost:8000/.git/objects/f4/52d4085347400afa8751aae3a5184d73113628

В моем случае просто вызывается окно загрузки, значит мы всё делаем правильно. При этом если я открываю файл или выполняю для него cat после окончания загрузки, то получаю вот такую тарабарщину:

xùéÀmB1�9ªäm »ˆ˙+!Dê„⁄fiM`2ÊêÓCBg£©kÔÀã~33ÿúıQíq¡'…≈*&4ùè≠ËZmQ7|ù Y,¢Ò>&#⁄',Ï–ŸB1JnË5'r,M - c~Ø>È|^·Hß«X`˜ÛG€˛OáØNÀe[◊æ„3b
⁄x¯–®µz⁄Áʉ∑JñqüÍ®_∏⁄K,

Но Git должен знать, что с ней делать. Создадим новую пустую директорию для выполнения некоторых тестов.

$ mkdir git-tests

$ cd git-tests

$ git init

Теперь поместим наш файл на его законное место — в .git/objects/. Из корня нашей директории git-tests выполните следующее (с вашим хэшем):

$ mkdir .git/objects/f4

$ curl localhost:8000/.git/objects/f4/52d4085347400afa8751aae3a5184d7311362
8 --output
 ./.git/objects/f4/52d4085347400afa8751aae3a5184d73113628

Теперь можно использовать встроенную команду Git для фактического считывания содержимого файла. Выглядит это так:

$ git cat-file -p f452d4085347400afa8751aae3a5184d73113628

Вот мы и получили первую часть конфиденциальной информации:

tree 2764257f81462ae8f9b26ab16d08de153db0cc2b

author Yakko Majuri <email_removed> 1593386015 -0300

committer Yakko Majuri <email_removed> 1593386015 -0300

first commit

Примечание: в случае наличия предыдущего коммита в истории вы бы также видели ссылку на коммит-родитель.

Мы получили идентификатор дерева, а также некоторую информацию об авторе коммита и разработчике приложения (это могут быть разные люди в случае, например, пул-реквестов), включая их e-mail адреса. В довершении к этому мы получили и само сообщение коммита. Вот здесь-то всё и становится весьма серьёзно.

Имейте ввиду, что для выполнения git-cat file вам не обязательно нужен весь хэш — будет достаточно его части. К примеру, тот же результат я получаю при помощи следующей команды:

$ git cat-file -p f452d40

Вам также следует знать, что можно выполнять команду, используя флаг -t. Так вы получите тип объекта, который может быть деревом, blob, коммитом или аннотированным тегом. Прежде всего нас интересуют первые 3, в особенности объекты blob (являющиеся файлами). Итак, если у нас есть хэш, и мы хотим узнать, какой тип объекта он представляет, то можно просто выполнить:

$ git cat-file -t f452d40
commit

К данному моменту мы рассмотрели уже немало, поэтому давайте прервёмся и проговорим некоторые моменты:

  • Вся информация, относящаяся к Git, хранится в директории .git/.
  • В HEAD хранится ссылка на head-ветки. В нашем случае присутствует только master
  • На Git всё хранится в виде объектов, которые сохраняются на основе своего хэша. Это отличное проектное решение, которое означает, что идентичные объекты сохраняются всего один раз, не порождая ненужных копий. 
  • Хранятся объекты в .git/objects внутри поддиректории с названием из двух первых знаков их хэша, например, .git/objects/f4.
  • Мы можем использовать команду git-cat file для получения типа объекта по его хэшу с помощью флага -t или для получения его содержимого в понятном для человека формате при помощи флага -p. Превосходно! Но мы ещё не закончили. Давайте немного углубимся.

Деревья и большие двоичные объекты (BLOB)

Исследованный нами объект коммита содержал ссылку на дерево. Давайте проверим, что это такое, тем же способом, что и ранее, а именно загрузив объект в нашу локальную директорию .git/ в git-tests. Фактически мы создаём зеркало Git-директории сервера, чтобы увидеть исходный код и при желании атаковать сайт. 

К сведению: в реальности это, естественно, делалось бы автоматизированным скриптом. Написать его достаточно просто, но здесь мы этого делать не будем. Нас интересует работа Git изнутри, а не скрипты Python, поэтому мы всё делаем вручную.

В моём случае ссылка на объект дерева в коммите была указана следующем образом:

tree 2764257f81462ae8f9b26ab16d08de153db0cc2b

Теперь я выполню ту же команду curl, но уже с этим новым хэшем:

$ mkdir .git/objects/27

$ curl localhost:8000/.git/objects/27/64257f81462ae8f9b26ab16d08de153db0cc2b --output 
./.git/objects/27/64257f81462ae8f9b26ab16d08de153db0cc2b

А затем снова git-cat file:

$ git cat-file -p 2764257

Взгляните-ка сюда!

100644 blob b7ea4906d2ac64060e01b35530a922c65f063831 config.php

100644 blob 5d4522cc30964b73ccd6855b42de805bf4ef59cc error.php

100644 blob 9f192b766a623127d8adbc6af01c120a58d8e1f0 index.php

Мы получили структуру корневой директории сервера!

Из этого можно сделать важное заключение: деревья в Git — это просто структуры данных со ссылками на другие объекты Git. Таким образом, директории в Git представлены в виде деревьев со ссылками на blob (файлы) и другие деревья (директории).

Например, если бы на нашем сервере была поддиректория vendor/, то показывалась бы она как дерево, на которое в свою очередь ссылается только что проинспектированное нами другое дерево.

100644 blob b7ea4906d2ac64060e01b35530a922c65f063831 config.php

100644 blob 5d4522cc30964b73ccd6855b42de805bf4ef59cc error.php

100644 blob 9f192b766a623127d8adbc6af01c120a58d8e1f0 index.php

040000 tree              SOME_HASH                   vendor

Кроме того, числа слева — это просто режимы Git. 040000 — это директории, потому что разрешения игнорируются. 100644 относится к неисполняемым файлам, а 100755 к исполняемым. Есть и другие, но эти основные. Прежде чем переходить к последнему шагу, ещё раз пройдёмся по некоторым важным моментам. Для начала вот диаграмма:

Стоп, на этой диаграмме стрелки указывают не в том направлении!

Вообще, несмотря на то, что большинство онлайн-диаграмм Git будут показывать стрелки, направленные от первого коммита к HEAD, здесь мы поняли, что коммиты фактически ссылаются на своих родителей, а не на потомков.

Ещё раз отмечу, что это тоже хорошее проектное решение. Если бы коммиты ссылались на своих потомков, то второму коммиту на диаграмме выше пришлось бы хранить 2 ссылки вместо одной. Кроме того, ему бы потребовалась возможность обрабатывать всё больше и больше потомков. 

С другой стороны, такой вариант гарантирует, что каждый коммит имеет только одну ссылку на другой, кроме случаев, когда он не имеет родителя или представляет результат слияния двух коммитов, т.е. имеет двух родителей. При этом на одного родителя может, в свою очередь, ссылаться любое число коммитов, создавая таким образом ветки. 

Наконец, как и ранее, для нахождения HEAD-ветки мы проверяем её наличие по адресу .git/refs/heads/<branch_name>. В Git у нас всегда будет ссылка на каждый концевой узел, представляющий каждую ветку, и перемещаться мы будем от этих узлов по направлению к корневому узлу, а не наоборот. Теперь о некоторых нюансах:

  • Когда вы выполняете для файлов команду git add, их объекты создаются в objects/, но объект коммита не создаётся.
  • Когда вы выполняете git commit, ссылки обновляются везде, отражая изменения в ветке. Более важно то, что предыдущий HEAD (коммит) получает ссылку на новый HEAD, а команда .git/HEAD в связи с этим будет указывать на новый HEAD через .git/refs/heads/<branch_name>.
  • Ещё раз напомню, что ветки — это просто ссылки поверх ссылок. Они разделяют общий коммит с другой веткой (вроде master), что даёт возможность с лёгкостью прослеживать изменения между ветками, например при слиянии. Для этого Git нужно лишь проследовать по ссылкам к родителю каждого коммита из HEAD, пока не будет достигнут коммит, в котором произошло разделение. Затем он проходит по потомкам этого коммита в другой ветке, пока не достигает HEAD и сравнивает деревья. Так можно проследовать из любой ветки в любую другую ветку, поскольку все они являются частью master
  • Описанное выше также означает, что если вы спуститесь вниз по ссылкам из HEAD любой ветки, то в итоге достигните первого коммита репозитория (за исключением особого случая изолированных веток).
  • Коммиты содержат ссылку на своего родителя, но более важны ссылки на дерево, являющееся представлением всего проекта после коммита. Именно поэтому мы можем с помощью Git заглядывать в прошлое и видеть совокупное состояние проекта во время любого сделанного ранее коммита.
  • Предыдущий пункт также раскрывает возможную угрозу при использовании Git, особенно когда этот ресурс работает в тандеме с такими платформами, как GitHub или GitLab. Если вы не удалите прошлые объекты и коммиты явно, то всё прошлое вашего проекта будет видимо всем, кто имеет доступ к репозиторию. Если же репозиторий ранее был закрытым, а затем был открыт, то всё его прошлое также станет доступным. Таким образом, если вы когда-нибудь добавите в проект то, что добавлять не стоит (вроде ключа API или, что ещё хуже, пароля), то удаления нового коммита будет недостаточно. Вам также придётся удалить все соответствующие объекты и коммиты. К счастью, в Git есть команды, которые помогут в этом, и вам не придётся повторно выполнять rm для множества странных файлов с хэш-подобными именами.

Отправляемся на охоту

Если вы тщательно следовали этой инструкции, то должны понимать, что следует далее и как это осуществить. Проделав вышеперечисленные шаги, мы можем загрузить файл config.php, для которого теперь известен хэш, и изучить его содержимое.

$ git cat-file -p b7ea4906d2

<?php

    if(!isset($_SERVER['HTTP_REFERER'])){
        header('location: ../error.php');
        exit;
    }

    define('DB_USER', 'admin');
    define('DB_PASSWORD', 'password123');
?>

В нашем случае мы “обнаружили” имя пользователя и пароль от базы данных. Для вас это может показаться смешным, но множество сайтов взламываются именно так. И даже если вы не получите конфиденциальную информацию в виде простого текста, то всё равно будете иметь доступ ко всему исходному коду, что позволит осуществить атаки другого рода.

Заключение

Надеюсь, что из этого руководства вы почерпнули полезную информацию как о принципах работы Git, так и о возможных угрозах, связанных с его неосторожным использованием. Несмотря на то, что масштабы Git существенно больше затронутых нами в этой статье, для начала рассмотренной информации вполне достаточно. 

Сам я далеко не эксперт в области этого ресурса, но обладание таким простым знанием позволило мне существенно улучшить работу над контролем версий.

В завершение могу посоветовать вам поэкспериментировать c собственной директорией .git/. Помимо этого, в случае особой заинтересованности вы можете заглянуть в исходный код Git.

Что же касается кода, приведённого мной, то он находится здесь

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Yakko Majuri: Tutorial: Learn the internals of Git by hacking a website

Предыдущая статьяОбзор шаблонов SnapML и их возможностей в Lens Studio
Следующая статьяTelegram-бот с помощью таблицы Google