diff --git a/community/detectors/joomla_cve_2023_23752/README.md b/community/detectors/joomla_cve_2023_23752/README.md new file mode 100644 index 000000000..0325fa25f --- /dev/null +++ b/community/detectors/joomla_cve_2023_23752/README.md @@ -0,0 +1,24 @@ +# CVE-2023-23752 VulnDetector + +An issue was discovered in Joomla! 4.0.0 through 4.2.7. An improper access check +allows unauthorized access to webservice endpoints. + +- https://nvd.nist.gov/vuln/detail/CVE-2023-23752 +- https://huntr.dev/bounties/a10cb87b-f425-43a7-af6f-1d2d6c896ac7 + +This vulnerability can lead to RCE if you expose your Joomla DB server to +outside, Also if administrator use same password other places, Attackers can +leverage leaked credentials to login in other services. For detailed information +please read following reference. + +- https://vulncheck.com/blog/joomla-for-rce + +## 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/joomla_cve_2023_23752/build.gradle b/community/detectors/joomla_cve_2023_23752/build.gradle new file mode 100644 index 000000000..47cc4951e --- /dev/null +++ b/community/detectors/joomla_cve_2023_23752/build.gradle @@ -0,0 +1,71 @@ +plugins { + id 'java-library' +} + +description = 'Joomla Info Disclosure CVE-2023-23752 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/javase/8/docs/api/' + } + + // 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' + mockitoVersion = '2.28.2' + truthVersion = '1.0.1' + okhttpVersion = '3.12.0' + autoValueVersion = '1.9' +} + +dependencies { + implementation "com.google.tsunami:tsunami-common:${tsunamiVersion}" + implementation "com.google.tsunami:tsunami-plugin:${tsunamiVersion}" + implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}" + compileOnly "com.google.auto.value:auto-value-annotations:${autoValueVersion}" + annotationProcessor "com.google.auto.value:auto-value:${autoValueVersion}" + + testImplementation "junit:junit:${junitVersion}" + testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation "com.google.truth:truth:${truthVersion}" + testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}" + testImplementation "com.google.truth.extensions:truth-java8-extension:${truthVersion}" + testImplementation "com.google.truth.extensions:truth-proto-extension:${truthVersion}" +} diff --git a/community/detectors/joomla_cve_2023_23752/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752DetectorBootstrapModule.java b/community/detectors/joomla_cve_2023_23752/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752DetectorBootstrapModule.java new file mode 100644 index 000000000..cb47f3fb6 --- /dev/null +++ b/community/detectors/joomla_cve_2023_23752/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752DetectorBootstrapModule.java @@ -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.cves.cve202323752; + +import com.google.tsunami.plugin.PluginBootstrapModule; + +/** Guice module that bootstraps the {@link Cve202323752VulnDetector}. */ +public final class Cve202323752DetectorBootstrapModule extends PluginBootstrapModule { + + @Override + protected void configurePlugin() { + registerPlugin(Cve202323752VulnDetector.class); + } +} diff --git a/community/detectors/joomla_cve_2023_23752/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752VulnDetector.java b/community/detectors/joomla_cve_2023_23752/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752VulnDetector.java new file mode 100644 index 000000000..5c08a19fc --- /dev/null +++ b/community/detectors/joomla_cve_2023_23752/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752VulnDetector.java @@ -0,0 +1,170 @@ +/* + * 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.cves.cve202323752; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.net.HttpHeaders.ACCEPT; +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.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +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.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.proto.AdditionalDetail; +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.TextData; +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 java.util.NoSuchElementException; +import javax.inject.Inject; + +/** A {@link VulnDetector} that detects the CVE-2023-23752 vulnerability. */ +@PluginInfo( + type = PluginType.VULN_DETECTION, + name = "Cve202323752VulnDetector", + version = "0.1", + description = + "Joomla CVE-2023-23752: An information disclosure allows to retrieve the database" + + " credentials", + author = "Am0o0", + bootstrapModule = Cve202323752DetectorBootstrapModule.class) +public final class Cve202323752VulnDetector implements VulnDetector { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + private final HttpClient httpClient; + private final Clock utcClock; + private String exposedConfig; + + @Inject + Cve202323752VulnDetector(@UtcClock Clock utcClock, HttpClient httpClient) { + this.httpClient = checkNotNull(httpClient); + this.utcClock = checkNotNull(utcClock); + } + + @Override + public DetectionReportList detect( + TargetInfo targetInfo, ImmutableList matchedServices) { + logger.atInfo().log("CVE-2023-23752 starts detecting."); + + return DetectionReportList.newBuilder() + .addAllDetectionReports( + matchedServices.stream() + .filter(this::isServiceVulnerable) + .map(networkService -> buildDetectionReport(targetInfo, networkService)) + .collect(toImmutableList())) + .build(); + } + + 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( + Vulnerability.newBuilder() + .setMainId( + VulnerabilityId.newBuilder() + .setPublisher("TSUNAMI_COMMUNITY") + .setValue("CVE_2023_23752")) + .setSeverity(Severity.HIGH) + .setTitle("Joomla unauthorized access to webservice endpoints") + .setDescription( + "CVE-2023-23752: An improper access check allows unauthorized access to" + + " webservice endpoints. attacker can get the host address " + + "and username and password of the configured joomla database.") + .setRecommendation("Upgrade Joomla to 4.2.8 and above versions.") + .addAdditionalDetails( + AdditionalDetail.newBuilder() + .setTextData(TextData.newBuilder().setText(exposedConfig)))) + .build(); + } + + private boolean isServiceVulnerable(NetworkService networkService) { + HttpHeaders httpHeaders = + HttpHeaders.builder() + .addHeader( + ACCEPT, + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + .build(); + + String appConfUrl = + NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + + "api/index.php/v1/config/application?public=true"; + try { + HttpResponse appConfHttpResponse = + httpClient.send(get(appConfUrl).setHeaders(httpHeaders).build(), networkService); + + // immediate checks for accelerating the scan + if (appConfHttpResponse.status().code() != HttpStatus.OK.code() + || appConfHttpResponse.bodyJson().isEmpty() + || appConfHttpResponse.bodyString().isEmpty()) { + return false; + } + + // Check if body values match our detection rules + if (!appConfHttpResponse.bodyString().get().contains("password") + || !appConfHttpResponse.bodyString().get().contains("user")) { + return false; + } + + // Check if body is JSON + if (appConfHttpResponse.bodyJson().isEmpty()) { + return false; + } + + JsonObject jsonResponse = appConfHttpResponse.bodyJson().get().getAsJsonObject(); + if (!jsonResponse.has("data")) { + return false; + } + + JsonArray jsonArray = jsonResponse.getAsJsonArray("data"); + for (int i = 0; i < jsonArray.size(); i++) { + if (jsonArray.get(i).getAsJsonObject().has("attributes")) { + exposedConfig = appConfHttpResponse.bodyString().get(); + return true; + } + } + } catch (NoSuchElementException | IllegalStateException | JsonSyntaxException e) { + return false; + } catch (IOException e) { + logger.atWarning().withCause(e).log("Request to target %s failed", networkService); + return false; + } + return false; + } +} diff --git a/community/detectors/joomla_cve_2023_23752/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752VulnDetectorTest.java b/community/detectors/joomla_cve_2023_23752/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752VulnDetectorTest.java new file mode 100644 index 000000000..04a22d51f --- /dev/null +++ b/community/detectors/joomla_cve_2023_23752/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202323752/Cve202323752VulnDetectorTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2022 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.cves.cve202323752; + +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Guice; +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.proto.AdditionalDetail; +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.Software; +import com.google.tsunami.proto.TargetInfo; +import com.google.tsunami.proto.TextData; +import com.google.tsunami.proto.TransportProtocol; +import com.google.tsunami.proto.Vulnerability; +import com.google.tsunami.proto.VulnerabilityId; +import java.io.IOException; +import java.time.Instant; +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; + +@RunWith(JUnit4.class) +public final class Cve202323752VulnDetectorTest { + + private final FakeUtcClock fakeUtcClock = + FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); + + @Inject private Cve202323752VulnDetector detector; + + private final MockWebServer mockWebServer = new MockWebServer(); + + private NetworkService joomlaService; + private static final String LEAKED_DATA_JSON_SAMPLE = + "{\n" + + " \"data\": [\n" + + " {\n" + + " \"type\": \"application\",\n" + + " \"id\": \"224\",\n" + + " \"attributes\": {\n" + + " \"host\": \"google.com\",\n" + + " \"id\": 224\n" + + " }\n" + + " },\n" + + " {\n" + + " \"type\": \"application\",\n" + + " \"id\": \"224\",\n" + + " \"attributes\": {\n" + + " \"user\": \"root\",\n" + + " \"id\": 224\n" + + " }\n" + + " },\n" + + " {\n" + + " \"type\": \"application\",\n" + + " \"id\": \"224\",\n" + + " \"attributes\": {\n" + + " \"password\": \"example\",\n" + + " \"id\": 224\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + private TargetInfo targetInfo; + + @Before + public void setUp() throws IOException { + mockWebServer.start(); + Guice.createInjector( + new FakeUtcClockModule(fakeUtcClock), + new HttpClientModule.Builder().build(), + new Cve202323752DetectorBootstrapModule()) + .injectMembers(this); + + joomlaService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setTransportProtocol(TransportProtocol.TCP) + .setSoftware(Software.newBuilder().setName("joomla 4.2.6-php8.0")) + .setServiceName("http") + .build(); + + targetInfo = + TargetInfo.newBuilder() + .addNetworkEndpoints( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .build(); + } + + @After + public void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + final Dispatcher dispatcher = + new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if (request.getPath().equals("/api/index.php/v1/config/application?public=true")) { + return new MockResponse() + .addHeader("Content-Type", "application/json; charset=utf-8") + .setBody(LEAKED_DATA_JSON_SAMPLE) + .setResponseCode(HttpStatus.OK.code()); + } + return new MockResponse().setResponseCode(404); + } + }; + + @Test + public void detect_whenVulnerable_returnsVulnerability() { + mockWebServer.setDispatcher(dispatcher); + DetectionReportList mockWebServerDetectionReports = + detector.detect(targetInfo, ImmutableList.of(joomlaService)); + + // all we need to check is Detection Status But I think it is very hard to set the + // addAdditionalDetails , so I add the original Report addAdditionalDetails here in + // expected and then check for AdditionalDetails in another assert + DetectionReport expectedDetectionReport = + DetectionReport.newBuilder() + .setTargetInfo(targetInfo) + .setNetworkService(joomlaService) + .setDetectionTimestamp(Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) + .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) + .setVulnerability( + Vulnerability.newBuilder() + .setMainId( + VulnerabilityId.newBuilder() + .setPublisher("TSUNAMI_COMMUNITY") + .setValue("CVE_2023_23752")) + .setSeverity(Severity.HIGH) + .setTitle("Joomla unauthorized access to webservice endpoints") + .setDescription( + "CVE-2023-23752: An improper access check allows unauthorized access to" + + " webservice endpoints. attacker can get the host address " + + "and username and password of the configured joomla database.") + .setRecommendation("Upgrade Joomla to 4.2.8 and above versions.") + .addAdditionalDetails( + AdditionalDetail.newBuilder() + .setTextData(TextData.newBuilder().setText(LEAKED_DATA_JSON_SAMPLE)))) + .build(); + + // Vulnerable to CVE202323752 + assertThat(mockWebServerDetectionReports.getDetectionReportsList()) + .containsExactly(expectedDetectionReport); + } + + @Test + public void detect_whenNotVulnerable_returnsNoVulnerability() { + mockWebServer.url("/notexistpath"); + MockResponse response = + new MockResponse() + .addHeader("Content-Type", "application/json; charset=utf-8") + .setBody("NotExistDetectionString") + .setResponseCode(200); + mockWebServer.enqueue(response); + + DetectionReportList mockWebServerDetectionReports = + detector.detect(targetInfo, ImmutableList.of(joomlaService)); + assertThat(mockWebServerDetectionReports.getDetectionReportsList()).isEmpty(); + } +}