habrahabr

Как хакнуть Github и заработать $35000?

  • вторник, 13 апреля 2021 г. в 00:39:46
https://habr.com/ru/company/skillfactory/blog/551846/
  • Блог компании SkillFactory
  • Информационная безопасность
  • Разработка веб-сайтов
  • JavaScript
  • CTF


Когда я нашёл эту уязвимость и сообщил о ней, она стала моим первым оплаченным баг-репортом на HackerOne. $35,000 — это также самая высокая награда, которую я получил от HackerOne (и я считаю, что самая высокая оплата от GitHub на сегодня). Многие найденные ошибки, кажется, — это удача и интуиция, вместе взятые. В этом посте я расскажу, как мыслил, приближаясь к цели.


Как началась история

Ковид ударил весной, в первый год старшей школы. От нечего делать между онлайн-занятиями я начал охоту за багами. Конкретно эта награда была за сообщение об уязвимости приватных страниц Github в рамках программы Баг Баунти. В частности, было и два бонуса CTF (capture the flag — состязание по типу захвата флага, здесь — в информационной безопасности):

  • $10000 чтение флага flag.private-org.github.io без взаимодействия с пользователем. Бонус в $5000 за то, что флаг читается с аккаунта внутри приватной организации.

  • $5000: чтение флага flag.private-org.github.io через взаимодействие с пользователем.

Поток аутентификации

Страницы GitHub размещаются в отдельном домене github.io, поэтому куки-файлы аутентификации github.com не отправляются на сервер приватных страниц. Таким образом, аутентификация частной страницы не имеет возможности определить личность пользователя без дополнительной интеграции с github.com, поэтому GitHub создал собственный поток аутентификации. И ввёл возможность существования багов. На момент отчёта этот поток выглядел так:

А теперь подробнее.

При посещении приватной страницы сервер проверяет, существует ли файл куки __Host-gh_pages_token. Если этот файл не установлен или установлен неправильно, сервер приватной страницы перенаправит на https://github.com/login. Этот начальный редирект также устанавливает nonce, который хранится в куки __Host-gh_pages_session.

Обратите внимание, что этот куки использует префикс куки __Host-, который, в теории, в качестве дополнительной защиты в глубину, предотвращает его установку из JS с родительского домена.

/login перенаправляет на /pages/auth?nonce=&page_id=&path=. Затем эта конечная точка генерирует временный куки-файл аутентификации, который она передаёт https://pages-auth.github.com/redirect в параметре token; nonce, page_id, и path присылаются аналогично.

/redirect просто перенаправляет на https://repo.org.github.io/__/auth. Эта последняя конечная точка затем устанавливает куки аутентификации для домена repo.org.github.io, __Host-gh_pages_token и __Host-gh_pages_id. Также вместе с ранее установленным __Host-gh_pages_session на валидность проверяется nonce.

На протяжении всего потока аутентификации такая информация, как исходный путь запроса и идентификатор страницы, хранится в параметрах запроса — path и page_id соответственно. Nonce также передаётся в параметре nonce. Хотя поток аутентификации, возможно, немного изменился (отчасти из-за этого отчёта), общая идея остаётся прежней.

Эксплоит

CRLF возвращается

Первая уязвимость — CRLF-инъекция в параметре page_id запроса https://repo.org.github.io/__/auth.

Возможно, лучший способ найти уязвимые места — поиграть. Исследуя поток аутентификации, я заметил, что парсинг page_id, похоже, игнорировал пробельные символы. Интересно, что он также рендерил параметр непосредственно в заголовок Set-Cookie. К примеру, page_id=12345%20 вернул это:

Set-Cookie: __Host-gh_pages_id=12345 ; Secure; HttpOnly; path=/

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

page_id = query.page_id

do_page_lookup(to_int(page_id))
set_page_id_cookie(page_id)

Другими словами, page_id преобразуется в целое число, а также отображается напрямую в заголовок Set-Cookie. Проблема была в том, что мы не можем отобразить текст напрямую. Несмотря на то, что у нас была классическая CRLF-инъекция, размещение любых непробельных символов привело к поломке парсинга целого. Мы могли бы прервать поток аутентификации, отправив page_id=12345%0d%0a%0d%0a, но, кроме интересного ответа, никакого немедленного воздействия не было.

