Skip to content

Commit e10d1d3

Browse files
Merge pull request #657 from doyensec:flyte-exposed-ui
PiperOrigin-RevId: 819636599 Change-Id: I92fd53830e1df04a2de75bee99c661a89874d6cd
2 parents d541c98 + 833b011 commit e10d1d3

File tree

9 files changed

+1179
-0
lines changed

9 files changed

+1179
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Exposed Flyte Console Detector
2+
3+
This Tsunami plugin identifies publicly exposed Flyte Consoles. Once detected,
4+
it creates a project and task within the console, executes the task to run
5+
remote code, and then receives a callback at the Tsunami callback server.
6+
7+
## Build jar file for this plugin
8+
9+
Using `gradlew`:
10+
11+
```shell
12+
./gradlew jar
13+
```
14+
15+
Tsunami identifiable jar file is located at `build/libs` directory.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
plugins {
2+
id 'java-library'
3+
}
4+
5+
description = 'Tsunami detector for exposed flyte console.'
6+
group = 'com.google.tsunami'
7+
version = '0.0.1-SNAPSHOT'
8+
9+
repositories {
10+
11+
maven { // The google mirror is less flaky than mavenCentral()
12+
url 'https://maven-central.storage-download.googleapis.com/repos/central/data/'
13+
}
14+
mavenCentral()
15+
mavenLocal()
16+
}
17+
18+
def coreRepoBranch = System.getenv("GITBRANCH_TSUNAMI_CORE") ?: "stable"
19+
def tcsRepoBranch = System.getenv("GITBRANCH_TSUNAMI_TCS") ?: "stable"
20+
21+
dependencies {
22+
implementation("com.google.tsunami:tsunami-common") {
23+
version { branch = "${coreRepoBranch}" }
24+
}
25+
implementation("com.google.tsunami:tsunami-plugin") {
26+
version { branch = "${coreRepoBranch}" }
27+
}
28+
implementation("com.google.tsunami:tsunami-proto") {
29+
version { branch = "${coreRepoBranch}" }
30+
}
31+
implementation "org.flyte:flyteidl-protos:0.4.60"
32+
33+
testImplementation "com.google.inject:guice:6.0.0"
34+
testImplementation "com.google.inject.extensions:guice-testlib:6.0.0"
35+
testImplementation "com.google.truth:truth:1.4.4"
36+
testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4"
37+
testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4"
38+
testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0"
39+
testImplementation "io.grpc:grpc-inprocess:1.60.0"
40+
testImplementation "io.grpc:grpc-testing:1.60.0"
41+
testImplementation "junit:junit:4.13.2"
42+
testImplementation "org.mockito:mockito-core:5.18.0"
43+
}
44+
45+
jar {
46+
from {
47+
configurations.runtimeClasspath.findAll {
48+
it.name.contains("flyte")
49+
}.collect { zipTree(it) }
50+
}
51+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
rootProject.name = 'flyte_exposed_console'
2+
3+
def coreRepository = System.getenv("GITREPO_TSUNAMI_CORE") ?: "https://github.com/google/tsunami-security-scanner.git"
4+
def tcsRepository = System.getenv("GITREPO_TSUNAMI_TCS") ?: "https://github.com/google/tsunami-security-scanner-callback-server.git"
5+
6+
sourceControl {
7+
gitRepository("${coreRepository}") {
8+
producesModule("com.google.tsunami:tsunami-common")
9+
producesModule("com.google.tsunami:tsunami-plugin")
10+
producesModule("com.google.tsunami:tsunami-proto")
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.tsunami.plugins.rce.flyteconsole;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
import static com.google.tsunami.common.net.http.HttpRequest.get;
21+
22+
import com.google.common.annotations.VisibleForTesting;
23+
import com.google.common.collect.ImmutableList;
24+
import com.google.common.flogger.GoogleLogger;
25+
import com.google.protobuf.util.Timestamps;
26+
import com.google.tsunami.common.data.NetworkServiceUtils;
27+
import com.google.tsunami.common.net.http.HttpClient;
28+
import com.google.tsunami.common.net.http.HttpResponse;
29+
import com.google.tsunami.common.net.http.HttpStatus;
30+
import com.google.tsunami.common.time.UtcClock;
31+
import com.google.tsunami.plugin.PluginType;
32+
import com.google.tsunami.plugin.VulnDetector;
33+
import com.google.tsunami.plugin.annotations.ForWebService;
34+
import com.google.tsunami.plugin.annotations.PluginInfo;
35+
import com.google.tsunami.plugin.payload.NotImplementedException;
36+
import com.google.tsunami.plugin.payload.Payload;
37+
import com.google.tsunami.plugin.payload.PayloadGenerator;
38+
import com.google.tsunami.proto.DetectionReport;
39+
import com.google.tsunami.proto.DetectionReportList;
40+
import com.google.tsunami.proto.DetectionReportList.Builder;
41+
import com.google.tsunami.proto.DetectionStatus;
42+
import com.google.tsunami.proto.NetworkService;
43+
import com.google.tsunami.proto.PayloadGeneratorConfig;
44+
import com.google.tsunami.proto.Severity;
45+
import com.google.tsunami.proto.TargetInfo;
46+
import com.google.tsunami.proto.Vulnerability;
47+
import com.google.tsunami.proto.VulnerabilityId;
48+
import java.io.IOException;
49+
import java.time.Clock;
50+
import java.time.Instant;
51+
import java.util.regex.Pattern;
52+
import javax.inject.Inject;
53+
54+
/** A VulnDetector plugin for Exposed Flyte Console Server. */
55+
@PluginInfo(
56+
type = PluginType.VULN_DETECTION,
57+
name = "Exposed Flyte Console Detector",
58+
version = "0.1",
59+
description =
60+
"This detector identifies instances of exposed Flyte Console, "
61+
+ "which could potentially allow for remote code execution (RCE).",
62+
author = "hayageek",
63+
bootstrapModule = ExposedFlyteConsoleDetectorModule.class)
64+
@ForWebService
65+
public final class ExposedFlyteConsoleDetector implements VulnDetector {
66+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
67+
@VisibleForTesting static final String VULNERABILITY_REPORT_PUBLISHER = "TSUNAMI_COMMUNITY";
68+
@VisibleForTesting static final String VULNERABILITY_REPORT_ID = "FLYTE_CONSOLE_EXPOSED";
69+
70+
@VisibleForTesting static final String VULNERABILITY_REPORT_TITLE = "Exposed Flyte Console";
71+
72+
@VisibleForTesting
73+
static final String VULN_DESCRIPTION =
74+
"An exposed Flyte Console can lead to severe security risks, "
75+
+ "including unauthorized access and potential remote code execution (RCE). "
76+
+ "Ensure that access controls and security measures are properly configured "
77+
+ "to prevent exploitation. Please refer to the remediation guidance section "
78+
+ "below for mitigation strategies.";
79+
80+
@VisibleForTesting
81+
static final String RECOMMENDATION =
82+
"Please disable public access to your flyte console instance.";
83+
84+
@VisibleForTesting
85+
private static final Pattern VULNERABILITY_RESPONSE_PATTERN = Pattern.compile("<title>Flyte");
86+
87+
@VisibleForTesting FlyteProtoClient flyteClient = new FlyteProtoClient();
88+
89+
private static final int MAX_TIMEOUT_FOR_RCE_IN_SECS = 180;
90+
private final Clock utcClock;
91+
private final HttpClient httpClient;
92+
private final PayloadGenerator payloadGenerator;
93+
94+
@Inject
95+
ExposedFlyteConsoleDetector(
96+
@UtcClock Clock utcClock, HttpClient httpClient, PayloadGenerator payloadGenerator) {
97+
this.utcClock = checkNotNull(utcClock);
98+
this.httpClient = checkNotNull(httpClient).modify().setFollowRedirects(true).build();
99+
this.payloadGenerator = checkNotNull(payloadGenerator);
100+
}
101+
102+
@Override
103+
public ImmutableList<Vulnerability> getAdvisories() {
104+
return ImmutableList.of(
105+
Vulnerability.newBuilder()
106+
.setMainId(
107+
VulnerabilityId.newBuilder()
108+
.setPublisher(VULNERABILITY_REPORT_PUBLISHER)
109+
.setValue(VULNERABILITY_REPORT_ID))
110+
.setSeverity(Severity.CRITICAL)
111+
.setTitle(VULNERABILITY_REPORT_TITLE)
112+
.setDescription(VULN_DESCRIPTION)
113+
.setRecommendation(RECOMMENDATION)
114+
.build());
115+
}
116+
117+
@Override
118+
public DetectionReportList detect(
119+
TargetInfo targetInfo, ImmutableList<NetworkService> matchedServices) {
120+
121+
Builder detectionReport = DetectionReportList.newBuilder();
122+
matchedServices.stream()
123+
.filter(NetworkServiceUtils::isWebService)
124+
.filter(this::isFlyteConsole)
125+
.forEach(
126+
networkService -> {
127+
if (isVulnerable(networkService)) {
128+
detectionReport.addDetectionReports(
129+
buildDetectionReport(
130+
targetInfo,
131+
networkService,
132+
"Flyte Console is misconfigured and can be accessed publicly, potentially"
133+
+ " leading to Remote Code Execution (RCE). Tsunami security scanner"
134+
+ " confirmed this by sending an HTTP request with a test connection"
135+
+ " API and receiving the corresponding callback on the tsunami"
136+
+ " callback server.",
137+
Severity.CRITICAL));
138+
}
139+
});
140+
return detectionReport.build();
141+
}
142+
143+
public boolean isFlyteConsole(NetworkService networkService) {
144+
logger.atInfo().log("probing flyte console home page ");
145+
String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService);
146+
var consolePageUrl = String.format("%s%s", rootUrl, "console");
147+
try {
148+
HttpResponse loginResponse =
149+
this.httpClient.send(get(consolePageUrl).withEmptyHeaders().build());
150+
if ((loginResponse.status() == HttpStatus.OK && loginResponse.bodyString().isPresent())) {
151+
String responseBody = loginResponse.bodyString().get();
152+
if (VULNERABILITY_RESPONSE_PATTERN.matcher(responseBody).find()) {
153+
return true;
154+
}
155+
}
156+
157+
} catch (IOException e) {
158+
logger.atWarning().withCause(e).log("Unable to query '%s'.", consolePageUrl);
159+
}
160+
logger.atWarning().log("unable to find flight console ");
161+
162+
return false;
163+
}
164+
165+
private boolean isVulnerable(NetworkService networkService) {
166+
Payload payload = getTsunamiCallbackHttpPayload();
167+
if (payload == null || !payload.getPayloadAttributes().getUsesCallbackServer()) {
168+
logger.atWarning().log("Tsunami callback server is not setup for this environment.");
169+
return false;
170+
}
171+
172+
String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService);
173+
try {
174+
175+
// Set the URL and build the client.
176+
flyteClient.buildService(rootUrl);
177+
178+
// Run the RCE and check the status in loop, until MAX_TIMEOUT_FOR_RCE_IN_SECS
179+
String payloadString = payload.getPayload();
180+
flyteClient.runShellScript(payloadString, MAX_TIMEOUT_FOR_RCE_IN_SECS);
181+
182+
return payload.checkIfExecuted();
183+
} catch (Exception e) {
184+
logger.atWarning().withCause(e).log("Failed to send request.%s", e.getMessage());
185+
return false;
186+
}
187+
}
188+
189+
private Payload getTsunamiCallbackHttpPayload() {
190+
try {
191+
return this.payloadGenerator.generate(
192+
PayloadGeneratorConfig.newBuilder()
193+
.setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE)
194+
.setInterpretationEnvironment(
195+
PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL)
196+
.setExecutionEnvironment(
197+
PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT)
198+
.build());
199+
} catch (NotImplementedException n) {
200+
logger.atWarning().withCause(n).log("Failed to generate payload.");
201+
return null;
202+
}
203+
}
204+
205+
private DetectionReport buildDetectionReport(
206+
TargetInfo targetInfo,
207+
NetworkService vulnerableNetworkService,
208+
String description,
209+
Severity severity) {
210+
return DetectionReport.newBuilder()
211+
.setTargetInfo(targetInfo)
212+
.setNetworkService(vulnerableNetworkService)
213+
.setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli()))
214+
.setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
215+
.setVulnerability(this.getAdvisories().getFirst())
216+
.build();
217+
}
218+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.tsunami.plugins.rce.flyteconsole;
18+
19+
import com.google.tsunami.plugin.PluginBootstrapModule;
20+
21+
/** A module registering the detector for Exposed Flyte Console. */
22+
public final class ExposedFlyteConsoleDetectorModule extends PluginBootstrapModule {
23+
@Override
24+
protected void configurePlugin() {
25+
registerPlugin(ExposedFlyteConsoleDetector.class);
26+
}
27+
}

0 commit comments

Comments
 (0)