diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c42d7ef --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog +All notable changes to this add-on will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [1.0.0] - 2021-08-05 + - First version of FileUpload Addon. + - Contains scan rule for finding vulnerabilities related to File Upload. + - Types of uploaded files include: + - HTML and its variants + - JSP and its variants + - JPEG and GIF images + - EICAR file + - SVG images \ No newline at end of file diff --git a/README.md b/README.md index 3ceda5c..905f081 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ -# owasp-zap-fileupload-addon -File Upload Scan Rule +# owasp-zap-fileupload-addon +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) ![Java CI with Gradle](https://github.com/SasanLabs/owasp-zap-fileupload-addon/workflows/Java%20CI%20with%20Gradle/badge.svg?branch=master) + +This project contains the File Upload scan rule which is used to find the vulnerabilities in File Upload functionality. + +## Why this addon is needed +File upload is becoming a more and more essential part of any application, where the user is able to upload their photo, their CV, or a video showcasing a project they are working on. The application should be able to fend off bogus and malicious files in a way to keep the application and the users safe. Generally file upload functionality is quite complex to automate and has huge attack surface hence there is a need to automate the process and also secure it. + +## Configuration +File upload functionality generally has 2 endpoints, one from where file is uploaded and one from where file is retrieved. It is necessary to know both these endpoints. While Active Scanning an application, file upload endpoint is already known but retrieval endpoint is not known to the scan rule hence there are configuration details specific to the retrieval endpoint. + +Under ZAP's Options dialog you will find a File Upload section as shown below: +![File Upload Options Panel](./docs/images/fileupload-options-panel.png) + +### Explanation +For finding the URL to retrieve/view the uploaded file, here are some options: +1. In some applications the URL to retrieve the uploaded file is static and doesn't change or only the file name is changed. For handling this type of configuration, options panel has `Static Location Configuration` where static URL is added into `URI Regex` field. `URI Regex` field also supports the dynamic file name by `${fileName}`. +parameter, for e.g. `http:///${fileName}` +2. In some applications the URL to retrieve the uploaded file is present in the file upload request's response. For handling this type of configuration, options panel has `Parse Http Response Configuration` which has 2 parameters `Start Identifier` and `End Identifier`. These identifiers are used to locate the URL within the response. +3. In some applications the URL to retrieve the uploaded file is present in the response of a different URL which is called a preflight request. E.g. Profile picture URL is part of profile page and hence we need to parse the response of the profile page to find the URL of the profile picture. For handling this type of configuration, the options panel has `Dynamic Location Configuration` which has a `URI Regex` and `Parse Http Response Configuration` which has `Start Identifier`, and `End Identifier`. So the File Upload add-on will invoke the URI mentioned in `URI Regex` and then parse the response using `Start Identifier` and `End Identifier`. `URI Regex` field also supports the dynamic file name by `${fileName}` + +### Note: +This addon fires a lot of requests to the target application hence can impact the performance of the targeted application. So please run this addon in non-prod environment only. + +## Contributing guidelines +Contributing guidelines are same as [ZAP](https://github.com/zaproxy/zaproxy). + +## Contact Us +For any Queries/Bugs or Enhancement please raise an issue in this repository or ask in [OWASP ZAP Developer Group](https://groups.google.com/g/zaproxy-develop). +For any other kind of issues please send an email to karan.sasan@owasp.org diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..13a92b6 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,55 @@ +import org.zaproxy.gradle.addon.AddOnPlugin +import org.zaproxy.gradle.addon.AddOnStatus +import org.zaproxy.gradle.addon.misc.ConvertMarkdownToHtml +import org.zaproxy.gradle.addon.misc.CreateGitHubRelease +import org.zaproxy.gradle.addon.misc.ExtractLatestChangesFromChangelog + +plugins { + id("com.diffplug.gradle.spotless") version "3.27.2" + id("com.github.ben-manes.versions") version "0.38.0" + `java-library` + id("org.zaproxy.add-on") version "0.5.0" +} + +repositories { + mavenCentral() + mavenLocal() + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +spotless { + java { + licenseHeaderFile("./gradle/spotless/License.java") + googleJavaFormat().aosp() + } +} + +tasks.compileJava { + dependsOn("spotlessApply") +} + +tasks.withType().configureEach { options.encoding = "utf-8"} + +version = "1.0.0" +description = "Detect File upload requests and scan them to find related vulnerabilities" + +zapAddOn { + addOnName.set("File upload Scan Rule") + addOnStatus.set(AddOnStatus.ALPHA) + zapVersion.set("2.11.0") + manifest { + author.set("KSASAN preetkaran20@gmail.com") + repo.set("https://github.com/SasanLabs/owasp-zap-fileupload-addon/") + } +} + +dependencies { + zap("org.zaproxy:zap:2.11.0-SNAPSHOT") +} diff --git a/docs/images/fileupload-options-panel.png b/docs/images/fileupload-options-panel.png new file mode 100644 index 0000000..758139a Binary files /dev/null and b/docs/images/fileupload-options-panel.png differ diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 0000000..ee0ff81 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.caching=true +org.gradle.parallel=false \ No newline at end of file diff --git a/gradle/spotless/License.java b/gradle/spotless/License.java new file mode 100644 index 0000000..047b094 --- /dev/null +++ b/gradle/spotless/License.java @@ -0,0 +1,15 @@ +/** + * Copyright $YEAR SasanLabs + * + * 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. + **/ \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..87b738c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9d17479 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionSha256Sum=81003f83b0056d20eedf48cddd4f52a9813163d4ba185bcf8abd34b8eeea4cbd +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..af6708f --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## 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"' + +# 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, switch paths to Windows format before running java +if $cygwin ; 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=$((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" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..0f8d593 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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 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" + +@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/settings.gradle.kts b/settings.gradle.kts new file mode 100755 index 0000000..5dbda81 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "fileupload" diff --git a/src/main/java/org/sasanlabs/fileupload/ExtensionFileUpload.java b/src/main/java/org/sasanlabs/fileupload/ExtensionFileUpload.java new file mode 100644 index 0000000..94a2d16 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/ExtensionFileUpload.java @@ -0,0 +1,57 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload; + +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.parosproxy.paros.extension.ExtensionAdaptor; +import org.parosproxy.paros.extension.ExtensionHook; +import org.sasanlabs.fileupload.configuration.FileUploadConfiguration; +import org.sasanlabs.fileupload.i18n.FileUploadI18n; +import org.sasanlabs.fileupload.ui.FileUploadOptionsPanel; + +/** + * @author KSASAN preetkaran20@gmail.com + * @since 1.0.0 + */ +public class ExtensionFileUpload extends ExtensionAdaptor { + + protected static final Logger LOGGER = LogManager.getLogger(ExtensionFileUpload.class); + + @Override + public String getAuthor() { + return "KSASAN preetkaran20@gmail.com"; + } + + @Override + public void hook(ExtensionHook extensionHook) { + FileUploadI18n.init(); + super.hook(extensionHook); + if (hasView()) { + extensionHook.getHookView().addOptionPanel(new FileUploadOptionsPanel()); + } + extensionHook.addOptionsParamSet(FileUploadConfiguration.getInstance()); + LOGGER.debug("FileUpload Extension loaded successfully"); + } + + @Override + public void unload() { + super.unload(); + } + + @Override + public boolean canUnload() { + return true; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/FileUploadScanRule.java b/src/main/java/org/sasanlabs/fileupload/FileUploadScanRule.java new file mode 100644 index 0000000..211182e --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/FileUploadScanRule.java @@ -0,0 +1,187 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload; + +import java.io.IOException; +import java.util.List; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.parosproxy.paros.core.scanner.AbstractAppParamPlugin; +import org.parosproxy.paros.core.scanner.Category; +import org.parosproxy.paros.core.scanner.NameValuePair; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.attacks.FileUploadAttackExecutor; +import org.sasanlabs.fileupload.i18n.FileUploadI18n; +import org.zaproxy.zap.core.scanner.InputVector; +import org.zaproxy.zap.core.scanner.InputVectorBuilder; + +/** + * {@code FileUploadScanRule} is used to find the vulnerabilities in File Upload functionality of + * applications. The scan rule uploads multiple types of files containing vulnerable code to check + * if the application is vulnerable. + * + *