; Secure; HttpOnly; path=/
Cache-Control: private
Location: https://83e02b43.near-dimension.github.io/
X-GLB-L

Дополнительное примечание: заголовок Location: был добавлен после заголовка Set-Cookie, поэтому наш ответ Location выталкивает за пределы отправленных HTTP-заголовков. Это редирект на 302, но, несмотря на это, заголовок Location игнорируется и отображается содержимое тела.

Из грязи в князи

Немного просмотрев на GitHub Enterprise (который дал доступ к исходному коду), я заподозрил, что сервер приватных страниц реализован на Nginx OpenResty. Будучи относительно низкоуровневым, возможно, он имел проблемы с нулевыми байтами. А попытка не пытка, правильно?

Оказалось, что добавление нулевого байта прерывает парсинг целого. Другими словами, можно передать такую полезную нагрузку:

"?page_id=" + encodeURIComponent("\r\n\r\n\x00<script>alert(origin)</script>")

А вот и XSS!

Обратите внимание, что, если в заголовке есть нулевой байт, ответ будет отклонён. Таким образом, нулевой байт должен прийти в начале тела (это означает, что мы не можем выполнить атаку через инъекцию в заголовок).

Мы добились выполнения произвольного JavaScript на странице приватного домена. Единственная проблема — нужен способ обойти nonce. Параметры page_id и path известны, а nonce не позволяет нам отправлять жертв вниз по потоку аутентификации с отравленным page_id. Нам нужно либо зафиксировать, либо спрогнозировать nonce.

Обходим nonce

Первое наблюдение — поддомены одной организации могут устанавливать куки-файлы друг на друга. Так происходит потому, что *.github.io нет в публичном списке суффиксов. Таким образом установленные на private-org.github.io куки передаются на private-page.private-org.github.io.

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

Ну, ладно... неправильно говорить не во всех. Похоже, в смысле этого обхода уязвим только IE. Придётся постараться. А что, если атаковать сам nonce? генерация выглядит надёжной, и, если честно, криптография — не моя сильная сторона. Найти обход применяемой в nonce энтропии, несмотря ни на что, — это казалось маловероятным. Как же тогда зафиксировать nonce?

Вернёмся к истокам — RFC. В конце концов, я наткнулся на интересную идею — как нормализовать куки? В частности, как следует обращаться с куки с учётом верхнего регистра. "__HOST-" — это то же самое, что "__Host-"? Легко убедиться, что в браузерах с куками разного регистра обращаются по-разному:

document.cookie = "__HOST-Test=1"; // works
document.cookie = "__Host-Test=1"; // fails

Оказывается, сервер приватных страниц GitHub при разборе куки-файлов игнорирует заглавные буквы. И у нас есть префиксный обход. А теперь собираем доказательство концепции атаки XSS!

<script>
const id = location.search.substring("?id=".length)

document.cookie = "__HOST-gh_pages_session=dea8c624-468f-4c5b-a4e6-9a32fe6b9b15; domain=.private-org.github.io";
location = "https://github.com/pages/auth?nonce=dea8c624-468f-4c5b-a4e6-9a32fe6b9b15&page_id=" + id + "%0d%0a%0d%0a%00<script>alert(origin)%3c%2fscript>&path=Lw";
</script>

Уже этого достаточно, чтобы заработать бонус в $5000. Но я хотел посмотреть, получится ли продвинуться дальше.

Отравление кэша

И вот ещё один недостаток дизайна — выяснилось, что в ответ на конечную точку /__/auth? кэшировалось только целое значение page_id. Само по себе это безвредно: установленный этой конечной точкой токен скопирован на личную страницу и не имеет никаких других привилегий.

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

Независимо от этого, такое кэширование — лёгкий способ усугубить атаку. Поскольку он делается на разобранном целочисленном значении, успешная атака на кэш с полезной нагрузкой XSS может повлиять на других пользователей, которые даже не взаимодействовали с вредоносной нагрузкой, как показано ниже:

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы