-
Notifications
You must be signed in to change notification settings - Fork 180
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a detector for exposed NodeRED instances as they allow for very t…
…rivial command execution. This plugin was tested against version `v3.1.5`. PiperOrigin-RevId: 611493248 Change-Id: Ic4a4d1e79d662df0c7a15017f4c682ece1a6e8cb
- Loading branch information
1 parent
ee9f5af
commit 5ec8083
Showing
6 changed files
with
470 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# NodeRED unprotected instance | ||
|
||
This detector checks whether a NodeRED instance is available without | ||
authentication (which allows very easy RCE). | ||
|
||
## Build jar file for this plugin | ||
|
||
Using `gradlew`: | ||
|
||
```shell | ||
./gradlew jar | ||
``` | ||
|
||
Tsunami identifiable jar file is located at `build/libs` directory. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
plugins { | ||
id 'java-library' | ||
} | ||
|
||
description = 'Tsunami VulnDetector plugin for exposed NodeRED.' | ||
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() | ||
} | ||
|
||
java { | ||
sourceCompatibility = JavaVersion.VERSION_11 | ||
targetCompatibility = JavaVersion.VERSION_11 | ||
|
||
jar.manifest { | ||
attributes('Implementation-Title': name, | ||
'Implementation-Version': version, | ||
'Built-By': System.getProperty('user.name'), | ||
'Built-JDK': System.getProperty('java.version'), | ||
'Source-Compatibility': sourceCompatibility, | ||
'Target-Compatibility': targetCompatibility) | ||
} | ||
|
||
javadoc.options { | ||
encoding = 'UTF-8' | ||
use = true | ||
links 'https://docs.oracle.com/javase/8/docs/api/' | ||
source = '8' | ||
} | ||
|
||
// Log stacktrace to console when test fails. | ||
test { | ||
testLogging { | ||
exceptionFormat = 'full' | ||
showExceptions true | ||
showCauses true | ||
showStackTraces true | ||
} | ||
maxHeapSize = '1500m' | ||
} | ||
} | ||
|
||
ext { | ||
floggerVersion = '0.5.1' | ||
guavaVersion = '28.2-jre' | ||
javaxInjectVersion = '1' | ||
jsoupVersion = '1.9.2' | ||
okhttpVersion = '3.12.0' | ||
protobufVersion = '3.11.4' | ||
tsunamiVersion = 'latest.release' | ||
|
||
junitVersion = '4.13' | ||
mockitoVersion = '2.28.2' | ||
truthVersion = '1.0.1' | ||
} | ||
|
||
dependencies { | ||
implementation "com.google.flogger:flogger:${floggerVersion}" | ||
implementation "com.google.flogger:google-extensions:${floggerVersion}" | ||
implementation "com.google.flogger:flogger-system-backend:${floggerVersion}" | ||
implementation "com.google.guava:guava:${guavaVersion}" | ||
implementation "com.google.protobuf:protobuf-java:${protobufVersion}" | ||
implementation "com.google.protobuf:protobuf-javalite:${protobufVersion}" | ||
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}" | ||
implementation "com.google.tsunami:tsunami-common:${tsunamiVersion}" | ||
implementation "com.google.tsunami:tsunami-plugin:${tsunamiVersion}" | ||
implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}" | ||
implementation "javax.inject:javax.inject:${javaxInjectVersion}" | ||
implementation "org.jsoup:jsoup:${jsoupVersion}" | ||
|
||
testImplementation "com.google.truth:truth:${truthVersion}" | ||
testImplementation "com.google.truth.extensions:truth-java8-extension:${truthVersion}" | ||
testImplementation "com.google.truth.extensions:truth-proto-extension:${truthVersion}" | ||
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}" | ||
testImplementation "junit:junit:${junitVersion}" | ||
testImplementation "org.mockito:mockito-core:${mockitoVersion}" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
rootProject.name = 'exposed_nodered' |
155 changes: 155 additions & 0 deletions
155
...java/com/google/tsunami/plugins/detectors/exposedui/nodered/NodeRedExposedUiDetector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
/* | ||
* 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.exposedui.nodered; | ||
|
||
import static com.google.common.base.Preconditions.checkNotNull; | ||
import static com.google.common.collect.ImmutableList.toImmutableList; | ||
import static com.google.tsunami.common.net.http.HttpRequest.get; | ||
|
||
import com.google.common.collect.ImmutableList; | ||
import com.google.common.flogger.GoogleLogger; | ||
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.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.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.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.Instant; | ||
import javax.inject.Inject; | ||
|
||
/** A {@link VulnDetector} that detects exposed NodeRED instances. */ | ||
@PluginInfo( | ||
type = PluginType.VULN_DETECTION, | ||
name = "NodeRedExposedUiDetector", | ||
version = "0.1", | ||
description = "Detects exposed NodeRED instances", | ||
author = "Pierre Precourt ([email protected])", | ||
bootstrapModule = NodeRedExposedUiDetectorBootstrapModule.class) | ||
public final class NodeRedExposedUiDetector implements VulnDetector { | ||
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); | ||
|
||
private final Clock utcClock; | ||
private final HttpClient httpClient; | ||
|
||
@Inject | ||
NodeRedExposedUiDetector(@UtcClock Clock utcClock, HttpClient httpClient) { | ||
this.utcClock = checkNotNull(utcClock); | ||
this.httpClient = checkNotNull(httpClient).modify().build(); | ||
} | ||
|
||
@Override | ||
public DetectionReportList detect( | ||
TargetInfo targetInfo, ImmutableList<NetworkService> matchedServices) { | ||
logger.atInfo().log("Starting detection: exposed NodeRED instances"); | ||
DetectionReportList detectionReports = | ||
DetectionReportList.newBuilder() | ||
.addAllDetectionReports( | ||
matchedServices.stream() | ||
.filter(NetworkServiceUtils::isWebService) | ||
.filter(this::isServiceVulnerable) | ||
.map(networkService -> buildDetectionReport(targetInfo, networkService)) | ||
.collect(toImmutableList())) | ||
.build(); | ||
|
||
logger.atInfo().log( | ||
"NodeRedExposedUiDetector finished, detected '%d' vulns.", | ||
detectionReports.getDetectionReportsCount()); | ||
return detectionReports; | ||
} | ||
|
||
/* | ||
* Checks if the settings are accessible. The /settings page will either return a JSON content or | ||
* a permission denied error depending on the configuration for authentication. | ||
* Because /settings can be a pretty common endpoint, we want to ensure that this is a rednode | ||
* instance whilst not really performing JSON parsing hence the pattern matching instead. | ||
*/ | ||
private boolean settingsAreAccessible(NetworkService networkService) { | ||
String targetUri = NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "settings"; | ||
|
||
try { | ||
HttpResponse response = httpClient.send(get(targetUri).withEmptyHeaders().build()); | ||
|
||
return response.status().isSuccess() | ||
&& response | ||
.bodyString() | ||
.map( | ||
body -> | ||
body.contains("\"httpNodeRoot\"") | ||
&& body.contains("\"version\"") | ||
&& body.contains("\"workflow\"")) | ||
.orElse(false); | ||
} catch (IOException e) { | ||
logger.atWarning().withCause(e).log("Unable to query '%s'.", targetUri); | ||
return false; | ||
} | ||
} | ||
|
||
private boolean isNodeRedInstance(NetworkService networkService) { | ||
String targetUri = | ||
NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "/red/tours/welcome.js"; | ||
|
||
try { | ||
HttpResponse response = httpClient.send(get(targetUri).withEmptyHeaders().build()); | ||
|
||
return response.status().isSuccess() | ||
&& response.bodyString().map(body -> body.contains("Welcome to Node-RED")).orElse(false); | ||
} catch (IOException e) { | ||
logger.atWarning().withCause(e).log("Unable to query '%s'.", targetUri); | ||
return false; | ||
} | ||
} | ||
|
||
private boolean isServiceVulnerable(NetworkService networkService) { | ||
return isNodeRedInstance(networkService) && settingsAreAccessible(networkService); | ||
} | ||
|
||
private DetectionReport buildDetectionReport( | ||
TargetInfo scannedTarget, NetworkService vulnerableNetworkService) { | ||
return DetectionReport.newBuilder() | ||
.setTargetInfo(scannedTarget) | ||
.setNetworkService(vulnerableNetworkService) | ||
.setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli())) | ||
.setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) | ||
.setVulnerability( | ||
Vulnerability.newBuilder() | ||
.setMainId( | ||
VulnerabilityId.newBuilder() | ||
.setPublisher("GOOGLE") | ||
.setValue("NODERED_EXPOSED_UI")) | ||
.setSeverity(Severity.CRITICAL) | ||
.setTitle("Exposed NodeRED instance") | ||
.setRecommendation( | ||
"Configure authentication or ensure the NodeRED instance is not exposed to the" | ||
+ " network. See" | ||
+ " https://nodered.org/docs/user-guide/runtime/securing-node-red for" | ||
+ " details") | ||
.setDescription( | ||
"NodeRED instance is exposed and can be used to compromise the system.")) | ||
.build(); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
.../tsunami/plugins/detectors/exposedui/nodered/NodeRedExposedUiDetectorBootstrapModule.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* 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.exposedui.nodered; | ||
|
||
import com.google.tsunami.plugin.PluginBootstrapModule; | ||
|
||
/** A {@link PluginBootstrapModule} for {@link NodeRedExposedUiDetector}. */ | ||
public final class NodeRedExposedUiDetectorBootstrapModule extends PluginBootstrapModule { | ||
|
||
@Override | ||
protected void configurePlugin() { | ||
registerPlugin(NodeRedExposedUiDetector.class); | ||
} | ||
} |
Oops, something went wrong.