├── AnonymousCloud.jar ├── README.md ├── build.gradle ├── settings.gradle └── src └── burp └── BurpExtender.java /AnonymousCloud.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codewatchorg/Burp-AnonymousCloud/03e33ccea70793f94ad9cd40e5faac6efd22660c/AnonymousCloud.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | configurations { 8 | extraLibs 9 | } 10 | 11 | dependencies { 12 | compile 'net.portswigger.burp.extender:burp-extender-api:2.1' 13 | extraLibs group: 'commons-codec', name: 'commons-codec', version: '1.11' 14 | extraLibs group: 'com.amazonaws', name: 'aws-java-sdk-core', version: '1.11.708' 15 | extraLibs group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.11.708' 16 | extraLibs group: 'commons-logging', name: 'commons-logging', version: '1.1.3' 17 | extraLibs group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.9' 18 | extraLibs group: 'org.apache.httpcomponents', name: 'httpcore', version: '4.4.11' 19 | extraLibs group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.6.7' 20 | extraLibs group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.6.7' 21 | extraLibs group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.6.7' 22 | extraLibs group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: '2.6.7' 23 | extraLibs group: 'joda-time', name: 'joda-time', version: '2.8.1' 24 | extraLibs group: 'org.json', name: 'json', version: '20190722' 25 | configurations.compile.extendsFrom(configurations.extraLibs) 26 | } 27 | 28 | sourceSets { 29 | main { 30 | java { 31 | srcDir 'src' 32 | } 33 | } 34 | } 35 | 36 | jar { 37 | baseName = project.name 38 | from { configurations.extraLibs.collect { it.isDirectory() ? it : zipTree(it) } } 39 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------