From 114b57116c3365ef3c896c08ab97726954872f66 Mon Sep 17 00:00:00 2001 From: ReK2 Date: Mon, 18 Aug 2025 21:17:50 +0200 Subject: [PATCH 1/3] Add ConfigMap code injection experiment Tests if malicious code can be injected and executed via ConfigMaps. Attackers could use this to escalate privileges or exfiltrate data from pods that mount untrusted ConfigMaps. --- experiments/configmap-code-injection.yaml | 42 +++ .../experiments_configmap_injection.go | 303 ++++++++++++++++++ internal/experiments/registry.go | 1 + 3 files changed, 346 insertions(+) create mode 100644 experiments/configmap-code-injection.yaml create mode 100644 internal/experiments/experiments_configmap_injection.go diff --git a/experiments/configmap-code-injection.yaml b/experiments/configmap-code-injection.yaml new file mode 100644 index 0000000..d8d8e6b --- /dev/null +++ b/experiments/configmap-code-injection.yaml @@ -0,0 +1,42 @@ +experiments: + - metadata: + name: configmap-injection-test + type: configmap-code-injection + namespace: default + parameters: + mountPath: "/scripts" + targetCommand: "/scripts/malicious.sh" + scriptContent: | + #!/bin/sh + echo "=== ConfigMap Code Injection Test ===" > /tmp/injection-success + echo "Timestamp: $(date)" >> /tmp/injection-success + echo "User: $(whoami)" >> /tmp/injection-success + echo "Container ID: $(cat /proc/self/cgroup | head -1)" >> /tmp/injection-success + + # Test 1: Check service account access + echo "" >> /tmp/injection-success + echo "[*] Testing Service Account Access:" >> /tmp/injection-success + if [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then + echo "SUCCESS: Service account token is accessible" >> /tmp/injection-success + echo "Token exists at: /var/run/secrets/kubernetes.io/serviceaccount/token" >> /tmp/injection-success + else + echo "FAIL: No service account token found" >> /tmp/injection-success + fi + + # Test 2: Check environment for secrets + echo "" >> /tmp/injection-success + echo "[*] Checking Environment Variables:" >> /tmp/injection-success + env | grep -i "secret\|key\|token\|pass" >> /tmp/injection-success 2>&1 || echo "No sensitive env vars found" >> /tmp/injection-success + + # Test 3: Check mounted volumes + echo "" >> /tmp/injection-success + echo "[*] Mounted Volumes:" >> /tmp/injection-success + mount | grep -v "proc\|sys\|dev" >> /tmp/injection-success + + # Test 4: Network access test + echo "" >> /tmp/injection-success + echo "[*] Testing Network Access:" >> /tmp/injection-success + wget -q -O - http://kubernetes.default.svc/version >> /tmp/injection-success 2>&1 || echo "Cannot reach K8s API" >> /tmp/injection-success + + echo "" >> /tmp/injection-success + echo "=== Test Complete ===" >> /tmp/injection-success \ No newline at end of file diff --git a/internal/experiments/experiments_configmap_injection.go b/internal/experiments/experiments_configmap_injection.go new file mode 100644 index 0000000..6828779 --- /dev/null +++ b/internal/experiments/experiments_configmap_injection.go @@ -0,0 +1,303 @@ +/* +Copyright 2025 Operant AI +*/ +package experiments + +import ( + "context" + "fmt" + + "github.com/operantai/woodpecker/internal/categories" + "github.com/operantai/woodpecker/internal/k8s" + "github.com/operantai/woodpecker/internal/verifier" + "gopkg.in/yaml.v3" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" +) + +// ConfigMapInjectionExperiment tests if malicious code can be injected via ConfigMaps +type ConfigMapInjectionExperiment struct { + Metadata ExperimentMetadata `yaml:"metadata"` + Parameters ConfigMapInjectionParams `yaml:"parameters"` +} + +type ConfigMapInjectionParams struct { + ScriptContent string `yaml:"scriptContent"` + MountPath string `yaml:"mountPath"` + TargetCommand string `yaml:"targetCommand"` +} + +func (e *ConfigMapInjectionExperiment) Type() string { + return "configmap-code-injection" +} + +func (e *ConfigMapInjectionExperiment) Description() string { + return "Test if malicious code can be injected and executed via ConfigMaps" +} + +func (e *ConfigMapInjectionExperiment) Technique() string { + return "T1055" // Process Injection +} + +func (e *ConfigMapInjectionExperiment) Tactic() string { + return "Execution" +} + +func (e *ConfigMapInjectionExperiment) Framework() string { + return string(categories.Mitre) +} + +func (e *ConfigMapInjectionExperiment) Run(ctx context.Context, experimentConfig *ExperimentConfig) error { + client, err := k8s.NewClient() + if err != nil { + return err + } + + var config ConfigMapInjectionExperiment + yamlObj, _ := yaml.Marshal(experimentConfig) + err = yaml.Unmarshal(yamlObj, &config) + if err != nil { + return err + } + + // Set defaults if not provided + if config.Parameters.ScriptContent == "" { + config.Parameters.ScriptContent = `#!/bin/sh +echo "MALICIOUS CODE EXECUTED" > /tmp/injection-success +echo "User: $(whoami)" >> /tmp/injection-success +echo "Hostname: $(hostname)" >> /tmp/injection-success +echo "Environment:" >> /tmp/injection-success +env >> /tmp/injection-success +# Attempt to access service account token +if [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then + echo "SERVICE ACCOUNT TOKEN ACCESSIBLE" >> /tmp/injection-success + echo "Token preview: $(head -c 50 /var/run/secrets/kubernetes.io/serviceaccount/token)..." >> /tmp/injection-success +fi +# Check for other mounted secrets +echo "Mounted volumes:" >> /tmp/injection-success +mount >> /tmp/injection-success +` + } + + if config.Parameters.MountPath == "" { + config.Parameters.MountPath = "/scripts" + } + + if config.Parameters.TargetCommand == "" { + config.Parameters.TargetCommand = "/scripts/malicious.sh" + } + + clientset := client.Clientset + + // Step 1: Create ConfigMap with malicious script + configMapName := fmt.Sprintf("%s-script", config.Metadata.Name) + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Labels: map[string]string{ + "experiment": config.Metadata.Name, + }, + }, + Data: map[string]string{ + "malicious.sh": config.Parameters.ScriptContent, + }, + } + + _, err = clientset.CoreV1().ConfigMaps(config.Metadata.Namespace).Create(ctx, configMap, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create configmap: %w", err) + } + + // Step 2: Create Deployment that mounts and executes the ConfigMap + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.Metadata.Name, + Labels: map[string]string{ + "experiment": config.Metadata.Name, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: pointer.Int32(1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": config.Metadata.Name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "experiment": config.Metadata.Name, + "app": config.Metadata.Name, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "target-container", + Image: "busybox:latest", + Command: []string{ + "sh", + "-c", + fmt.Sprintf("sh %s && tail -f /dev/null", + config.Parameters.TargetCommand), + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "script-volume", + MountPath: config.Parameters.MountPath, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "script-volume", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMapName, + }, + DefaultMode: pointer.Int32(0755), + }, + }, + }, + }, + }, + }, + }, + } + + _, err = clientset.AppsV1().Deployments(config.Metadata.Namespace).Create(ctx, deployment, metav1.CreateOptions{}) + if err != nil { + // Clean up ConfigMap if deployment fails + _ = clientset.CoreV1().ConfigMaps(config.Metadata.Namespace).Delete(ctx, configMapName, metav1.DeleteOptions{}) + return fmt.Errorf("failed to create deployment: %w", err) + } + + return nil +} + +func (e *ConfigMapInjectionExperiment) Verify(ctx context.Context, experimentConfig *ExperimentConfig) (*verifier.LegacyOutcome, error) { + client, err := k8s.NewClient() + if err != nil { + return nil, err + } + + var config ConfigMapInjectionExperiment + yamlObj, _ := yaml.Marshal(experimentConfig) + err = yaml.Unmarshal(yamlObj, &config) + if err != nil { + return nil, err + } + + v := verifier.NewLegacy( + config.Metadata.Name, + e.Description(), + e.Framework(), + e.Tactic(), + e.Technique(), + ) + + clientset := client.Clientset + + // Check if the deployment was created successfully + deployment, err := clientset.AppsV1().Deployments(config.Metadata.Namespace).Get(ctx, config.Metadata.Name, metav1.GetOptions{}) + if err != nil { + v.Fail("deployment-created") + return v.GetOutcome(), nil + } + + if deployment.Status.ReadyReplicas > 0 { + v.Success("deployment-created") + } else { + v.Fail("deployment-created") + } + + // Check if the malicious script was executed + listOptions := metav1.ListOptions{ + LabelSelector: fmt.Sprintf("app=%s", config.Metadata.Name), + } + pods, err := clientset.CoreV1().Pods(config.Metadata.Namespace).List(ctx, listOptions) + if err != nil || len(pods.Items) == 0 { + v.Fail("script-executed") + return v.GetOutcome(), nil + } + + // Try to exec into the pod and check for injection success + pod := pods.Items[0] + if pod.Status.Phase == corev1.PodRunning { + // Check if script executed by looking for the output file + command := []string{"cat", "/tmp/injection-success"} + stdout, stderr, err := client.ExecuteRemoteCommand(ctx, config.Metadata.Namespace, pod.Name, "target-container", command) + + if err == nil && stdout != "" { + v.Success("script-executed") + v.StoreResultOutputs("injection-output", stdout) + } else { + v.Fail("script-executed") + if stderr != "" { + v.StoreResultOutputs("error", stderr) + } + } + } else { + v.Fail("script-executed") + } + + // Check if ConfigMap was successfully mounted + mountCommand := []string{"ls", "-la", config.Parameters.MountPath} + mountStdout, _, mountErr := client.ExecuteRemoteCommand(ctx, config.Metadata.Namespace, pod.Name, "target-container", mountCommand) + if mountErr == nil && mountStdout != "" { + v.Success("configmap-mounted") + v.StoreResultOutputs("mount-details", mountStdout) + } else { + v.Fail("configmap-mounted") + } + + return v.GetOutcome(), nil +} + +func (e *ConfigMapInjectionExperiment) Cleanup(ctx context.Context, experimentConfig *ExperimentConfig) error { + client, err := k8s.NewClient() + if err != nil { + return err + } + + var config ConfigMapInjectionExperiment + yamlObj, _ := yaml.Marshal(experimentConfig) + err = yaml.Unmarshal(yamlObj, &config) + if err != nil { + return err + } + + clientset := client.Clientset + + // Delete deployment (this will also delete the pods it created) + err = clientset.AppsV1().Deployments(config.Metadata.Namespace).Delete(ctx, config.Metadata.Name, metav1.DeleteOptions{}) + if err != nil && !errors.IsNotFound(err) { + // Only return error if it's not a "not found" error (resource might already be deleted) + return fmt.Errorf("failed to delete deployment: %w", err) + } + + // Delete ConfigMap + configMapName := fmt.Sprintf("%s-script", config.Metadata.Name) + err = clientset.CoreV1().ConfigMaps(config.Metadata.Namespace).Delete(ctx, configMapName, metav1.DeleteOptions{}) + if err != nil && !errors.IsNotFound(err) { + // Only return error if it's not a "not found" error + return fmt.Errorf("failed to delete configmap: %w", err) + } + + // Also delete any pods that might be stuck (belt and suspenders approach) + listOptions := metav1.ListOptions{ + LabelSelector: fmt.Sprintf("experiment=%s", config.Metadata.Name), + } + err = clientset.CoreV1().Pods(config.Metadata.Namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions) + if err != nil && !errors.IsNotFound(err) { + // Non-critical error, just log it + fmt.Printf("Warning: failed to delete pods: %v\n", err) + } + + return nil +} \ No newline at end of file diff --git a/internal/experiments/registry.go b/internal/experiments/registry.go index 153f76b..4a64e71 100644 --- a/internal/experiments/registry.go +++ b/internal/experiments/registry.go @@ -12,6 +12,7 @@ var ExperimentsRegistry = []Experiment{ &LLMDataLeakageExperiment{}, &LLMDataPoisoningExperiment{}, &KubeExec{}, + &ConfigMapInjectionExperiment{}, } func ListExperiments() map[string]string { From b82ed462ac8a82205111aeeeeb4900f49c9a6e71 Mon Sep 17 00:00:00 2001 From: ReK2 Date: Mon, 18 Aug 2025 21:22:22 +0200 Subject: [PATCH 2/3] change copyright to myself since doing this from personal git repo --- internal/experiments/experiments_configmap_injection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/experiments/experiments_configmap_injection.go b/internal/experiments/experiments_configmap_injection.go index 6828779..bdb2b70 100644 --- a/internal/experiments/experiments_configmap_injection.go +++ b/internal/experiments/experiments_configmap_injection.go @@ -1,5 +1,5 @@ /* -Copyright 2025 Operant AI +Copyright 2025 Christian (ReK2) F.N. */ package experiments From 52f3004242f6a5c5729f71180cb910e9d9332432 Mon Sep 17 00:00:00 2001 From: ReK2 Date: Tue, 26 Aug 2025 18:18:14 +0200 Subject: [PATCH 3/3] Refactor ConfigMap injection experiment - Remove hardcoded script content from Go implementation - Make all parameters required (scriptContent, mountPath, targetCommand) - Clean up YAML example to be more concise and professional --- experiments/configmap-code-injection.yaml | 46 +++++++------------ .../experiments_configmap_injection.go | 38 +++++---------- 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/experiments/configmap-code-injection.yaml b/experiments/configmap-code-injection.yaml index d8d8e6b..3a65d7b 100644 --- a/experiments/configmap-code-injection.yaml +++ b/experiments/configmap-code-injection.yaml @@ -1,42 +1,28 @@ experiments: - metadata: - name: configmap-injection-test + name: configmap-injection type: configmap-code-injection namespace: default parameters: - mountPath: "/scripts" - targetCommand: "/scripts/malicious.sh" + mountPath: "/injected-scripts" + targetCommand: "/injected-scripts/payload.sh" scriptContent: | #!/bin/sh - echo "=== ConfigMap Code Injection Test ===" > /tmp/injection-success - echo "Timestamp: $(date)" >> /tmp/injection-success - echo "User: $(whoami)" >> /tmp/injection-success - echo "Container ID: $(cat /proc/self/cgroup | head -1)" >> /tmp/injection-success + # ConfigMap code injection test payload + # This demonstrates how untrusted ConfigMaps can execute arbitrary code - # Test 1: Check service account access - echo "" >> /tmp/injection-success - echo "[*] Testing Service Account Access:" >> /tmp/injection-success + # Create evidence file + echo "[+] ConfigMap injection successful" > /tmp/configmap-injection-evidence + echo "[+] Executed at: $(date)" >> /tmp/configmap-injection-evidence + echo "[+] Running as: $(whoami)" >> /tmp/configmap-injection-evidence + + # Test for privilege escalation opportunities if [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then - echo "SUCCESS: Service account token is accessible" >> /tmp/injection-success - echo "Token exists at: /var/run/secrets/kubernetes.io/serviceaccount/token" >> /tmp/injection-success - else - echo "FAIL: No service account token found" >> /tmp/injection-success + echo "[!] Service account token found - potential privilege escalation vector" >> /tmp/configmap-injection-evidence fi - # Test 2: Check environment for secrets - echo "" >> /tmp/injection-success - echo "[*] Checking Environment Variables:" >> /tmp/injection-success - env | grep -i "secret\|key\|token\|pass" >> /tmp/injection-success 2>&1 || echo "No sensitive env vars found" >> /tmp/injection-success - - # Test 3: Check mounted volumes - echo "" >> /tmp/injection-success - echo "[*] Mounted Volumes:" >> /tmp/injection-success - mount | grep -v "proc\|sys\|dev" >> /tmp/injection-success - - # Test 4: Network access test - echo "" >> /tmp/injection-success - echo "[*] Testing Network Access:" >> /tmp/injection-success - wget -q -O - http://kubernetes.default.svc/version >> /tmp/injection-success 2>&1 || echo "Cannot reach K8s API" >> /tmp/injection-success + # Check for sensitive environment variables + env | grep -i "secret\|password\|token\|key" >> /tmp/configmap-injection-evidence 2>/dev/null || echo "[*] No sensitive environment variables found" >> /tmp/configmap-injection-evidence - echo "" >> /tmp/injection-success - echo "=== Test Complete ===" >> /tmp/injection-success \ No newline at end of file + # Log completion + echo "[+] Injection test completed" >> /tmp/configmap-injection-evidence \ No newline at end of file diff --git a/internal/experiments/experiments_configmap_injection.go b/internal/experiments/experiments_configmap_injection.go index bdb2b70..a985187 100644 --- a/internal/experiments/experiments_configmap_injection.go +++ b/internal/experiments/experiments_configmap_injection.go @@ -20,7 +20,7 @@ import ( // ConfigMapInjectionExperiment tests if malicious code can be injected via ConfigMaps type ConfigMapInjectionExperiment struct { - Metadata ExperimentMetadata `yaml:"metadata"` + Metadata ExperimentMetadata `yaml:"metadata"` Parameters ConfigMapInjectionParams `yaml:"parameters"` } @@ -63,31 +63,17 @@ func (e *ConfigMapInjectionExperiment) Run(ctx context.Context, experimentConfig return err } - // Set defaults if not provided + // Validate required parameters if config.Parameters.ScriptContent == "" { - config.Parameters.ScriptContent = `#!/bin/sh -echo "MALICIOUS CODE EXECUTED" > /tmp/injection-success -echo "User: $(whoami)" >> /tmp/injection-success -echo "Hostname: $(hostname)" >> /tmp/injection-success -echo "Environment:" >> /tmp/injection-success -env >> /tmp/injection-success -# Attempt to access service account token -if [ -f /var/run/secrets/kubernetes.io/serviceaccount/token ]; then - echo "SERVICE ACCOUNT TOKEN ACCESSIBLE" >> /tmp/injection-success - echo "Token preview: $(head -c 50 /var/run/secrets/kubernetes.io/serviceaccount/token)..." >> /tmp/injection-success -fi -# Check for other mounted secrets -echo "Mounted volumes:" >> /tmp/injection-success -mount >> /tmp/injection-success -` + return fmt.Errorf("scriptContent parameter is required") } if config.Parameters.MountPath == "" { - config.Parameters.MountPath = "/scripts" + return fmt.Errorf("mountPath parameter is required") } if config.Parameters.TargetCommand == "" { - config.Parameters.TargetCommand = "/scripts/malicious.sh" + return fmt.Errorf("targetCommand parameter is required") } clientset := client.Clientset @@ -102,7 +88,7 @@ mount >> /tmp/injection-success }, }, Data: map[string]string{ - "malicious.sh": config.Parameters.ScriptContent, + "payload.sh": config.Parameters.ScriptContent, }, } @@ -141,7 +127,7 @@ mount >> /tmp/injection-success Command: []string{ "sh", "-c", - fmt.Sprintf("sh %s && tail -f /dev/null", + fmt.Sprintf("sh %s && tail -f /dev/null", config.Parameters.TargetCommand), }, VolumeMounts: []corev1.VolumeMount{ @@ -229,10 +215,10 @@ func (e *ConfigMapInjectionExperiment) Verify(ctx context.Context, experimentCon // Try to exec into the pod and check for injection success pod := pods.Items[0] if pod.Status.Phase == corev1.PodRunning { - // Check if script executed by looking for the output file - command := []string{"cat", "/tmp/injection-success"} + // Check if script executed by looking for the evidence file + command := []string{"cat", "/tmp/configmap-injection-evidence"} stdout, stderr, err := client.ExecuteRemoteCommand(ctx, config.Metadata.Namespace, pod.Name, "target-container", command) - + if err == nil && stdout != "" { v.Success("script-executed") v.StoreResultOutputs("injection-output", stdout) @@ -273,7 +259,7 @@ func (e *ConfigMapInjectionExperiment) Cleanup(ctx context.Context, experimentCo } clientset := client.Clientset - + // Delete deployment (this will also delete the pods it created) err = clientset.AppsV1().Deployments(config.Metadata.Namespace).Delete(ctx, config.Metadata.Name, metav1.DeleteOptions{}) if err != nil && !errors.IsNotFound(err) { @@ -300,4 +286,4 @@ func (e *ConfigMapInjectionExperiment) Cleanup(ctx context.Context, experimentCo } return nil -} \ No newline at end of file +}