├── .gitignore
├── .gradle
├── 4.4.1
│ ├── fileChanges
│ │ └── last-build.bin
│ ├── fileHashes
│ │ ├── fileHashes.bin
│ │ ├── fileHashes.lock
│ │ └── resourceHashesCache.bin
│ ├── taskHistory
│ │ ├── taskHistory.bin
│ │ └── taskHistory.lock
│ └── fileContent
│ │ ├── fileContent.lock
│ │ └── annotation-processors.bin
└── buildOutputCleanup
│ ├── cache.properties
│ ├── outputFiles.bin
│ └── buildOutputCleanup.lock
├── AnonymousCloud.jar
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── BappManifest.bmf
├── BappDescription.html
├── gradlew.bat
├── README.md
├── gradlew
└── src
└── burp
└── BurpExtender.java
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | build/
3 |
--------------------------------------------------------------------------------
/.gradle/4.4.1/fileChanges/last-build.bin:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gradle/buildOutputCleanup/cache.properties:
--------------------------------------------------------------------------------
1 | #Wed Feb 10 13:38:39 UTC 2021
2 | gradle.version=4.4.1
3 |
--------------------------------------------------------------------------------
/AnonymousCloud.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/AnonymousCloud.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gradle/4.4.1/fileHashes/fileHashes.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/4.4.1/fileHashes/fileHashes.bin
--------------------------------------------------------------------------------
/.gradle/4.4.1/fileHashes/fileHashes.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/4.4.1/fileHashes/fileHashes.lock
--------------------------------------------------------------------------------
/.gradle/4.4.1/taskHistory/taskHistory.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/4.4.1/taskHistory/taskHistory.bin
--------------------------------------------------------------------------------
/.gradle/4.4.1/fileContent/fileContent.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/4.4.1/fileContent/fileContent.lock
--------------------------------------------------------------------------------
/.gradle/4.4.1/taskHistory/taskHistory.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/4.4.1/taskHistory/taskHistory.lock
--------------------------------------------------------------------------------
/.gradle/buildOutputCleanup/outputFiles.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/buildOutputCleanup/outputFiles.bin
--------------------------------------------------------------------------------
/.gradle/4.4.1/fileHashes/resourceHashesCache.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/4.4.1/fileHashes/resourceHashesCache.bin
--------------------------------------------------------------------------------
/.gradle/4.4.1/fileContent/annotation-processors.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/4.4.1/fileContent/annotation-processors.bin
--------------------------------------------------------------------------------
/.gradle/buildOutputCleanup/buildOutputCleanup.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PortSwigger/anonymous-cloud/master/.gradle/buildOutputCleanup/buildOutputCleanup.lock
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by the Gradle 'init' task.
3 | *
4 | * The settings file is used to specify which projects to include in your build.
5 | *
6 | * Detailed information about configuring a multi-project build in Gradle can be found
7 | * in the user manual at https://docs.gradle.org/6.6.1/userguide/multi_project_builds.html
8 | */
9 |
10 | rootProject.name = 'AnonymousCloud'
11 |
--------------------------------------------------------------------------------
/BappManifest.bmf:
--------------------------------------------------------------------------------
1 | Uuid: ea60f107b25d44ddb59c1aee3786c6a1
2 | ExtensionType: 1
3 | Name: Anonymous Cloud, Configuration and Subdomain Takeover Scanner
4 | RepoName: anonymous-cloud
5 | ScreenVersion: 0.1.14
6 | SerialVersion: 4
7 | MinPlatformVersion: 0
8 | ProOnly: True
9 | Author: codewatchorg
10 | ShortDescription: Burp extension that performs a passive scan to identify cloud buckets and then test them for publicly accessible vulnerabilities.
11 | EntryPoint: build/libs/AnonymousCloud.jar
12 | BuildCommand: ./gradlew jar
13 | SupportedProducts: Pro
14 |
--------------------------------------------------------------------------------
/BappDescription.html:
--------------------------------------------------------------------------------
1 |
Burp extension that performs a passive scan to identify cloud buckets and then test them for publicly accessible vulnerabilities.
2 |
3 | The extension looks at all responses and will note:
4 |
5 | - AWS S3 bucket URLs.
6 | - Azure Storage container URLs.
7 | - Google Storage container URLs.
8 |
9 |
10 | Usage:
11 |
12 | - Add the JAR as an extension in Burp.
13 | - Add the appropriate targets to scope.
14 | - Begin manually browsing and scanning the target.
15 | - If you want to test for permissions issues that allow all authenticated AWS/GCP users, then add your personal AWS/GCP credentials, and click the "Set Configuration" button.
16 | - If you want to check for potential subdomain takeover vulnerabilities, add API keys for Shodan and Censys (if you want to use both), in addition to a text file list of subdomains (if you want), check the subdomain takeover configuration box, and click the "Set Configuration" button.
17 |
18 |
19 | For a full list of the features, please check out the GitHub link below.
20 |
21 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Burp-AnonymousCloud
2 | Burp extension that performs a passive scan to identify cloud buckets and then test them for publicly accessible vulnerabilities.
3 |
4 | The extension looks at all responses and will note:
5 | 1. AWS S3 bucket URLs.
6 | 2. Azure Storage container URLs.
7 | 3. Google Storage container URLs.
8 |
9 | The extension checks the following things as an anonymous user:
10 | 1. Publicly accessible S3 buckets which will be enumerated by the extension.
11 | 2. Publicly accessible ACLs on S3 buckets which will be enumerated by the extension.
12 | 3. Publicly writable S3 buckets, to which a sample file will be written.
13 | 4. Publicly writable ACLs on S3 buckets.
14 | 5. Publicly accessible Google Storage containers which will be enumerated by the extension.
15 | 6. Publicly accessible ACLs on Google Storage containers which will be enumerated by the extension.
16 | 7. Publicly writable Google Storage containers, to which a sample file will be written.
17 | 8. Publicly accessible Azure Storage containers which will be enumerated by the extension.
18 | 9. Publicly accessible Firebase DBs and anonymous read/write access.
19 |
20 | The extension checks the following things in AWS/Google as an authenticated AWS/Google user (though not a defined user for the bucket itself):
21 | 1. Any authenticated AWS user accessible S3 buckets which will be enumerated by the extension.
22 | 2. Any authenticated AWS user accessible ACLs on S3 buckets which will be enumerated by the extension.
23 | 3. Any authenticated AWS user writable S3 buckets, to which a sample file will be written.
24 | 4. Any authenticated AWS user writable ACLs on S3 buckets.
25 | 5. Any authenticated Google user accessible Google Storage containers which will be enumerated by the extension.
26 | 6. Any authenticated Google user accessible ACLs on Google Storage containers which will be enumerated by the extension.
27 | 7. Any authenticated Google user writable Google Storage containers, to which a sample file will be written.
28 |
29 | The extension performs subdomain takeover testing for the following resoures:
30 | 1. CNAMEs pointing to non-existent AWS S3 buckets.
31 | 2. CNAMEs pointing to non-existent Azure resources.
32 | 3. CNAMEs pointing to non-existent Heroku services.
33 | 4. CNAMEs pointing to non-existent Github pages.
34 |
35 | Subdomains are collected from the following:
36 | 1. HackerTarget
37 | 2. BufferOver
38 | 3. Wayback Machine
39 | 4. Crt.sh
40 | 5. File list
41 | 6. Shodan (with an API key)
42 | 7. Censys (with an API key)
43 |
44 | Usage
45 | =====
46 |
47 | All you have to do is:
48 | 1. Add the JAR as an extension in Burp.
49 | 2. Add the appropriate targets to scope.
50 | 3. Begin manually browsing and scanning the target.
51 | 4. If you want to test for permissions issues that allow all authenticated AWS/GCP users, then add your personal AWS/GCP credentials, and click the "Set Configuration" button.
52 | 5. If you want to check for potential subdomain takeover vulnerabilities, add API keys for Shodan and Censys (if you want to use both), in addition to a text file list of subdomains (if you want), check the subdomain takeover configuration box, and click the "Set Configuration" button.
53 |
54 |
55 | Future
56 | ======
57 |
58 | Continue adding features to support identification and enumeration of other resources such Azure database.
59 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/src/burp/BurpExtender.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Name: Burp Anonymous Cloud
3 | * Version: 0.1.14
4 | * Date: 1/21/2020
5 | * Author: Josh Berry - josh.berry@codewatch.org
6 | * Github: https://github.com/codewatchorg/Burp-AnonymousCloud
7 | *
8 | * Description: This plugin checks for insecure AWS/Azure/Google application configurations
9 | *
10 | * Contains regex work from Cloud Storage Tester by VirtueSecurity: https://github.com/VirtueSecurity/aws-extender
11 | * Implemented an idea from https://github.com/0xSearches/sandcastle
12 | * Implemented AWS checks included in https://gist.github.com/fransr/a155e5bd7ab11c93923ec8ce788e3368
13 | *
14 | */
15 |
16 | package burp;
17 |
18 | import java.util.List;
19 | import java.util.ArrayList;
20 | import java.util.Random;
21 | import java.io.InputStreamReader;
22 | import java.io.BufferedReader;
23 | import java.io.FileReader;
24 | import java.io.PrintWriter;
25 | import java.io.File;
26 | import java.net.URL;
27 | import java.net.InetAddress;
28 | import java.util.regex.Matcher;
29 | import java.util.regex.Pattern;
30 | import java.util.Base64;
31 | import java.nio.charset.StandardCharsets;
32 | import java.awt.Component;
33 | import javax.swing.JPanel;
34 | import javax.swing.JLabel;
35 | import javax.swing.JTextField;
36 | import javax.swing.JButton;
37 | import java.awt.event.ActionEvent;
38 | import java.awt.event.ActionListener;
39 | import javax.swing.JCheckBox;
40 | import javax.swing.JFileChooser;
41 | import javax.swing.filechooser.FileNameExtensionFilter;
42 | import com.amazonaws.services.s3.AmazonS3;
43 | import com.amazonaws.services.s3.AmazonS3ClientBuilder;
44 | import com.amazonaws.auth.AWSCredentials;
45 | import com.amazonaws.auth.AnonymousAWSCredentials;
46 | import com.amazonaws.auth.BasicAWSCredentials;
47 | import com.amazonaws.auth.AWSStaticCredentialsProvider;
48 | import com.amazonaws.services.s3.model.ObjectListing;
49 | import com.amazonaws.services.s3.model.S3ObjectSummary;
50 | import com.amazonaws.services.s3.model.S3Object;
51 | import com.amazonaws.services.s3.model.HeadBucketRequest;
52 | import com.amazonaws.services.s3.model.AccessControlList;
53 | import com.amazonaws.services.s3.model.GroupGrantee;
54 | import com.amazonaws.services.s3.model.Permission;
55 | import com.amazonaws.regions.Regions;
56 | import org.apache.http.HttpResponse;
57 | import org.apache.http.client.HttpClient;
58 | import org.apache.http.Header;
59 | import org.apache.http.message.BasicHeader;
60 | import org.apache.http.client.methods.HttpGet;
61 | import org.apache.http.client.methods.HttpPost;
62 | import org.apache.http.impl.client.HttpClientBuilder;
63 | import org.apache.http.entity.StringEntity;
64 | import org.apache.http.util.EntityUtils;
65 | import java.util.Iterator;
66 | import org.json.JSONObject;
67 | import org.json.JSONArray;
68 | import org.xml.sax.*;
69 | import org.xml.sax.helpers.*;
70 | import javax.xml.parsers.*;
71 | import java.io.StringReader;
72 | import javax.naming.directory.InitialDirContext;
73 | import javax.naming.Context;
74 | import java.util.Properties;
75 | import java.util.Arrays;
76 | import java.time.format.DateTimeFormatter;
77 | import java.time.ZonedDateTime;
78 | import java.time.ZoneOffset;
79 |
80 | class SubdomainTakeover implements IBurpExtender, Runnable {
81 | private Thread t;
82 | private final String domainname;
83 | private String censysApiKey = "";
84 | private String censysApiSecret = "";
85 | private Boolean isShodanSet = false;
86 | private Boolean isCensysSet = false;
87 | private Boolean isFileListSet = false;
88 | private final PrintWriter printOut;
89 | private ArrayList subdomainList = new ArrayList();
90 | private static final String certTransUrl = "https://crt.sh/?output=json&q=";
91 | private static final String bufferOverUrl = "https://dns.bufferover.run/dns?q=";
92 | private static final String waybackMachineUrl = "http://web.archive.org/cdx/search/cdx?output=json&url=";
93 | private static final String hackerTargetUrl = "http://api.hackertarget.com/hostsearch/?q=";
94 | private static final String shodanBaseUrl = "https://api.shodan.io/dns/domain/";
95 | private static final String censysBaseUrl = "https://search.censys.io/api/v1/search/";
96 | private static final Pattern censysCertPattern = Pattern.compile("([\\w.-]*CN\\=(.*)?)", Pattern.CASE_INSENSITIVE );
97 | private String shodanUrl = "";
98 | private String censysUrl = "";
99 | private File subdomainFileList;
100 | private IBurpExtenderCallbacks extCallbacks;
101 | private IHttpRequestResponse messageInfo;
102 | public IExtensionHelpers extHelpers;
103 |
104 | public SubdomainTakeover(IBurpExtenderCallbacks callbacks, IHttpRequestResponse messageInfo, String fqdn, PrintWriter burpPrint, String shodan, String censys, File subdomainFile) {
105 | domainname = fqdn;
106 | printOut = burpPrint;
107 | extCallbacks = callbacks;
108 | this.messageInfo = messageInfo;
109 | extHelpers = extCallbacks.getHelpers();
110 |
111 | try {
112 | if (subdomainFile.exists() && subdomainFile.length() > 0) {
113 | subdomainFileList = subdomainFile;
114 | isFileListSet = true;
115 | }
116 | } catch (Exception ignore) {}
117 |
118 | if (shodan.matches("^[a-zA-Z0-9]+")) {
119 | shodanUrl = shodanBaseUrl + domainname + "?key=" + shodan;
120 | isShodanSet = true;
121 | }
122 |
123 | if (censys.length() > 10 && censys.split(":").length == 2) {
124 | if (censys.split(":")[0].matches("^[a-zA-Z0-9\\-]+") && censys.split(":")[1].matches("^[a-zA-Z0-9]+")) {
125 | censysApiKey = censys.split(":")[0];
126 | censysApiSecret = censys.split(":")[1];
127 | censysUrl = censysBaseUrl + "certificates";
128 | isCensysSet = true;
129 | }
130 | }
131 | }
132 |
133 | // helper method to search a response for occurrences of a literal match string
134 | // and return a list of start/end offsets
135 | private List getMatches(byte[] response, byte[] match) {
136 | List matches = new ArrayList<>();
137 |
138 | int start = 0;
139 | while (start < response.length) {
140 | start = extHelpers.indexOf(response, match, true, start, response.length);
141 | if (start == -1)
142 | break;
143 | matches.add(new int[] { start, start + match.length });
144 | start += match.length;
145 | }
146 |
147 | return matches;
148 | }
149 |
150 | public void start() {
151 | if (t == null) {
152 | t = new Thread(this, domainname);
153 | t.start();
154 | }
155 | }
156 |
157 | // Create domains from file list
158 | private void getListSubdomains() {
159 | // Try to open and read the file
160 | try {
161 | BufferedReader rd = new BufferedReader(new FileReader(subdomainFileList));
162 | String line = null;
163 |
164 | printOut.println("Building a list from the file: " + subdomainFileList.getPath());
165 | // Loop through each line
166 | while((line = rd.readLine()) != null) {
167 |
168 | // Add to subdomain list if unique
169 | if (!subdomainList.contains(line + "." + domainname) && !line.equals(domainname) && !line.contains("*")) {
170 | subdomainList.add(line + "." + domainname);
171 | }
172 | }
173 | } catch (Exception ignore) {}
174 | }
175 |
176 | // Get subdomains from Censys, because they are different than the rest
177 | private void getCensysSubdomains(String urlType, String srcUrl) {
178 | // Create a client to check the source URL for domains
179 | String credentials = Base64.getEncoder().encodeToString((censysApiKey + ":" + censysApiSecret).getBytes(StandardCharsets.UTF_8));
180 | HttpPost reqSubdomain = new HttpPost(srcUrl);
181 | HttpClient subdomainClient = HttpClientBuilder.create().build();
182 |
183 | // Connect to the site to get subdomains
184 | try {
185 | reqSubdomain.setHeader("Authorization", "Basic " + credentials);
186 | reqSubdomain.setEntity(new StringEntity("{ \"query\": \"" + domainname + "\" }"));
187 | HttpResponse resp = subdomainClient.execute(reqSubdomain);
188 | String headers = resp.getStatusLine().toString();
189 | printOut.println("Building a request to: " + srcUrl);
190 |
191 | // If the status is 200, then hopefully we got JSON or plaintext response with subdomains
192 | if (headers.contains("200 OK")) {
193 |
194 | // Read the response and get the JSON
195 | BufferedReader rd = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
196 | String jsonStr = "";
197 | String line = "";
198 | while ((line = rd.readLine()) != null) {
199 | jsonStr = jsonStr + line;
200 | }
201 |
202 | // Read JSON results
203 | JSONObject json = new JSONObject(jsonStr);
204 | JSONArray subdomainObjs = json.getJSONArray("results");
205 |
206 | // Loop through our list to build create unique objects
207 | for (int i = 0; i < subdomainObjs.length(); i++) {
208 | String obj = subdomainObjs.getJSONObject(i).getString("parsed.subject_dn");
209 | Matcher censysCertMatcher = censysCertPattern.matcher(obj);
210 |
211 | if (censysCertMatcher.find()) {
212 | String subdomainLine = censysCertMatcher.group(0).split("=")[1];
213 |
214 | // Add to subdomain list if unique
215 | if (!subdomainList.contains(subdomainLine) && !subdomainLine.equals(domainname) && !subdomainLine.contains("*")) {
216 | subdomainList.add(subdomainLine);
217 | }
218 | }
219 | }
220 | } else { }
221 | } catch (Exception ignore) { }
222 | }
223 |
224 | // Get subdomains from common sources
225 | private void getSubdomains(String urlType, String srcUrl) {
226 | // Create a client to check the source URL for domains
227 | HttpGet reqSubdomain = new HttpGet(srcUrl);
228 | HttpClient subdomainClient = HttpClientBuilder.create().build();
229 |
230 | // Connect to the site to get subdomains
231 | try {
232 | HttpResponse resp = subdomainClient.execute(reqSubdomain);
233 | String headers = resp.getStatusLine().toString();
234 | printOut.println("Building a request to: " + srcUrl);
235 |
236 | // If the status is 200, then hopefully we got JSON or plaintext response with subdomains
237 | if (headers.contains("200 OK")) {
238 |
239 | // Read the response and get the JSON
240 | BufferedReader rd = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
241 |
242 | // Perform lookiup on crt.sh
243 | if (urlType.contains("crt.sh")) {
244 | String jsonStr = "";
245 | String line = "";
246 | while ((line = rd.readLine()) != null) {
247 | jsonStr = jsonStr + line;
248 | }
249 |
250 | // Read JSON results
251 | JSONArray subdomainObjs = new JSONArray(jsonStr);
252 |
253 | // Loop through our list to build create unique objects
254 | for (int i = 0; i < subdomainObjs.length(); i++) {
255 | JSONObject obj = subdomainObjs.getJSONObject(i);
256 | BufferedReader subdomainBuffer = new BufferedReader(new StringReader(obj.getString("name_value")));
257 | String subdomainLine = "";
258 |
259 | // Loop through each line in the result
260 | while((subdomainLine = subdomainBuffer.readLine()) != null) {
261 |
262 | // Add to subdomain list if unique
263 | if (!subdomainList.contains(subdomainLine) && !subdomainLine.equals(domainname) && !subdomainLine.contains("*")) {
264 | subdomainList.add(subdomainLine);
265 | }
266 | }
267 | }
268 |
269 | // Perform lookup on BufferOver
270 | } else if (urlType.contains("BufferOver")) {
271 | String jsonStr = "";
272 | String line = "";
273 | while ((line = rd.readLine()) != null) {
274 | jsonStr = jsonStr + line;
275 | }
276 |
277 | // Read JSON results
278 | JSONObject json = new JSONObject(jsonStr);
279 | JSONArray subdomainAObjs = json.getJSONArray("FDNS_A");
280 | JSONArray subdomainRObjs = json.getJSONArray("RDNS");
281 |
282 | // Loop through our list to build create unique objects
283 | for (int i = 0; i < subdomainAObjs.length(); i++) {
284 |
285 | // Add to subdomain list if unique
286 | if (!subdomainList.contains(subdomainAObjs.get(i).toString().split(",")[1]) &&
287 | !subdomainAObjs.get(i).toString().split(",")[1].equals(domainname) &&
288 | !subdomainAObjs.get(i).toString().split(",")[1].contains("*")) {
289 | subdomainList.add(subdomainAObjs.get(i).toString().split(",")[1]);
290 | }
291 | }
292 |
293 | // Loop through our list to build create unique objects
294 | for (int i = 0; i < subdomainRObjs.length(); i++) {
295 |
296 | // Add to subdomain list if unique
297 | if (!subdomainList.contains(subdomainRObjs.get(i).toString().split(",")[1]) &&
298 | !subdomainRObjs.get(i).toString().split(",")[1].equals(domainname) &&
299 | !subdomainRObjs.get(i).toString().split(",")[1].contains("*")) {
300 | subdomainList.add(subdomainRObjs.get(i).toString().split(",")[1]);
301 | }
302 | }
303 |
304 | // Perform lookup on Wayback Machine
305 | } else if (urlType.contains("WaybackMachine")) {
306 | String jsonStr = "";
307 | String line = "";
308 | int lineCount = 0;
309 |
310 | // Loop through each line of output, skip the first line
311 | while ((line = rd.readLine()) != null) {
312 | if (lineCount == 0) {
313 | lineCount++;
314 | } else {
315 |
316 | // Pull out subdomain
317 | String subdomainUrl = line.split(",")[3].split("/")[2].split(":")[0];
318 | subdomainUrl = subdomainUrl.replace("\"", "");
319 |
320 | // Add to subdomain list if unique
321 | if (!subdomainList.contains(subdomainUrl) && !subdomainUrl.equals(domainname) && !subdomainUrl.contains("*") && subdomainUrl.contains(domainname)) {
322 | subdomainList.add(subdomainUrl);
323 | }
324 | }
325 | }
326 |
327 | // Perform lookup on Hacker Target
328 | } else if (urlType.contains("HackerTarget")) {
329 | String jsonStr = "";
330 | String line = "";
331 |
332 | // Loop through each line of output
333 | while ((line = rd.readLine()) != null) {
334 |
335 | // Pull out subdomain
336 | String subdomainUrl = line.split(",")[0];
337 |
338 | // Add to subdomain list if unique
339 | if (!subdomainList.contains(subdomainUrl) && !subdomainUrl.equals(domainname) && !subdomainUrl.contains("*") && !subdomainUrl.contains("error check")) {
340 | subdomainList.add(subdomainUrl);
341 | }
342 | }
343 |
344 | // Perform lookup on Shodan
345 | } else if (urlType.contains("Shodan")) {
346 | String jsonStr = "";
347 | String line = "";
348 |
349 | while ((line = rd.readLine()) != null) {
350 | jsonStr = jsonStr + line;
351 | }
352 |
353 | // Read JSON results
354 | JSONObject json = new JSONObject(jsonStr);
355 | JSONArray subdomainObjs = json.getJSONArray("subdomains");
356 |
357 | // Loop through our list to build create unique objects
358 | for (int i = 0; i < subdomainObjs.length(); i++) {
359 |
360 | // Add to subdomain list if unique
361 | if (!subdomainList.contains(subdomainObjs.get(i).toString()) &&
362 | !subdomainObjs.get(i).toString().equals(domainname) &&
363 | !subdomainObjs.get(i).toString().contains("*")) {
364 | subdomainList.add(subdomainObjs.get(i).toString() + "." + domainname);
365 | }
366 | }
367 | }
368 | }
369 | } catch (Exception ignore) { }
370 | }
371 |
372 | // Return the CNAME status
373 | private Boolean checkDns(String domain, Pattern cnamePattern) {
374 | Boolean cnameValid = false;
375 |
376 | // Perform the lookup to get CNAMEs
377 | try {
378 | Properties env = new Properties();
379 | env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
380 | env.put(Context.PROVIDER_URL, "dns://1.1.1.1");
381 | InitialDirContext idc = new InitialDirContext(env);
382 | javax.naming.directory.Attributes attrs = idc.getAttributes(domain, new String[]{"CNAME"});
383 | javax.naming.directory.Attribute attr = attrs.get("CNAME");
384 |
385 | Matcher cnameMatcher = cnamePattern.matcher(attr.get().toString());
386 |
387 | // if the cname part matches, then likely vulnerable
388 | if (cnameMatcher.find()) {
389 | cnameValid = true;
390 | }
391 | } catch (Exception ignore) { }
392 |
393 | return cnameValid;
394 | }
395 |
396 | // Return the CNAME value
397 | private String checkDnsOnly(String domain) {
398 | String cnameValue = "";
399 |
400 | // Perform the lookup to get CNAMEs
401 | try {
402 | Properties env = new Properties();
403 | env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
404 | env.put(Context.PROVIDER_URL, "dns://1.1.1.1");
405 | InitialDirContext idc = new InitialDirContext(env);
406 | javax.naming.directory.Attributes attrs = idc.getAttributes(domain, new String[]{"CNAME"});
407 | javax.naming.directory.Attribute attr = attrs.get("CNAME");
408 | cnameValue = attr.get().toString();
409 | } catch (Exception ignore) { }
410 |
411 | return cnameValue;
412 | }
413 |
414 | // Get subdomains from common sources
415 | private void scanSubdomains() {
416 |
417 | // Create patterns for matching reponses indicating subdomain takeover potential
418 | Pattern s3Pattern = Pattern.compile("(NoSuchBucket)", Pattern.CASE_INSENSITIVE );
419 | Pattern s3CnamePattern = Pattern.compile("(\\.s3\\.amazonaws\\.com)", Pattern.CASE_INSENSITIVE );
420 | Pattern herokuPattern = Pattern.compile("(herokucdn\\.com\\/error-pages\\/no-such-app\\.html)", Pattern.CASE_INSENSITIVE );
421 | Pattern herokuCnamePattern = Pattern.compile("(\\.herokuapp\\.com|\\.herokudns\\.com|\\.herokussl\\.com)", Pattern.CASE_INSENSITIVE );
422 | Pattern githubIoPattern = Pattern.compile("(There isn't a GitHub Pages site here\\.)", Pattern.CASE_INSENSITIVE );
423 | Pattern githubCnamePattern = Pattern.compile("(\\.github\\.io)", Pattern.CASE_INSENSITIVE );
424 |
425 | // Loop through the list of subdomains to test
426 | for (int i = 0; i < subdomainList.size(); i++) {
427 | Boolean subdomainSuccess = false;
428 |
429 | // Create a client to check for subdomain takeover
430 | HttpGet reqSubdomainHttp = new HttpGet("http://" + subdomainList.get(i));
431 | HttpClient subdomainClientHttp = HttpClientBuilder.create().build();
432 |
433 | // Connect to the site via http to get response for potential subdomain takeover
434 | try {
435 | HttpResponse resp = subdomainClientHttp.execute(reqSubdomainHttp);
436 | String headers = resp.getStatusLine().toString();
437 |
438 | // If the status is 404, then it might be vulnerable
439 | if (headers.contains("404 Not Found")) {
440 | String respStr = EntityUtils.toString(resp.getEntity());
441 | Matcher s3Matcher = s3Pattern.matcher(respStr);
442 | Matcher herokuMatcher = herokuPattern.matcher(respStr);
443 | Matcher githubIoMatcher = githubIoPattern.matcher(respStr);
444 |
445 | // If there is a match for the s3 pattern then it is probably vulnerable
446 | if (s3Matcher.find()) {
447 |
448 | // Validate CNAME
449 | if (checkDns(subdomainList.get(i).toString(), s3CnamePattern)) {
450 | printOut.println("Potential subdomain takeover of an S3 bucket found for: http://" + subdomainList.get(i));
451 | URL subdomainUrl = new URL("http://" + subdomainList.get(i));
452 |
453 | // Create an issue from the finding
454 | List s3SubdomainMatches = getMatches(messageInfo.getResponse(), s3Matcher.group(0).getBytes());
455 | IScanIssue subdomainS3IdIssue = new CustomScanIssue(
456 | messageInfo.getHttpService(),
457 | subdomainUrl,
458 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, s3SubdomainMatches) },
459 | "[Anonymous Cloud] Subdomain Takeover - " + subdomainList.get(i),
460 | "The response for the following subdomain returned 'NoSuchDomain', indicating vulnerability to subdomain takeover via s3 bucket.
See: also: https://github.com/EdOverflow/can-i-take-over-xyz.",
461 | "High",
462 | "Firm"
463 | );
464 |
465 | // Add the S3 subdomain takeover issue
466 | extCallbacks.addScanIssue(subdomainS3IdIssue);
467 | subdomainSuccess = true;
468 | }
469 | }
470 |
471 | // If there is a match for the Heroku pattern then it is probably vulnerable
472 | if (herokuMatcher.find()) {
473 |
474 | // Validate CNAME
475 | if (checkDns(subdomainList.get(i).toString(), herokuCnamePattern)) {
476 | printOut.println("Potential subdomain takeover of a Heroku app found for: http://" + subdomainList.get(i));
477 | URL subdomainUrl = new URL("http://" + subdomainList.get(i));
478 |
479 | // Create an issue from the finding
480 | List herokuSubdomainMatches = getMatches(messageInfo.getResponse(), herokuMatcher.group(0).getBytes());
481 | IScanIssue subdomainHerokuIdIssue = new CustomScanIssue(
482 | messageInfo.getHttpService(),
483 | subdomainUrl,
484 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, herokuSubdomainMatches) },
485 | "[Anonymous Cloud] Subdomain Takeover - " + subdomainList.get(i),
486 | "The response for the following subdomain returned 'herokucdn.com/error-pages/no-such-app.html', indicating vulnerability to subdomain takeover via Heroku app.
See: also: https://github.com/EdOverflow/can-i-take-over-xyz.",
487 | "High",
488 | "Firm"
489 | );
490 |
491 | // Add the Heroku subdomain takeover issue
492 | extCallbacks.addScanIssue(subdomainHerokuIdIssue);
493 | subdomainSuccess = true;
494 | }
495 | }
496 |
497 | // If there is a match for the Github.io pattern then it is probably vulnerable
498 | if (githubIoMatcher.find()) {
499 |
500 | // Validate CNAME
501 | if (checkDns(subdomainList.get(i).toString(), githubCnamePattern)) {
502 | printOut.println("Potential subdomain takeover of a Github.io pages result for: http://" + subdomainList.get(i));
503 | URL subdomainUrl = new URL("http://" + subdomainList.get(i));
504 |
505 | // Create an issue from the finding
506 | List githubIoSubdomainMatches = getMatches(messageInfo.getResponse(), githubIoMatcher.group(0).getBytes());
507 | IScanIssue subdomainGithubIoIdIssue = new CustomScanIssue(
508 | messageInfo.getHttpService(),
509 | subdomainUrl,
510 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, githubIoSubdomainMatches) },
511 | "[Anonymous Cloud] Subdomain Takeover - " + subdomainList.get(i),
512 | "The response for the following subdomain returned 'There isn't a GitHub Pages site here.', indicating vulnerability to subdomain takeover via Github.io pages.
See: also: https://github.com/EdOverflow/can-i-take-over-xyz.",
513 | "High",
514 | "Firm"
515 | );
516 |
517 | // Add the Heroku subdomain takeover issue
518 | extCallbacks.addScanIssue(subdomainGithubIoIdIssue);
519 | subdomainSuccess = true;
520 | }
521 | }
522 | }
523 | } catch (Exception ignore) { }
524 |
525 | // Try again with https if we didn't already find something
526 | if (!subdomainSuccess) {
527 | // Create an http client to check for subdomain takeover
528 | HttpGet reqSubdomainHttps = new HttpGet("https://" + subdomainList.get(i));
529 | HttpClient subdomainClientHttps = HttpClientBuilder.create().build();
530 |
531 | // Connect to the site via https to get response for potential subdomain takeover
532 | try {
533 | HttpResponse resp = subdomainClientHttps.execute(reqSubdomainHttps);
534 | String headers = resp.getStatusLine().toString();
535 |
536 | // If the status is 200, then hopefully we got JSON or plaintext response with subdomains
537 | if (headers.contains("404 Not Found")) {
538 | String respStr = EntityUtils.toString(resp.getEntity());
539 | Matcher s3Matcher = s3Pattern.matcher(respStr);
540 | Matcher herokuMatcher = herokuPattern.matcher(respStr);
541 | Matcher githubIoMatcher = githubIoPattern.matcher(respStr);
542 |
543 | // If there is a match for the s3 pattern then it is probably vulnerable
544 | if (s3Matcher.find()) {
545 |
546 | // Validate CNAME
547 | if (checkDns(subdomainList.get(i).toString(), s3CnamePattern)) {
548 | printOut.println("Potential subdomain takeover of an S3 bucket found for: https://" + subdomainList.get(i));
549 | URL subdomainUrl = new URL("https://" + subdomainList.get(i));
550 |
551 | // Create an issue from the finding
552 | List s3SubdomainMatches = getMatches(messageInfo.getResponse(), s3Matcher.group(0).getBytes());
553 | IScanIssue subdomainS3IdIssue = new CustomScanIssue(
554 | messageInfo.getHttpService(),
555 | subdomainUrl,
556 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, s3SubdomainMatches) },
557 | "[Anonymous Cloud] Subdomain Takeover - " + subdomainList.get(i),
558 | "The response for the following subdomain returned 'NoSuchDomain', indicating vulnerability to subdomain takeover via s3 bucket.
See: also: https://github.com/EdOverflow/can-i-take-over-xyz.",
559 | "High",
560 | "Firm"
561 | );
562 |
563 | // Add the S3 subdomain takeover issue
564 | extCallbacks.addScanIssue(subdomainS3IdIssue);
565 | subdomainSuccess = true;
566 | }
567 | }
568 |
569 | // If there is a match for the Heroku pattern then it is probably vulnerable
570 | if (herokuMatcher.find()) {
571 |
572 | // Validate CNAME
573 | if (checkDns(subdomainList.get(i).toString(), herokuCnamePattern)) {
574 | printOut.println("Potential subdomain takeover of a Heroku app found for: https://" + subdomainList.get(i));
575 | URL subdomainUrl = new URL("https://" + subdomainList.get(i));
576 |
577 | // Create an issue from the finding
578 | List herokuSubdomainMatches = getMatches(messageInfo.getResponse(), herokuMatcher.group(0).getBytes());
579 | IScanIssue subdomainHerokuIdIssue = new CustomScanIssue(
580 | messageInfo.getHttpService(),
581 | subdomainUrl,
582 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, herokuSubdomainMatches) },
583 | "[Anonymous Cloud] Subdomain Takeover - " + subdomainList.get(i),
584 | "The response for the following subdomain returned 'herokucdn.com/error-pages/no-such-app.html', indicating vulnerability to subdomain takeover via Heroku app.
See: also: https://github.com/EdOverflow/can-i-take-over-xyz.",
585 | "High",
586 | "Firm"
587 | );
588 |
589 | // Add the Heroku subdomain takeover issue
590 | extCallbacks.addScanIssue(subdomainHerokuIdIssue);
591 | subdomainSuccess = true;
592 | }
593 | }
594 |
595 | // If there is a match for the Github.io pattern then it is probably vulnerable
596 | if (githubIoMatcher.find()) {
597 |
598 | // Validate CNAME
599 | if (checkDns(subdomainList.get(i).toString(), githubCnamePattern)) {
600 | printOut.println("Potential subdomain takeover of a Github.io pages result for: https://" + subdomainList.get(i));
601 | URL subdomainUrl = new URL("https://" + subdomainList.get(i));
602 |
603 | // Create an issue from the finding
604 | List githubIoSubdomainMatches = getMatches(messageInfo.getResponse(), githubIoMatcher.group(0).getBytes());
605 | IScanIssue subdomainGithubIoIdIssue = new CustomScanIssue(
606 | messageInfo.getHttpService(),
607 | subdomainUrl,
608 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, githubIoSubdomainMatches) },
609 | "[Anonymous Cloud] Subdomain Takeover - " + subdomainList.get(i),
610 | "The response for the following subdomain returned 'There isn't a GitHub Pages site here.', indicating vulnerability to subdomain takeover via Github.io pages.
See: also: https://github.com/EdOverflow/can-i-take-over-xyz.",
611 | "High",
612 | "Firm"
613 | );
614 |
615 | // Add the Github Pages subdomain takeover issue
616 | extCallbacks.addScanIssue(subdomainGithubIoIdIssue);
617 | subdomainSuccess = true;
618 | }
619 | }
620 | }
621 | } catch (Exception ignore) { }
622 | }
623 | }
624 | }
625 |
626 | // Get subdomains from common sources
627 | private void scanDnsSubdomains() {
628 |
629 | // Create patterns for Azure resources
630 | Pattern[] azurePatterns = {
631 | Pattern.compile("(\\.cloudapp\\.net)", Pattern.CASE_INSENSITIVE ),
632 | Pattern.compile("(\\.cloudapp\\.azure\\.com)", Pattern.CASE_INSENSITIVE ),
633 | Pattern.compile("(\\.azurewebsites\\.com)", Pattern.CASE_INSENSITIVE ),
634 | Pattern.compile("(\\.blob\\.core\\.windows\\.net)", Pattern.CASE_INSENSITIVE ),
635 | Pattern.compile("(\\.azure-api\\.com)", Pattern.CASE_INSENSITIVE ),
636 | Pattern.compile("(\\.azurecontainer\\.io)", Pattern.CASE_INSENSITIVE ),
637 | Pattern.compile("(\\.database\\.windows\\.net)", Pattern.CASE_INSENSITIVE ),
638 | Pattern.compile("(\\.azuredatalakestore\\.net)", Pattern.CASE_INSENSITIVE ),
639 | Pattern.compile("(\\.search\\.windows\\.net)", Pattern.CASE_INSENSITIVE ),
640 | Pattern.compile("(\\.redis\\.cache\\.windows\\.net)", Pattern.CASE_INSENSITIVE )
641 | };
642 |
643 | // Create strings of Azure resources
644 | String[] azureDomains = {
645 | ".cloudapp.net",
646 | ".cloudapp.azure.com",
647 | ".azurewebsites.com",
648 | ".blob.core.windows.net",
649 | ".azure-api.com",
650 | ".azurecontainer.io",
651 | ".database.windows.net",
652 | ".azuredatalakestore.net",
653 | ".search.windows.net",
654 | ".redis.cache.windows.net"
655 | };
656 |
657 | // Loop through the list of subdomains to test
658 | for (int i = 0; i < subdomainList.size(); i++) {
659 |
660 | // Get cname result
661 | String cnameResult = checkDnsOnly(subdomainList.get(i).toString());
662 |
663 | // Loop through patterns if anything was returned
664 | if (cnameResult.length() > 10) {
665 | for (int j = 0; j < azurePatterns.length; j++) {
666 | Matcher azureMatcher = azurePatterns[j].matcher(cnameResult);
667 |
668 | // Check against the pattern
669 | if (azureMatcher.find()) {
670 |
671 | // Create an http client to check for subdomain takeover
672 | String azureDomain = subdomainList.get(i).toString().split("\\.")[0];
673 | String[] testing = subdomainList.get(i).toString().split("\\.");
674 | HttpGet reqSubdomainHttp = new HttpGet("http://" + azureDomain + azureDomains[j]);
675 | HttpClient subdomainClientHttp = HttpClientBuilder.create().build();
676 | Boolean isNotResponding = true;
677 |
678 | // Connect to the site via https to get response for potential subdomain takeover
679 | try {
680 | HttpResponse resp = subdomainClientHttp.execute(reqSubdomainHttp);
681 | String headers = resp.getStatusLine().toString();
682 | isNotResponding = false;
683 | } catch (Exception ignore) { }
684 |
685 | // If CNAME result points to Azure resource but website does not respond, potentially vulnerable
686 | if (isNotResponding) {
687 | try {
688 | printOut.println("Potential subdomain takeover of an Azure for: https://" + subdomainList.get(i));
689 | // Create an issue from the finding
690 | URL subdomainUrl = new URL("https://" + subdomainList.get(i));
691 | List azureSubdomainMatches = getMatches(messageInfo.getResponse(), azureMatcher.group(0).getBytes());
692 | IScanIssue subdomainAzureIdIssue = new CustomScanIssue(
693 | messageInfo.getHttpService(),
694 | subdomainUrl,
695 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, azureSubdomainMatches) },
696 | "[Anonymous Cloud] Subdomain Takeover - " + subdomainList.get(i),
697 | "A CNAME points to an Azure resource that does not respond the a web request, indicating vulnerability to subdomain takeover for: " + azureDomain + azureDomains[j] + "
See: also: https://github.com/EdOverflow/can-i-take-over-xyz.",
698 | "High",
699 | "Firm"
700 | );
701 |
702 | // Add the Azure subdomain takeover issue
703 | extCallbacks.addScanIssue(subdomainAzureIdIssue);
704 | } catch (Exception ignore) { }
705 | }
706 | }
707 | }
708 | }
709 | }
710 | }
711 |
712 | @Override
713 | public void run() {
714 |
715 | printOut.println("Beginning subdomain scanning, gathering subdomain lists.");
716 | // Get subdomains from a file if one was provided
717 | if (isFileListSet) {
718 | getListSubdomains();
719 | }
720 |
721 | // Get subdomains from open sources
722 | getSubdomains("crt.sh", certTransUrl + domainname);
723 | getSubdomains("BufferOver", bufferOverUrl + domainname);
724 | getSubdomains("WaybackMachine", waybackMachineUrl + domainname);
725 | getSubdomains("HackerTarget", hackerTargetUrl + domainname);
726 |
727 | // if a Shodan API key was provided, get subdomains
728 | if (isShodanSet) {
729 | getSubdomains("Shodan", shodanUrl);
730 | }
731 |
732 | // if a Censys API key was provided, get subdomains
733 | if (isCensysSet) {
734 | getCensysSubdomains("Censys", censysUrl);
735 | }
736 |
737 | // Begin scan based on HTTP
738 | printOut.println("Beginning HTTP/HTTPS subdomain scanning for AWS S3/Heroku/Github.");
739 | scanSubdomains();
740 |
741 | // Begin scan based on DNS
742 | printOut.println("Beginning DNS subdomain scanning for Azure.");
743 | scanDnsSubdomains();
744 |
745 | printOut.println("Subdomain scanning has completed.");
746 | }
747 |
748 | @Override
749 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
750 | throw new UnsupportedOperationException("Not supported yet.");
751 | }
752 | }
753 |
754 | public class BurpExtender implements IBurpExtender, IScannerCheck, ITab {
755 |
756 | // Setup extension wide variables
757 | public IBurpExtenderCallbacks extCallbacks;
758 | public IExtensionHelpers extHelpers;
759 | private static final String burpAnonCloudVersion = "0.1.14";
760 | private static final Pattern S3BucketPattern = Pattern.compile("((?:\\w+://)?(?:([\\w.-]+)\\.s3[\\w.-]*\\.amazonaws\\.com|s3(?:[\\w.-]*\\.amazonaws\\.com(?:(?::\\d+)?\\\\?/)*|://)([\\w.-]+))(?:(?::\\d+)?\\\\?/)?(?:.*?\\?.*Expires=(\\d+))?)", Pattern.CASE_INSENSITIVE);
761 | private static final Pattern GoogleBucketPattern = Pattern.compile("((?:\\w+://)?(?:([\\w.-]+)\\.storage[\\w-]*\\.googleapis\\.com|(?:(?:console\\.cloud\\.google\\.com/storage/browser/|storage\\.cloud\\.google\\.com|storage[\\w-]*\\.googleapis\\.com)(?:(?::\\d+)?\\\\?/)*|gs://)([\\w.-]+))(?:(?::\\d+)?\\\\?/([^\\s?'\"#]*))?(?:.*\\?.*Expires=(\\d+))?)", Pattern.CASE_INSENSITIVE);
762 | private static final Pattern GcpFirebase = Pattern.compile("([\\w.-]+\\.firebaseio\\.com)", Pattern.CASE_INSENSITIVE );
763 | private static final Pattern GcpFirestorePattern = Pattern.compile("(firestore\\.googleapis\\.com.*)", Pattern.CASE_INSENSITIVE );
764 | private static final Pattern AzureBucketPattern = Pattern.compile("(([\\w.-]+\\.blob\\.core\\.windows\\.net(?::\\d+)?\\\\?/[\\w.-]+)(?:.*?\\?.*se=([\\w%-]+))?)", Pattern.CASE_INSENSITIVE);
765 | private static final Pattern AzureTablePattern = Pattern.compile("(([\\w.-]+\\.table\\.core\\.windows\\.net(?::\\d+)?\\\\?/[\\w.-]+)(?:.*?\\?.*se=([\\w%-]+))?)", Pattern.CASE_INSENSITIVE);
766 | private static final Pattern AzureQueuePattern = Pattern.compile("(([\\w.-]+\\.queue\\.core\\.windows\\.net(?::\\d+)?\\\\?/[\\w.-]+)(?:.*?\\?.*se=([\\w%-]+))?)", Pattern.CASE_INSENSITIVE);
767 | private static final Pattern AzureFilePattern = Pattern.compile("(([\\w.-]+\\.file\\.core\\.windows\\.net(?::\\d+)?\\\\?/[\\w.-]+)(?:.*?\\?.*se=([\\w%-]+))?)", Pattern.CASE_INSENSITIVE);
768 | private static final Pattern AzureCosmosPattern = Pattern.compile("(([\\w.-]+\\.documents\\.azure\\.com(?::\\d+)?\\\\?/[\\w.-]+)(?:.*?\\?.*se=([\\w%-]+))?)", Pattern.CASE_INSENSITIVE);
769 | private static final Pattern ParseServerPattern = Pattern.compile("(X\\-Parse\\-Application\\-Id:)", Pattern.CASE_INSENSITIVE);
770 | public JPanel anonCloudPanel;
771 | private String awsAccessKey = "";
772 | private String awsSecretAccessKey = "";
773 | private String googleBearerToken = "";
774 | private String shodanApiKey = "";
775 | private String censysApiKey = "";
776 | private String censysApiSecret = "";
777 | private String anonCloudConfig = "anon-cloud-config.conf";
778 | private static final String GoogleValidationUrl = "https://storage.googleapis.com/storage/v1/b/";
779 | private static final String GoogleBucketUploadUrl = "https://storage.googleapis.com/upload/storage/v1/b/";
780 | private Boolean isAwsAuthSet = false;
781 | private Boolean isGoogleAuthSet = false;
782 | private Boolean isShodanApiSet = false;
783 | private Boolean isCensysApiSet = false;
784 | private Boolean isSubdomainTakeoverSet = false;
785 | private Boolean isBucketSubsSet = false;
786 | private ArrayList SubdomainThreads = new ArrayList();
787 | private File subdomainFileList;
788 | private File bucketFileList;
789 | private ArrayList bucketList = new ArrayList();
790 | private ArrayList firebaseList = new ArrayList();
791 | private ArrayList firebaseCheckList = new ArrayList();
792 | private ArrayList firestoreCheckList = new ArrayList();
793 | private ArrayList bucketCheckList = new ArrayList();
794 | private ArrayList siteOnBucketCheckList = new ArrayList();
795 | AWSCredentials anonCredentials = new AnonymousAWSCredentials();
796 | AWSCredentials authCredentials;
797 | AmazonS3 anonS3client = AmazonS3ClientBuilder
798 | .standard()
799 | .withForceGlobalBucketAccessEnabled(true)
800 | .withRegion(Regions.DEFAULT_REGION)
801 | .withCredentials(new AWSStaticCredentialsProvider(anonCredentials))
802 | .build();
803 | AmazonS3 authS3client;
804 | private PrintWriter printOut;
805 |
806 | // Basic extension setup
807 | @Override
808 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {
809 | extCallbacks = callbacks;
810 | extHelpers = extCallbacks.getHelpers();
811 | extCallbacks.setExtensionName("Anonymous Cloud");
812 | printOut = new PrintWriter(extCallbacks.getStdout(), true);
813 | extCallbacks.registerScannerCheck(this);
814 |
815 | // Create a tab to configure credential values
816 | anonCloudPanel = new JPanel(null);
817 | JLabel anonCloudAwsKeyLabel = new JLabel();
818 | JLabel anonCloudAwsKeyDescLabel = new JLabel();
819 | JLabel anonCloudAwsSecretKeyLabel = new JLabel();
820 | JLabel anonCloudAwsSecretKeyDescLabel = new JLabel();
821 | JLabel anonCloudGoogleBearerLabel = new JLabel();
822 | JLabel anonCloudGoogleBearerDescLabel = new JLabel();
823 | JLabel anonCloudSubdomainTakeoverLabel = new JLabel();
824 | JLabel anonCloudSubdomainTakeoverDescLabel = new JLabel();
825 | JLabel anonCloudSubdomainShodanLabel = new JLabel();
826 | JLabel anonCloudSubdomainShodanDescLabel = new JLabel();
827 | JLabel anonCloudSubdomainCensysLabel = new JLabel();
828 | JLabel anonCloudSubdomainCensysDescLabel = new JLabel();
829 | JLabel anonCloudSubdomainCensysSecretLabel = new JLabel();
830 | JLabel anonCloudSubdomainCensysSecretDescLabel = new JLabel();
831 | JLabel anonCloudSubdomainTakeoverListLabel = new JLabel();
832 | JLabel anonCloudSubdomainTakeoverListDescLabel = new JLabel();
833 | JLabel anonCloudBucketSubsLabel = new JLabel();
834 | JLabel anonCloudBucketSubsDescLabel = new JLabel();
835 | final JCheckBox anonCloudSubdomainTakeoverCheck = new JCheckBox();
836 | JLabel anonCloudBucketSubsListLabel = new JLabel();
837 | JLabel anonCloudBucketSubsListDescLabel = new JLabel();
838 | final JCheckBox anonCloudBucketSubsCheck = new JCheckBox();
839 | final JTextField anonCloudAwsKeyText = new JTextField();
840 | final JTextField anonCloudAwsSecretKeyText = new JTextField();
841 | final JTextField anonCloudGoogleBearerText = new JTextField();
842 | final JTextField anonCloudSubdomainShodanText = new JTextField();
843 | final JTextField anonCloudSubdomainCensysText = new JTextField();
844 | final JTextField anonCloudSubdomainCensysSecretText = new JTextField();
845 | final JButton anonCloudSubdomainTakeoverListButton = new JButton("Subdomain List");
846 | final JButton anonCloudBucketSubsListButton = new JButton("Bucket List");
847 | JButton anonCloudSetHeaderBtn = new JButton("Set Configuration");
848 | JLabel anonCloudSetHeaderDescLabel = new JLabel();
849 |
850 | // Set values for labels, panels, locations, for AWS stuff
851 | // AWS Access Key GUI
852 | anonCloudAwsKeyLabel.setText("AWS Access Key:");
853 | anonCloudAwsKeyDescLabel.setText("Any AWS authenticated user test: AWS Access Key.");
854 | anonCloudAwsKeyLabel.setBounds(16, 15, 145, 20);
855 | anonCloudAwsKeyText.setBounds(166, 12, 310, 26);
856 | anonCloudAwsKeyDescLabel.setBounds(606, 15, 600, 20);
857 |
858 | // AWS Secret Access Key GUI
859 | anonCloudAwsSecretKeyLabel.setText("AWS Secret Access Key:");
860 | anonCloudAwsSecretKeyDescLabel.setText("Any AWS authenticated user test: AWS Secret Access Key.");
861 | anonCloudAwsSecretKeyLabel.setBounds(16, 50, 145, 20);
862 | anonCloudAwsSecretKeyText.setBounds(166, 47, 310, 26);
863 | anonCloudAwsSecretKeyDescLabel.setBounds(606, 50, 600, 20);
864 |
865 | // Set values for labels, panels, locations, for Google stuff
866 | // Google Bearer Token
867 | anonCloudGoogleBearerLabel.setText("Google Bearer Token:");
868 | anonCloudGoogleBearerDescLabel.setText("Any Google authenticated user test: Google Bearer Token (use 'gcloud auth print-access-token')");
869 | anonCloudGoogleBearerLabel.setBounds(16, 85, 145, 20);
870 | anonCloudGoogleBearerText.setBounds(166, 82, 310, 26);
871 | anonCloudGoogleBearerDescLabel.setBounds(606, 85, 600, 20);
872 |
873 | // Set values for labels, panels, locations, for Shodan stuff
874 | // Shodan API key
875 | anonCloudSubdomainShodanLabel.setText("Shodan API Key:");
876 | anonCloudSubdomainShodanDescLabel.setText("Shodan API key for use with subdomain takeover testing.");
877 | anonCloudSubdomainShodanLabel.setBounds(16, 120, 145, 20);
878 | anonCloudSubdomainShodanText.setBounds(166, 117, 310, 26);
879 | anonCloudSubdomainShodanDescLabel.setBounds(606, 120, 600, 20);
880 |
881 | // Set values for labels, panels, locations, for Censys.io stuff
882 | // Censys API key
883 | anonCloudSubdomainCensysLabel.setText("Censys API Key:");
884 | anonCloudSubdomainCensysDescLabel.setText("Censys API key for use with subdomain takeover testing.");
885 | anonCloudSubdomainCensysLabel.setBounds(16, 155, 145, 20);
886 | anonCloudSubdomainCensysText.setBounds(166, 152, 310, 26);
887 | anonCloudSubdomainCensysDescLabel.setBounds(606, 155, 600, 20);
888 |
889 | // Set values for labels, panels, locations, for Censys.io stuff
890 | // Censys API Secret
891 | anonCloudSubdomainCensysSecretLabel.setText("Censys API Secret:");
892 | anonCloudSubdomainCensysSecretDescLabel.setText("Censys API Secret for use with subdomain takeover testing.");
893 | anonCloudSubdomainCensysSecretLabel.setBounds(16, 190, 145, 20);
894 | anonCloudSubdomainCensysSecretText.setBounds(166, 187, 310, 26);
895 | anonCloudSubdomainCensysSecretDescLabel.setBounds(606, 190, 600, 20);
896 |
897 | // Checkbox for Subdomain Takeovers
898 | anonCloudSubdomainTakeoverLabel.setText("Enable Subdomain Takeover:");
899 | anonCloudSubdomainTakeoverDescLabel.setText("Automate discovery of subdomains that might be vulnerable to takeover.");
900 | anonCloudSubdomainTakeoverLabel.setBounds(16, 225, 145, 20);
901 | anonCloudSubdomainTakeoverCheck.setBounds(456, 222, 20, 26);
902 | anonCloudSubdomainTakeoverDescLabel.setBounds(606, 225, 600, 20);
903 |
904 | // Checkbox for Subdomain Takeovers
905 | anonCloudSubdomainTakeoverListLabel.setText("Subdomain List:");
906 | anonCloudSubdomainTakeoverListDescLabel.setText("File to provide subdomains (will append each item with ..com/net/org/etc).");
907 | anonCloudSubdomainTakeoverListLabel.setBounds(16, 260, 145, 20);
908 | anonCloudSubdomainTakeoverListButton.setBounds(166, 257, 310, 26);
909 | anonCloudSubdomainTakeoverListDescLabel.setBounds(606, 260, 600, 20);
910 |
911 | // Checkbox for checking additional bucket names
912 | anonCloudBucketSubsLabel.setText("Extra Bucket Checks:");
913 | anonCloudBucketSubsDescLabel.setText("If a valid bucket/Firebase DB is found, append various common names to discover additional resources.");
914 | anonCloudBucketSubsLabel.setBounds(16, 295, 145, 20);
915 | anonCloudBucketSubsCheck.setBounds(456, 292, 20, 26);
916 | anonCloudBucketSubsDescLabel.setBounds(606, 295, 600, 20);
917 |
918 | // Checkbox for checking additional bucket names
919 | anonCloudBucketSubsListLabel.setText("Bucket List:");
920 | anonCloudBucketSubsListDescLabel.setText("File to provide bucket/Firebase DB names to append to a valid bucket/DB.");
921 | anonCloudBucketSubsListLabel.setBounds(16, 330, 145, 20);
922 | anonCloudBucketSubsListButton.setBounds(166, 327, 310, 26);
923 | anonCloudBucketSubsListDescLabel.setBounds(606, 330, 600, 20);
924 |
925 | // Create button for setting options
926 | anonCloudSetHeaderDescLabel.setText("Enable access configuration.");
927 | anonCloudSetHeaderDescLabel.setBounds(606, 365, 600, 20);
928 | anonCloudSetHeaderBtn.setBounds(166, 365, 310, 26);
929 |
930 | // Print extension header
931 | printHeader();
932 |
933 | File anonCloudConfigFile = new File(extCallbacks.getExtensionFilename().replace("AnonymousCloud.jar", "") + anonCloudConfig);
934 | if (anonCloudConfigFile.isFile()) {
935 | printOut.println("Reading configuration file: " + extCallbacks.getExtensionFilename().replace("AnonymousCloud.jar", "") + anonCloudConfig.toString());
936 |
937 | try {
938 | BufferedReader br = new BufferedReader(new FileReader(extCallbacks.getExtensionFilename().replace("AnonymousCloud.jar", "") + anonCloudConfig));
939 |
940 | for (String line = br.readLine(); line != null; line = br.readLine()) {
941 | String configLine[] = line.split(":",0);
942 | if (configLine[0].equals("AWSAccessKey") && !configLine[1].equals("blank")) {
943 | awsAccessKey = configLine[1];
944 | } else if (configLine[0].equals("AWSSecretKey") && !configLine[1].equals("blank")) {
945 | awsSecretAccessKey = configLine[1];
946 | } else if (configLine[0].equals("GoogleBearerToken") && !configLine[1].equals("blank")) {
947 | googleBearerToken = configLine[1];
948 |
949 | // Add Google Bearer Token if set
950 | if (googleBearerToken.matches("^ya29\\.[0-9A-Za-z\\-_]+")) {
951 | isGoogleAuthSet = true;
952 | anonCloudGoogleBearerText.setText(googleBearerToken);
953 | }
954 | } else if (configLine[0].equals("ShodanKey") && !configLine[1].equals("blank")) {
955 | shodanApiKey = configLine[1];
956 |
957 | // Add Shodan API key if set
958 | if (shodanApiKey.matches("^[a-zA-Z0-9]+")) {
959 | isShodanApiSet = true;
960 | anonCloudSubdomainShodanText.setText(shodanApiKey);
961 | }
962 | } else if (configLine[0].equals("CensysKey") && !configLine[1].equals("blank")) {
963 | censysApiKey = configLine[1];
964 | } else if (configLine[0].equals("CensysSecret") && !configLine[1].equals("blank")) {
965 | censysApiSecret = configLine[1];
966 | } else if (configLine[0].equals("SubdomainTakeover") && configLine[1].equals("true")) {
967 | anonCloudSubdomainTakeoverCheck.setSelected(true);
968 | isSubdomainTakeoverSet = true;
969 | } else if (isSubdomainTakeoverSet && configLine[0].equals("SubdomainList") && !configLine[1].equals("blank")) {
970 | File subdomainFile = new File(configLine[1] + ":" + configLine[2]);
971 |
972 | if (subdomainFile.length() > 0) {
973 | subdomainFileList = subdomainFile;
974 | printOut.println("Setting subdomain file to: " + subdomainFile.toString());
975 | }
976 | } else if (configLine[0].equals("ExtraBuckets") && configLine[1].equals("true")) {
977 | anonCloudBucketSubsCheck.setSelected(true);
978 | isBucketSubsSet = true;
979 | } else if (isBucketSubsSet && configLine[0].equals("ExtraBucketsList") && !configLine[1].equals("blank")) {
980 | File bucketFile = new File(configLine[1] + ":" + configLine[2]);
981 |
982 | if (bucketFile.length() > 0) {
983 | bucketFileList = bucketFile;
984 | printOut.println("Setting buckets file to: " + bucketFile.toString());
985 | }
986 | }
987 | }
988 |
989 | // Auth to AWS
990 | if (awsAccessKey.matches("^(AIza[0-9A-Za-z-_]{35}|A3T[A-Z0-9]|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AGPA[A-Z0-9]{16}|AIDA[A-Z0-9]{16}|AROA[A-Z0-9]{16}|AIPA[A-Z0-9]{16}|ANPA[A-Z0-9]{16}|ANVA[A-Z0-9]{16})") && awsSecretAccessKey.length() == 40) {
991 |
992 | // Setup an authenticated S3 client for buckets configured to allow all authenticated AWS users
993 | authCredentials = new BasicAWSCredentials(awsAccessKey, awsSecretAccessKey);
994 | authS3client = AmazonS3ClientBuilder
995 | .standard()
996 | .withForceGlobalBucketAccessEnabled(true)
997 | .withRegion(Regions.DEFAULT_REGION)
998 | .withCredentials(new AWSStaticCredentialsProvider(authCredentials))
999 | .build();
1000 |
1001 | isAwsAuthSet = true;
1002 | anonCloudAwsKeyText.setText(awsAccessKey);
1003 | anonCloudAwsSecretKeyText.setText(awsSecretAccessKey);
1004 | }
1005 |
1006 | // Add Censys API key if set
1007 | if (censysApiKey.matches("^[a-zA-Z0-9\\-]+") && censysApiSecret.matches("^[a-zA-Z0-9]+")) {
1008 | isCensysApiSet = true;
1009 | anonCloudSubdomainCensysText.setText(censysApiKey);
1010 | anonCloudSubdomainCensysSecretText.setText(censysApiSecret);
1011 | }
1012 |
1013 | br.close();
1014 | } catch (Exception ignore) {}
1015 | }
1016 |
1017 | // Process and set subdomain file list
1018 | anonCloudSubdomainTakeoverListButton.addActionListener(new ActionListener() {
1019 | public void actionPerformed(ActionEvent e) {
1020 |
1021 | // Select the file
1022 | JFileChooser selectFile = new JFileChooser();
1023 | FileNameExtensionFilter filter = new FileNameExtensionFilter("Text files only", "txt");
1024 | selectFile.setFileFilter(filter);
1025 | int returnFile = selectFile.showDialog(anonCloudPanel, "Subdomain List");
1026 |
1027 | // If a file was chosen, process it
1028 | if (returnFile == JFileChooser.APPROVE_OPTION) {
1029 | File subdomainFile = selectFile.getSelectedFile();
1030 |
1031 | if (subdomainFile.length() > 0) {
1032 | subdomainFileList = subdomainFile;
1033 | printOut.println("Setting subdomain file to: " + subdomainFile.toString() + "\n");
1034 | }
1035 | }
1036 | }
1037 | });
1038 |
1039 | // Process and set bucket append/prepend file list
1040 | anonCloudBucketSubsListButton.addActionListener(new ActionListener() {
1041 | public void actionPerformed(ActionEvent e) {
1042 |
1043 | // Select the file
1044 | JFileChooser selectFile = new JFileChooser();
1045 | FileNameExtensionFilter filter = new FileNameExtensionFilter("Text files only", "txt");
1046 | selectFile.setFileFilter(filter);
1047 | int returnFile = selectFile.showDialog(anonCloudPanel, "Bucket List");
1048 |
1049 | // If a file was chosen, process it
1050 | if (returnFile == JFileChooser.APPROVE_OPTION) {
1051 | File bucketFile = selectFile.getSelectedFile();
1052 |
1053 | if (bucketFile.length() > 0) {
1054 | bucketFileList = bucketFile;
1055 | printOut.println("Setting buckets file to: " + bucketFile.toString() + "\n");
1056 | }
1057 | }
1058 | }
1059 | });
1060 |
1061 | // Process and set configuration options
1062 | anonCloudSetHeaderBtn.addActionListener(new ActionListener() {
1063 | public void actionPerformed(ActionEvent e) {
1064 | awsAccessKey = anonCloudAwsKeyText.getText();
1065 | awsSecretAccessKey = anonCloudAwsSecretKeyText.getText();
1066 | googleBearerToken = anonCloudGoogleBearerText.getText();
1067 | shodanApiKey = anonCloudSubdomainShodanText.getText();
1068 | censysApiKey = anonCloudSubdomainCensysText.getText();
1069 | censysApiSecret = anonCloudSubdomainCensysSecretText.getText();
1070 | String awsAccessText = "blank";
1071 | String awsSecretText = "blank";
1072 | String googleText = "blank";
1073 | String shodanText = "blank";
1074 | String censysKeyText = "blank";
1075 | String censysSecretText = "blank";
1076 | String subdomainText = "blank";
1077 | String subdomainFileText = "blank";
1078 | String bucketsText = "blank";
1079 | String bucketsFileText = "blank";
1080 |
1081 | // If valid AWS keys were entered, setup a client
1082 | if (awsAccessKey.matches("^(AIza[0-9A-Za-z-_]{35}|A3T[A-Z0-9]|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AGPA[A-Z0-9]{16}|AIDA[A-Z0-9]{16}|AROA[A-Z0-9]{16}|AIPA[A-Z0-9]{16}|ANPA[A-Z0-9]{16}|ANVA[A-Z0-9]{16})") && awsSecretAccessKey.length() == 40) {
1083 |
1084 | // Setup an authenticated S3 client for buckets configured to allow all authenticated AWS users
1085 | authCredentials = new BasicAWSCredentials(awsAccessKey, awsSecretAccessKey);
1086 | authS3client = AmazonS3ClientBuilder
1087 | .standard()
1088 | .withForceGlobalBucketAccessEnabled(true)
1089 | .withRegion(Regions.DEFAULT_REGION)
1090 | .withCredentials(new AWSStaticCredentialsProvider(authCredentials))
1091 | .build();
1092 |
1093 | isAwsAuthSet = true;
1094 | awsAccessText = awsAccessKey;
1095 | awsSecretText = awsSecretAccessKey;
1096 | }
1097 |
1098 | // Add Google Bearer Token if set
1099 | if (googleBearerToken.matches("^ya29\\.[0-9A-Za-z\\-_]+")) {
1100 | isGoogleAuthSet = true;
1101 | googleText = googleBearerToken;
1102 | }
1103 |
1104 | // Add Shodan API key if set
1105 | if (shodanApiKey.matches("^[a-zA-Z0-9]+")) {
1106 | isShodanApiSet = true;
1107 | shodanText = shodanApiKey;
1108 | }
1109 |
1110 | // Add Censys API key if set
1111 | if (censysApiKey.matches("^[a-zA-Z0-9\\-]+") && censysApiSecret.matches("^[a-zA-Z0-9]+")) {
1112 | isCensysApiSet = true;
1113 | censysKeyText = censysApiKey;
1114 | censysSecretText = censysApiSecret;
1115 | }
1116 |
1117 | // Check for Subdomain Takeover being enabled
1118 | if (anonCloudSubdomainTakeoverCheck.isSelected()){
1119 | isSubdomainTakeoverSet = true;
1120 | subdomainText = "true";
1121 |
1122 | if (subdomainFileList != null) {
1123 | subdomainFileText = subdomainFileList.toString();
1124 | }
1125 | }
1126 |
1127 | // Check for extra bucket checks being enabled
1128 | if (anonCloudBucketSubsCheck.isSelected()){
1129 | isBucketSubsSet = true;
1130 | bucketsText = "true";
1131 |
1132 | if (bucketFileList != null) {
1133 | bucketsFileText = bucketFileList.toString();
1134 | }
1135 | }
1136 |
1137 | try {
1138 | printOut.println("Writing config file: " + extCallbacks.getExtensionFilename().replace("AnonymousCloud.jar", "") + anonCloudConfig.toString() + "\n");
1139 | PrintWriter anonCloudConfigFileObj = new PrintWriter(extCallbacks.getExtensionFilename().replace("AnonymousCloud.jar", "") + anonCloudConfig);
1140 | String configText = "AWSAccessKey:" + awsAccessText + "\nAWSSecretKey:" + awsSecretText + "\nGoogleBearerToken:" + googleText + "\nShodanKey:" + shodanText + "\nCensysKey:" + censysKeyText + "\nCensysSecret:" + censysSecretText + "\nSubdomainTakeover:" + subdomainText + "\nSubdomainList:" + subdomainFileText + "\nExtraBuckets:" + bucketsText + "\nExtraBucketsList:" + bucketsFileText;
1141 | anonCloudConfigFileObj.println(configText);
1142 | anonCloudConfigFileObj.close();
1143 | } catch (Exception ignore) {}
1144 | }
1145 | });
1146 |
1147 | // Add labels and fields to tab
1148 | anonCloudPanel.add(anonCloudAwsKeyLabel);
1149 | anonCloudPanel.add(anonCloudAwsKeyDescLabel);
1150 | anonCloudPanel.add(anonCloudAwsKeyText);
1151 | anonCloudPanel.add(anonCloudAwsSecretKeyLabel);
1152 | anonCloudPanel.add(anonCloudAwsSecretKeyDescLabel);
1153 | anonCloudPanel.add(anonCloudAwsSecretKeyText);
1154 | anonCloudPanel.add(anonCloudGoogleBearerLabel);
1155 | anonCloudPanel.add(anonCloudGoogleBearerDescLabel);
1156 | anonCloudPanel.add(anonCloudGoogleBearerText);
1157 | anonCloudPanel.add(anonCloudSubdomainTakeoverLabel);
1158 | anonCloudPanel.add(anonCloudSubdomainTakeoverDescLabel);
1159 | anonCloudPanel.add(anonCloudSubdomainTakeoverCheck);
1160 | anonCloudPanel.add(anonCloudSubdomainShodanLabel);
1161 | anonCloudPanel.add(anonCloudSubdomainShodanDescLabel);
1162 | anonCloudPanel.add(anonCloudSubdomainShodanText);
1163 | anonCloudPanel.add(anonCloudSubdomainCensysLabel);
1164 | anonCloudPanel.add(anonCloudSubdomainCensysDescLabel);
1165 | anonCloudPanel.add(anonCloudSubdomainCensysText);
1166 | anonCloudPanel.add(anonCloudSubdomainCensysSecretLabel);
1167 | anonCloudPanel.add(anonCloudSubdomainCensysSecretDescLabel);
1168 | anonCloudPanel.add(anonCloudSubdomainCensysSecretText);
1169 | anonCloudPanel.add(anonCloudSubdomainTakeoverListLabel);
1170 | anonCloudPanel.add(anonCloudSubdomainTakeoverListDescLabel);
1171 | anonCloudPanel.add(anonCloudSubdomainTakeoverListButton);
1172 | anonCloudPanel.add(anonCloudBucketSubsLabel);
1173 | anonCloudPanel.add(anonCloudBucketSubsDescLabel);
1174 | anonCloudPanel.add(anonCloudBucketSubsCheck);
1175 | anonCloudPanel.add(anonCloudBucketSubsListLabel);
1176 | anonCloudPanel.add(anonCloudBucketSubsListDescLabel);
1177 | anonCloudPanel.add(anonCloudBucketSubsListButton);
1178 | anonCloudPanel.add(anonCloudSetHeaderBtn);
1179 | anonCloudPanel.add(anonCloudSetHeaderDescLabel);
1180 |
1181 |
1182 | // Add the tab to Burp
1183 | extCallbacks.customizeUiComponent(anonCloudPanel);
1184 | extCallbacks.addSuiteTab(BurpExtender.this);
1185 | }
1186 |
1187 | // Tab caption
1188 | @Override
1189 | public String getTabCaption() { return "Anonymous Cloud"; }
1190 |
1191 | // Java component to return to Burp
1192 | @Override
1193 | public Component getUiComponent() { return anonCloudPanel; }
1194 |
1195 | // Print to extension output tab
1196 | public void printHeader() {
1197 | printOut.println("Anonymous Cloud: " + burpAnonCloudVersion + "\n====================\nMonitor requests and responses for AWS S3 Buckets, Google Storage Buckets, and Azure Storage Containers. Checks for unauthenticated read/write access to buckets, in addition to bucket enumeration attempts.\n\n"
1198 | + "josh.berry@codewatch.org\n\n");
1199 | }
1200 |
1201 | // Perform a passive check for cloud buckets
1202 | @Override
1203 | public List doPassiveScan(IHttpRequestResponse messageInfo) {
1204 |
1205 | // Only process requests if the URL is in scope
1206 | if (extCallbacks.isInScope(extHelpers.analyzeRequest(messageInfo).getUrl())) {
1207 |
1208 | // Start thread for subdomain takeovers
1209 | if (isSubdomainTakeoverSet) {
1210 | String fqdn = extHelpers.analyzeRequest(messageInfo).getUrl().getHost();
1211 | String[] strUrl = fqdn.split("\\.");
1212 |
1213 | // If a thread has not already been started for this FQDN, then start one, but only if wildcard DNS fails
1214 | if (!SubdomainThreads.contains(strUrl[strUrl.length-2] + "." + strUrl[strUrl.length-1])) {
1215 | SubdomainThreads.add(strUrl[strUrl.length-2] + "." + strUrl[strUrl.length-1]);
1216 |
1217 | // Perform a lookup on a non-existent DNS address first to make sure wildcard responses are not on
1218 | InetAddress[] firstDnsTest;
1219 | Boolean lookupStatus = false;
1220 | String wildcardTest = Base64.getEncoder().encodeToString((genRandStr()).getBytes(StandardCharsets.UTF_8)).replace("=", "").replace("/", "").replace("+", "");
1221 |
1222 | // Lookup the address
1223 | try {
1224 | firstDnsTest = InetAddress.getAllByName(wildcardTest + "." + strUrl[strUrl.length-2] + "." + strUrl[strUrl.length-1]);
1225 |
1226 | // If we got a response, set status to true
1227 | if (firstDnsTest.length > 0) {
1228 | lookupStatus = true;
1229 | }
1230 | } catch (Exception ignore) { }
1231 |
1232 | // If the lookup failed, wildcard responses are not on and we can proceed
1233 | if (!lookupStatus) {
1234 | String censysCredential = censysApiKey + ":" + censysApiSecret;
1235 | SubdomainTakeover t = new SubdomainTakeover(extCallbacks, messageInfo, strUrl[strUrl.length-2] + "." + strUrl[strUrl.length-1], printOut, shodanApiKey, censysCredential, subdomainFileList);
1236 | t.start();
1237 | }
1238 | }
1239 | }
1240 |
1241 | // Setup default request/response body variables
1242 | String respRaw = new String(messageInfo.getResponse());
1243 | String reqRaw = new String(messageInfo.getRequest());
1244 | String respBody = respRaw.substring(extHelpers.analyzeResponse(messageInfo.getResponse()).getBodyOffset());
1245 |
1246 | // Create patter matchers for each type
1247 | Matcher S3BucketMatch = S3BucketPattern.matcher(respBody);
1248 | Matcher GoogleBucketMatch = GoogleBucketPattern.matcher(respBody);
1249 | Matcher AzureBucketMatch = AzureBucketPattern.matcher(respBody);
1250 | Matcher AzureTableMatch = AzureTablePattern.matcher(respBody);
1251 | Matcher AzureQueueMatch = AzureQueuePattern.matcher(respBody);
1252 | Matcher AzureFileMatch = AzureFilePattern.matcher(respBody);
1253 | Matcher AzureCosmosMatch = AzureCosmosPattern.matcher(respBody);
1254 | Matcher GcpFirebaseMatch = GcpFirebase.matcher(respBody);
1255 | Matcher GcpFirestoreRespMatch = GcpFirestorePattern.matcher(respBody);
1256 | Matcher ParseServerMatch = ParseServerPattern.matcher(reqRaw);
1257 |
1258 | // Create an issue noting an AWS S3 Bucket was identified in the response
1259 | if (S3BucketMatch.find()) {
1260 | List S3BucketMatches = getMatches(messageInfo.getResponse(), S3BucketMatch.group(0).getBytes());
1261 | IScanIssue awsIdIssue = new CustomScanIssue(
1262 | messageInfo.getHttpService(),
1263 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1264 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, S3BucketMatches) },
1265 | "[Anonymous Cloud] AWS S3 Bucket Identified",
1266 | "The response body contained the following bucket: " + S3BucketMatch.group(0),
1267 | "Information",
1268 | "Firm"
1269 | );
1270 |
1271 | // Add the S3 bucket identification issue
1272 | extCallbacks.addScanIssue(awsIdIssue);
1273 |
1274 | // Get the actual name of the bucket
1275 | String BucketName = getBucketName("AWS", S3BucketMatch.group(0));
1276 |
1277 | // Perform anonymous checks
1278 | if (validateBucket("AWS", "anonymous", BucketName) && !bucketCheckList.contains(BucketName + "-" + "AWS-Anonymous")) {
1279 | bucketCheckList.add(BucketName + "-" + "AWS-Anonymous");
1280 |
1281 | // Create a finding noting that the bucket is valid
1282 | IScanIssue awsConfirmIssue = new CustomScanIssue(
1283 | messageInfo.getHttpService(),
1284 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1285 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, S3BucketMatches) },
1286 | "[Anonymous Cloud] AWS S3 Bucket Exists",
1287 | "The following bucket was confirmed to be valid: " + BucketName,
1288 | "Low",
1289 | "Certain"
1290 | );
1291 |
1292 | // Add confirmed bucket issue
1293 | extCallbacks.addScanIssue(awsConfirmIssue);
1294 |
1295 | // Check for public read bucket anonymous access
1296 | try {
1297 | publicReadCheck("AWS", messageInfo, S3BucketMatches, BucketName);
1298 | } catch (Exception ignore) {}
1299 |
1300 | // Check for public write bucket anonymous access
1301 | try {
1302 | publicWriteCheck("AWS", messageInfo, S3BucketMatches, BucketName);
1303 | } catch (Exception ignore) {}
1304 |
1305 | // If enabled, append common bucket names to original valid bucket
1306 | if (isBucketSubsSet) {
1307 | try {
1308 | appendBucketName(BucketName.replaceAll("\\.(com|net|org|edu|io)", ""), "AWS", messageInfo, S3BucketMatches);
1309 | } catch (Exception ignore) {}
1310 | }
1311 | }
1312 |
1313 | // Perform checks from the perspecitve of any authenticated AWS user
1314 | if (validateBucket("AWS", "anyuser", BucketName) && !bucketCheckList.contains(BucketName + "-" + "AWS-Any")) {
1315 | bucketCheckList.add(BucketName + "-" + "AWS-Any");
1316 |
1317 | // Check for any authenticated AWS user read bucket access
1318 | try {
1319 | anyAuthReadCheck("AWS", messageInfo, S3BucketMatches, BucketName);
1320 | } catch (Exception ignore) {}
1321 |
1322 | // Check for any authenticated AWS user write bucket access
1323 | try {
1324 | anyAuthWriteCheck("AWS", messageInfo, S3BucketMatches, BucketName);
1325 | } catch (Exception ignore) {}
1326 | }
1327 | }
1328 |
1329 | // Create an issue noting a Google Bucket was identified in the response
1330 | if (GoogleBucketMatch.find()) {
1331 | List GoogleBucketMatches = getMatches(messageInfo.getResponse(), GoogleBucketMatch.group(0).getBytes());
1332 | IScanIssue googleIdIssue = new CustomScanIssue(
1333 | messageInfo.getHttpService(),
1334 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1335 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, GoogleBucketMatches) },
1336 | "[Anonymous Cloud] Google Storage Container Identified",
1337 | "The response body contained the following bucket: " + GoogleBucketMatch.group(0),
1338 | "Information",
1339 | "Firm"
1340 | );
1341 |
1342 | // Add the Google bucket identification issue
1343 | extCallbacks.addScanIssue(googleIdIssue);
1344 |
1345 | // Get the actual name of the bucket
1346 | String BucketName = getBucketName("Google", GoogleBucketMatch.group(0));
1347 |
1348 | // Perform anonymous checks for Google
1349 | if (validateBucket("Google", "anonymous", BucketName) && !bucketCheckList.contains(BucketName + "-" + "Google-Anonymous")) {
1350 | bucketCheckList.add(BucketName + "-" + "Google-Anonymous");
1351 |
1352 | // Create a finding noting that the bucket is valid
1353 | IScanIssue googleConfirmIssue = new CustomScanIssue(
1354 | messageInfo.getHttpService(),
1355 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1356 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, GoogleBucketMatches) },
1357 | "[Anonymous Cloud] Google Storage Container Exists",
1358 | "The following bucket was confirmed to be valid: " + BucketName,
1359 | "Low",
1360 | "Certain"
1361 | );
1362 |
1363 | // Add confirmed bucket issue
1364 | extCallbacks.addScanIssue(googleConfirmIssue);
1365 |
1366 | // Check for public read anonymous access
1367 | try {
1368 | publicReadCheck("Google", messageInfo, GoogleBucketMatches, BucketName);
1369 | } catch (Exception ignore) {}
1370 |
1371 | // Check for publc read ACL access
1372 | try {
1373 | publicReadAclCheck("Google", messageInfo, GoogleBucketMatches, BucketName);
1374 | } catch (Exception ignore) {}
1375 |
1376 | // Check for publc write anonymous access
1377 | try {
1378 | publicWriteCheck("Google", messageInfo, GoogleBucketMatches, BucketName);
1379 | } catch (Exception ignore) { }
1380 |
1381 | // If enabled, append common bucket names to original valid bucket
1382 | if (isBucketSubsSet) {
1383 | try {
1384 | appendBucketName(BucketName.replaceAll("\\.(com|net|org|edu|io)", ""), "Google", messageInfo, GoogleBucketMatches);
1385 | } catch (Exception ignore) {}
1386 | }
1387 | }
1388 |
1389 | // Perform checks from the perspecitve of any authenticated Google user
1390 | if (validateBucket("Google", "anyuser", BucketName) && !bucketCheckList.contains(BucketName + "-" + "Google-Any")) {
1391 | bucketCheckList.add(BucketName + "-" + "Google-Any");
1392 |
1393 | // Check for any authenticated Google user read bucket access
1394 | try {
1395 | anyAuthReadCheck("Google", messageInfo, GoogleBucketMatches, BucketName);
1396 | } catch (Exception ignore) {}
1397 |
1398 | // Check for any authenticated Google user read bucket ACL access
1399 | try {
1400 | anyAuthReadAclCheck("Google", messageInfo, GoogleBucketMatches, BucketName);
1401 | } catch (Exception ignore) {}
1402 |
1403 | // Check for any authenticated Google user write bucket access
1404 | try {
1405 | anyAuthWriteCheck("Google", messageInfo, GoogleBucketMatches, BucketName);
1406 | } catch (Exception ignore) {}
1407 | }
1408 | }
1409 |
1410 | // Create an issue noting an Azure Bucket was identified in the response
1411 | if (AzureBucketMatch.find()) {
1412 | List AzureBucketMatches = getMatches(messageInfo.getResponse(), AzureBucketMatch.group(0).getBytes());
1413 | IScanIssue azureIdIssue = new CustomScanIssue(
1414 | messageInfo.getHttpService(),
1415 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1416 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, AzureBucketMatches) },
1417 | "[Anonymous Cloud] Azure Storage Container Identified - Blob",
1418 | "The response body contained the following bucket: " + AzureBucketMatch.group(0),
1419 | "Information",
1420 | "Firm"
1421 | );
1422 |
1423 | // Add the Azure bucket identification issue
1424 | extCallbacks.addScanIssue(azureIdIssue);
1425 |
1426 | // Get the actual name of the bucket
1427 | String BucketName = getBucketName("Azure", AzureBucketMatch.group(0));
1428 |
1429 | // Perform anonymous checks for Azure
1430 | if (validateBucket("Azure", "anonymous", BucketName) && !bucketCheckList.contains(BucketName + "-" + "Azure-Anonymous")) {
1431 | bucketCheckList.add(BucketName + "-" + "Azure-Anonymous");
1432 |
1433 | IScanIssue azureAccountIssue = new CustomScanIssue(
1434 | messageInfo.getHttpService(),
1435 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1436 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, AzureBucketMatches) },
1437 | "[Anonymous Cloud] Azure Storage Container Blob Account Identified",
1438 | "The response confirmed the Azure Storage account exists: " + AzureBucketMatch.group(0),
1439 | "Low",
1440 | "Certain"
1441 | );
1442 |
1443 | // Add the Azure bucket identification issue
1444 | extCallbacks.addScanIssue(azureAccountIssue);
1445 |
1446 | // Check for public read/write anonymous access
1447 | try {
1448 | publicReadCheck("Azure", messageInfo, AzureBucketMatches, BucketName);
1449 | } catch (Exception ignore) {}
1450 |
1451 | // If enabled, append common bucket names to original valid bucket
1452 | if (isBucketSubsSet) {
1453 | try {
1454 | appendBucketName(BucketName.replaceAll("\\.(com|net|org|edu|io)", ""), "Azure", messageInfo, AzureBucketMatches);
1455 | } catch (Exception ignore) {}
1456 | }
1457 | }
1458 | }
1459 |
1460 | // Create an issue noting an Azure Table was identified in the response
1461 | if (AzureTableMatch.find()) {
1462 | List AzureTableMatches = getMatches(messageInfo.getResponse(), AzureTableMatch.group(0).getBytes());
1463 | IScanIssue azureTableIdIssue = new CustomScanIssue(
1464 | messageInfo.getHttpService(),
1465 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1466 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, AzureTableMatches) },
1467 | "[Anonymous Cloud] Azure Storage Container Identified - Table",
1468 | "The response body contained the following table: " + AzureTableMatch.group(0),
1469 | "Information",
1470 | "Firm"
1471 | );
1472 |
1473 | // Add the Azure bucket identification issue
1474 | extCallbacks.addScanIssue(azureTableIdIssue);
1475 | }
1476 |
1477 | // Create an issue noting an Azure Queue was identified in the response
1478 | if (AzureQueueMatch.find()) {
1479 | List AzureQueueMatches = getMatches(messageInfo.getResponse(), AzureQueueMatch.group(0).getBytes());
1480 | IScanIssue azureQueueIdIssue = new CustomScanIssue(
1481 | messageInfo.getHttpService(),
1482 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1483 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, AzureQueueMatches) },
1484 | "[Anonymous Cloud] Azure Storage Container Identified - Queue",
1485 | "The response body contained the following queue: " + AzureQueueMatch.group(0),
1486 | "Information",
1487 | "Firm"
1488 | );
1489 |
1490 | // Add the Azure bucket identification issue
1491 | extCallbacks.addScanIssue(azureQueueIdIssue);
1492 | }
1493 |
1494 | // Create an issue noting an Azure Share was identified in the response
1495 | if (AzureFileMatch.find()) {
1496 | List AzureFileMatches = getMatches(messageInfo.getResponse(), AzureFileMatch.group(0).getBytes());
1497 | IScanIssue azureFileIdIssue = new CustomScanIssue(
1498 | messageInfo.getHttpService(),
1499 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1500 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, AzureFileMatches) },
1501 | "[Anonymous Cloud] Azure Storage Container Identified - Share",
1502 | "The response body contained the following share: " + AzureFileMatch.group(0),
1503 | "Information",
1504 | "Firm"
1505 | );
1506 |
1507 | // Add the Azure bucket identification issue
1508 | extCallbacks.addScanIssue(azureFileIdIssue);
1509 | }
1510 |
1511 | // Create an issue noting an Azure Cosmos DB was identified in the response
1512 | if (AzureCosmosMatch.find()) {
1513 | List AzureCosmosMatches = getMatches(messageInfo.getResponse(), AzureCosmosMatch.group(0).getBytes());
1514 | IScanIssue azureCosmosIdIssue = new CustomScanIssue(
1515 | messageInfo.getHttpService(),
1516 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1517 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, AzureCosmosMatches) },
1518 | "[Anonymous Cloud] Azure Cosmos Database Identified",
1519 | "The response body contained the following Cosmos DB: " + AzureCosmosMatch.group(0),
1520 | "Information",
1521 | "Firm"
1522 | );
1523 |
1524 | // Add the Azure bucket identification issue
1525 | extCallbacks.addScanIssue(azureCosmosIdIssue);
1526 | }
1527 |
1528 | // Check for open Firebase access
1529 | if (GcpFirebaseMatch.find()) {
1530 | List GcpFirebaseMatches = getMatches(messageInfo.getResponse(), GcpFirebaseMatch.group(0).getBytes());
1531 | IScanIssue firebaseIdIssue = new CustomScanIssue(
1532 | messageInfo.getHttpService(),
1533 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1534 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, GcpFirebaseMatches) },
1535 | "[Anonymous Cloud] Firebase Database Identified",
1536 | "The response body contained the following database: " + GcpFirebaseMatch.group(0),
1537 | "Information",
1538 | "Firm"
1539 | );
1540 |
1541 | // Add the Firebase identification issue
1542 | extCallbacks.addScanIssue(firebaseIdIssue);
1543 |
1544 | String firebaseFix = GcpFirebaseMatch.group(0).replaceAll("\\\\", "");
1545 | if (!firebaseCheckList.contains(firebaseFix)) {
1546 | firebaseCheckList.add(firebaseFix);
1547 | // Check for public read/write anonymous access
1548 | try {
1549 | gcpFirebaseCheck(messageInfo, GcpFirebaseMatches, GcpFirebaseMatch.group(0));
1550 | } catch (Exception ignore) {}
1551 |
1552 | // Check common other database names
1553 | try {
1554 | appendFirebaseName(GcpFirebaseMatch.group(0), messageInfo, GcpFirebaseMatches);
1555 | } catch (Exception ignore) {}
1556 | }
1557 | }
1558 |
1559 | // Check for open Firestore access
1560 | if (GcpFirestoreRespMatch.find()) {
1561 | List GcpFirestoreRespMatches = getMatches(messageInfo.getResponse(), GcpFirestoreRespMatch.group(0).getBytes());
1562 | IScanIssue firestoreIdIssue = new CustomScanIssue(
1563 | messageInfo.getHttpService(),
1564 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1565 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, GcpFirestoreRespMatches) },
1566 | "[Anonymous Cloud] Firestore Database Identified",
1567 | "The response body contained the following Firestore database: " + GcpFirestoreRespMatch.group(0),
1568 | "Information",
1569 | "Firm"
1570 | );
1571 |
1572 | // Add the Firebase identification issue
1573 | extCallbacks.addScanIssue(firestoreIdIssue);
1574 |
1575 | String firestoreFix = GcpFirestoreRespMatch.group(0).replaceAll("\\\\", "");
1576 | if (!firestoreCheckList.contains(firestoreFix)) {
1577 | firestoreCheckList.add(firestoreFix);
1578 | // Check for public read/write anonymous access
1579 | try {
1580 | gcpFirestoreCheck(messageInfo, null, GcpFirestoreRespMatches, GcpFirestoreRespMatch.group(0));
1581 | } catch (Exception ignore) {}
1582 | }
1583 | }
1584 |
1585 | // Create an issue noting the use of Parse Server based on the request
1586 | if (ParseServerMatch.find()) {
1587 | List ParseServerMatches = getMatches(messageInfo.getRequest(), ParseServerMatch.group(0).getBytes());
1588 | IScanIssue parseServerIdIssue = new CustomScanIssue(
1589 | messageInfo.getHttpService(),
1590 | extHelpers.analyzeRequest(messageInfo).getUrl(),
1591 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, ParseServerMatches, null) },
1592 | "[Anonymous Cloud] Parse Server Identified",
1593 | "The response headers contained the following Parse Server application ID: " + ParseServerMatch.group(0),
1594 | "Information",
1595 | "Firm"
1596 | );
1597 |
1598 | // Add the Parse Server identification issue
1599 | extCallbacks.addScanIssue(parseServerIdIssue);
1600 | }
1601 | }
1602 |
1603 | return null;
1604 | }
1605 |
1606 | // No active scanning for this but still must define it
1607 | @Override
1608 | public List doActiveScan(IHttpRequestResponse messageInfo, IScannerInsertionPoint insertionPoint) {
1609 | // Only process requests if the URL is in scope and the domain has not been checked yet
1610 | if (extCallbacks.isInScope(extHelpers.analyzeRequest(messageInfo).getUrl())) {
1611 | // Proceeding checks obtained from https://gist.github.com/fransr/a155e5bd7ab11c93923ec8ce788e3368
1612 | // Build basic request
1613 | Boolean isConfirmedAlready = false;
1614 | String webDomain = extHelpers.analyzeRequest(messageInfo).getUrl().getHost();
1615 | String webProto = extHelpers.analyzeRequest(messageInfo).getUrl().getProtocol();
1616 | int webPort = extHelpers.analyzeRequest(messageInfo).getUrl().getPort();
1617 | String langHeader = "Accept-Language: en-US,en;q=0.9,sv;q=0.8,zh-TW;q=0.7,zh;q=0.6,fi;q=0.5,it;q=0.4,de;q=0.3";
1618 | String uaHeader = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36";
1619 | String dateHeader = "Date: " + DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC));
1620 |
1621 | if (!siteOnBucketCheckList.contains(webDomain)) {
1622 | siteOnBucketCheckList.add(webDomain);
1623 |
1624 | // Try to create an invalid character URL
1625 | try {
1626 |
1627 | // Create Burp service
1628 | IHttpService httpService = extHelpers.buildHttpService(webDomain, webPort, webProto);
1629 | List headersInit = Arrays.asList("GET /%C0 HTTP/1.1", "Host: " + webDomain, langHeader, uaHeader);
1630 | byte[] requestInit = extHelpers.buildHttpMessage(headersInit, new byte[0]);
1631 |
1632 | // Native Burp request
1633 | IHttpRequestResponse httpReqResp = extCallbacks.makeHttpRequest(httpService, requestInit);
1634 |
1635 | // Get the response information
1636 | String httpReqRespRaw = new String(httpReqResp.getResponse());
1637 | String httpReqRespBody = httpReqRespRaw.substring(extHelpers.analyzeResponse(httpReqResp.getResponse()).getBodyOffset());
1638 |
1639 | // Create a pattern and matcher
1640 | Pattern invalidCharPattern = Pattern.compile("(InvalidURI|Code: InvalidURI|NoSuchKey)", Pattern.CASE_INSENSITIVE);
1641 | Matcher invalidCharMatch = invalidCharPattern.matcher(httpReqRespBody);
1642 |
1643 | // Create an issue noting the domain is hosted on an AWS S3 bucket
1644 | if (invalidCharMatch.find()) {
1645 | // Create a finding noting that the domain is hosted on a bucket
1646 | List invalidCharMatches = getMatches(httpReqResp.getResponse(), invalidCharMatch.group(0).getBytes());
1647 | IScanIssue domainIsBucketIssue = new CustomScanIssue(
1648 | httpReqResp.getHttpService(),
1649 | extHelpers.analyzeRequest(httpReqResp).getUrl(),
1650 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(httpReqResp, null, invalidCharMatches) },
1651 | "[Anonymous Cloud] Domain Hosted on AWS S3 Bucket",
1652 | "The domain appears to be hosted on an AWS S3 bucket based on the response: " + invalidCharMatch.group(0),
1653 | "Information",
1654 | "Firm"
1655 | );
1656 |
1657 | // Add confirmed bucket issue
1658 | extCallbacks.addScanIssue(domainIsBucketIssue);
1659 | isConfirmedAlready = true;
1660 | }
1661 |
1662 | if (!isConfirmedAlready) {
1663 | // Create Burp service
1664 | List headers = Arrays.asList("POST /soap HTTP/1.1", "Host: " + webDomain, langHeader, uaHeader);
1665 | byte[] request = extHelpers.buildHttpMessage(headers, new byte[0]);
1666 |
1667 | // Native Burp request
1668 | httpReqResp = extCallbacks.makeHttpRequest(httpService, request);
1669 |
1670 | // Get the response information
1671 | httpReqRespRaw = new String(httpReqResp.getResponse());
1672 | httpReqRespBody = httpReqRespRaw.substring(extHelpers.analyzeResponse(httpReqResp.getResponse()).getBodyOffset());
1673 |
1674 | // Create a pattern and matcher
1675 | Pattern soapPattern = Pattern.compile("(>Missing SOAPAction header<)", Pattern.CASE_INSENSITIVE);
1676 | Matcher soapMatch = soapPattern.matcher(httpReqRespBody);
1677 |
1678 | // Create an issue noting the domain is hosted on an AWS S3 bucket
1679 | if (soapMatch.find()) {
1680 | // Create a finding noting that the domain is hosted on a bucket
1681 | List soapMatches = getMatches(httpReqResp.getResponse(), soapMatch.group(0).getBytes());
1682 | IScanIssue domainIsBucketIssue = new CustomScanIssue(
1683 | httpReqResp.getHttpService(),
1684 | extHelpers.analyzeRequest(httpReqResp).getUrl(),
1685 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(httpReqResp, null, soapMatches) },
1686 | "[Anonymous Cloud] Domain Hosted on AWS S3 Bucket",
1687 | "The domain appears to be hosted on an AWS S3 bucket based on the response: " + soapMatch.group(0),
1688 | "Information",
1689 | "Firm"
1690 | );
1691 |
1692 | // Add confirmed bucket issue
1693 | extCallbacks.addScanIssue(domainIsBucketIssue);
1694 | isConfirmedAlready = true;
1695 | }
1696 | }
1697 |
1698 | if (!isConfirmedAlready) {
1699 | // Create Burp service
1700 | List headers = Arrays.asList("POSTX /doesnotexist HTTP/1.1", "Host: " + webDomain, langHeader, uaHeader);
1701 | byte[] request = extHelpers.buildHttpMessage(headers, new byte[0]);
1702 |
1703 | // Native Burp request
1704 | httpReqResp = extCallbacks.makeHttpRequest(httpService, request);
1705 |
1706 | // Get the response information
1707 | httpReqRespRaw = new String(httpReqResp.getResponse());
1708 | httpReqRespBody = httpReqRespRaw.substring(extHelpers.analyzeResponse(httpReqResp.getResponse()).getBodyOffset());
1709 |
1710 | // Create a pattern and matcher
1711 | Pattern methodPattern = Pattern.compile("(>Missing SOAPAction header<)", Pattern.CASE_INSENSITIVE);
1712 | Matcher methodMatch = methodPattern.matcher(httpReqRespBody);
1713 |
1714 | // Create an issue noting the domain is hosted on an AWS S3 bucket
1715 | if (methodMatch.find()) {
1716 | // Create a finding noting that the domain is hosted on a bucket
1717 | List methodMatches = getMatches(httpReqResp.getResponse(), methodMatch.group(0).getBytes());
1718 | IScanIssue domainIsBucketIssue = new CustomScanIssue(
1719 | httpReqResp.getHttpService(),
1720 | extHelpers.analyzeRequest(httpReqResp).getUrl(),
1721 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(httpReqResp, null, methodMatches) },
1722 | "[Anonymous Cloud] Domain Hosted on AWS S3 Bucket",
1723 | "The domain appears to be hosted on an AWS S3 bucket based on the response: " + methodMatch.group(0),
1724 | "Information",
1725 | "Firm"
1726 | );
1727 |
1728 | // Add confirmed bucket issue
1729 | extCallbacks.addScanIssue(domainIsBucketIssue);
1730 | isConfirmedAlready = true;
1731 | }
1732 | }
1733 |
1734 | if (!isConfirmedAlready && isAwsAuthSet) {
1735 | // Create Burp service
1736 | List headers = Arrays.asList("POST /doesnotexist?123 HTTP/1.1", "Host: " + webDomain, "Authorization: AWS " + awsAccessKey + ":x", dateHeader, langHeader, uaHeader);
1737 | byte[] request = extHelpers.buildHttpMessage(headers, new byte[0]);
1738 |
1739 | // Native Burp request
1740 | httpReqResp = extCallbacks.makeHttpRequest(httpService, request);
1741 |
1742 | // Get the response information
1743 | httpReqRespRaw = new String(httpReqResp.getResponse());
1744 | httpReqRespBody = httpReqRespRaw.substring(extHelpers.analyzeResponse(httpReqResp.getResponse()).getBodyOffset());
1745 |
1746 | // Create a pattern and matcher
1747 | Pattern postSignPattern = Pattern.compile("()", Pattern.CASE_INSENSITIVE);
1748 | Matcher postSignMatch = postSignPattern.matcher(httpReqRespBody);
1749 |
1750 | // Create an issue noting the domain is hosted on an AWS S3 bucket
1751 | if (postSignMatch.find()) {
1752 | // Create a finding noting that the domain is hosted on a bucket
1753 | List postSignMatches = getMatches(httpReqResp.getResponse(), postSignMatch.group(0).getBytes());
1754 | IScanIssue domainIsBucketIssue = new CustomScanIssue(
1755 | httpReqResp.getHttpService(),
1756 | extHelpers.analyzeRequest(httpReqResp).getUrl(),
1757 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(httpReqResp, null, postSignMatches) },
1758 | "[Anonymous Cloud] Domain Hosted on AWS S3 Bucket",
1759 | "The domain appears to be hosted on an AWS S3 bucket based on the response: " + postSignMatch.group(0),
1760 | "Information",
1761 | "Firm"
1762 | );
1763 |
1764 | // Add confirmed bucket issue
1765 | extCallbacks.addScanIssue(domainIsBucketIssue);
1766 | isConfirmedAlready = true;
1767 | }
1768 | }
1769 |
1770 | if (!isConfirmedAlready && isAwsAuthSet) {
1771 | // Create Burp service
1772 | List headers = Arrays.asList("GET /doesnotexist?AWSAccessKeyId=" + awsAccessKey + "&Expires=1603060100&Signature=x HTTP/1.1", "Host: " + webDomain, dateHeader, langHeader, uaHeader);
1773 | byte[] request = extHelpers.buildHttpMessage(headers, new byte[0]);
1774 |
1775 | // Native Burp request
1776 | httpReqResp = extCallbacks.makeHttpRequest(httpService, request);
1777 |
1778 | // Get the response information
1779 | httpReqRespRaw = new String(httpReqResp.getResponse());
1780 | httpReqRespBody = httpReqRespRaw.substring(extHelpers.analyzeResponse(httpReqResp.getResponse()).getBodyOffset());
1781 |
1782 | // Create a pattern and matcher
1783 | Pattern getSignPattern = Pattern.compile("()", Pattern.CASE_INSENSITIVE);
1784 | Matcher getSignMatch = getSignPattern.matcher(httpReqRespBody);
1785 |
1786 | // Create an issue noting the domain is hosted on an AWS S3 bucket
1787 | if (getSignMatch.find()) {
1788 | // Create a finding noting that the domain is hosted on a bucket
1789 | List getSignMatches = getMatches(httpReqResp.getResponse(), getSignMatch.group(0).getBytes());
1790 | IScanIssue domainIsBucketIssue = new CustomScanIssue(
1791 | httpReqResp.getHttpService(),
1792 | extHelpers.analyzeRequest(httpReqResp).getUrl(),
1793 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(httpReqResp, null, getSignMatches) },
1794 | "[Anonymous Cloud] Domain Hosted on AWS S3 Bucket",
1795 | "The domain appears to be hosted on an AWS S3 bucket based on the response: " + getSignMatch.group(0),
1796 | "Information",
1797 | "Firm"
1798 | );
1799 |
1800 | // Add confirmed bucket issue
1801 | extCallbacks.addScanIssue(domainIsBucketIssue);
1802 | isConfirmedAlready = true;
1803 | }
1804 | }
1805 |
1806 | if (!isConfirmedAlready && isAwsAuthSet) {
1807 | // Create Burp service
1808 | List headers = Arrays.asList("PUT /doesnotexist?AWSAccessKeyId=" + awsAccessKey + "&Expires=1603060100&Signature=x HTTP/1.1", "Host: " + webDomain, dateHeader, langHeader, uaHeader);
1809 | byte[] request = extHelpers.buildHttpMessage(headers, new byte[0]);
1810 |
1811 | // Native Burp request
1812 | httpReqResp = extCallbacks.makeHttpRequest(httpService, request);
1813 |
1814 | // Get the response information
1815 | httpReqRespRaw = new String(httpReqResp.getResponse());
1816 | httpReqRespBody = httpReqRespRaw.substring(extHelpers.analyzeResponse(httpReqResp.getResponse()).getBodyOffset());
1817 |
1818 | // Create a pattern and matcher
1819 | Pattern putSignPattern = Pattern.compile("()", Pattern.CASE_INSENSITIVE);
1820 | Matcher putSignMatch = putSignPattern.matcher(httpReqRespBody);
1821 |
1822 | // Create an issue noting the domain is hosted on an AWS S3 bucket
1823 | if (putSignMatch.find()) {
1824 | // Create a finding noting that the domain is hosted on a bucket
1825 | List putSignMatches = getMatches(httpReqResp.getResponse(), putSignMatch.group(0).getBytes());
1826 | IScanIssue domainIsBucketIssue = new CustomScanIssue(
1827 | httpReqResp.getHttpService(),
1828 | extHelpers.analyzeRequest(httpReqResp).getUrl(),
1829 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(httpReqResp, null, putSignMatches) },
1830 | "[Anonymous Cloud] Domain Hosted on AWS S3 Bucket",
1831 | "The domain appears to be hosted on an AWS S3 bucket based on the response: " + putSignMatch.group(0),
1832 | "Information",
1833 | "Firm"
1834 | );
1835 |
1836 | // Add confirmed bucket issue
1837 | extCallbacks.addScanIssue(domainIsBucketIssue);
1838 | isConfirmedAlready = true;
1839 | }
1840 | }
1841 |
1842 | if (!isConfirmedAlready && isAwsAuthSet) {
1843 | // Create Burp service
1844 | List headers = Arrays.asList("POST /doesnotexist?987 HTTP/1.1", "Host: " + webDomain, "Authorization: AWS " + awsAccessKey + ":x", dateHeader, langHeader, uaHeader);
1845 | byte[] request = extHelpers.buildHttpMessage(headers, extCallbacks.getHelpers().stringToBytes("a=b"));
1846 |
1847 | // Native Burp request
1848 | httpReqResp = extCallbacks.makeHttpRequest(httpService, request);
1849 |
1850 | // Get the response information
1851 | httpReqRespRaw = new String(httpReqResp.getResponse());
1852 | httpReqRespBody = httpReqRespRaw.substring(extHelpers.analyzeResponse(httpReqResp.getResponse()).getBodyOffset());
1853 |
1854 | // Create a pattern and matcher
1855 | Pattern multipartSignPattern = Pattern.compile("()", Pattern.CASE_INSENSITIVE);
1856 | Matcher multipartSignMatch = multipartSignPattern.matcher(httpReqRespBody);
1857 |
1858 | // Create an issue noting the domain is hosted on an AWS S3 bucket
1859 | if (multipartSignMatch.find()) {
1860 | // Create a finding noting that the domain is hosted on a bucket
1861 | List multipartSignMatches = getMatches(httpReqResp.getResponse(), multipartSignMatch.group(0).getBytes());
1862 | IScanIssue domainIsBucketIssue = new CustomScanIssue(
1863 | httpReqResp.getHttpService(),
1864 | extHelpers.analyzeRequest(httpReqResp).getUrl(),
1865 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(httpReqResp, null, multipartSignMatches) },
1866 | "[Anonymous Cloud] Domain Hosted on AWS S3 Bucket",
1867 | "The domain appears to be hosted on an AWS S3 bucket based on the response: " + multipartSignMatch.group(0),
1868 | "Information",
1869 | "Firm"
1870 | );
1871 |
1872 | // Add confirmed bucket issue
1873 | extCallbacks.addScanIssue(domainIsBucketIssue);
1874 | isConfirmedAlready = true;
1875 | }
1876 | }
1877 |
1878 | if (!isConfirmedAlready && isAwsAuthSet) {
1879 | // Create Burp service
1880 | List headers = Arrays.asList("GET /doesnotexist?456 HTTP/1.1", "Host: " + webDomain, "Authorization: AWS4-HMAC-SHA256 Credential=" + awsAccessKey + "/20180101/ap-south-1/s3/aws4_request,SignedHeaders=date;host;x-amz-acl;x-amz-content-sha256;x-amz-date,Signature=x", dateHeader, "x-amz-content-sha256: STREAMING-AWS4-HMAC-SHA256-PAYLOAD", langHeader, uaHeader);
1881 | byte[] request = extHelpers.buildHttpMessage(headers, new byte[0]);
1882 |
1883 | // Native Burp request
1884 | httpReqResp = extCallbacks.makeHttpRequest(httpService, request);
1885 |
1886 | // Get the response information
1887 | httpReqRespRaw = new String(httpReqResp.getResponse());
1888 | httpReqRespBody = httpReqRespRaw.substring(extHelpers.analyzeResponse(httpReqResp.getResponse()).getBodyOffset());
1889 |
1890 | // Create a pattern and matcher
1891 | Pattern streamingSignPattern = Pattern.compile("()", Pattern.CASE_INSENSITIVE);
1892 | Matcher streamingSignMatch = streamingSignPattern.matcher(httpReqRespBody);
1893 |
1894 | // Create an issue noting the domain is hosted on an AWS S3 bucket
1895 | if (streamingSignMatch.find()) {
1896 | // Create a finding noting that the domain is hosted on a bucket
1897 | List streamingSignMatches = getMatches(httpReqResp.getResponse(), streamingSignMatch.group(0).getBytes());
1898 | IScanIssue domainIsBucketIssue = new CustomScanIssue(
1899 | httpReqResp.getHttpService(),
1900 | extHelpers.analyzeRequest(httpReqResp).getUrl(),
1901 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(httpReqResp, null, streamingSignMatches) },
1902 | "[Anonymous Cloud] Domain Hosted on AWS S3 Bucket",
1903 | "The domain appears to be hosted on an AWS S3 bucket based on the response: " + streamingSignMatch.group(0),
1904 | "Information",
1905 | "Firm"
1906 | );
1907 |
1908 | // Add confirmed bucket issue
1909 | extCallbacks.addScanIssue(domainIsBucketIssue);
1910 | isConfirmedAlready = true;
1911 | }
1912 | }
1913 | } catch (Exception ignore) {}
1914 | }
1915 | }
1916 | return null;
1917 | }
1918 |
1919 | // Grab bucket name from matched bucket URL
1920 | public String getBucketName(String BucketType, String BucketUrl) {
1921 | String BucketName = "";
1922 |
1923 | // Get buckets based on type
1924 | if (BucketType.equals("AWS")) {
1925 | // Get the actual bucket name either in the form of bucketname.s3.amazonaws.com or s3.amazonaws.com/bucketname
1926 | if (BucketUrl.startsWith("http://s3.amazonaws") || BucketUrl.startsWith("https://s3.amazonaws")) {
1927 | String[] Bucket = BucketUrl.split("/");
1928 | int BucketLen = Bucket.length;
1929 | BucketName = BucketUrl.split("/")[BucketLen-1];
1930 | } else {
1931 | String[] Bucket = BucketUrl.split("/");
1932 | int BucketLen = Bucket.length;
1933 | BucketName = BucketUrl.split("/")[BucketLen-1];
1934 | BucketName = BucketName.replaceAll("\\.s3.*\\.amazonaws\\.com", "");
1935 | }
1936 | } else if (BucketType.equals("Azure")) {
1937 | BucketName = BucketUrl;
1938 | } else if (BucketType.equals("Google")) {
1939 | // Get the actual bucket name in the form of bucket.storage.googleapis.com, storage.googleapis.com/storage/v1/b/bucket, or console.cloud.google.com/storage/browser/bucket
1940 | if (BucketUrl.startsWith("http://storage.googleapis.com") || BucketUrl.startsWith("https://storage.googleapis.com")) {
1941 | String BucketPart = BucketUrl.replaceAll("(http|https)://storage.googleapis.com/storage/v1/b/", "");
1942 | BucketName = BucketPart.split("/")[0];
1943 | } else if (BucketUrl.startsWith("http://console.cloud.google.com") || BucketUrl.startsWith("https://console.cloud.google.com")) {
1944 | String BucketPart = BucketUrl.replaceAll("(http|https)://console.cloud.google.com/storage/browser/", "");
1945 | BucketName = BucketPart.split("/")[0];
1946 | } else if (BucketUrl.startsWith("http://storage.cloud.google.com") || BucketUrl.startsWith("https://storage.cloud.google.com")) {
1947 | String BucketPart = BucketUrl.replaceAll("(http|https)://storage.cloud.google.com/", "");
1948 | BucketName = BucketPart.split("/")[0];
1949 | } else {
1950 | BucketName = BucketUrl.split("\\.")[0].replaceAll("(http|https)://", "");
1951 | }
1952 | }
1953 | BucketName = BucketName.replaceAll("\\\\", "");
1954 | return BucketName;
1955 | }
1956 |
1957 | // Validate a bucket exists
1958 | public Boolean validateBucket(String bucketType, String authType, String BucketName) {
1959 |
1960 | // Get buckets based on type
1961 | if (bucketType.equals("AWS")) {
1962 | // Call s3client to validate bucket
1963 | if (authType.equals("anonymous")) {
1964 | if (this.anonS3client.doesBucketExistV2(BucketName)) {
1965 | return true;
1966 | } else {
1967 | return false;
1968 | }
1969 | } else if (authType.equals("anyuser") && isAwsAuthSet) {
1970 | if (this.authS3client.doesBucketExistV2(BucketName)) {
1971 | return true;
1972 | } else {
1973 | return false;
1974 | }
1975 | } else {
1976 | return false;
1977 | }
1978 | } else if (bucketType.equals("Azure")) {
1979 | // Create a client to check Azure for the storage account
1980 | HttpClient client = HttpClientBuilder.create().build();
1981 | HttpGet req = new HttpGet("https://" + BucketName + "?restype=container&comp=list");
1982 | HttpResponse resp;
1983 | Boolean bucketExists = false;
1984 |
1985 | // Connect to Azure services
1986 | try {
1987 | resp = client.execute(req);
1988 | String headers = resp.getStatusLine().toString();
1989 |
1990 | // If we get a status then it exists
1991 | if (headers.contains("200 OK") || headers.contains("401 Unauthorized") || headers.contains("404 The specified resource does not exist.")) {
1992 | bucketExists = true;
1993 | } else {
1994 | bucketExists = false;
1995 | }
1996 | } catch (Exception ignore) {}
1997 |
1998 | return bucketExists;
1999 | } else if (bucketType.equals("Google")) {
2000 | if (authType.equals("anonymous")) {
2001 | // Create a client to check Google for the bucket
2002 | HttpClient client = HttpClientBuilder.create().build();
2003 | HttpGet req = new HttpGet(GoogleValidationUrl + BucketName);
2004 | HttpResponse resp;
2005 | Boolean bucketExists = false;
2006 |
2007 | // Connect to GCP services
2008 | try {
2009 | resp = client.execute(req);
2010 | String headers = resp.getStatusLine().toString();
2011 |
2012 | // If the status is 200, it is public, of 401 then private, otherwise doesn't exist
2013 | if (headers.contains("200 OK") || headers.contains("401 Unauthorized")) {
2014 | bucketExists = true;
2015 | } else {
2016 | bucketExists = false;
2017 | }
2018 | } catch (Exception ignore) {}
2019 |
2020 | return bucketExists;
2021 | } else if (authType.equals("anyuser") && isGoogleAuthSet) {
2022 |
2023 | // Create a client to check Google for the bucket
2024 | HttpClient client = HttpClientBuilder.create().build();
2025 | HttpGet req = new HttpGet(GoogleValidationUrl + BucketName);
2026 | HttpResponse resp;
2027 | Boolean bucketExists = false;
2028 |
2029 | // Connect to GCP services
2030 | try {
2031 | resp = client.execute(req);
2032 | String headers = resp.getStatusLine().toString();
2033 |
2034 | // If the status is 200, it is public, of 401 then private, otherwise doesn't exist
2035 | if (headers.contains("200 OK") || headers.contains("401 Unauthorized")) {
2036 | bucketExists = true;
2037 | } else {
2038 | bucketExists = false;
2039 | }
2040 | } catch (Exception ignore) {}
2041 |
2042 | return bucketExists;
2043 | } else {
2044 | return false;
2045 | }
2046 | } else {
2047 | return false;
2048 | }
2049 | }
2050 |
2051 | // Append bucket names to validated bucket
2052 | private void appendBucketName(String BucketName, String BucketType, IHttpRequestResponse messageInfo, ListBucketMatches) {
2053 | try {
2054 |
2055 | // If provided with a file, use it, otherwise use default
2056 | if (bucketFileList.exists() && bucketFileList.length() > 0) {
2057 |
2058 | BufferedReader rd = new BufferedReader(new FileReader(bucketFileList));
2059 | String line = null;
2060 |
2061 | // Loop through each line
2062 | while((line = rd.readLine()) != null) {
2063 | // Add to bucket list if unique
2064 | if (!bucketList.contains(BucketName + line) && !line.equals(BucketName)) {
2065 | bucketList.add(BucketName + line);
2066 | }
2067 | }
2068 |
2069 | // Loop through the list of buckets to test
2070 | for (int i = 0; i < bucketList.size(); i++) {
2071 |
2072 | // Perform anonymous checks
2073 | if (validateBucket(BucketType, "anonymous", bucketList.get(i).toString())) {
2074 |
2075 | // Create a finding noting that the bucket is valid
2076 | IScanIssue bucketConfirmIssue = new CustomScanIssue(
2077 | messageInfo.getHttpService(),
2078 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2079 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, BucketMatches) },
2080 | "[Anonymous Cloud] " + BucketType + " Bucket Exists",
2081 | "The following bucket was confirmed to be valid: " + bucketList.get(i).toString(),
2082 | "Low",
2083 | "Certain"
2084 | );
2085 |
2086 | // Add confirmed bucket issue
2087 | extCallbacks.addScanIssue(bucketConfirmIssue);
2088 |
2089 | // Check for public read bucket anonymous access
2090 | try {
2091 | publicReadCheck(BucketType, messageInfo, BucketMatches, bucketList.get(i).toString());
2092 | } catch (Exception ignore) {}
2093 |
2094 | // Perform other read/write checks as long as it isn't Azure
2095 | if (!BucketType.contains("Azure")) {
2096 | // Check for public write bucket anonymous access
2097 | try {
2098 | publicWriteCheck(BucketType, messageInfo, BucketMatches, bucketList.get(i).toString());
2099 | } catch (Exception ignore) {}
2100 |
2101 | // Check for any authenticated AWS user read bucket access
2102 | try {
2103 | anyAuthReadCheck(BucketType, messageInfo, BucketMatches, bucketList.get(i).toString());
2104 | } catch (Exception ignore) {}
2105 |
2106 | // Check for any authenticated AWS user write bucket access
2107 | try {
2108 | anyAuthWriteCheck(BucketType, messageInfo, BucketMatches, bucketList.get(i).toString());
2109 | } catch (Exception ignore) {}
2110 | }
2111 | }
2112 | }
2113 | }
2114 | } catch (Exception ignore) {}
2115 | }
2116 |
2117 | // Append bucket names to validated bucket
2118 | private void appendFirebaseName(String firebaseDb, IHttpRequestResponse messageInfo, ListFirebaseMatches) {
2119 | String FirebaseName = firebaseDb.replaceAll("\\.firebaseio\\.com.*", "");
2120 |
2121 | try {
2122 |
2123 | // If provided with a file, use it, otherwise use default
2124 | if (bucketFileList.exists() && bucketFileList.length() > 0) {
2125 |
2126 | BufferedReader rd = new BufferedReader(new FileReader(bucketFileList));
2127 | String line = null;
2128 |
2129 | // Loop through each line
2130 | while((line = rd.readLine()) != null) {
2131 | // Add to bucket list if unique
2132 | if (!firebaseList.contains(FirebaseName + line) && !line.equals(FirebaseName) && !line.contains(".")) {
2133 | firebaseList.add(FirebaseName + line + ".firebaseio.com");
2134 | }
2135 | }
2136 |
2137 | // Loop through the list of buckets to test
2138 | for (int i = 0; i < firebaseList.size(); i++) {
2139 | gcpFirebaseCheck(messageInfo, FirebaseMatches, firebaseList.get(i).toString());
2140 | }
2141 | }
2142 | } catch (Exception ignore) {}
2143 | }
2144 |
2145 | // Generate random strings for write test
2146 | public String genRandStr() {
2147 | int leftLimit = 48; // numeral '0'
2148 | int rightLimit = 122; // letter 'z'
2149 | int targetStringLength = 12;
2150 | Random random = new Random();
2151 |
2152 | String generatedString = random.ints(leftLimit, rightLimit + 1)
2153 | .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
2154 | .limit(targetStringLength)
2155 | .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
2156 | .toString();
2157 |
2158 | return generatedString;
2159 | }
2160 |
2161 | // Perform anonymous public read access check
2162 | private void publicReadCheck(String BucketType, IHttpRequestResponse messageInfo, Listmatches, String BucketName) {
2163 |
2164 | // AWS specific checks
2165 | if (BucketType.equals("AWS")) {
2166 |
2167 | // Obtain the buckets region and then create a client based on this region
2168 | String strRegion = anonS3client.headBucket(new HeadBucketRequest(BucketName)).getBucketRegion();
2169 | AmazonS3 s3clientList = AmazonS3ClientBuilder
2170 | .standard()
2171 | .withRegion(strRegion)
2172 | .withCredentials(new AWSStaticCredentialsProvider(anonCredentials))
2173 | .build();
2174 |
2175 | try {
2176 | // Get a list of bucket objects
2177 | ObjectListing bucketObjsListing = s3clientList.listObjects(BucketName);
2178 | ListbucketObjs = new ArrayList<>();
2179 |
2180 | // Look through the objects and add to our list
2181 | do {
2182 | for (S3ObjectSummary objItem : bucketObjsListing.getObjectSummaries()) {
2183 | bucketObjs.add(objItem.getKey());
2184 | }
2185 | } while (bucketObjsListing.isTruncated());
2186 |
2187 | // Setup basic variables for enumerating and building string of objects
2188 | int firstBucket = 0;
2189 | int bucketCounter = 1;
2190 | int totalBuckets = bucketObjs.size();
2191 | String ObjList = "";
2192 |
2193 | // Loop through our list to build a string of all objects
2194 | for (Iterator it = bucketObjs.iterator(); it.hasNext();) {
2195 | String obj = it.next();
2196 |
2197 | if (firstBucket == 0 && totalBuckets >= 1) {
2198 | ObjList = obj;
2199 | firstBucket = 1;
2200 | } else if (totalBuckets == 2 && firstBucket == 1) {
2201 | ObjList = ObjList + " and " + obj;
2202 | } else if (firstBucket == 1 && bucketCounter == totalBuckets) {
2203 | ObjList = ObjList + ", and " + obj;
2204 | } else {
2205 | ObjList = ObjList + ", " + obj;
2206 | }
2207 |
2208 | bucketCounter++;
2209 | }
2210 |
2211 | // Create public read access issue with the list of objects included
2212 | IScanIssue publicReadIssue = new CustomScanIssue(
2213 | messageInfo.getHttpService(),
2214 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2215 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2216 | "[Anonymous Cloud] Publicly Accessible AWS S3 Bucket",
2217 | "The following bucket contents were enumerated from " + BucketName + ": " + ObjList + ".",
2218 | "Medium",
2219 | "Certain"
2220 | );
2221 |
2222 | // Add public read access issue
2223 | extCallbacks.addScanIssue(publicReadIssue);
2224 |
2225 | // Attempt to read the bucket ACL
2226 | AccessControlList readAcl = s3clientList.getBucketAcl(BucketName);
2227 |
2228 | // Create public read ACL issue with the full ACL included
2229 | if (readAcl.toString().contains("AccessControlList")) {
2230 |
2231 | // Create public read access issue with the list of objects included
2232 | IScanIssue publicReadAclIssue = new CustomScanIssue(
2233 | messageInfo.getHttpService(),
2234 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2235 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2236 | "[Anonymous Cloud] Publicly Accessible AWS S3 Bucket ACL",
2237 | "The following bucket ACL was enumerated from " + BucketName + ": " + readAcl.toString() + ".",
2238 | "Medium",
2239 | "Certain"
2240 | );
2241 |
2242 | // Add public read ACL access issue
2243 | extCallbacks.addScanIssue(publicReadAclIssue);
2244 | }
2245 | } catch (Exception ignore) {}
2246 |
2247 | // Google specific checks
2248 | } else if (BucketType.equals("Google")) {
2249 |
2250 | // Create a client to check Google for the bucket
2251 | HttpClient bucketClient = HttpClientBuilder.create().build();
2252 | HttpGet reqBucket = new HttpGet(GoogleValidationUrl + BucketName + "/o");
2253 |
2254 | // Connect to GCP services for bucket access
2255 | try {
2256 | HttpResponse resp = bucketClient.execute(reqBucket);
2257 | String headers = resp.getStatusLine().toString();
2258 |
2259 | // If the status is 200, it is public, of 401 then private, otherwise doesn't exist
2260 | if (headers.contains("200 OK")) {
2261 |
2262 | // Read the response and get the JSON
2263 | BufferedReader rd = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
2264 | String jsonStr = "";
2265 | String line = "";
2266 | while ((line = rd.readLine()) != null) {
2267 | jsonStr = jsonStr + line;
2268 | }
2269 |
2270 | // Read JSON results from public bucket
2271 | JSONObject json = new JSONObject(jsonStr);
2272 | JSONArray bucketObjs = json.getJSONArray("items");
2273 |
2274 | // Setup basic variables for enumerating and building string of objects
2275 | int firstBucket = 0;
2276 | int bucketCounter = 1;
2277 | int totalBuckets = bucketObjs.length();
2278 | String ObjList = "";
2279 |
2280 | // Loop through our list to build a string of all objects
2281 | for (int i = 0; i < bucketObjs.length(); i++) {
2282 | String obj = bucketObjs.getJSONObject(i).getString("name");
2283 |
2284 | if (firstBucket == 0 && totalBuckets >= 1) {
2285 | ObjList = obj;
2286 | firstBucket = 1;
2287 | } else if (totalBuckets == 2 && firstBucket == 1) {
2288 | ObjList = ObjList + " and " + obj;
2289 | } else if (firstBucket == 1 && bucketCounter == totalBuckets) {
2290 | ObjList = ObjList + ", and " + obj;
2291 | } else {
2292 | ObjList = ObjList + ", " + obj;
2293 | }
2294 |
2295 | bucketCounter++;
2296 | }
2297 |
2298 | // Create public read access issue with the list of objects included
2299 | IScanIssue publicReadIssue = new CustomScanIssue(
2300 | messageInfo.getHttpService(),
2301 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2302 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2303 | "[Anonymous Cloud] Publicly Accessible Google Storage Container",
2304 | "The following bucket contents were enumerated from " + BucketName + ": " + ObjList + ".",
2305 | "Medium",
2306 | "Certain"
2307 | );
2308 |
2309 | // Add public read access issue
2310 | extCallbacks.addScanIssue(publicReadIssue);
2311 | }
2312 | } catch (Exception ignore) {}
2313 | } else if (BucketType.equals("Azure")) {
2314 |
2315 | // Create a client to check Azure for the bucket
2316 | HttpClient bucketClient = HttpClientBuilder.create().build();
2317 | HttpGet reqBucket = new HttpGet("https://" + BucketName + "?restype=container&comp=list");
2318 |
2319 | // Connect to Azure services for bucket access
2320 | try {
2321 | HttpResponse resp = bucketClient.execute(reqBucket);
2322 | String headers = resp.getStatusLine().toString();
2323 |
2324 | // If the status is 200, it is public, otherwise it isn't
2325 | if (headers.contains("200 OK")) {
2326 |
2327 | // Read the response and get the XML
2328 | BufferedReader rd = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
2329 | String xmlStr = "";
2330 | String line = "";
2331 | int lineOne = 0;
2332 | ArrayList blobContents = new ArrayList();
2333 |
2334 | // Put XML string together
2335 | while ((line = rd.readLine()) != null) {
2336 | if (lineOne == 0) {
2337 | xmlStr = line.substring(3, line.length());
2338 | } else {
2339 | xmlStr = xmlStr + line;
2340 | }
2341 | }
2342 |
2343 | // Read XML results from public bucket
2344 | SAXParserFactory factory = SAXParserFactory.newInstance();
2345 | SAXParser saxParser = factory.newSAXParser();
2346 |
2347 | // Create a handler for the XML
2348 | DefaultHandler handler = new DefaultHandler() {
2349 |
2350 | // boolean to confirm name value
2351 | boolean isName = false;
2352 |
2353 | // setup a handler for each element
2354 | @Override
2355 | public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
2356 |
2357 | // if the element contains blobs, lookup details
2358 | if (qName.equalsIgnoreCase("Name")) {
2359 | isName = true;
2360 | } else {
2361 | isName = false;
2362 | }
2363 | }
2364 |
2365 | // setup hander for data in between tags
2366 | @Override
2367 | public void characters(char[] ch, int start, int length) {
2368 | if (isName) {
2369 | blobContents.add(new String(ch, start, length));
2370 | }
2371 | }
2372 | };
2373 |
2374 | // process the XML data
2375 | InputSource xmlSrc = new InputSource(new StringReader(xmlStr));
2376 | saxParser.parse(xmlSrc, handler);
2377 |
2378 | // Setup basic variables for enumerating and building string of objects
2379 | int firstBucket = 0;
2380 | int bucketCounter = 1;
2381 | int totalBuckets = blobContents.size();
2382 | String ObjList = "";
2383 |
2384 | // Loop through our list to build a string of all objects
2385 | for (int i = 0; i < blobContents.size(); i++) {
2386 | String obj = blobContents.get(i);
2387 |
2388 | if (firstBucket == 0 && totalBuckets >= 1) {
2389 | ObjList = obj;
2390 | firstBucket = 1;
2391 | } else if (totalBuckets == 2 && firstBucket == 1) {
2392 | ObjList = ObjList + " and " + obj;
2393 | } else if (firstBucket == 1 && bucketCounter == totalBuckets) {
2394 | ObjList = ObjList + ", and " + obj;
2395 | } else {
2396 | ObjList = ObjList + ", " + obj;
2397 | }
2398 |
2399 | bucketCounter++;
2400 | }
2401 |
2402 | // Create public read access issue with the list of objects included
2403 | IScanIssue publicReadIssue = new CustomScanIssue(
2404 | messageInfo.getHttpService(),
2405 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2406 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2407 | "[Anonymous Cloud] Publicly Accessible Azure Storage Container",
2408 | "The following bucket contents were enumerated from " + BucketName + ": " + ObjList + ".",
2409 | "Medium",
2410 | "Certain"
2411 | );
2412 |
2413 | // Add public read access issue
2414 | extCallbacks.addScanIssue(publicReadIssue);
2415 | }
2416 | } catch (Exception ignore) { }
2417 | }
2418 | }
2419 |
2420 | // Perform anonymous public read ACL access check
2421 | private void publicReadAclCheck(String BucketType, IHttpRequestResponse messageInfo, Listmatches, String BucketName) {
2422 |
2423 | // Google specific checks
2424 | if (BucketType.equals("Google")) {
2425 |
2426 | // Create a client to check Google for the bucket
2427 | HttpClient iamClient = HttpClientBuilder.create().build();
2428 | HttpGet reqIam = new HttpGet(GoogleValidationUrl + BucketName + "/iam");
2429 |
2430 | // Connect to GCP services for bucket ACL access
2431 | try {
2432 | HttpResponse resp = iamClient.execute(reqIam);
2433 | String headers = resp.getStatusLine().toString();
2434 |
2435 | // If the status is 200, it is public, of 401 then private, otherwise doesn't exist
2436 | if (headers.contains("200 OK")) {
2437 |
2438 | // Read the response and get the JSON
2439 | BufferedReader rd = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
2440 | String jsonStr = "";
2441 | String line = "";
2442 | while ((line = rd.readLine()) != null) {
2443 | jsonStr = jsonStr + line;
2444 | }
2445 |
2446 | // Read JSON results from public bucket
2447 | JSONObject json = new JSONObject(jsonStr);
2448 | JSONArray bucketObjs = json.getJSONArray("bindings");
2449 |
2450 | // Setup basic variables for enumerating and building string of objects
2451 | int firstBucket = 0;
2452 | int bucketCounter = 1;
2453 | int totalBuckets = bucketObjs.length();
2454 | String ObjRoleList = "";
2455 |
2456 | // Loop through our list to build a string of all objects
2457 | for (int i = 0; i < bucketObjs.length(); i++) {
2458 | String objRole = bucketObjs.getJSONObject(i).getString("role");
2459 | JSONArray memberObjs = bucketObjs.getJSONObject(i).getJSONArray("members");
2460 | String objMembers = "";
2461 |
2462 | // Loop through ACL members
2463 | for (int j = 0; j < memberObjs.length(); j++) {
2464 | objMembers = objMembers + memberObjs.getString(j) + "; ";
2465 | }
2466 |
2467 | if (firstBucket == 0 && totalBuckets >= 1) {
2468 | ObjRoleList = "Role: " + objRole + " | Members: " + objMembers;
2469 | firstBucket = 1;
2470 | } else if (totalBuckets == 2 && firstBucket == 1) {
2471 | ObjRoleList = ObjRoleList + " and Role: " + objRole + " | Members: " + objMembers;
2472 | } else if (firstBucket == 1 && bucketCounter == totalBuckets) {
2473 | ObjRoleList = ObjRoleList + ", and Role: " + objRole + " | Members: " + objMembers;
2474 | } else {
2475 | ObjRoleList = ObjRoleList + ", Role: " + objRole + " | Members: " + objMembers;
2476 | }
2477 |
2478 | bucketCounter++;
2479 | }
2480 |
2481 | // Create public read access issue with the list of objects included
2482 | IScanIssue publicReadAclIssue = new CustomScanIssue(
2483 | messageInfo.getHttpService(),
2484 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2485 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2486 | "[Anonymous Cloud] Publicly Accessible Google Storage Container ACL",
2487 | "The following bucket contents were enumerated from " + BucketName + ": " + ObjRoleList + ".",
2488 | "Medium",
2489 | "Certain"
2490 | );
2491 |
2492 | // Add public read access issue
2493 | extCallbacks.addScanIssue(publicReadAclIssue);
2494 | }
2495 | } catch (Exception ignore) {}
2496 | }
2497 | }
2498 |
2499 | // Perform anonymous public write access check
2500 | private void publicWriteCheck(String BucketType, IHttpRequestResponse messageInfo, Listmatches, String BucketName) {
2501 |
2502 | // AWS specific checks
2503 | if (BucketType.equals("AWS")) {
2504 |
2505 | // Obtain the buckets region and then create a client based on this region
2506 | String strRegion = anonS3client.headBucket(new HeadBucketRequest(BucketName)).getBucketRegion();
2507 | AmazonS3 s3clientList = AmazonS3ClientBuilder
2508 | .standard()
2509 | .withRegion(strRegion)
2510 | .withCredentials(new AWSStaticCredentialsProvider(anonCredentials))
2511 | .build();
2512 |
2513 | // Attempt to write to the bucket
2514 | try {
2515 | // Create a random string as the key
2516 | String bucketItem = "Burp-AnonymousCloud-" + genRandStr() + ".txt";
2517 |
2518 | // Attempt the bucket write
2519 | s3clientList.putObject(BucketName, bucketItem, "Burp-AnonymousCloud Extension Public Write Test!");
2520 |
2521 | // Check the bucket item
2522 | S3Object writeObj = s3clientList.getObject(BucketName, bucketItem);
2523 |
2524 | // Check size of bucket item
2525 | if (writeObj.getObjectMetadata().getContentLength() >= 47) {
2526 |
2527 | // Create public write access issue with object written
2528 | IScanIssue publicWriteIssue = new CustomScanIssue(
2529 | messageInfo.getHttpService(),
2530 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2531 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2532 | "[Anonymous Cloud] Publicly Writable AWS S3 Bucket",
2533 | "The following bucket object was created in " + BucketName + ": " + bucketItem + ".",
2534 | "High",
2535 | "Certain"
2536 | );
2537 |
2538 | // Add public write access issue
2539 | extCallbacks.addScanIssue(publicWriteIssue);
2540 |
2541 | // Attempt to write an ACL to the previously created object
2542 | try {
2543 | // Get the uploaded objects ACL
2544 | AccessControlList ObjAcl = s3clientList.getObjectAcl(BucketName, bucketItem);
2545 |
2546 | // Clear the ACL
2547 | ObjAcl.getGrantsAsList().clear();
2548 |
2549 | // Set the permissions
2550 | ObjAcl.grantPermission(GroupGrantee.AuthenticatedUsers, Permission.FullControl);
2551 |
2552 | // Set the ACL on the object
2553 | s3clientList.setObjectAcl(BucketName, bucketItem, ObjAcl);
2554 |
2555 | // Make sure ACL was assigned
2556 | if (s3clientList.getObjectAcl(BucketName, bucketItem).toString().contains("/groups/global/AuthenticatedUsers")) {
2557 | // Create any authenticated AWS user write ACL issue with ACL written
2558 | IScanIssue publicWriteAclIssue = new CustomScanIssue(
2559 | messageInfo.getHttpService(),
2560 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2561 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2562 | "[Anonymous Cloud] Publicly Writable AWS S3 ACL",
2563 | "Full permission was given to the " + bucketItem + " in the " + BucketName + " bucket for any authenticated AWS user.",
2564 | "High",
2565 | "Certain"
2566 | );
2567 |
2568 | // Add public write ACL access issue
2569 | extCallbacks.addScanIssue(publicWriteAclIssue);
2570 | }
2571 | } catch (Exception ignore) {}
2572 | }
2573 |
2574 | } catch (Exception ignore) {}
2575 |
2576 | // Google specific checks
2577 | } else if (BucketType.equals("Google")) {
2578 |
2579 | // Create a client to check Google for the bucket
2580 | String bucketItem = "Burp-AnonymousCloud-" + genRandStr() + ".txt";
2581 | HttpClient client = HttpClientBuilder.create().build();
2582 | HttpPost req = new HttpPost(GoogleBucketUploadUrl + BucketName + "/o?uploadType=media&name=" + bucketItem);
2583 | String bucketContent = "Burp-AnonymousCloud Extension Public Write Test!";
2584 |
2585 | // Create and set headers for posting content
2586 | Header headers[] = {
2587 | new BasicHeader("Content-Type", "text/html")
2588 | };
2589 | req.setHeaders(headers);
2590 |
2591 | // Connect to GCP services for bucket ACL access
2592 | try {
2593 | req.setEntity(new StringEntity(bucketContent));
2594 | HttpResponse resp = client.execute(req);
2595 | String respHeaders = resp.getStatusLine().toString();
2596 |
2597 | // If the status is 200, it is public, of 401 then private, otherwise doesn't exist
2598 | if (respHeaders.contains("200 OK")) {
2599 | // Create public write access issue with object written
2600 | IScanIssue publicWriteIssue = new CustomScanIssue(
2601 | messageInfo.getHttpService(),
2602 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2603 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2604 | "[Anonymous Cloud] Publicly Writable Google Storage Container",
2605 | "The following bucket object was created in " + BucketName + ": " + bucketItem + ".",
2606 | "High",
2607 | "Certain"
2608 | );
2609 |
2610 | // Add public write bucket access issue
2611 | extCallbacks.addScanIssue(publicWriteIssue);
2612 | }
2613 | } catch (Exception ignore) { }
2614 | }
2615 | }
2616 |
2617 | // Perform check for allowing any authenticated user read access
2618 | private void anyAuthReadCheck(String BucketType, IHttpRequestResponse messageInfo, Listmatches, String BucketName) {
2619 |
2620 | // AWS specific checks
2621 | if (BucketType.equals("AWS")) {
2622 |
2623 | // Obtain the buckets region and then create a client based on this region
2624 | String strRegion = authS3client.headBucket(new HeadBucketRequest(BucketName)).getBucketRegion();
2625 | AmazonS3 s3clientList = AmazonS3ClientBuilder
2626 | .standard()
2627 | .withRegion(strRegion)
2628 | .withCredentials(new AWSStaticCredentialsProvider(authCredentials))
2629 | .build();
2630 |
2631 | // Get a list of bucket objects
2632 | ObjectListing bucketObjsListing = s3clientList.listObjects(BucketName);
2633 | ListbucketObjs = new ArrayList<>();
2634 |
2635 | // Look through the objects and add to our list
2636 | do {
2637 | for (S3ObjectSummary objItem : bucketObjsListing.getObjectSummaries()) {
2638 | bucketObjs.add(objItem.getKey());
2639 | }
2640 | } while (bucketObjsListing.isTruncated());
2641 |
2642 | // Setup basic variables for enumerating and building string of objects
2643 | int firstBucket = 0;
2644 | int bucketCounter = 1;
2645 | int totalBuckets = bucketObjs.size();
2646 | String ObjList = "";
2647 |
2648 | // Loop through our list to build a string of all objects
2649 | for (Iterator it = bucketObjs.iterator(); it.hasNext();) {
2650 | String obj = it.next();
2651 |
2652 | if (firstBucket == 0 && totalBuckets >= 1) {
2653 | ObjList = obj;
2654 | firstBucket = 1;
2655 | } else if (totalBuckets == 2 && firstBucket == 1) {
2656 | ObjList = ObjList + " and " + obj;
2657 | } else if (firstBucket == 1 && bucketCounter == totalBuckets) {
2658 | ObjList = ObjList + ", and " + obj;
2659 | } else {
2660 | ObjList = ObjList + ", " + obj;
2661 | }
2662 |
2663 | bucketCounter++;
2664 | }
2665 |
2666 | // Create any authenticated AWS user read access issue with the list of objects included
2667 | IScanIssue anyAuthReadIssue = new CustomScanIssue(
2668 | messageInfo.getHttpService(),
2669 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2670 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2671 | "[Anonymous Cloud] Any Authenticated AWS User Accessible AWS S3 Bucket",
2672 | "The following bucket contents were enumerated from " + BucketName + ": " + ObjList + ".",
2673 | "Medium",
2674 | "Certain"
2675 | );
2676 |
2677 | // Add any authenticated AWS user read access issue
2678 | extCallbacks.addScanIssue(anyAuthReadIssue);
2679 |
2680 | // Attempt to read the bucket ACL
2681 | AccessControlList readAcl = s3clientList.getBucketAcl(BucketName);
2682 |
2683 | // Create public read ACL issue with the full ACL included
2684 | if (readAcl.toString().contains("AccessControlList")) {
2685 |
2686 | // Create public read access issue with the list of objects included
2687 | IScanIssue anyAuthReadAclIssue = new CustomScanIssue(
2688 | messageInfo.getHttpService(),
2689 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2690 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2691 | "[Anonymous Cloud] Any Authenticated AWS User Accessible AWS S3 Bucket ACL",
2692 | "The following bucket ACL was enumerated from " + BucketName + ": " + readAcl.toString() + ".",
2693 | "Medium",
2694 | "Certain"
2695 | );
2696 |
2697 | // Add public read ACL access issue
2698 | extCallbacks.addScanIssue(anyAuthReadAclIssue);
2699 | }
2700 | } else if (BucketType.equals("Google")) {
2701 |
2702 | // Create a client to check Google for the bucket
2703 | HttpClient bucketClient = HttpClientBuilder.create().build();
2704 | HttpGet reqBucket = new HttpGet(GoogleValidationUrl + BucketName + "/o");
2705 |
2706 | // Create and set headers for posting content
2707 | Header headers[] = {
2708 | new BasicHeader("Authorization", "Bearer " + googleBearerToken)
2709 | };
2710 | reqBucket.setHeaders(headers);
2711 |
2712 | // Connect to GCP services for bucket access
2713 | try {
2714 | HttpResponse resp = bucketClient.execute(reqBucket);
2715 | String respHeaders = resp.getStatusLine().toString();
2716 |
2717 | // If the status is 200, it is public, of 401 then private, otherwise doesn't exist
2718 | if (respHeaders.contains("200 OK")) {
2719 |
2720 | // Read the response and get the JSON
2721 | BufferedReader rd = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
2722 | String jsonStr = "";
2723 | String line = "";
2724 | while ((line = rd.readLine()) != null) {
2725 | jsonStr = jsonStr + line;
2726 | }
2727 |
2728 | // Read JSON results from public bucket
2729 | JSONObject json = new JSONObject(jsonStr);
2730 | JSONArray bucketObjs = json.getJSONArray("items");
2731 |
2732 | // Setup basic variables for enumerating and building string of objects
2733 | int firstBucket = 0;
2734 | int bucketCounter = 1;
2735 | int totalBuckets = bucketObjs.length();
2736 | String ObjList = "";
2737 |
2738 | // Loop through our list to build a string of all objects
2739 | for (int i = 0; i < bucketObjs.length(); i++) {
2740 | String obj = bucketObjs.getJSONObject(i).getString("name");
2741 |
2742 | if (firstBucket == 0 && totalBuckets >= 1) {
2743 | ObjList = obj;
2744 | firstBucket = 1;
2745 | } else if (totalBuckets == 2 && firstBucket == 1) {
2746 | ObjList = ObjList + " and " + obj;
2747 | } else if (firstBucket == 1 && bucketCounter == totalBuckets) {
2748 | ObjList = ObjList + ", and " + obj;
2749 | } else {
2750 | ObjList = ObjList + ", " + obj;
2751 | }
2752 |
2753 | bucketCounter++;
2754 | }
2755 |
2756 | // Create public read access issue with the list of objects included
2757 | IScanIssue publicReadIssue = new CustomScanIssue(
2758 | messageInfo.getHttpService(),
2759 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2760 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2761 | "[Anonymous Cloud] Any Authenticated Google User Accessible Google Storage Container",
2762 | "The following bucket contents were enumerated from " + BucketName + ": " + ObjList + ".",
2763 | "Medium",
2764 | "Certain"
2765 | );
2766 |
2767 | // Add public read access issue
2768 | extCallbacks.addScanIssue(publicReadIssue);
2769 | }
2770 | } catch (Exception ignore) {}
2771 | }
2772 | }
2773 |
2774 | // Perform anonymous public read ACL access check
2775 | private void anyAuthReadAclCheck(String BucketType, IHttpRequestResponse messageInfo, Listmatches, String BucketName) {
2776 |
2777 | // Google specific checks
2778 | if (BucketType.equals("Google")) {
2779 |
2780 | // Create a client to check Google for the bucket
2781 | HttpClient iamClient = HttpClientBuilder.create().build();
2782 | HttpGet reqIam = new HttpGet(GoogleValidationUrl + BucketName + "/iam");
2783 |
2784 | // Create and set headers for posting content
2785 | Header headers[] = {
2786 | new BasicHeader("Authorization", "Bearer " + googleBearerToken)
2787 | };
2788 | reqIam.setHeaders(headers);
2789 |
2790 |
2791 | // Connect to GCP services for bucket ACL access
2792 | try {
2793 | HttpResponse resp = iamClient.execute(reqIam);
2794 | String respHeaders = resp.getStatusLine().toString();
2795 |
2796 | // If the status is 200, it is public, of 401 then private, otherwise doesn't exist
2797 | if (respHeaders.contains("200 OK")) {
2798 |
2799 | // Read the response and get the JSON
2800 | BufferedReader rd = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
2801 | String jsonStr = "";
2802 | String line = "";
2803 | while ((line = rd.readLine()) != null) {
2804 | jsonStr = jsonStr + line;
2805 | }
2806 |
2807 | // Read JSON results from public bucket
2808 | JSONObject json = new JSONObject(jsonStr);
2809 | JSONArray bucketObjs = json.getJSONArray("bindings");
2810 |
2811 | // Setup basic variables for enumerating and building string of objects
2812 | int firstBucket = 0;
2813 | int bucketCounter = 1;
2814 | int totalBuckets = bucketObjs.length();
2815 | String ObjRoleList = "";
2816 |
2817 | // Loop through our list to build a string of all objects
2818 | for (int i = 0; i < bucketObjs.length(); i++) {
2819 | String objRole = bucketObjs.getJSONObject(i).getString("role");
2820 | JSONArray memberObjs = bucketObjs.getJSONObject(i).getJSONArray("members");
2821 | String objMembers = "";
2822 |
2823 | // Loop through ACL members
2824 | for (int j = 0; j < memberObjs.length(); j++) {
2825 | objMembers = objMembers + memberObjs.getString(j) + "; ";
2826 | }
2827 |
2828 | if (firstBucket == 0 && totalBuckets >= 1) {
2829 | ObjRoleList = "Role: " + objRole + " | Members: " + objMembers;
2830 | firstBucket = 1;
2831 | } else if (totalBuckets == 2 && firstBucket == 1) {
2832 | ObjRoleList = ObjRoleList + " and Role: " + objRole + " | Members: " + objMembers;
2833 | } else if (firstBucket == 1 && bucketCounter == totalBuckets) {
2834 | ObjRoleList = ObjRoleList + ", and Role: " + objRole + " | Members: " + objMembers;
2835 | } else {
2836 | ObjRoleList = ObjRoleList + ", Role: " + objRole + " | Members: " + objMembers;
2837 | }
2838 |
2839 | bucketCounter++;
2840 | }
2841 |
2842 | // Create public read access issue with the list of objects included
2843 | IScanIssue publicReadAclIssue = new CustomScanIssue(
2844 | messageInfo.getHttpService(),
2845 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2846 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2847 | "[Anonymous Cloud] Any Authenticated Google User Accessible Google Storage Container ACL",
2848 | "The following bucket contents were enumerated from " + BucketName + ": " + ObjRoleList + ".",
2849 | "Medium",
2850 | "Certain"
2851 | );
2852 |
2853 | // Add public read access issue
2854 | extCallbacks.addScanIssue(publicReadAclIssue);
2855 | }
2856 | } catch (Exception ignore) {}
2857 | }
2858 | }
2859 |
2860 | // Perform check for allowing any authenticated user write access
2861 | private void anyAuthWriteCheck(String BucketType, IHttpRequestResponse messageInfo, Listmatches, String BucketName) {
2862 |
2863 | // AWS specific checks
2864 | if (BucketType.equals("AWS")) {
2865 |
2866 | // Obtain the buckets region and then create a client based on this region
2867 | String strRegion = authS3client.headBucket(new HeadBucketRequest(BucketName)).getBucketRegion();
2868 | AmazonS3 s3clientList = AmazonS3ClientBuilder
2869 | .standard()
2870 | .withRegion(strRegion)
2871 | .withCredentials(new AWSStaticCredentialsProvider(authCredentials))
2872 | .build();
2873 |
2874 | // Attempt to write to the bucket
2875 | try {
2876 | // Create a random string as the key
2877 | String bucketItem = "Burp-AnonymousCloud-" + genRandStr() + ".txt";
2878 |
2879 | // Attempt the bucket write
2880 | s3clientList.putObject(BucketName, bucketItem, "Burp-AnonymousCloud Extension Any Authenticated AWS User Write Test!");
2881 |
2882 | // Check the bucket item
2883 | S3Object writeObj = s3clientList.getObject(BucketName, bucketItem);
2884 |
2885 | // Check size of bucket item
2886 | if (writeObj.getObjectMetadata().getContentLength() >= 47) {
2887 |
2888 | // Create any authenticated AWS user write issue with object written
2889 | IScanIssue anyAuthWriteIssue = new CustomScanIssue(
2890 | messageInfo.getHttpService(),
2891 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2892 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2893 | "[Anonymous Cloud] Any Authenticated AWS User Writable AWS S3 Bucket",
2894 | "The following bucket object was created in " + BucketName + ": " + bucketItem + ".",
2895 | "High",
2896 | "Certain"
2897 | );
2898 |
2899 | // Add public write access issue
2900 | extCallbacks.addScanIssue(anyAuthWriteIssue);
2901 |
2902 | // Attempt to write an ACL to the previously created object
2903 | try {
2904 | // Get the uploaded objects ACL
2905 | AccessControlList ObjAcl = s3clientList.getObjectAcl(BucketName, bucketItem);
2906 |
2907 | // Clear the ACL
2908 | ObjAcl.getGrantsAsList().clear();
2909 |
2910 | // Set the permissions
2911 | ObjAcl.grantPermission(GroupGrantee.AuthenticatedUsers, Permission.FullControl);
2912 |
2913 | // Set the ACL on the object
2914 | s3clientList.setObjectAcl(BucketName, bucketItem, ObjAcl);
2915 |
2916 | // Make sure ACL was assigned
2917 | if (s3clientList.getObjectAcl(BucketName, bucketItem).toString().contains("/groups/global/AuthenticatedUsers")) {
2918 | // Create any authenticated AWS user write ACL issue with ACL written
2919 | IScanIssue anyAuthWriteAclIssue = new CustomScanIssue(
2920 | messageInfo.getHttpService(),
2921 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2922 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2923 | "[Anonymous Cloud] Any Authenticated AWS User Writable AWS S3 ACL",
2924 | "Full permission was given to the " + bucketItem + " in the " + BucketName + " bucket for any authenticated AWS user.",
2925 | "High",
2926 | "Certain"
2927 | );
2928 |
2929 | // Add public write ACL access issue
2930 | extCallbacks.addScanIssue(anyAuthWriteAclIssue);
2931 | }
2932 | } catch (Exception ignore) {}
2933 | }
2934 |
2935 | } catch (Exception ignore) {}
2936 | } else if (BucketType.equals("Google")) {
2937 | // Create a client to check Google for the bucket
2938 | String bucketItem = "Burp-AnonymousCloud-" + genRandStr() + ".txt";
2939 | HttpClient client = HttpClientBuilder.create().build();
2940 | HttpPost req = new HttpPost(GoogleBucketUploadUrl + BucketName + "/o?uploadType=media&name=" + bucketItem);
2941 | String bucketContent = "Burp-AnonymousCloud Extension Public Write Test!";
2942 |
2943 | // Create and set headers for posting content
2944 | Header headers[] = {
2945 | new BasicHeader("Authorization", "Bearer " + googleBearerToken),
2946 | new BasicHeader("Content-Type", "text/html")
2947 | };
2948 | req.setHeaders(headers);
2949 |
2950 | // Connect to GCP services for bucket ACL access
2951 | try {
2952 | req.setEntity(new StringEntity(bucketContent));
2953 | HttpResponse resp = client.execute(req);
2954 | String respHeaders = resp.getStatusLine().toString();
2955 |
2956 | // If the status is 200, it is public, of 401 then private, otherwise doesn't exist
2957 | if (respHeaders.contains("200 OK")) {
2958 | // Create public write access issue with object written
2959 | IScanIssue publicWriteIssue = new CustomScanIssue(
2960 | messageInfo.getHttpService(),
2961 | extHelpers.analyzeRequest(messageInfo).getUrl(),
2962 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
2963 | "[Anonymous Cloud] Any Authenticated Google User Writable Google Storage Container",
2964 | "The following bucket object was created in " + BucketName + ": " + bucketItem + ".",
2965 | "High",
2966 | "Certain"
2967 | );
2968 |
2969 | // Add public write bucket access issue
2970 | extCallbacks.addScanIssue(publicWriteIssue);
2971 | }
2972 | } catch (Exception ignore) { }
2973 | }
2974 | }
2975 |
2976 | // Perform anonymous public read access check for a discovered Firebase DB
2977 | private void gcpFirebaseCheck(IHttpRequestResponse messageInfo, Listmatches, String firebaseDb) {
2978 | // Create a client to check Google for the Firebase DB
2979 | HttpClient readClient = HttpClientBuilder.create().build();
2980 | firebaseDb = firebaseDb.replaceAll("\\\\", "");
2981 | HttpGet readReq = new HttpGet("https://" + firebaseDb + "/.json");
2982 |
2983 | // Connect to GCP services for Firebase DB access
2984 | try {
2985 | HttpResponse readResp = readClient.execute(readReq);
2986 | String readRespHeaders = readResp.getStatusLine().toString();
2987 |
2988 | // If the status is 200, it is public, otherwise doesn't exist
2989 | if (readRespHeaders.contains("200 OK")) {
2990 |
2991 | // Read the response and get the XML
2992 | BufferedReader rd = new BufferedReader(new InputStreamReader(readResp.getEntity().getContent()));
2993 | String respStr = "";
2994 | String line = "";
2995 |
2996 | // Put XML string together
2997 | while ((line = rd.readLine()) != null) {
2998 | respStr = respStr + line;
2999 | }
3000 | // Create public access issue with object written
3001 | IScanIssue publicReadIssue = new CustomScanIssue(
3002 | messageInfo.getHttpService(),
3003 | extHelpers.analyzeRequest(messageInfo).getUrl(),
3004 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
3005 | "[Anonymous Cloud] Publicly Accessible Firebase Database",
3006 | "The following Firebase database is publicly readable: " + firebaseDb + ", and returned: " + respStr,
3007 | "Medium",
3008 | "Certain"
3009 | );
3010 |
3011 | // Add public read firebase db access issue
3012 | extCallbacks.addScanIssue(publicReadIssue);
3013 | } else if (readRespHeaders.contains("401 Unauthorized") || readRespHeaders.contains("402 Payment")) {
3014 | // Create a finding noting that the Firebase DB is valid
3015 | IScanIssue firebaseConfirmIssue = new CustomScanIssue(
3016 | messageInfo.getHttpService(),
3017 | extHelpers.analyzeRequest(messageInfo).getUrl(),
3018 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
3019 | "[Anonymous Cloud] Firebase Database Exists",
3020 | "The following Firebase database was confirmed to be valid: " + firebaseDb,
3021 | "Low",
3022 | "Certain"
3023 | );
3024 |
3025 | // Add valid firebase db access issue
3026 | extCallbacks.addScanIssue(firebaseConfirmIssue);
3027 | }
3028 | } catch (Exception ignore) { }
3029 |
3030 | // Create a client to check Google for the Firebase DB
3031 | String firebaseItem = "Burp-AnonymousCloud-" + genRandStr();
3032 | String firebaseContent = "Burp-AnonymousCloud Extension Public Write Test!";
3033 | HttpClient writeClient = HttpClientBuilder.create().build();
3034 | HttpPost writeReq = new HttpPost("https://" + firebaseDb + "/.json");
3035 |
3036 | // Connect to GCP services for Firebase DB access
3037 | try {
3038 | writeReq.setEntity(new StringEntity("{ \"" + firebaseItem + "\": \"" + firebaseContent + "\" }"));
3039 | HttpResponse writeResp = writeClient.execute(writeReq);
3040 | String writeRespHeaders = writeResp.getStatusLine().toString();
3041 |
3042 | // If the status is 200, it is public, otherwise doesn't exist
3043 | if (writeRespHeaders.contains("200 OK")) {
3044 | // Create public access issue with object written
3045 | IScanIssue publicReadIssue = new CustomScanIssue(
3046 | messageInfo.getHttpService(),
3047 | extHelpers.analyzeRequest(messageInfo).getUrl(),
3048 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, null, matches) },
3049 | "[Anonymous Cloud] Publicly Writable Firebase Database",
3050 | "The Firebase database " + firebaseDb + " had a value of: " + firebaseItem + " written to it.",
3051 | "High",
3052 | "Certain"
3053 | );
3054 |
3055 | // Add public write bucket access issue
3056 | extCallbacks.addScanIssue(publicReadIssue);
3057 | }
3058 | } catch (Exception ignore) { }
3059 | }
3060 |
3061 | // Perform anonymous public read access check for a discovered Firestore DB
3062 | private void gcpFirestoreCheck(IHttpRequestResponse messageInfo, ListreqMatches, ListrespMatches, String firebaseDb) {
3063 |
3064 | // First validate we can check
3065 | firebaseDb = firebaseDb.replaceAll("\\\\", "");
3066 | Pattern GcpFirestoreFullPattern = Pattern.compile("(firestore\\.googleapis\\.com\\/v1\\/projects\\/[\\w.-]+\\/databases\\/\\(default\\)\\/documents\\/[\\w.-~]+)", Pattern.CASE_INSENSITIVE);
3067 | Matcher GcpFirestoreFullMatch = GcpFirestoreFullPattern.matcher(firebaseDb);
3068 |
3069 | if (GcpFirestoreFullMatch.find()) {
3070 | // Create a client to check Google for the Firestore DB
3071 | HttpClient readClient = HttpClientBuilder.create().build();
3072 | HttpGet readReq = new HttpGet("https://" + GcpFirestoreFullMatch.group(0));
3073 |
3074 | // Connect to GCP services for Firestore DB access
3075 | try {
3076 | HttpResponse readResp = readClient.execute(readReq);
3077 | String readRespHeaders = readResp.getStatusLine().toString();
3078 |
3079 | // If the status is 200, it is public, otherwise doesn't exist or requires auth
3080 | if (readRespHeaders.contains("200 OK")) {
3081 | // Create public access issue
3082 | IScanIssue publicReadIssue = new CustomScanIssue(
3083 | messageInfo.getHttpService(),
3084 | extHelpers.analyzeRequest(messageInfo).getUrl(),
3085 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, reqMatches, respMatches) },
3086 | "[Anonymous Cloud] Publicly Accessible Firestore Database",
3087 | "The following Firestore database is publicly readable: " + GcpFirestoreFullMatch.group(0),
3088 | "Medium",
3089 | "Certain"
3090 | );
3091 |
3092 | // Add public read Firestore db access issue
3093 | extCallbacks.addScanIssue(publicReadIssue);
3094 | } else if (readRespHeaders.contains("403 Forbidden")) {
3095 | // Create a finding noting that the Firestore DB is valid
3096 | IScanIssue firestoreConfirmIssue = new CustomScanIssue(
3097 | messageInfo.getHttpService(),
3098 | extHelpers.analyzeRequest(messageInfo).getUrl(),
3099 | new IHttpRequestResponse[] { extCallbacks.applyMarkers(messageInfo, reqMatches, respMatches) },
3100 | "[Anonymous Cloud] Firestore Database Exists",
3101 | "The following Firestore database was confirmed to be valid: " + GcpFirestoreFullMatch.group(0),
3102 | "Low",
3103 | "Certain"
3104 | );
3105 |
3106 | // Add valid firestore db access issue
3107 | extCallbacks.addScanIssue(firestoreConfirmIssue);
3108 | }
3109 | } catch (Exception ignore) { }
3110 | }
3111 | }
3112 |
3113 | @Override
3114 | public int consolidateDuplicateIssues(IScanIssue existingIssue, IScanIssue newIssue) {
3115 | // This method is called when multiple issues are reported for the same URL
3116 | // path by the same extension-provided check. The value we return from this
3117 | // method determines how/whether Burp consolidates the multiple issues
3118 | // to prevent duplication
3119 | //
3120 | // Since the issue name is sufficient to identify our issues as different,
3121 | // if both issues have the same name, only report the existing issue
3122 | // otherwise report both issues
3123 | if (existingIssue.getIssueName().equals(newIssue.getIssueName()))
3124 | return -1;
3125 | else return 0;
3126 | }
3127 |
3128 | // helper method to search a response for occurrences of a literal match string
3129 | // and return a list of start/end offsets
3130 | private List getMatches(byte[] response, byte[] match) {
3131 | List matches = new ArrayList<>();
3132 |
3133 | int start = 0;
3134 | while (start < response.length) {
3135 | start = extHelpers.indexOf(response, match, true, start, response.length);
3136 | if (start == -1)
3137 | break;
3138 | matches.add(new int[] { start, start + match.length });
3139 | start += match.length;
3140 | }
3141 |
3142 | return matches;
3143 | }
3144 | }
3145 |
3146 | class CustomScanIssue implements IScanIssue {
3147 | private IHttpService httpService;
3148 | private URL url;
3149 | private IHttpRequestResponse[] httpMessages;
3150 | private String name;
3151 | private String detail;
3152 | private String severity;
3153 | private String confidence;
3154 |
3155 | public CustomScanIssue(IHttpService httpService, URL url, IHttpRequestResponse[] httpMessages, String name, String detail, String severity, String confidence) {
3156 | this.httpService = httpService;
3157 | this.url = url;
3158 | this.httpMessages = httpMessages;
3159 | this.name = name;
3160 | this.detail = detail;
3161 | this.severity = severity;
3162 | this.confidence = confidence;
3163 | }
3164 |
3165 | @Override
3166 | public URL getUrl() {
3167 | return url;
3168 | }
3169 |
3170 | @Override
3171 | public String getIssueName() {
3172 | return name;
3173 | }
3174 |
3175 | @Override
3176 | public int getIssueType() {
3177 | return 0;
3178 | }
3179 |
3180 | @Override
3181 | public String getSeverity() {
3182 | return severity;
3183 | }
3184 |
3185 | @Override
3186 | public String getConfidence() {
3187 | return confidence;
3188 | }
3189 |
3190 | @Override
3191 | public String getIssueBackground() {
3192 | return null;
3193 | }
3194 |
3195 | @Override
3196 | public String getRemediationBackground() {
3197 | return null;
3198 | }
3199 |
3200 | @Override
3201 | public String getIssueDetail() {
3202 | return detail;
3203 | }
3204 |
3205 | @Override
3206 | public String getRemediationDetail() {
3207 | return null;
3208 | }
3209 |
3210 | @Override
3211 | public IHttpRequestResponse[] getHttpMessages() {
3212 | return httpMessages;
3213 | }
3214 |
3215 | @Override
3216 | public IHttpService getHttpService() {
3217 | return httpService;
3218 | }
3219 | }
--------------------------------------------------------------------------------