This addon fires a lot of requests to the target application hence can impact the performance + * of the targeted application. So please run this addon in non-prod environment only. + * + * @author KSASAN preetkaran20@gmail.com + * @since 1.0.0 + */ +public class FileUploadScanRule extends AbstractAppParamPlugin { + + private static final int PLUGIN_ID = 40041; + private static final String NAME = FileUploadI18n.getMessage("fileupload.scanrule.name"); + private static final String DESCRIPTION = + FileUploadI18n.getMessage("fileupload.scanrule.description"); + private static final String SOLUTION = FileUploadI18n.getMessage("fileupload.scanrule.soln"); + private static final String REFERENCE = FileUploadI18n.getMessage("fileupload.scanrule.refs"); + private static final Logger LOGGER = LogManager.getLogger(FileUploadScanRule.class); + + private int maxRequestCount; + + @Override + public void init() { + switch (this.getAttackStrength()) { + case LOW: + maxRequestCount = 30; + break; + case MEDIUM: + maxRequestCount = 60; + break; + case HIGH: + maxRequestCount = 90; + break; + case INSANE: + maxRequestCount = 150; + break; + default: + maxRequestCount = 60; + break; + } + } + + @Override + public boolean isStop() { + return super.isStop() || (this.maxRequestCount <= 0); + } + + public void decreaseRequestCount() { + this.maxRequestCount--; + } + + @Override + protected void scan(List nameValuePairs) { + try { + boolean isMultipart = false; + String originalFileName = null, originalContentType = null; + if (nameValuePairs != null) { + for (NameValuePair nameValuePair : nameValuePairs) { + if (nameValuePair.getType() == NameValuePair.TYPE_MULTIPART_DATA_FILE_NAME) { + originalFileName = nameValuePair.getValue(); + isMultipart = true; + } else if (nameValuePair.getType() + == NameValuePair.TYPE_MULTIPART_DATA_FILE_CONTENTTYPE) { + originalContentType = nameValuePair.getValue(); + isMultipart = true; + } else if (nameValuePair.getType() + == NameValuePair.TYPE_MULTIPART_DATA_FILE_PARAM) { + isMultipart = true; + } + } + } + if (isMultipart) { + FileUploadAttackExecutor fileUploadAttackExecutor = + new FileUploadAttackExecutor( + this, nameValuePairs, originalFileName, originalContentType); + fileUploadAttackExecutor.executeAttack(); + } + } catch (Exception ex) { + LOGGER.error("Error occurred while scanning", ex); + } + } + + @Override + public InputVectorBuilder getBuilder() { + return super.getBuilder(); + } + + @Override + public void setParameters(HttpMessage message, List inputVectors) { + super.setParameters(message, inputVectors); + } + + public void raiseAlert( + int risk, + int confidence, + String name, + String description, + String uri, + String param, + String attack, + String otherInfo, + String solution, + HttpMessage msg) { + newAlert() + .setRisk(risk) + .setConfidence(confidence) + .setName(name) + .setDescription(description) + .setUri(uri) + .setParam(param) + .setAttack(attack) + .setOtherInfo(otherInfo) + .setSolution(solution) + .setMessage(msg) + .raise(); + } + + @Override + public void sendAndReceive(HttpMessage msg) throws IOException { + super.sendAndReceive(msg); + } + + @Override + public HttpMessage getBaseMsg() { + return super.getBaseMsg(); + } + + @Override + public int getId() { + return PLUGIN_ID; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public String getSolution() { + return SOLUTION; + } + + @Override + public String getReference() { + return REFERENCE; + } + + @Override + public int getCategory() { + return Category.MISC; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/FileUploadUtils.java b/src/main/java/org/sasanlabs/fileupload/FileUploadUtils.java new file mode 100644 index 0000000..5fcd32d --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/FileUploadUtils.java @@ -0,0 +1,213 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; +import org.parosproxy.paros.network.HttpHeader; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.attacks.model.FileExtensionOperation; +import org.sasanlabs.fileupload.attacks.model.FileInformationProvider; +import org.sasanlabs.fileupload.attacks.model.FileInformationProviderBuilder; + +/** + * Contains the String constants or other utility functions used by the Addon. + * + * @author preetkaran20@gmail.com KSASAN + * @since 1.0.0 + */ +public interface FileUploadUtils { + String EMPTY_STRING = ""; + String PERIOD = "."; + String SLASH = "/"; + String NULL_BYTE_CHARACTER = String.valueOf((char) 0); + String HTTP_SCHEME = "http://"; + String HTTP_SECURED_SCHEME = "https://"; + String HTML_MIME_TYPE = "text/html"; + String XHTML_MIME_TYPE = "application/xhtml+xml"; + String SVG_MIME_TYPE = "image/svg+xml"; + String GET_HTTP_METHOD = "GET"; + String JSP_FILE_EXTENSION = "jsp"; + String JSPX_FILE_EXTENSION = "jspx"; + + /** + * Appends the Period Character to the provided String. + * + * @param extension, extension of the file + * @return appends {@link #PERIOD} to the provided extension if not {@code null} or empty else + * returns same provided extension. + */ + static String prefixExtensionWithPeriodCharacter(String extension) { + if (StringUtils.isBlank(extension) || extension.startsWith(FileUploadUtils.PERIOD)) { + return extension; + } else { + return FileUploadUtils.PERIOD + extension; + } + } + + /** + * returns the extension of the provided fileName. + * + * @param fileName, name of the file + * @return extension of the provided fileName if not null else throws {@code + * NullPointerException} + */ + static String getExtension(String fileName) { + Objects.requireNonNull(fileName, "FileName cannot be null"); + int firstIndexOfPeriodCharacter = fileName.indexOf(FileUploadUtils.PERIOD); + if (firstIndexOfPeriodCharacter >= 0) { + return fileName.substring(firstIndexOfPeriodCharacter + 1); + } + return null; + } + + /** + * Checks whether {@code Content-Disposition} header is inline and if so returns {@code True} + * else {@code False} + * + *

This utility is useful to find if XSS is possible or not because if {@code + * Content-Disposition} header is inline then only XSS is possible. + * + * @param httpMsg, HttpMessage representing request and response + * @return {@code True} if {@code Content-Disposition} header is inline + */ + static boolean isContentDispositionInline(HttpMessage httpMsg) { + String headerValue = httpMsg.getResponseHeader().getHeader("Content-Disposition"); + if (headerValue == null + || headerValue.trim().equals(FileUploadUtils.EMPTY_STRING) + || headerValue.equals("inline")) { + return true; + } + return false; + } + + /** + * Utility to check if the {@code HttpHeader#CONTENT_TYPE} header is present in the {@code + * HttpMessage} + * + * @param httpMsg, HttpMessage representing request and response + * @return {@code True} if {@code HttpHeader#CONTENT_TYPE} header is present in httpMsg else + * {@code False} + */ + static boolean isContentTypeHeaderPresent(HttpMessage httpMsg) { + String headerValue = httpMsg.getResponseHeader().getHeader(HttpHeader.CONTENT_TYPE); + return StringUtils.isNotBlank(headerValue); + } + + /** + * Documents with active scripts can be executed by browser based on the {@code + * HttpHeader#CONTENT_TYPE} header. + * + *

References {@link + * https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#RULE_.233.1_-_HTML_escape_JSON_values_in_an_HTML_context_and_read_the_data_with_JSON.parse:#:~:text=Good%20HTTP%20response:} + * and {@link + * https://security.stackexchange.com/questions/169427/impact-of-the-response-content-type-on-the-exploitability-of-xss} + * + * @param httpMsg, HttpMessage representing request and response + * @return {@code True} if content type is one of {@code FileUploadUtils#HTML_MIME_TYPE} or + * {@code FileUploadUtils#XHTML_MIME_TYPE} or {@code FileUploadUtils#SVG_MIME_TYPE} + */ + static boolean isContentTypeCausesJavascriptExecution(HttpMessage httpMsg) { + String headerValue = httpMsg.getResponseHeader().getHeader(HttpHeader.CONTENT_TYPE); + return StringUtils.isNotBlank(headerValue) + && (headerValue.equalsIgnoreCase(HTML_MIME_TYPE) + || headerValue.equalsIgnoreCase(XHTML_MIME_TYPE) + || headerValue.equalsIgnoreCase(SVG_MIME_TYPE)); + } + + /** + * Provides extended list of FileInformationProvider for JSP. + * + * @param baseFileName, base file name of uploaded jsp file. + * @return list of FileInformationProvider for JSP + */ + static List getFileInformationProvidersExtendedJsp( + String baseFileName) { + return Arrays.asList( + new FileInformationProviderBuilder(baseFileName) + .withExtension("Jsp") + .withFileExtensionOperation(FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension("JSP") + .withFileExtensionOperation(FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension("Jsp") + .withContentType("application/x-jsp") + .withFileExtensionOperation(FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension("JSP") + .withContentType("application/x-jsp") + .withFileExtensionOperation(FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build()); + } + + /** + * Provides default list of FileInformationProvider for JSP. + * + * @param baseFileName, base file name of uploaded jsp file. + * @param extension, extension of the uploaded jsp file. + * @return list of FileInformationProvider for JSP + */ + static List getFileInformationProvidersDefaultJsp( + String baseFileName, String extension) { + return Arrays.asList( + new FileInformationProviderBuilder(baseFileName) + .withExtension(extension) + .withFileExtensionOperation(FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension(extension) + .withContentType("application/x-jsp") + .withFileExtensionOperation(FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension(extension) + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension(extension) + .withContentType("application/x-jsp") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension(extension + NULL_BYTE_CHARACTER) + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension(extension + NULL_BYTE_CHARACTER) + .withContentType("application/x-jsp") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension(extension + "%00") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(baseFileName) + .withExtension(extension + "%00") + .withContentType("application/x-jsp") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build()); + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/AttackVector.java b/src/main/java/org/sasanlabs/fileupload/attacks/AttackVector.java new file mode 100644 index 0000000..f505a02 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/AttackVector.java @@ -0,0 +1,271 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.List; +import java.util.Objects; +import org.apache.commons.httpclient.URI; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.parosproxy.paros.core.scanner.Alert; +import org.parosproxy.paros.core.scanner.NameValuePair; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.FileUploadScanRule; +import org.sasanlabs.fileupload.attacks.antivirus.EicarAntivirusTestFileUpload; +import org.sasanlabs.fileupload.attacks.model.FileInformationProvider; +import org.sasanlabs.fileupload.attacks.model.VulnerabilityType; +import org.sasanlabs.fileupload.exception.FileUploadException; +import org.sasanlabs.fileupload.function.ConsumerWithException; +import org.sasanlabs.fileupload.i18n.FileUploadI18n; +import org.sasanlabs.fileupload.locator.URILocatorImpl; +import org.sasanlabs.fileupload.matcher.ContentMatcher; +import org.zaproxy.zap.core.scanner.InputVector.PayloadFormat; +import org.zaproxy.zap.core.scanner.InputVectorBuilder; + +/** + * {@code AttackVector} is an abstract template class for file upload attacks. This class also + * contains few utility methods for raising alerts and firing Http requests. + * + * @author KSASAN preetkaran20@gmail.com + */ +public abstract class AttackVector { + + private static final Logger LOGGER = LogManager.getLogger(EicarAntivirusTestFileUpload.class); + + /** + * In general, for file upload functionalities, file is uploaded from one endpoint and retrieved + * from another endpoint. This method finds the url of the file retrieval endpoint, invokes that + * endpoint to retrieve the uploaded file and returns the {@code HttpMessage}. + * + * @param httpMsg, HttpMessage containing uploaded file's request and response + * @param fileName, uploaded file's name + * @param sendAndRecieveHttpMsg, consumer to send and Receive {@code HttpMessage} + * @return {@code HttpMessage}, representing the file retrieval request and response. It returns + * {@code null} if unable to find the uploaded file. + * @throws FileUploadException, in case of failure in retrieval of uploaded file. + */ + private HttpMessage getUploadedFileHttpMessage( + HttpMessage httpMsg, + String fileName, + ConsumerWithException sendAndRecieveHttpMsg) + throws FileUploadException { + HttpMessage uploadedFileRetrievalMsg = new HttpMessage(); + URI uri; + try { + uri = + new URILocatorImpl() + .get( + httpMsg, + fileName, + (httpmessage) -> sendAndRecieveHttpMsg.accept(httpmessage)); + + if (Objects.isNull(uri)) { + return null; + } + + uploadedFileRetrievalMsg.getRequestHeader().setURI(uri); + uploadedFileRetrievalMsg.getRequestHeader().setMethod("GET"); + uploadedFileRetrievalMsg + .getRequestHeader() + .setCookies(httpMsg.getRequestHeader().getHttpCookies()); + uploadedFileRetrievalMsg.setRequestBody(""); + sendAndRecieveHttpMsg.accept(uploadedFileRetrievalMsg); + } catch (IOException e) { + throw new FileUploadException( + "Following exception occurred while retrieving uploaded file ", e); + } + return uploadedFileRetrievalMsg; + } + + /** + * This method is used to raise the alert with the provided details. + * + * @param fileUploadScanRule, File Upload scan rule + * @param vulnerabilityType, type of the vulnerability exposed by the {@code modifiedMsg} + * @param modifiedMsg, {@code HttpMessage} representing the uploaded file's request and + * response. + * @param uploadedFileRetrievalMsg, {@code HttpMessage} representing the file retrieval's + * request and response. + */ + private void raiseAlert( + FileUploadScanRule fileUploadScanRule, + VulnerabilityType vulnerabilityType, + HttpMessage modifiedMsg, + HttpMessage uploadedFileRetrievalMsg) { + fileUploadScanRule.raiseAlert( + vulnerabilityType.getAlertLevel(), + Alert.CONFIDENCE_MEDIUM, + FileUploadI18n.getMessage(vulnerabilityType.getMessageKey() + ".name"), + FileUploadI18n.getMessage(vulnerabilityType.getMessageKey() + ".desc"), + uploadedFileRetrievalMsg.getRequestHeader().getURI().toString(), + modifiedMsg.getRequestHeader().toString() + modifiedMsg.getRequestBody().toString(), + MessageFormat.format( + FileUploadI18n.getMessage("fileupload.alert.attack"), + uploadedFileRetrievalMsg.getRequestHeader().toString() + + uploadedFileRetrievalMsg.getRequestBody().toString(), + uploadedFileRetrievalMsg.getResponseHeader().toString() + + uploadedFileRetrievalMsg.getResponseBody().toString()), + FileUploadI18n.getMessage(vulnerabilityType.getMessageKey() + ".refs"), + FileUploadI18n.getMessage(vulnerabilityType.getMessageKey() + ".soln"), + modifiedMsg); + } + + /** + * Utility method to upload the provided file. It modifies the {@code HttpMessage} based on the + * provided {@code fileInformationProvider}, {@code payload} and then sends the HttpMessage. + * + * @param fileUploadAttackExecutor, holds original {@code HttpMessage}, {@code NameValuePair} + * and {@code FileUploadScanRule} + * @param payload, the content of the file which needs to be uploaded + * @param fileInformationProvider, the modifications provider for a file. + * @return {@code HttpMessage}, representing the file retrieval request and response. It returns + * {@code null} if unable to find the uploaded file. + * @throws FileUploadException, in case of any failure while uploadingfile. + */ + private HttpMessage uploadFile( + FileUploadAttackExecutor fileUploadAttackExecutor, + String payload, + FileInformationProvider fileInformationProvider) + throws FileUploadException { + List nameValuePairs = fileUploadAttackExecutor.getNameValuePairs(); + HttpMessage originalMsg = fileUploadAttackExecutor.getOriginalHttpMessage(); + FileUploadScanRule fileUploadScanRule = fileUploadAttackExecutor.getFileUploadScanRule(); + HttpMessage uploadFileMsg = originalMsg.cloneRequest(); + InputVectorBuilder inputVectorBuilder = + fileUploadAttackExecutor.getFileUploadScanRule().getBuilder(); + for (NameValuePair nameValuePair : nameValuePairs) { + if (nameValuePair.getType() == NameValuePair.TYPE_MULTIPART_DATA_FILE_NAME) { + inputVectorBuilder.setValue( + nameValuePair, + fileInformationProvider.getFileName( + fileUploadAttackExecutor.getOriginalFileName()), + PayloadFormat.ALREADY_ESCAPED); + } else if (nameValuePair.getType() == NameValuePair.TYPE_MULTIPART_DATA_FILE_PARAM) { + inputVectorBuilder.setValue(nameValuePair, payload, PayloadFormat.ALREADY_ESCAPED); + } else if (nameValuePair.getType() + == NameValuePair.TYPE_MULTIPART_DATA_FILE_CONTENTTYPE) { + inputVectorBuilder.setValue( + nameValuePair, + fileInformationProvider.getContentType( + fileUploadAttackExecutor.getOriginalContentType()), + PayloadFormat.ALREADY_ESCAPED); + } + } + fileUploadScanRule.setParameters(uploadFileMsg, inputVectorBuilder.build()); + try { + fileUploadScanRule.sendAndReceive(uploadFileMsg); + } catch (IOException ex) { + throw new FileUploadException("Exception occurred while sending modified message", ex); + } + return uploadFileMsg; + } + + /** + * Generic Attack Executor utility method is used to execute attack by uploading a file, finding + * uploaded file and then raising alerts in case attack is successful. + * + * @param fileUploadAttackExecutor, holds original {@code HttpMessage}, {@code NameValuePair} + * and {@code FileUploadScanRule} + * @param payload, content of the file which will be uploaded + * @param fileInformationProvider, provides information about modifications to the file. + * @param contentMatcher, for matching the uploaded file's content with expected file content. + * @param vulnerabilityType, type of the vulnerability in case attack is successful + * @return {@code True} if attack is successful, else {@code False} + */ + private boolean genericAttackExecutor( + FileUploadAttackExecutor fileUploadAttackExecutor, + String payload, + FileInformationProvider fileInformationProvider, + ContentMatcher contentMatcher, + VulnerabilityType vulnerabilityType) { + try { + HttpMessage uploadFileMsg = + this.uploadFile(fileUploadAttackExecutor, payload, fileInformationProvider); + HttpMessage retrieveUploadedFile = + this.getUploadedFileHttpMessage( + uploadFileMsg, + fileUploadAttackExecutor.getOriginalFileName(), + fileUploadAttackExecutor.getFileUploadScanRule()::sendAndReceive); + if (Objects.nonNull(retrieveUploadedFile) + && contentMatcher.match(retrieveUploadedFile)) { + raiseAlert( + fileUploadAttackExecutor.getFileUploadScanRule(), + vulnerabilityType, + uploadFileMsg, + retrieveUploadedFile); + return true; + } + } catch (FileUploadException e) { + LOGGER.debug("Following exception occurred: ", e); + } + return false; + } + + /** + * Generic Attack Executor utility method is used to execute attack by uploading files, finding + * uploaded files and then raising alerts in case attack is successful. + * + * @param fileUploadAttackExecutor, holds original {@code HttpMessage}, {@code NameValuePair} + * and {@code FileUploadScanRule} + * @param payload, content of the file which will be uploaded + * @param fileInformationProviders, provides list of file property modification details. + * @param contentMatcher, for matching the uploaded file's content with expected file content. + * @param vulnerabilityType, type of the vulnerability in case attack is successful + * @return {@code True} if attack is successful, else {@code False} + */ + protected boolean genericAttackExecutor( + FileUploadAttackExecutor fileUploadAttackExecutor, + String payload, + List fileInformationProviders, + ContentMatcher contentMatcher, + VulnerabilityType vulnerabilityType) { + for (FileInformationProvider fileInformationProvider : fileInformationProviders) { + if (fileUploadAttackExecutor.getFileUploadScanRule().isStop()) { + return false; + } + fileUploadAttackExecutor.getFileUploadScanRule().decreaseRequestCount(); + if (this.genericAttackExecutor( + fileUploadAttackExecutor, + payload, + fileInformationProvider, + contentMatcher, + vulnerabilityType)) { + return true; + } + } + return false; + } + + /** + * TODO: As we are only handling Multipart requests hence in case User interface of Application + * is using Javascript filereader Api or using some other ways our scan rule will not work. + * + *

Upload Scanner addon of Burp has handled this by asking the sample request {@link + * https://github.com/portswigger/upload-scanner#flexiinjector---detecting-requests-with-uploads} + */ + + /** + * Executes the attack and checks if it is successful or not and then raise alert in case of + * successful execution. + * + * @param fileUploadAttackExecutor, holds original {@code HttpMessage}, {@code NameValuePair} + * and {@code FileUploadScanRule} + * @return {@code true} if attack is successful else {@code false} + * @throws FileUploadException, in case of any failure while executing attack + */ + public abstract boolean execute(FileUploadAttackExecutor fileUploadAttackExecutor) + throws FileUploadException; +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/FileUploadAttackExecutor.java b/src/main/java/org/sasanlabs/fileupload/attacks/FileUploadAttackExecutor.java new file mode 100644 index 0000000..645717c --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/FileUploadAttackExecutor.java @@ -0,0 +1,98 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.parosproxy.paros.core.scanner.NameValuePair; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.FileUploadScanRule; +import org.sasanlabs.fileupload.attacks.antivirus.EicarAntivirusTestFileUpload; +import org.sasanlabs.fileupload.attacks.rce.jsp.ImageWithJSPSnippetFileUpload; +import org.sasanlabs.fileupload.attacks.rce.jsp.SimpleJSPFileUpload; +import org.sasanlabs.fileupload.attacks.rce.jsp.SimpleJSPXFileUpload; +import org.sasanlabs.fileupload.attacks.xss.HtmlFileUpload; +import org.sasanlabs.fileupload.attacks.xss.SVGFileUpload; +import org.sasanlabs.fileupload.exception.FileUploadException; + +/** + * {@code FileUploadAttackExecutor} class is used to find File Upload vulnerability by executing + * list of {@code AttackVector}. + * + * @author KSASAN preetkaran20@gmail.com + */ +public class FileUploadAttackExecutor { + + private HttpMessage originalHttpMessage; + private FileUploadScanRule fileUploadScanRule; + private List nameValuePairs = new ArrayList<>(); + private String originalFileName; + private String originalContentType; + private List attackVectors = + Arrays.asList( + new HtmlFileUpload(), + new SVGFileUpload(), + new SimpleJSPFileUpload(), + new SimpleJSPXFileUpload(), + new ImageWithJSPSnippetFileUpload(), + new EicarAntivirusTestFileUpload()); + + public FileUploadAttackExecutor( + FileUploadScanRule fileUploadScanRule, + List nameValuePairs, + String originalFileName, + String originalContentType) { + super(); + this.originalHttpMessage = fileUploadScanRule.getBaseMsg(); + this.fileUploadScanRule = fileUploadScanRule; + this.nameValuePairs.addAll(nameValuePairs); + this.originalFileName = originalFileName; + this.originalContentType = originalContentType; + } + + public boolean executeAttack() throws FileUploadException { + for (AttackVector attackVector : attackVectors) { + if (this.fileUploadScanRule.isStop()) { + return false; + } else { + if (attackVector.execute(this)) { + return true; + } + } + } + return false; + } + + public HttpMessage getOriginalHttpMessage() { + return originalHttpMessage.cloneAll(); + } + + public FileUploadScanRule getFileUploadScanRule() { + return fileUploadScanRule; + } + + public List getNameValuePairs() { + return Collections.unmodifiableList(nameValuePairs); + } + + public String getOriginalFileName() { + return originalFileName; + } + + public String getOriginalContentType() { + return originalContentType; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/antivirus/EicarAntivirusTestFileUpload.java b/src/main/java/org/sasanlabs/fileupload/attacks/antivirus/EicarAntivirusTestFileUpload.java new file mode 100644 index 0000000..b7f012d --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/antivirus/EicarAntivirusTestFileUpload.java @@ -0,0 +1,120 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.antivirus; + +import static org.sasanlabs.fileupload.FileUploadUtils.NULL_BYTE_CHARACTER; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import org.sasanlabs.fileupload.attacks.AttackVector; +import org.sasanlabs.fileupload.attacks.FileUploadAttackExecutor; +import org.sasanlabs.fileupload.attacks.model.FileExtensionOperation; +import org.sasanlabs.fileupload.attacks.model.FileInformationProvider; +import org.sasanlabs.fileupload.attacks.model.FileInformationProviderBuilder; +import org.sasanlabs.fileupload.attacks.model.VulnerabilityType; +import org.sasanlabs.fileupload.matcher.ContentMatcher; +import org.sasanlabs.fileupload.matcher.impl.MD5HashResponseMatcher; + +/** + * {@code EicarAntivirusTestFileUpload} attack vector is used to check if antivirus is present and + * working properly by uploading the Eicar test file. General idea is to upload the Eicar Test file + * and if we are able to download it again then that means there are chances that Antivirus is + * either not present or not working properly. + * + *

For more information about Eicar file please visit Eicar File Wiki link + * + *

Tested Eicar test file on {@link https://www.virustotal.com/} 63 out of 67 AV softwares detect + * it as a virus. + * + * @author KSASAN preetkaran20@gmail.com + */ +public class EicarAntivirusTestFileUpload extends AttackVector { + private static final String EICAR_FILE_CONTENT = + new String( + Base64.getDecoder() + .decode( + "WDVPIVAlQEFQWzRcUFpYNTQ" + + "oUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVEl" + + "WSVJVUy1URVNULUZJTEUhJEgrSCo="), + StandardCharsets.UTF_8); + private static final String UPLOADED_BASE_FILE_NAME = "EicarAntivirusTestFileUpload_"; + + private static final ContentMatcher CONTENT_MATCHER = + new MD5HashResponseMatcher(EICAR_FILE_CONTENT); + + private static final List FILE_PARAMETERS_DEFAULT = + Arrays.asList( + /** + * Tested that the file content only matters in case of Eicar file and any + * extension with the same content will be flagged as a Virus file. Tested this + * hypothesis with {@link https://github.com/malice-plugins/mcafee} as well as + * on {@link https://www.virustotal.com/} Out of 67 antivirus softwares 63 + * detect the Eicar file as virus file. + */ + originalFileName -> originalFileName, + + // Below file parameters might not be needed but just for a safe side added + // those. + new FileInformationProviderBuilder(UPLOADED_BASE_FILE_NAME) + .withExtension("com") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(UPLOADED_BASE_FILE_NAME) + .withExtension("exe") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(UPLOADED_BASE_FILE_NAME) + .withExtension("com") + .withContentType("application/octet-stream") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(UPLOADED_BASE_FILE_NAME) + .withExtension("exe") + .withContentType("vnd.microsoft.portable-executable") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(UPLOADED_BASE_FILE_NAME) + .withExtension("com") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(UPLOADED_BASE_FILE_NAME) + .withExtension("com" + NULL_BYTE_CHARACTER) + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(UPLOADED_BASE_FILE_NAME) + .withExtension("com" + NULL_BYTE_CHARACTER) + .withContentType("application/octet-stream") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build()); + + @Override + public boolean execute(FileUploadAttackExecutor fileUploadAttackExecutor) { + return this.genericAttackExecutor( + fileUploadAttackExecutor, + EICAR_FILE_CONTENT, + FILE_PARAMETERS_DEFAULT, + CONTENT_MATCHER, + VulnerabilityType.EICAR_FILE); + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/model/FileExtensionOperation.java b/src/main/java/org/sasanlabs/fileupload/attacks/model/FileExtensionOperation.java new file mode 100644 index 0000000..c2b857b --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/model/FileExtensionOperation.java @@ -0,0 +1,96 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.model; + +import org.apache.commons.lang3.StringUtils; +import org.sasanlabs.fileupload.FileUploadUtils; +import org.sasanlabs.fileupload.exception.FileUploadException; + +/** + * {@code FileExtensionOperation} is used to denote the operation on the file name extensions. + * + * @author preetkaran20@gmail.com KSASAN + */ +public enum FileExtensionOperation { + + /** don't append any extension to the filename. */ + NO_EXTENSION, + + /** + * prefixes the Original File Extension before the provided extension. e.g. if provided + * extension is {@code html} and original file extension is {@code pdf} then the final extension + * will be {@code pdf.html} + */ + PREFIX_ORIGINAL_EXTENSION, + + /** + * appends the Original File Extension after the provided extension e.g. if provided extension + * is {@code html%00} and original file extension is {@code pdf} then the final extension will + * be {@code html%00.pdf} + */ + SUFFIX_ORIGINAL_EXTENSION, + + /** + * only appends the provided extension e.g. if provided extension is {@code html} and original + * file extension is {@code pdf} then final extension will be {@code .html} + */ + ONLY_PROVIDED_EXTENSION, + + /** + * don't change the extension of original file and use the original extension. e.g. if provided + * extension is {@code html} and original file extension is {@code pdf} then final extension + * will be {@code .pdf} + */ + ONLY_ORIGINAL_EXTENSION; + + public String operate(String providedExtension, String originalFileName) + throws FileUploadException { + if (StringUtils.isBlank(providedExtension) + && !(this.equals(ONLY_ORIGINAL_EXTENSION) || this.equals(NO_EXTENSION))) { + throw new FileUploadException( + "Provided extension cannot be null for FileExtensionOperation: " + this.name()); + } + String extension; + String originalExtension = ""; + if (originalFileName != null) { + originalExtension = FileUploadUtils.getExtension(originalFileName); + } + switch (this) { + case PREFIX_ORIGINAL_EXTENSION: + extension = + FileUploadUtils.prefixExtensionWithPeriodCharacter( + originalExtension + + FileUploadUtils.prefixExtensionWithPeriodCharacter( + providedExtension)); + break; + case SUFFIX_ORIGINAL_EXTENSION: + extension = + FileUploadUtils.prefixExtensionWithPeriodCharacter( + providedExtension + + FileUploadUtils.prefixExtensionWithPeriodCharacter( + originalExtension)); + break; + case ONLY_PROVIDED_EXTENSION: + extension = FileUploadUtils.prefixExtensionWithPeriodCharacter(providedExtension); + break; + case ONLY_ORIGINAL_EXTENSION: + extension = FileUploadUtils.prefixExtensionWithPeriodCharacter(originalExtension); + case NO_EXTENSION: + extension = ""; + default: + extension = FileUploadUtils.prefixExtensionWithPeriodCharacter(providedExtension); + } + return extension; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProvider.java b/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProvider.java new file mode 100644 index 0000000..8f61974 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProvider.java @@ -0,0 +1,44 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.model; + +import org.sasanlabs.fileupload.exception.FileUploadException; + +/** + * {@code FileInformationProvider} interface is used to represent the new file properties. + * + * @author KSASAN preetkaran20@gmail.com + */ +@FunctionalInterface +public interface FileInformationProvider { + + /** + * Represents the content type of the file. + * + * @param originalContentType + * @return content type + */ + default String getContentType(String originalContentType) { + return originalContentType; + }; + + /** + * Represents the file name. + * + * @param originalFileName + * @return file name + * @throws FileUploadException + */ + String getFileName(String originalFileName) throws FileUploadException; +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProviderBuilder.java b/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProviderBuilder.java new file mode 100644 index 0000000..f1b270f --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProviderBuilder.java @@ -0,0 +1,64 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.model; + +import java.util.Objects; + +/** + * {@code FileInformationProviderBuilder} is used to build the complex {@code + * FileInformationProvider} object. + * + * @author KSASAN preetkaran20@gmail.com + */ +public class FileInformationProviderBuilder { + private FileInformationProviderImpl fileInformationProviderImpl; + + public FileInformationProviderBuilder(String baseFileName) { + Objects.requireNonNull(baseFileName, "FileName cannot be null"); + fileInformationProviderImpl = new FileInformationProviderImpl(baseFileName); + } + + public FileInformationProviderBuilder withFileExtensionOperation( + FileExtensionOperation fileExtensionOperation) { + fileInformationProviderImpl.setFileExtensionOperation(fileExtensionOperation); + return this; + } + + public FileInformationProviderBuilder withExtension(String providedExtension) { + fileInformationProviderImpl.setExtension(providedExtension); + return this; + } + + public FileInformationProviderBuilder withContentType(String contentType) { + fileInformationProviderImpl.setContentType(contentType); + return this; + } + + public FileInformationProvider build() { + if ((fileInformationProviderImpl + .getFileExtensionOperation() + .equals(FileExtensionOperation.NO_EXTENSION) + || fileInformationProviderImpl + .getFileExtensionOperation() + .equals(FileExtensionOperation.ONLY_ORIGINAL_EXTENSION)) + && fileInformationProviderImpl.getExtension() != null) { + throw new RuntimeException( + "Invalid combination, For FileExtensionOperation: " + + this.fileInformationProviderImpl.getFileExtensionOperation() + + ", ProvidedExtension should be null"); + } + + return fileInformationProviderImpl; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProviderImpl.java b/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProviderImpl.java new file mode 100644 index 0000000..0c3d97a --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/model/FileInformationProviderImpl.java @@ -0,0 +1,65 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.model; + +import java.util.Date; +import java.util.Random; +import org.sasanlabs.fileupload.exception.FileUploadException; + +/** @author KSASAN preetkaran20@gmail.com */ +class FileInformationProviderImpl implements FileInformationProvider { + + private String baseFileName; + private String extension; + private String contentType; + private FileExtensionOperation fileExtensionOperation = FileExtensionOperation.NO_EXTENSION; + + FileInformationProviderImpl(String baseFileName) { + this.baseFileName = baseFileName + (new Random(new Date().getTime()).nextLong()); + } + + void setExtension(String extension) { + this.extension = extension; + } + + void setContentType(String contentType) { + this.contentType = contentType; + } + + void setFileExtensionOperation(FileExtensionOperation fileExtensionOperation) { + this.fileExtensionOperation = fileExtensionOperation; + } + + String getExtension() { + return extension; + } + + FileExtensionOperation getFileExtensionOperation() { + return fileExtensionOperation; + } + + @Override + public String getContentType(String originalContentType) { + return contentType == null ? originalContentType : this.contentType; + } + + @Override + public String getFileName(String originalFileName) throws FileUploadException { + if (originalFileName == null) { + throw new FileUploadException("Provided original File Name is null"); + } + + return this.baseFileName + fileExtensionOperation.operate(this.extension, originalFileName); + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/model/VulnerabilityType.java b/src/main/java/org/sasanlabs/fileupload/attacks/model/VulnerabilityType.java new file mode 100644 index 0000000..f8440cd --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/model/VulnerabilityType.java @@ -0,0 +1,51 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.model; + +import org.parosproxy.paros.core.scanner.Alert; + +/** + * {@code VulnerabilityType} enum represents the types of vulnerabilities exploited by the addon. + * This enum also contains the Alert Level and the Message Key. + * + * @author preetkaran20@gmail.com KSASAN + */ +public enum VulnerabilityType { + XSS_HTML_FILE("xssHtmlFile", Alert.RISK_MEDIUM), + XSS_SVG_FILE("xssSvgFile", Alert.RISK_MEDIUM), + RCE_PHP_FILE("rcePhpFile", Alert.RISK_HIGH), + RCE_JSP_FILE("rceJspFile", Alert.RISK_HIGH), + RCE_GIF_JSP_FILE("rceGifJspFile", Alert.RISK_HIGH), + RCE_JPEG_JSP_FILE("rceJpegJspFile", Alert.RISK_HIGH), + RCE_JSPX_FILE("rceJspxFile", Alert.RISK_HIGH), + EICAR_FILE("antiVirusEicarFile", Alert.RISK_MEDIUM); + + private String messageKey; + private int alertLevel; + + private static final String PREFIX = "fileupload.scanner.vulnerability."; + + private VulnerabilityType(String message, int alertLevel) { + this.messageKey = message; + this.alertLevel = alertLevel; + } + + public String getMessageKey() { + return PREFIX + this.messageKey; + } + + public int getAlertLevel() { + return this.alertLevel; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/ImageWithJSPSnippetFileUpload.java b/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/ImageWithJSPSnippetFileUpload.java new file mode 100644 index 0000000..5506037 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/ImageWithJSPSnippetFileUpload.java @@ -0,0 +1,117 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.rce.jsp; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.parosproxy.paros.core.scanner.Plugin.AttackStrength; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.FileUploadUtils; +import org.sasanlabs.fileupload.attacks.AttackVector; +import org.sasanlabs.fileupload.attacks.FileUploadAttackExecutor; +import org.sasanlabs.fileupload.attacks.model.FileInformationProvider; +import org.sasanlabs.fileupload.attacks.model.VulnerabilityType; +import org.sasanlabs.fileupload.exception.FileUploadException; +import org.sasanlabs.fileupload.matcher.impl.ContainsExpectedValueMatcher; + +/** @author preetkaran20@gmail.com KSASAN */ +public class ImageWithJSPSnippetFileUpload extends AttackVector { + + private static final String GIF_IMAGE_JSP_INJECTED_IN_EXIF_BASE64_ENCODED = + "R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAAIf5JPCU9ICJJbWFnZVdpdGhKU1BTbmlwcGV0RmlsZVVwbG9hZF8iICsgIlNhc2FuTGFic18iICsgIlpBUF9JZGVudGlmaWVyIiAlPgAsAAAAAAEAAQAAAgJEAQA7"; + private static final String GIF_IMAGE_APPENDED_WITH_JSP_SNIPPET_BASE64_ENCODED = + "R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOzwlPSAiSW1hZ2VXaXRoSlNQU25pcHBldEZpbGVVcGxvYWRfIiArICJTYXNhbkxhYnNfIiArICJaQVBfSWRlbnRpZmllciIgJT4K"; + + // As JPEG is most widely used format for images hence in case application is + // specifically + // looking for JPEG magic numbers then this below configuration can help + private static final String JPEG_IMAGE_JSP_INJECTED_IN_EXIF_BASE64_ENCODED = + "/9j/4AAQSkZJRgABAQEAYABgAAD//gBLPCU9ICJJbWFnZVdpdGhKU1BTbmlwcGV0RmlsZVVwbG9hZF8iICsgIlNhc2FuTGFic18iICsgIlpBUF9JZGVudGlmaWVyIiAlPv/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAkLicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAAEAAQMBIgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APf6KKKAP//Z"; + private static final String JPEG_IMAGE_APPENDED_WITH_JSP_SNIPPET_BASE64_ENCODED = + "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2TwlPSAiSW1hZ2VXaXRoSlNQU25pcHBldEZpbGVVcGxvYWRfIiArICJTYXNhbkxhYnNfIiArICJaQVBfSWRlbnRpZmllciIgJT4K"; + + private static final String FILE_EXPECTED_VALUE = + "ImageWithJSPSnippetFileUpload_SasanLabs_ZAP_Identifier"; + + private static final String BASE_FILE_NAME = "ImageWithJSPSnippetFileUpload_"; + + private static final Map> PAYLOADS = getPayloads(); + + private static Map> getPayloads() { + Map> payloads = new HashMap<>(); + payloads.put( + VulnerabilityType.RCE_GIF_JSP_FILE, + Arrays.asList( + GIF_IMAGE_JSP_INJECTED_IN_EXIF_BASE64_ENCODED, + GIF_IMAGE_APPENDED_WITH_JSP_SNIPPET_BASE64_ENCODED)); + payloads.put( + VulnerabilityType.RCE_JPEG_JSP_FILE, + Arrays.asList( + JPEG_IMAGE_JSP_INJECTED_IN_EXIF_BASE64_ENCODED, + JPEG_IMAGE_APPENDED_WITH_JSP_SNIPPET_BASE64_ENCODED)); + return payloads; + } + + private static final List FILE_PARAMETERS_EXTENDED = + FileUploadUtils.getFileInformationProvidersExtendedJsp(BASE_FILE_NAME); + private static final List FILE_PARAMETERS_DEFAULT = + FileUploadUtils.getFileInformationProvidersDefaultJsp( + BASE_FILE_NAME, FileUploadUtils.JSP_FILE_EXTENSION); + + @Override + public boolean execute(FileUploadAttackExecutor fileUploadAttackExecutor) + throws FileUploadException { + boolean result = false; + for (VulnerabilityType vulnerabilityType : PAYLOADS.keySet()) { + for (String payloads : PAYLOADS.get(vulnerabilityType)) { + byte[] imagePayload = Base64.getDecoder().decode(payloads); + HttpMessage originalMessage = fileUploadAttackExecutor.getOriginalHttpMessage(); + String charSet = originalMessage.getRequestHeader().getCharset(); + Charset requestCharSet = + charSet != null ? Charset.forName(charSet) : StandardCharsets.ISO_8859_1; + String requestPayload = new String(imagePayload, requestCharSet); + result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + requestPayload, + FILE_PARAMETERS_DEFAULT, + new ContainsExpectedValueMatcher(FILE_EXPECTED_VALUE), + vulnerabilityType); + + if (!result + && fileUploadAttackExecutor + .getFileUploadScanRule() + .getAttackStrength() + .equals(AttackStrength.INSANE)) { + result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + requestPayload, + FILE_PARAMETERS_EXTENDED, + new ContainsExpectedValueMatcher(FILE_EXPECTED_VALUE), + vulnerabilityType); + } + if (result) { + return result; + } + } + } + return result; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/SimpleJSPFileUpload.java b/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/SimpleJSPFileUpload.java new file mode 100644 index 0000000..e1c7c66 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/SimpleJSPFileUpload.java @@ -0,0 +1,91 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.rce.jsp; + +import java.util.List; +import org.parosproxy.paros.core.scanner.Plugin.AttackStrength; +import org.sasanlabs.fileupload.FileUploadUtils; +import org.sasanlabs.fileupload.attacks.AttackVector; +import org.sasanlabs.fileupload.attacks.FileUploadAttackExecutor; +import org.sasanlabs.fileupload.attacks.model.FileInformationProvider; +import org.sasanlabs.fileupload.attacks.model.VulnerabilityType; +import org.sasanlabs.fileupload.exception.FileUploadException; +import org.sasanlabs.fileupload.matcher.ContentMatcher; +import org.sasanlabs.fileupload.matcher.impl.MD5HashResponseMatcher; + +/** @author KSASAN preetkaran20@gmail.com */ +public class SimpleJSPFileUpload extends AttackVector { + + private static final String JSP_UPLOADED_FILE_BASE_NAME = "SimpleJSPFileUpload_"; + + private static final String JSP_EL_PAYLOAD = + "${\"SimpleJSPFileUpload\"}${\"_SasanLabs_ZAP_Identifier\"}"; + + private static final String JSP_SCRIPTLET_PAYLOAD = + "<% out.print(\"SimpleJSPFileUpload\"); out.print(\"_SasanLabs_ZAP_Identifier\"); %>"; + + private static final ContentMatcher CONTENT_MATCHER = + new MD5HashResponseMatcher("SimpleJSPFileUpload_SasanLabs_ZAP_Identifier"); + + private static final List FILE_PARAMETERS_EXTENDED = + FileUploadUtils.getFileInformationProvidersExtendedJsp(JSP_UPLOADED_FILE_BASE_NAME); + private static final List FILE_PARAMETERS_DEFAULT = + FileUploadUtils.getFileInformationProvidersDefaultJsp( + JSP_UPLOADED_FILE_BASE_NAME, FileUploadUtils.JSP_FILE_EXTENSION); + + @Override + public boolean execute(FileUploadAttackExecutor fileUploadAttackExecutor) + throws FileUploadException { + boolean result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + JSP_SCRIPTLET_PAYLOAD, + FILE_PARAMETERS_DEFAULT, + CONTENT_MATCHER, + VulnerabilityType.RCE_JSP_FILE); + if (!result) { + result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + JSP_EL_PAYLOAD, + FILE_PARAMETERS_DEFAULT, + CONTENT_MATCHER, + VulnerabilityType.RCE_JSP_FILE); + } + if (!result + && fileUploadAttackExecutor + .getFileUploadScanRule() + .getAttackStrength() + .equals(AttackStrength.INSANE)) { + result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + JSP_SCRIPTLET_PAYLOAD, + FILE_PARAMETERS_EXTENDED, + CONTENT_MATCHER, + VulnerabilityType.RCE_JSP_FILE); + if (!result) { + result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + JSP_EL_PAYLOAD, + FILE_PARAMETERS_EXTENDED, + CONTENT_MATCHER, + VulnerabilityType.RCE_JSP_FILE); + } + } + + return result; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/SimpleJSPXFileUpload.java b/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/SimpleJSPXFileUpload.java new file mode 100644 index 0000000..645db5c --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/rce/jsp/SimpleJSPXFileUpload.java @@ -0,0 +1,127 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.rce.jsp; + +import java.util.Arrays; +import java.util.List; +import org.parosproxy.paros.core.scanner.Plugin.AttackStrength; +import org.sasanlabs.fileupload.FileUploadUtils; +import org.sasanlabs.fileupload.attacks.AttackVector; +import org.sasanlabs.fileupload.attacks.FileUploadAttackExecutor; +import org.sasanlabs.fileupload.attacks.model.FileExtensionOperation; +import org.sasanlabs.fileupload.attacks.model.FileInformationProvider; +import org.sasanlabs.fileupload.attacks.model.FileInformationProviderBuilder; +import org.sasanlabs.fileupload.attacks.model.VulnerabilityType; +import org.sasanlabs.fileupload.exception.FileUploadException; +import org.sasanlabs.fileupload.matcher.ContentMatcher; +import org.sasanlabs.fileupload.matcher.impl.MD5HashResponseMatcher; + +/** @author KSASAN preetkaran20@gmail.com */ +public class SimpleJSPXFileUpload extends AttackVector { + + private static final String JSPX_UPLOADED_FILE_BASE_NAME = "SimpleJSPXFileUpload_"; + // Payload from resource file: "jspx_payload.jspx" + private static final String JSPX_PAYLOAD = + " \n" + + " \n" + + " \n" + + " out.print(\"SimpleJSPXFileUpload_\"); \n" + + " out.print(\"SasanLabs_ZAP_Identifier\");" + + " \n" + + ""; + + private static final ContentMatcher CONTENT_MATCHER = + new MD5HashResponseMatcher("SimpleJSPXFileUpload_SasanLabs_ZAP_Identifier"); + + private static final List FILE_PARAMETERS_EXTENDED = + Arrays.asList( + new FileInformationProviderBuilder(JSPX_UPLOADED_FILE_BASE_NAME) + .withExtension("JspX") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(JSPX_UPLOADED_FILE_BASE_NAME) + .withExtension("JSPX") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(JSPX_UPLOADED_FILE_BASE_NAME) + .withExtension("JspX") + .withContentType("application/x-jsp") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(JSPX_UPLOADED_FILE_BASE_NAME) + .withExtension("JSPX") + .withContentType("application/x-jsp") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(JSPX_UPLOADED_FILE_BASE_NAME) + .withExtension("JspX") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(JSPX_UPLOADED_FILE_BASE_NAME) + .withExtension("JSPX") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(JSPX_UPLOADED_FILE_BASE_NAME) + .withExtension("JspX") + .withContentType("application/x-jsp") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(JSPX_UPLOADED_FILE_BASE_NAME) + .withExtension("JSPX") + .withContentType("application/x-jsp") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build()); + + // Need to validate + // application/x-httpd-jsp + // text/x-jsp + private static final List FILE_PARAMETERS = + FileUploadUtils.getFileInformationProvidersDefaultJsp( + JSPX_UPLOADED_FILE_BASE_NAME, FileUploadUtils.JSPX_FILE_EXTENSION); + + @Override + public boolean execute(FileUploadAttackExecutor fileUploadAttackExecutor) + throws FileUploadException { + boolean result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + JSPX_PAYLOAD, + FILE_PARAMETERS, + CONTENT_MATCHER, + VulnerabilityType.RCE_JSPX_FILE); + if (!result + && fileUploadAttackExecutor + .getFileUploadScanRule() + .getAttackStrength() + .equals(AttackStrength.INSANE)) { + result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + JSPX_PAYLOAD, + FILE_PARAMETERS_EXTENDED, + CONTENT_MATCHER, + VulnerabilityType.RCE_JSPX_FILE); + } + + return result; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/xss/HtmlFileUpload.java b/src/main/java/org/sasanlabs/fileupload/attacks/xss/HtmlFileUpload.java new file mode 100644 index 0000000..938499a --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/xss/HtmlFileUpload.java @@ -0,0 +1,602 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.xss; + +import static org.sasanlabs.fileupload.FileUploadUtils.NULL_BYTE_CHARACTER; + +import java.util.Arrays; +import java.util.List; +import org.parosproxy.paros.core.scanner.Plugin.AttackStrength; +import org.sasanlabs.fileupload.FileUploadUtils; +import org.sasanlabs.fileupload.attacks.AttackVector; +import org.sasanlabs.fileupload.attacks.FileUploadAttackExecutor; +import org.sasanlabs.fileupload.attacks.model.FileExtensionOperation; +import org.sasanlabs.fileupload.attacks.model.FileInformationProvider; +import org.sasanlabs.fileupload.attacks.model.FileInformationProviderBuilder; +import org.sasanlabs.fileupload.attacks.model.VulnerabilityType; +import org.sasanlabs.fileupload.matcher.ContentMatcher; +import org.sasanlabs.fileupload.matcher.impl.MD5HashResponseMatcher; + +/** + * {@code HtmlFileUpload} attack vector will upload {@code html}, {@code htm}, {@code xhtml} etc + * schemes in order to evaluate whether the application is vulnerable to {@code XSS} vulnerability. + *
+ * General logic is if Application is allowing to upload {@code html} file and while downloading the + * same file it is shown {@code inline} by the browser which is controlled by {@code + * Content-Disposition} header then it is vulnerable to Stored XSS. + * + * @author preetkaran20@gmail.com KSASAN + */ +public class HtmlFileUpload extends AttackVector { + + private static final String XSS_UPLOADED_FILE_BASE_NAME = "HtmlFileUpload_XSS_"; + private static final String XSS_PAYLOAD_HTML_FILE = + "Testing XSS"; + + /** + * Precondition:
+ * 1. ContentDisposition should be inline
+ * 2. ContentType should be {@link FileUploadUtils#HTML_MIME_TYPE} or {@link + * FileUploadUtils#XHTML_MIME_TYPE}
+ * or blank Note: here we are not considering the invalid content type header values because + * that should not be there. + */ + static final ContentMatcher CONTENT_MATCHER = + new MD5HashResponseMatcher( + httpMsg -> + FileUploadUtils.isContentDispositionInline(httpMsg) + && (!FileUploadUtils.isContentTypeHeaderPresent(httpMsg) + || FileUploadUtils + .isContentTypeCausesJavascriptExecution( + httpMsg)), + XSS_PAYLOAD_HTML_FILE); + + // Extended list for breaking black-listing strategy. + private static final List FILE_PARAMETERS_EXTENDED = + Arrays.asList( + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Htm") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("hTM") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTM") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HtML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTMl") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Xhtml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xHTml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xhTML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xHTML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("XHTML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHtml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHtml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHTml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHTml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHTML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHTML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Htm") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("hTM") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTM") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Html") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HtML") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTMl") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTML") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Xhtml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xHTml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xhTML") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xHTML") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("XHTML") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHtml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHtml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHTml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHTml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHTML") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHTML") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Htm") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("hTM") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTM") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Html") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HtML") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTMl") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("HTML") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Xhtml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xHTml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xhTML") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xHTML") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("XHTML") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHtml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHtml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHTml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHTml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dHTML") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("sHTML") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build()); + + private static final List FILE_PARAMETERS_DEFAULT = + Arrays.asList( + /** + * No extension is required, actually browser reads the content-type header of + * response from server and if that is not present then it tried to guess the + * file type based on the extension and in case content type and extensions are + * missing then browser finds it by reading the response called content + * sniffing. More information :
+ * {@link https://en.wikipedia.org/wiki/Content_sniffing}
+ * {@link + * https://stackoverflow.com/questions/2148443/does-file-extensions-matter-for-browsers} + */ + /** + * Sometimes the logic is broken in a way that it fetches fileName after {@code + * .} character if dot is not present then it returns the entire fileName and + * then compares it with valid extension list. + * + *

e.g. php code {@code + * strtolower(end(explode('.',$_FILES['image']['name'])));} + */ + originalFileName -> FileUploadUtils.getExtension(originalFileName), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withFileExtensionOperation(FileExtensionOperation.NO_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xhtml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + /** + * Server Parsed Html for server side includes + * https://stackoverflow.com/questions/519619/what-is-the-purpose-and-uniqueness-shtml + */ + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dhtml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("shtml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + /** + * The way apache server considers the file extension is: it takes the right + * most valid extension type which in this case is html. so this can bypass the + * blacklist validation of extensions. For more information {@link + * https://www.acunetix.com/websitesecurity/upload-forms-threat/#:~:text=Double%20Extensions} + */ + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("html.123") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("html") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xhtml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("shtml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dhtml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("html") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xhtml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("shtml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("dhtml") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + /** + * Say original extension is {@code .gif} then this will change it to {@code + * .gif.html} If validator only validates contains {@code .gif} extension then + * validator will be bypassed. + */ + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("html") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xhtml") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("html") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xhtml") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm" + NULL_BYTE_CHARACTER) + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm" + NULL_BYTE_CHARACTER) + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm" + NULL_BYTE_CHARACTER) + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm%00") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm%00") + .withContentType("text/html") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("htm%00") + .withContentType("text/plain") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build() + /** + * My thought is that if server is vulnerable to Null Byte then it is sure that + * "htm" only will work and there is no need to verify other extensions + */ + ); + + @Override + public boolean execute(FileUploadAttackExecutor fileUploadAttackExecutor) { + + boolean result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + XSS_PAYLOAD_HTML_FILE, + FILE_PARAMETERS_DEFAULT, + CONTENT_MATCHER, + VulnerabilityType.XSS_HTML_FILE); + if (!result + && fileUploadAttackExecutor + .getFileUploadScanRule() + .getAttackStrength() + .equals(AttackStrength.INSANE)) { + result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + XSS_PAYLOAD_HTML_FILE, + FILE_PARAMETERS_EXTENDED, + CONTENT_MATCHER, + VulnerabilityType.XSS_HTML_FILE); + } + return result; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/attacks/xss/SVGFileUpload.java b/src/main/java/org/sasanlabs/fileupload/attacks/xss/SVGFileUpload.java new file mode 100644 index 0000000..50d7864 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/attacks/xss/SVGFileUpload.java @@ -0,0 +1,293 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.attacks.xss; + +import static org.sasanlabs.fileupload.FileUploadUtils.NULL_BYTE_CHARACTER; + +import java.util.Arrays; +import java.util.List; +import org.parosproxy.paros.core.scanner.Plugin.AttackStrength; +import org.sasanlabs.fileupload.attacks.AttackVector; +import org.sasanlabs.fileupload.attacks.FileUploadAttackExecutor; +import org.sasanlabs.fileupload.attacks.model.FileExtensionOperation; +import org.sasanlabs.fileupload.attacks.model.FileInformationProvider; +import org.sasanlabs.fileupload.attacks.model.FileInformationProviderBuilder; +import org.sasanlabs.fileupload.attacks.model.VulnerabilityType; + +/** + * {@code SVGFileUpload} attack vector will upload {@code svg} and its various different extension + * schemes in order to evaluate whether the application is vulnerable to {@code XSS} vulnerability. + *
+ * General logic is if Application is allowing to upload {@code svg} file and while downloading the + * same file it is shown {@code inline} by the browser which is controlled by {@code + * Content-Disposition} header then it is vulnerable to Stored XSS. + * + * @author preetkaran20@gmail.com KSASAN + */ +public class SVGFileUpload extends AttackVector { + + private static final String XSS_UPLOADED_FILE_BASE_NAME = "SVGFileUpload_XSS_"; + private static final String XSS_PAYLOAD_SVG_FILE = + "\n" + + "\n" + + ""; + + // Extended list for breaking black-listing strategy. + private static final List FILE_PARAMETERS_EXTENDED = + Arrays.asList( + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Svg") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SvG") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SVG") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Svgz") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SvGz") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SVGZ") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("XML") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Svg") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SvG") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SVG") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Svgz") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SvGz") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SVGZ") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xML") + .withContentType("text/xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Xml") + .withContentType("text/xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("XML") + .withContentType("text/xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Svg") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SVG") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("Svgz") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("SVGZ") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xML") + .withContentType("text/xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("XML") + .withContentType("text/xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build()); + + /** + * Extensions working with SVG payload for XSS: svg,html,xml,htm,xhtml,Null byte + * (assumption),shtml,svgz,dhtml Extensions not working: png,gif,swf,pdf,mvg,xbm,ssi,jpeg + * + *

Didn't pick html and its related variant extensions because those attack vectors will be + * covered under {@link HtmlFileUpload}. + */ + private static final List FILE_PARAMETERS_DEFAULT = + Arrays.asList( + // Not adding empty extension for svg because it is not working in browsers + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svg") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svgz") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svg") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xml") + .withContentType("text/xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svgz") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.ONLY_PROVIDED_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svg") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("xml") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svgz") + .withFileExtensionOperation( + FileExtensionOperation.PREFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svg" + NULL_BYTE_CHARACTER) + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svg" + NULL_BYTE_CHARACTER) + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svg%00") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build(), + new FileInformationProviderBuilder(XSS_UPLOADED_FILE_BASE_NAME) + .withExtension("svg%00") + .withContentType("image/svg+xml") + .withFileExtensionOperation( + FileExtensionOperation.SUFFIX_ORIGINAL_EXTENSION) + .build()); + + @Override + public boolean execute(FileUploadAttackExecutor fileUploadAttackExecutor) { + boolean result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + XSS_PAYLOAD_SVG_FILE, + FILE_PARAMETERS_DEFAULT, + HtmlFileUpload.CONTENT_MATCHER, + VulnerabilityType.XSS_SVG_FILE); + if (!result + && fileUploadAttackExecutor + .getFileUploadScanRule() + .getAttackStrength() + .equals(AttackStrength.INSANE)) { + result = + this.genericAttackExecutor( + fileUploadAttackExecutor, + XSS_PAYLOAD_SVG_FILE, + FILE_PARAMETERS_EXTENDED, + HtmlFileUpload.CONTENT_MATCHER, + VulnerabilityType.XSS_SVG_FILE); + } + + return result; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/configuration/FileUploadConfiguration.java b/src/main/java/org/sasanlabs/fileupload/configuration/FileUploadConfiguration.java new file mode 100644 index 0000000..c912275 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/configuration/FileUploadConfiguration.java @@ -0,0 +1,132 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.configuration; + +import org.apache.log4j.Logger; +import org.zaproxy.zap.common.VersionedAbstractParam; + +/** + * This class holds configuration related to FileUpload Addon. + * + * @author KSASAN preetkaran20@gmail.com + * @since 1.0.0 + */ +public class FileUploadConfiguration extends VersionedAbstractParam { + + protected static final Logger LOGGER = Logger.getLogger(FileUploadConfiguration.class); + + /** The base configuration key for all fileupload configurations. */ + private static final String PARAM_BASE_KEY = "fileupload"; + + private static final String CONFIG_VERSION_KEY = PARAM_BASE_KEY + VERSION_ATTRIBUTE; + private static final int CURRENT_CONFIG_VERSION = 1; + private static final String PARAM_STATIC_LOCATION_CONFIGURATION_URI_REGEX = + PARAM_BASE_KEY + ".staticlocation.uriregex"; + private static final String PARAM_DYNAMIC_LOCATION_CONFIGURATION_URI_REGEX = + PARAM_BASE_KEY + ".dynamiclocation.uriregex"; + private static final String PARAM_DYNAMIC_LOCATION_CONFIGURATION_START_IDENTIFIER = + PARAM_BASE_KEY + ".dynamiclocation.startidentifier"; + private static final String PARAM_DYNAMIC_LOCATION_CONFIGURATION_END_IDENTIFIER = + PARAM_BASE_KEY + ".dynamiclocation.endidentifier"; + + private String staticLocationURIRegex; + private String dynamicLocationURIRegex; + private String dynamicLocationStartIdentifier; + private String dynamicLocationEndIdentifier; + + private static volatile FileUploadConfiguration fileUploadConfiguration; + + private FileUploadConfiguration() {} + + public static FileUploadConfiguration getInstance() { + if (fileUploadConfiguration == null) { + synchronized (FileUploadConfiguration.class) { + if (fileUploadConfiguration == null) { + fileUploadConfiguration = new FileUploadConfiguration(); + } + } + } + return fileUploadConfiguration; + } + + public String getStaticLocationURIRegex() { + return staticLocationURIRegex; + } + + public void setStaticLocationURIRegex(String staticLocationURIRegex) { + this.staticLocationURIRegex = staticLocationURIRegex; + this.getConfig() + .setProperty(PARAM_STATIC_LOCATION_CONFIGURATION_URI_REGEX, staticLocationURIRegex); + } + + public String getDynamicLocationURIRegex() { + return dynamicLocationURIRegex; + } + + public void setDynamicLocationURIRegex(String dynamicLocationURIRegex) { + this.dynamicLocationURIRegex = dynamicLocationURIRegex; + this.getConfig() + .setProperty( + PARAM_DYNAMIC_LOCATION_CONFIGURATION_URI_REGEX, dynamicLocationURIRegex); + } + + public String getDynamicLocationStartIdentifier() { + return dynamicLocationStartIdentifier; + } + + public void setDynamicLocationStartIdentifier(String dynamicLocationStartIdentifier) { + this.dynamicLocationStartIdentifier = dynamicLocationStartIdentifier; + this.getConfig() + .setProperty( + PARAM_DYNAMIC_LOCATION_CONFIGURATION_START_IDENTIFIER, + dynamicLocationStartIdentifier); + } + + public String getDynamicLocationEndIdentifier() { + return dynamicLocationEndIdentifier; + } + + public void setDynamicLocationEndIdentifier(String dynamicLocationEndIdentifier) { + this.dynamicLocationEndIdentifier = dynamicLocationEndIdentifier; + this.getConfig() + .setProperty( + PARAM_DYNAMIC_LOCATION_CONFIGURATION_END_IDENTIFIER, + dynamicLocationEndIdentifier); + } + + @Override + protected String getConfigVersionKey() { + return CONFIG_VERSION_KEY; + } + + @Override + protected int getCurrentVersion() { + return CURRENT_CONFIG_VERSION; + } + + @Override + protected void parseImpl() { + this.setStaticLocationURIRegex( + getConfig().getString(PARAM_STATIC_LOCATION_CONFIGURATION_URI_REGEX)); + this.setDynamicLocationURIRegex( + getConfig().getString(PARAM_DYNAMIC_LOCATION_CONFIGURATION_URI_REGEX)); + this.setDynamicLocationStartIdentifier( + getConfig().getString(PARAM_DYNAMIC_LOCATION_CONFIGURATION_START_IDENTIFIER)); + this.setDynamicLocationEndIdentifier( + getConfig().getString(PARAM_DYNAMIC_LOCATION_CONFIGURATION_END_IDENTIFIER)); + } + + @Override + protected void updateConfigsImpl(int fileVersion) {} +} diff --git a/src/main/java/org/sasanlabs/fileupload/exception/FileUploadException.java b/src/main/java/org/sasanlabs/fileupload/exception/FileUploadException.java new file mode 100644 index 0000000..2d8c53c --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/exception/FileUploadException.java @@ -0,0 +1,36 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.exception; + +/** + * {@code FileUploadException} is an exception class for FileUpload Scan Rule. It wraps around + * multiple other types of exception and might be very useful in future. + * + * @author preetkaran20@gmail.com KSASAN + */ +public class FileUploadException extends Exception { + private static final long serialVersionUID = 372527297369183960L; + + public FileUploadException(Throwable th) { + super(th); + } + + public FileUploadException(String message) { + super(message); + } + + public FileUploadException(String message, Throwable th) { + super(message, th); + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/function/ConsumerWithException.java b/src/main/java/org/sasanlabs/fileupload/function/ConsumerWithException.java new file mode 100644 index 0000000..dfc0bae --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/function/ConsumerWithException.java @@ -0,0 +1,20 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.function; + +/** @author preetkaran20@gmail.com KSASAN */ +@FunctionalInterface +public interface ConsumerWithException { + void accept(T val) throws E; +} diff --git a/src/main/java/org/sasanlabs/fileupload/i18n/FileUploadI18n.java b/src/main/java/org/sasanlabs/fileupload/i18n/FileUploadI18n.java new file mode 100644 index 0000000..118a423 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/i18n/FileUploadI18n.java @@ -0,0 +1,45 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.i18n; + +import java.util.ResourceBundle; +import org.parosproxy.paros.Constant; + +/** + * Message Bundle + * + * @author KSASAN preetkaran20@gmail.com + * @since 1.0.0 + */ +public class FileUploadI18n { + private static ResourceBundle message; + + public static void init() { + message = + ResourceBundle.getBundle( + FileUploadI18n.class.getPackage().getName() + ".Messages", + Constant.getLocale()); + } + + public static String getMessage(String key) { + if (key != null && message != null && message.containsKey(key)) { + return message.getString(key); + } + return ""; + } + + public static ResourceBundle getResourceBundle() { + return message; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/locator/URILocator.java b/src/main/java/org/sasanlabs/fileupload/locator/URILocator.java new file mode 100644 index 0000000..b0a83ef --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/locator/URILocator.java @@ -0,0 +1,34 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.locator; + +import java.io.IOException; +import org.apache.commons.httpclient.URI; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.exception.FileUploadException; +import org.sasanlabs.fileupload.function.ConsumerWithException; + +/** + * {@code URILocator} class is used to find the URL either by parsing the {@code HttpMessage} or + * reading the configuration mentioned in the options tab. + * + * @author preetkaran20@gmail.com KSASAN + */ +public interface URILocator { + URI get( + HttpMessage msg, + String fileName, + ConsumerWithException sendAndRecieve) + throws FileUploadException, IOException; +} diff --git a/src/main/java/org/sasanlabs/fileupload/locator/URILocatorImpl.java b/src/main/java/org/sasanlabs/fileupload/locator/URILocatorImpl.java new file mode 100644 index 0000000..b150eb7 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/locator/URILocatorImpl.java @@ -0,0 +1,146 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.locator; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import org.apache.commons.httpclient.URI; +import org.apache.commons.httpclient.URIException; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; +import org.apache.log4j.Logger; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.FileUploadUtils; +import org.sasanlabs.fileupload.configuration.FileUploadConfiguration; +import org.sasanlabs.fileupload.exception.FileUploadException; +import org.sasanlabs.fileupload.function.ConsumerWithException; + +/** + * {@code URILocatorImpl} class is used to find the URL either by 1. returning the static url + * mentioned by user in Options tab. This also handles the dynamic file names e.g. + * http:///${fileName} 2. parsing the original {@code HttpMessage} and using dynamic + * configuration to find the complete URI. 3. Invokes the preflight request as mentioned by dynamic + * configuration and then parsing the preflighted {@code HttpMessage} + * + * @author preetkaran20@gmail.com KSASAN + */ +public class URILocatorImpl implements URILocator { + + protected static final Logger LOGGER = Logger.getLogger(URILocatorImpl.class); + + private URI getCompleteURI(String uriRegex, String fileName, HttpMessage msg) + throws URIException, FileUploadException { + if (fileName.contains(FileUploadUtils.NULL_BYTE_CHARACTER)) { + fileName = fileName.substring(0, fileName.indexOf(FileUploadUtils.NULL_BYTE_CHARACTER)); + } + Map replacerKeyValuePair = Collections.singletonMap("filename", fileName); + StringSubstitutor stringSubstitutor = new StringSubstitutor(replacerKeyValuePair); + String uriFragment = stringSubstitutor.replace(uriRegex); + if (uriFragment.startsWith(FileUploadUtils.HTTP_SCHEME) + || uriRegex.startsWith(FileUploadUtils.HTTP_SECURED_SCHEME)) { + return new URI(uriFragment, true); + } else { + if (!uriFragment.startsWith(FileUploadUtils.SLASH)) { + uriFragment = FileUploadUtils.SLASH + uriFragment; + } + String authority = msg.getRequestHeader().getURI().getAuthority(); + String scheme = msg.getRequestHeader().getURI().getScheme(); + return new URI(scheme, authority, uriFragment, ""); + } + } + + private URI parseResponseAndGetCompleteURI( + HttpMessage msg, String fileName, HttpMessage originalMsg) + throws FileUploadException, URIException { + int startIndex = + msg.getResponseBody() + .toString() + .indexOf( + FileUploadConfiguration.getInstance() + .getDynamicLocationStartIdentifier()); + int endIndex = + msg.getResponseBody() + .toString() + .indexOf( + FileUploadConfiguration.getInstance() + .getDynamicLocationEndIdentifier()); + if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) { + throw new FileUploadException( + "StartIndex or EndIndex configuration is either not present in the response or invalid. Start index:" + + startIndex + + " End index:" + + endIndex); + } + String uriRegex = + msg.getResponseBody() + .toString() + .substring( + startIndex + + FileUploadConfiguration.getInstance() + .getDynamicLocationStartIdentifier() + .length(), + endIndex); + return this.getCompleteURI(uriRegex, fileName, originalMsg); + } + + @Override + public URI get( + HttpMessage msg, + String fileName, + ConsumerWithException sendAndRecieve) + throws FileUploadException { + URI uri = null; + try { + if (StringUtils.isNotBlank( + FileUploadConfiguration.getInstance().getStaticLocationURIRegex())) { + return this.getCompleteURI( + FileUploadConfiguration.getInstance().getStaticLocationURIRegex(), + fileName, + msg); + } else if (StringUtils.isNotBlank( + FileUploadConfiguration.getInstance().getDynamicLocationURIRegex())) { + // Do an HttpCall and then find URI of uploaded content in Response. + HttpMessage preflightRequest = msg.cloneRequest(); + preflightRequest + .getRequestHeader() + .setURI( + this.getCompleteURI( + FileUploadConfiguration.getInstance() + .getDynamicLocationURIRegex(), + fileName, + msg)); + sendAndRecieve.accept(preflightRequest); + return this.parseResponseAndGetCompleteURI(preflightRequest, fileName, msg); + } else { + if (StringUtils.isNotBlank( + FileUploadConfiguration.getInstance() + .getDynamicLocationStartIdentifier()) + && StringUtils.isNotBlank( + FileUploadConfiguration.getInstance() + .getDynamicLocationEndIdentifier())) { + try { + return this.parseResponseAndGetCompleteURI(msg, fileName, msg); + } catch (FileUploadException e) { + // Eating exception because upload request might not have the uri + LOGGER.debug(e); + } + } + } + } catch (IOException ex) { + throw new FileUploadException(ex); + } + return uri; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/matcher/ContentMatcher.java b/src/main/java/org/sasanlabs/fileupload/matcher/ContentMatcher.java new file mode 100644 index 0000000..882db9d --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/matcher/ContentMatcher.java @@ -0,0 +1,33 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.matcher; + +import org.parosproxy.paros.network.HttpMessage; + +/** + * {@code ContentMatcher} class is used to match the contents of provided {@code HttpMessage} with + * the expected values + * + * @author preetkaran20@gmail.com KSASAN + */ +@FunctionalInterface +public interface ContentMatcher { + + /** + * @param msg, {@code HttpMessage} for comparing with the expected values + * @return {@code True} if {@code HttpMessage} matches with the expected values else {@code + * False} + */ + boolean match(HttpMessage msg); +} diff --git a/src/main/java/org/sasanlabs/fileupload/matcher/impl/ContainsExpectedValueMatcher.java b/src/main/java/org/sasanlabs/fileupload/matcher/impl/ContainsExpectedValueMatcher.java new file mode 100644 index 0000000..3656d26 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/matcher/impl/ContainsExpectedValueMatcher.java @@ -0,0 +1,53 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.matcher.impl; + +import java.util.Objects; +import java.util.function.Predicate; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.matcher.ContentMatcher; + +/** + * {@code ContainsExpectedValueMatcher} is used to match if the expected value is present in the + * {@code HttpMessage} response body or not. + * + * @author KSASAN preetkaran20@gmail.com + */ +public class ContainsExpectedValueMatcher implements ContentMatcher { + + private static final Predicate DEFAULT_PRECONDITION = (httpMessage) -> true; + + private Predicate precondition; + private String expectedValue; + + public ContainsExpectedValueMatcher(String expectedValue) { + this.precondition = DEFAULT_PRECONDITION; + this.expectedValue = expectedValue; + } + + public ContainsExpectedValueMatcher(Predicate precondition, String expectedValue) { + Objects.requireNonNull(precondition, "Precondition cannot be null"); + this.precondition = precondition; + this.expectedValue = expectedValue; + } + + @Override + public boolean match(HttpMessage msg) { + if (!this.precondition.test(msg)) { + return false; + } + // Assumption is toString is handled correctly by Owasp ZAP. + return msg.getResponseBody().toString().contains(this.expectedValue); + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/matcher/impl/MD5HashResponseMatcher.java b/src/main/java/org/sasanlabs/fileupload/matcher/impl/MD5HashResponseMatcher.java new file mode 100644 index 0000000..a08dd7e --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/matcher/impl/MD5HashResponseMatcher.java @@ -0,0 +1,70 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.matcher.impl; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Predicate; +import org.apache.log4j.Logger; +import org.parosproxy.paros.network.HttpMessage; +import org.sasanlabs.fileupload.matcher.ContentMatcher; + +/** + * {@code MD5HashResponseMatcher} matches if the expected value's {@code MD5} hash is same as + * provided {@code HttpMessage} response's {@code MD5} hash. + * + * @author KSASAN preetkaran20@gmail.com + */ +public class MD5HashResponseMatcher implements ContentMatcher { + private static final Logger LOGGER = Logger.getLogger(MD5HashResponseMatcher.class); + + private static final Predicate DEFAULT_PRECONDITION = (httpMessage) -> true; + + private Predicate precondition; + private String expectedValue; + + public MD5HashResponseMatcher(String expectedValue) { + this.precondition = DEFAULT_PRECONDITION; + this.expectedValue = expectedValue; + } + + public MD5HashResponseMatcher(Predicate precondition, String expectedValue) { + Objects.requireNonNull(precondition, "Precondition cannot be null"); + this.precondition = precondition; + this.expectedValue = expectedValue; + } + + @Override + public boolean match(HttpMessage msg) { + if (!this.precondition.test(msg)) { + return false; + } + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + // Assumption is ZAP will check the charset and returns the response body as "String" as + // per the charset. + byte[] digest = + messageDigest.digest( + msg.getResponseBody().toString().getBytes(StandardCharsets.UTF_8)); + return Arrays.equals( + digest, messageDigest.digest(expectedValue.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException ex) { + LOGGER.debug("Error occurred while comparing MD5 Hash ", ex); + } + return false; + } +} diff --git a/src/main/java/org/sasanlabs/fileupload/ui/FileUploadOptionsPanel.java b/src/main/java/org/sasanlabs/fileupload/ui/FileUploadOptionsPanel.java new file mode 100644 index 0000000..d4b8858 --- /dev/null +++ b/src/main/java/org/sasanlabs/fileupload/ui/FileUploadOptionsPanel.java @@ -0,0 +1,287 @@ +/** + * Copyright 2021 SasanLabs + * + *

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 org.sasanlabs.fileupload.ui; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.ScrollPaneConstants; +import javax.swing.border.TitledBorder; +import org.apache.commons.lang3.StringUtils; +import org.parosproxy.paros.model.OptionsParam; +import org.parosproxy.paros.view.AbstractParamPanel; +import org.sasanlabs.fileupload.configuration.FileUploadConfiguration; +import org.sasanlabs.fileupload.i18n.FileUploadI18n; + +/** + * FileUpload options panel for specifying settings which are used by {@code FileUploadScanRule} for + * finding vulnerabilities related to FileUpload functionality in the applications. + * + * @author KSASAN preetkaran20@gmail.com + * @since 1.0.0 + */ +public class FileUploadOptionsPanel extends AbstractParamPanel { + private static final long serialVersionUID = 1L; + + private JScrollPane settingsScrollPane; + private JPanel footerPanel; + + // UI components + private JTextField staticLocationConfigurationURIRegex; + private JTextField dynamicLocationConfigurationURIRegex; + private JTextField parseResponseStartIdentifier; + private JTextField parseResponseEndIdentifier; + + public FileUploadOptionsPanel() { + super(); + this.setName(FileUploadI18n.getMessage("fileupload.settings.title")); + this.setLayout(new BorderLayout()); + JPanel settingsPanel = new JPanel(); + settingsPanel.setLayout(new BoxLayout(settingsPanel, BoxLayout.Y_AXIS)); + settingsScrollPane = + new JScrollPane( + settingsPanel, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + this.add(settingsScrollPane, BorderLayout.NORTH); + footerPanel = new JPanel(); + this.add(footerPanel, BorderLayout.SOUTH); + footerPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); + init(settingsPanel); + } + + private void init(JPanel settingsPanel) { + settingsPanel.add(uriLocatorConfiguration()); + footerPanel.add(getResetButton()); + } + + private JButton getResetButton() { + JButton resetButton = new JButton(); + resetButton.setText(FileUploadI18n.getMessage("fileupload.settings.button.reset")); + resetButton.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + resetOptionsPanel(); + } + }); + return resetButton; + } + + private JPanel staticURILocatorConfiguration() { + GridBagLayout gridBagLayout = new GridBagLayout(); + JPanel staticLocationConfigurationPanel = new JPanel(); + staticLocationConfigurationPanel.setLayout(gridBagLayout); + TitledBorder staticLocationConfigurationPanelBorder = + new TitledBorder( + FileUploadI18n.getMessage( + "fileupload.settings.urilocator.staticlocation.title")); + staticLocationConfigurationPanel.setBorder(staticLocationConfigurationPanelBorder); + GridBagConstraints staticLocationConfigurationGridBagConstriants = getGridBagConstraints(); + JLabel uriRegexLabel = + new JLabel( + FileUploadI18n.getMessage( + "fileupload.settings.urilocator.staticlocation.uriregex")); + staticLocationConfigurationPanel.add( + uriRegexLabel, staticLocationConfigurationGridBagConstriants); + staticLocationConfigurationGridBagConstriants.gridx++; + staticLocationConfigurationURIRegex = new JTextField(); + staticLocationConfigurationURIRegex.setColumns(15); + staticLocationConfigurationPanel.add( + staticLocationConfigurationURIRegex, staticLocationConfigurationGridBagConstriants); + staticLocationConfigurationGridBagConstriants.gridy++; + staticLocationConfigurationGridBagConstriants.gridx = 0; + return staticLocationConfigurationPanel; + } + + private JPanel dynamicURILocatorConfiguration() { + TitledBorder dynamicLocationConfigurationPanelBorder = + new TitledBorder( + FileUploadI18n.getMessage( + "fileupload.settings.urilocator.dynamiclocation.title")); + JPanel dynamicLocationConfigurationPanel = new JPanel(); + GridBagLayout gridBagLayout = new GridBagLayout(); + dynamicLocationConfigurationPanel.setLayout(gridBagLayout); + dynamicLocationConfigurationPanel.setBorder(dynamicLocationConfigurationPanelBorder); + GridBagConstraints dynamicLocationConfigurationGridBagConstriants = getGridBagConstraints(); + JLabel dynamicLocationConfigurationURIRegexLabel = + new JLabel( + FileUploadI18n.getMessage( + "fileupload.settings.urilocator.dynamiclocation.uriregex")); + dynamicLocationConfigurationPanel.add( + dynamicLocationConfigurationURIRegexLabel, + dynamicLocationConfigurationGridBagConstriants); + dynamicLocationConfigurationGridBagConstriants.gridx++; + dynamicLocationConfigurationURIRegex = new JTextField(); + dynamicLocationConfigurationURIRegex.setColumns(15); + dynamicLocationConfigurationPanel.add( + dynamicLocationConfigurationURIRegex, + dynamicLocationConfigurationGridBagConstriants); + dynamicLocationConfigurationGridBagConstriants.gridy++; + dynamicLocationConfigurationGridBagConstriants.gridx = 0; + return dynamicLocationConfigurationPanel; + } + + private JPanel parseResponseConfiguration() { + TitledBorder parseResponseConfigurationPanelBorder = + new TitledBorder( + FileUploadI18n.getMessage( + "fileupload.settings.urilocator.parseresponseconfiguration.title")); + JPanel parseResponseConfigurationPanel = new JPanel(); + GridBagLayout gridBagLayout = new GridBagLayout(); + parseResponseConfigurationPanel.setLayout(gridBagLayout); + parseResponseConfigurationPanel.setBorder(parseResponseConfigurationPanelBorder); + GridBagConstraints parseResponseConfigurationGridBagConstriants = getGridBagConstraints(); + + JLabel parseResponseConfigurationStartIdentifierLabel = + new JLabel( + FileUploadI18n.getMessage( + "fileupload.settings.urilocator.parseresponseconfiguration.startidentifer")); + parseResponseConfigurationPanel.add( + parseResponseConfigurationStartIdentifierLabel, + parseResponseConfigurationGridBagConstriants); + parseResponseConfigurationGridBagConstriants.gridx++; + parseResponseStartIdentifier = new JTextField(); + dynamicLocationConfigurationURIRegex.setColumns(15); + parseResponseConfigurationPanel.add( + parseResponseStartIdentifier, parseResponseConfigurationGridBagConstriants); + parseResponseConfigurationGridBagConstriants.gridy++; + parseResponseConfigurationGridBagConstriants.gridx = 0; + + JLabel parseResponseConfigurationEndIdentifierLabel = + new JLabel( + FileUploadI18n.getMessage( + "fileupload.settings.urilocator.parseresponseconfiguration.endidentifer")); + parseResponseConfigurationPanel.add( + parseResponseConfigurationEndIdentifierLabel, + parseResponseConfigurationGridBagConstriants); + parseResponseConfigurationGridBagConstriants.gridx++; + parseResponseEndIdentifier = new JTextField(); + dynamicLocationConfigurationURIRegex.setColumns(15); + parseResponseConfigurationPanel.add( + parseResponseEndIdentifier, parseResponseConfigurationGridBagConstriants); + parseResponseConfigurationGridBagConstriants.gridy++; + parseResponseConfigurationGridBagConstriants.gridx = 0; + return parseResponseConfigurationPanel; + } + + private JPanel uriLocatorConfiguration() { + JPanel uriLocatorConfigurationPanel = new JPanel(); + uriLocatorConfigurationPanel.setSize(uriLocatorConfigurationPanel.getPreferredSize()); + GridBagLayout gridBagLayout = new GridBagLayout(); + uriLocatorConfigurationPanel.setLayout(gridBagLayout); + GridBagConstraints gridBagConstraints = getGridBagConstraints(); + + TitledBorder uriLocatorPanelBorder = + new TitledBorder(FileUploadI18n.getMessage("fileupload.settings.urilocator.title")); + uriLocatorConfigurationPanel.setBorder(uriLocatorPanelBorder); + + // Static Configuration + uriLocatorConfigurationPanel.add(this.staticURILocatorConfiguration(), gridBagConstraints); + gridBagConstraints.gridy++; + // Dynamic Configuration + uriLocatorConfigurationPanel.add(this.dynamicURILocatorConfiguration(), gridBagConstraints); + gridBagConstraints.gridy++; + // Parse Response Configuration + uriLocatorConfigurationPanel.add(this.parseResponseConfiguration(), gridBagConstraints); + gridBagConstraints.gridy++; + return uriLocatorConfigurationPanel; + } + + public static GridBagConstraints getGridBagConstraints() { + GridBagConstraints gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.fill = GridBagConstraints.BOTH; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridx = 0; + gridBagConstraints.weightx = 1.0D; + gridBagConstraints.weighty = 1.0D; + return gridBagConstraints; + } + + /** Resets entire panel to default values. */ + private void resetOptionsPanel() { + staticLocationConfigurationURIRegex.setText(""); + dynamicLocationConfigurationURIRegex.setText(""); + parseResponseStartIdentifier.setText(""); + parseResponseEndIdentifier.setText(""); + } + + @Override + public void initParam(Object optionParams) { + this.resetOptionsPanel(); + FileUploadConfiguration fileUploadConfiguration = + ((OptionsParam) optionParams).getParamSet(FileUploadConfiguration.class); + staticLocationConfigurationURIRegex.setText( + fileUploadConfiguration.getStaticLocationURIRegex()); + dynamicLocationConfigurationURIRegex.setText( + fileUploadConfiguration.getDynamicLocationURIRegex()); + parseResponseStartIdentifier.setText( + fileUploadConfiguration.getDynamicLocationStartIdentifier()); + parseResponseEndIdentifier.setText( + fileUploadConfiguration.getDynamicLocationEndIdentifier()); + } + + @Override + public void validateParam(Object optionParams) throws Exception { + boolean isStaticUrlPresent = + StringUtils.isNotEmpty(staticLocationConfigurationURIRegex.getText()); + boolean isDynamicUrlPresent = + StringUtils.isNotEmpty(dynamicLocationConfigurationURIRegex.getText()); + boolean isStartIdentifierPresent = + StringUtils.isNotEmpty(parseResponseStartIdentifier.getText()); + boolean isEndIdentifierPresent = + StringUtils.isNotEmpty(parseResponseEndIdentifier.getText()); + + if (isStaticUrlPresent + && (isDynamicUrlPresent || isStartIdentifierPresent || isEndIdentifierPresent)) { + throw new IllegalArgumentException( + FileUploadI18n.getMessage( + "fileupload.settings.alert.static.dynamicconfiguration.both.present")); + } else if ((isStartIdentifierPresent && !isEndIdentifierPresent) + || (!isStartIdentifierPresent && isEndIdentifierPresent)) { + throw new IllegalArgumentException( + FileUploadI18n.getMessage( + "fileupload.settings.alert.invalid.httpresponseparseconfiguration")); + } + } + + @Override + public String getHelpIndex() { + return "ui.dialog.options.fileupload"; + } + + @Override + public void saveParam(Object optionParams) throws Exception { + FileUploadConfiguration fileUploadConfiguration = + ((OptionsParam) optionParams).getParamSet(FileUploadConfiguration.class); + fileUploadConfiguration.setStaticLocationURIRegex( + this.staticLocationConfigurationURIRegex.getText()); + fileUploadConfiguration.setDynamicLocationURIRegex( + this.dynamicLocationConfigurationURIRegex.getText()); + fileUploadConfiguration.setDynamicLocationStartIdentifier( + this.parseResponseStartIdentifier.getText()); + fileUploadConfiguration.setDynamicLocationEndIdentifier( + this.parseResponseEndIdentifier.getText()); + } +} diff --git a/src/main/javahelp/org/sasanlabs/fileupload/resources/help/contents/fileupload.html b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/contents/fileupload.html new file mode 100644 index 0000000..b756f34 --- /dev/null +++ b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/contents/fileupload.html @@ -0,0 +1,56 @@ + + + + +FileUpload addon + + + +

FileUpload

+This is a File Upload addon which is used to find the vulnerabilities in File Upload functionality. +

+

Why this addon is needed

+File upload is becoming a more and more essential part of any application, +where the user is able to upload their photo, their CV, or a video showcasing a +project they are working on. The application should be able to fend off bogus and +malicious files in a way to keep the application and the users safe. +Generally file upload functionality is quite complex to automate and has huge +attack surface hence there is a need to automate the process and also secure it. + +

Configuration

+File upload functionality generally has 2 endpoints, one from where file is uploaded and one +from where file is retrieved. It is necessary to know both these endpoints. +While Active Scanning an application, file upload endpoint is already known but retrieval +endpoint is not known to the scan rule hence there are configuration details specific +to the retrieval endpoint. + +

Explanation about Options Panel

+For finding the URL to retrieve/view the uploaded file, here are following options: +
    +
  1. +In some applications the URL to retrieve the uploaded file is static and doesn't change or +only the file name is changed. For handling this type of configuration, +options panel has Static Location Configuration where +static URL is added into URI Regex field. URI Regex field also supports the dynamic file name +by ${fileName}. parameter, for e.g. http:///${fileName} +
  2. +
  3. +In some applications the URL to retrieve the uploaded file is present in the file upload request's response. +For handling this type of configuration, options panel has Parse Http Response Configuration +which has 2 parameters Start Identifier and End Identifier. +These identifiers are used to locate the URL within the response. +
  4. +
  5. +In some applications the URL to retrieve the uploaded file is present in the response of a different URL which is called a +preflight request. E.g. Profile picture URL is part of profile page and +hence we need to parse the response of the profile page to find the URL of the profile picture. +For handling this type of configuration, the options panel has Dynamic Location Configuration +which has a URI Regex and Parse Http Response Configuration which has Start Identifier and End Identifier. +So the File Upload add-on will invoke the URI mentioned in URI Regex and +then parse the response using Start Identifier and End Identifier. URI Regex field also +supports the dynamic file name by ${fileName} +
  6. +
+ + + diff --git a/src/main/javahelp/org/sasanlabs/fileupload/resources/help/helpset.hs b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/helpset.hs new file mode 100644 index 0000000..a8ed0e7 --- /dev/null +++ b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/helpset.hs @@ -0,0 +1,41 @@ + + + + Simple Example Add-On + + + top + + + + + TOC + + org.zaproxy.zap.extension.help.ZapTocView + toc.xml + + + + Index + + javax.help.IndexView + index.xml + + + + Search + + javax.help.SearchView + + JavaHelpSearch + + + + + Favorites + + javax.help.FavoritesView + + diff --git a/src/main/javahelp/org/sasanlabs/fileupload/resources/help/index.xml b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/index.xml new file mode 100644 index 0000000..09b7959 --- /dev/null +++ b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/index.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/main/javahelp/org/sasanlabs/fileupload/resources/help/map.jhm b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/map.jhm new file mode 100644 index 0000000..b0aa20c --- /dev/null +++ b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/map.jhm @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/main/javahelp/org/sasanlabs/fileupload/resources/help/toc.xml b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/toc.xml new file mode 100644 index 0000000..afafeea --- /dev/null +++ b/src/main/javahelp/org/sasanlabs/fileupload/resources/help/toc.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/main/resources/org/sasanlabs/attackvectors/images/1*1_Default.gif b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_Default.gif new file mode 100644 index 0000000..2799b45 Binary files /dev/null and b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_Default.gif differ diff --git a/src/main/resources/org/sasanlabs/attackvectors/images/1*1_Default.jpeg b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_Default.jpeg new file mode 100644 index 0000000..1cda9a5 Binary files /dev/null and b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_Default.jpeg differ diff --git a/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_Appended.gif b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_Appended.gif new file mode 100644 index 0000000..c4d2f64 Binary files /dev/null and b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_Appended.gif differ diff --git a/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_Appended.jpeg b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_Appended.jpeg new file mode 100644 index 0000000..122adf4 Binary files /dev/null and b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_Appended.jpeg differ diff --git a/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_EXIF_Comment.gif b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_EXIF_Comment.gif new file mode 100644 index 0000000..d8245a5 Binary files /dev/null and b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_EXIF_Comment.gif differ diff --git a/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_EXIF_Comment.jpeg b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_EXIF_Comment.jpeg new file mode 100644 index 0000000..3aa8444 Binary files /dev/null and b/src/main/resources/org/sasanlabs/attackvectors/images/1*1_JSP_EXIF_Comment.jpeg differ diff --git a/src/main/resources/org/sasanlabs/fileupload/attackvectors/files/jspx_payload.jspx b/src/main/resources/org/sasanlabs/fileupload/attackvectors/files/jspx_payload.jspx new file mode 100644 index 0000000..15829d7 --- /dev/null +++ b/src/main/resources/org/sasanlabs/fileupload/attackvectors/files/jspx_payload.jspx @@ -0,0 +1,7 @@ + + + + out.print("PlainOldJSPXRemoteCodeExecution_"); + out.print("SasanLabs_ZAP_Identifier"); + + \ No newline at end of file diff --git a/src/main/resources/org/sasanlabs/fileupload/attackvectors/files/svg_xss_payload.svg b/src/main/resources/org/sasanlabs/fileupload/attackvectors/files/svg_xss_payload.svg new file mode 100644 index 0000000..3afdbe9 --- /dev/null +++ b/src/main/resources/org/sasanlabs/fileupload/attackvectors/files/svg_xss_payload.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/resources/org/sasanlabs/fileupload/i18n/Messages.properties b/src/main/resources/org/sasanlabs/fileupload/i18n/Messages.properties new file mode 100755 index 0000000..4d1224f --- /dev/null +++ b/src/main/resources/org/sasanlabs/fileupload/i18n/Messages.properties @@ -0,0 +1,95 @@ +# Options Panel +fileupload.settings.title=File Upload +fileupload.settings.button.reset=Reset +fileupload.settings.urilocator.title=URI Locator +fileupload.settings.urilocator.staticlocation.title=Static Location Configuration +fileupload.settings.urilocator.staticlocation.uriregex=URI Regex + +fileupload.settings.urilocator.dynamiclocation.title=Dynamic Location Configuration +fileupload.settings.urilocator.dynamiclocation.uriregex=URI Regex +fileupload.settings.urilocator.parseresponseconfiguration.title=Parse Http Response Configuration +fileupload.settings.urilocator.parseresponseconfiguration.startidentifer=Start Identifier +fileupload.settings.urilocator.parseresponseconfiguration.endidentifer=End Identifier + +fileupload.settings.alert.invalid.httpresponseparseconfiguration=Both dynamic start and end identifier should be present. +fileupload.settings.alert.static.dynamicconfiguration.both.present=Only one of the static and dynamic configuration should be present. + +# Alert details +fileupload.alert.attack=Retrieval Request: {0} \n Retrieval Response: {1} + +# FileUpload Scan Rule +fileupload.scanrule.name=File Upload +fileupload.scanrule.description=File Upload scan rule is used to scan the vulnerabilities in the File Upload functionality of web applications. +fileupload.scanrule.refs=https://cwe.mitre.org/data/definitions/434.html +fileupload.scanrule.soln=Follow the suggestions mentioned in following links: \ +1. https://portswigger.net/kb/issues/00500980_file-upload-functionality \ +2. https://www.youtube.com/watch?v=CmF9sEyKZNo +3. https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload + +fileupload.scanner.vulnerability.xssHtmlFile.name=Upload and download of html/htm/xhtml or other variants extension file. +fileupload.scanner.vulnerability.xssHtmlFile.description=Html or its variants if rendered in the browser can cause client-side attacks like XSS. +fileupload.scanner.vulnerability.xssHtmlFile.refs=https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload +fileupload.scanner.vulnerability.xssHtmlFile.soln=Follow the suggestions mentioned in following links: \ +1. https://wiki.owasp.org/index.php/Testing_for_Stored_Cross_site_scripting_(OTG-INPVAL-002)#:~:text=File%20Upload \ +2. https://portswigger.net/kb/issues/00500980_file-upload-functionality \ +3. https://www.youtube.com/watch?v=CmF9sEyKZNo \ + +fileupload.scanner.vulnerability.xssSvgFile.name=Upload and download of svg or other variants extension file. +fileupload.scanner.vulnerability.xssSvgFile.description=svg or its variants if rendered in the browser can cause client-side attacks like XSS. +fileupload.scanner.vulnerability.xssSvgFile.refs=https://cwe.mitre.org/data/definitions/434.html +fileupload.scanner.vulnerability.xssSvgFile.soln=Follow the suggestions mentioned in following links: \ +1. https://wiki.owasp.org/index.php/Testing_for_Stored_Cross_site_scripting_(OTG-INPVAL-002)#:~:text=File%20Upload \ +2. https://portswigger.net/kb/issues/00500980_file-upload-functionality \ +3. https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload \ +4. https://www.youtube.com/watch?v=CmF9sEyKZNo + +fileupload.scanner.vulnerability.rcePhpFile.name=Remote code execution by uploading Php file. +fileupload.scanner.vulnerability.rcePhpFile.description=Php file can be uploaded and executed on server hence server is Vulnerable to Remote code execution vulnerability. +fileupload.scanner.vulnerability.rcePhpFile.refs=https://cwe.mitre.org/data/definitions/434.html +fileupload.scanner.vulnerability.rcePhpFile.soln=Follow the suggestions mentioned in following links: \ +1. https://portswigger.net/kb/issues/00500980_file-upload-functionality \ +2. https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload \ +3. https://www.youtube.com/watch?v=CmF9sEyKZNo + +fileupload.scanner.vulnerability.rceJspFile.name=Remote code execution by uploading Jsp file. +fileupload.scanner.vulnerability.rceJspFile.description=Jsp file can be uploaded and executed on server hence server is Vulnerable to Remote code execution vulnerability. +fileupload.scanner.vulnerability.rceJspFile.refs=https://cwe.mitre.org/data/definitions/434.html +fileupload.scanner.vulnerability.rceJspFile.soln=Follow the suggestions mentioned in following links: \ +1. https://portswigger.net/kb/issues/00500980_file-upload-functionality \ +2. https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload \ +3. https://www.youtube.com/watch?v=CmF9sEyKZNo \ +4. https://cwe.mitre.org/data/definitions/434.html + +fileupload.scanner.vulnerability.rceGifJspFile.name=Remote code execution by uploading Gif image containing Jsp content. +fileupload.scanner.vulnerability.rceGifJspFile.description=Gif file containing Jsp content can be uploaded and Jsp code is executed on server hence server is Vulnerable to Remote code execution vulnerability. +fileupload.scanner.vulnerability.rceGifJspFile.refs=https://cwe.mitre.org/data/definitions/434.html +fileupload.scanner.vulnerability.rceGifJspFile.soln=Follow the suggestions mentioned in following links: \ +1. https://portswigger.net/kb/issues/00500980_file-upload-functionality \ +2. https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload \ +3. https://www.youtube.com/watch?v=CmF9sEyKZNo \ +4. https://cwe.mitre.org/data/definitions/434.html + +fileupload.scanner.vulnerability.rceJpegJspFile.name=Remote code execution by uploading Jpeg image containing Jsp content. +fileupload.scanner.vulnerability.rceJpegJspFile.description=Jpeg file containing Jsp content can be uploaded and Jsp code is executed on server hence server is Vulnerable to Remote code execution vulnerability. +fileupload.scanner.vulnerability.rceJpegJspFile.refs=https://cwe.mitre.org/data/definitions/434.html +fileupload.scanner.vulnerability.rceJpegJspFile.soln=Follow the suggestions mentioned in following links: \ +1. https://portswigger.net/kb/issues/00500980_file-upload-functionality \ +2. https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload \ +3. https://www.youtube.com/watch?v=CmF9sEyKZNo \ +4. https://cwe.mitre.org/data/definitions/434.html + +fileupload.scanner.vulnerability.rceJspxFile.name=Remote code execution by uploading Jspx file. +fileupload.scanner.vulnerability.rceJspxFile.description=Jspx file can be uploaded and executed on server hence server is Vulnerable to Remote code execution vulnerability. +fileupload.scanner.vulnerability.rceJspxFile.refs=https://cwe.mitre.org/data/definitions/434.html +fileupload.scanner.vulnerability.rceJspxFile.soln=Follow the suggestions mentioned in following links: \ +1. https://portswigger.net/kb/issues/00500980_file-upload-functionality \ +2. https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload \ +3. https://www.youtube.com/watch?v=CmF9sEyKZNo \ +4. https://cwe.mitre.org/data/definitions/434.html + +fileupload.scanner.vulnerability.antiVirusEicarFile.name=Eicar antivirus file upload attack +fileupload.scanner.vulnerability.antiVirusEicarFile.description=Eicar antivirus file was uploaded and downloaded which should be\ +detected by the antivirus. This means that either antivirus is not present or antivirus doesn't support Eicar. As per https://www.virustotal.com/ eicar file is supported by 63 Antiviruses out of 67. +fileupload.scanner.vulnerability.antiVirusEicarFile.refs=https://en.wikipedia.org/wiki/EICAR_test_file +fileupload.scanner.vulnerability.antiVirusEicarFile.soln= Consider checking the antivirus setup in server, in case the antivirus supports Eicar standards and still doesn't report issue, try fixing it.\ +https://owasp.org/www-project-web-security-testing-guide/stable/4-Web_Application_Security_Testing/10-Business_Logic_Testing/09-Test_Upload_of_Malicious_Files.html#malware \ No newline at end of file