├── example.png ├── README.md └── s3scan.py /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhn/S3Scan/HEAD/example.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # S3Scan 2 | A simple script to find open Amazon AWS S3 buckets in your target websites. S3 buckets are a popular way of storing static contents among web developers. Often, developers tend to set the bucket permissions insecurely during development, and forget to set them correctly in prod, leading to (security) issues. 3 | 4 | ### Usecase 5 | * Searching for insecure S3 buckets in a target website during reconnaissance stage. 6 | * Differentiating between publicly unavailable, secured, read only, read + write and full access buckets 7 | * Automated crawling and searching for Bucket URLs in website's page source. 8 | 9 | ### Demo 10 | ![demo](https://raw.githubusercontent.com/abhn/S3Scan/master/example.png) 11 | 12 | ### Prerequisites 13 | No worries if you don't have them. You'll install them in 'Installation' section anyway. 14 | * Python 15 | * Pip 16 | * BeautifulSoup 17 | * Boto3 18 | * AWS account for access and secret token 19 | 20 | ### Installation 21 | Install Python Pip using your OS's package manager 22 | ``` 23 | pip2 install beautifulsoup boto3 24 | git clone https://github.com/abhn/S3Scan.git 25 | cd S3Scan 26 | ``` 27 | If you already have ```awscli``` installed and configured, you should have the necessary tokens with you. If not, follow the steps. 28 | 29 | Login to your AWS panel and generate your ACCESS_KEY and SECRET_KEY. Although fairly straightforward, if you are lost, [here is a guide](http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html). Now you can either add them to your .bashrc (recommended), or add them to the script itself (not recommended). 30 | 31 | To add the credentials to your .bashrc, 32 | ``` 33 | echo export AWS_ACCESS_KEY_ID="API7IQGDJ4G26S3VWYIZ" >> ~/.bashrc 34 | echo export AWS_SECRET_ACCESS_KEY="jfur8d6djePf5s5fk62P5s3I6q3pvxsheysnehs" >> ~/.bashrc 35 | source ~/.bashrc 36 | ``` 37 | 38 | Done! 39 | 40 | ### Usage 41 | ``` 42 | Usage: $ python ./s3scan.py [-u] url 43 | 44 | Options: 45 | --version show program's version number and exit 46 | -h, --help show this help message and exit 47 | -u URL, --url=URL url to scan 48 | -d turn on debug messages 49 | ``` 50 | 51 | ### License 52 | MIT -------------------------------------------------------------------------------- /s3scan.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys, os 3 | import httplib2 4 | import boto3, botocore 5 | from BeautifulSoup import BeautifulSoup, SoupStrainer 6 | from optparse import OptionParser 7 | from botocore.client import Config 8 | 9 | 10 | # Saving myself from passing around these variables 11 | # Sorry! 12 | globalBaseUrl = "" 13 | globalLinkList = [] 14 | s3 = None 15 | 16 | 17 | # https://stackoverflow.com/questions/287871/print-in-terminal-with-colors-using-python 18 | class bcolors: 19 | HEADER = '\033[95m' 20 | OKBLUE = '\033[94m' 21 | OKGREEN = '\033[92m' 22 | WARNING = '\033[93m' 23 | FAIL = '\033[91m' 24 | ENDC = '\033[0m' 25 | BOLD = '\033[1m' 26 | UNDERLINE = '\033[4m' 27 | 28 | 29 | # helper functions for coloring 30 | def printGreen(text): 31 | return bcolors.OKGREEN + text + bcolors.ENDC 32 | 33 | 34 | def printBlue(text): 35 | return bcolors.OKBLUE + text + bcolors.ENDC 36 | 37 | 38 | def printWarning(text): 39 | return bcolors.WARNING + text + bcolors.ENDC 40 | 41 | 42 | def printFail(text): 43 | return bcolors.FAIL + text + bcolors.ENDC 44 | 45 | def printScreen(text, color): 46 | return { 47 | 'green': printGreen(text), 48 | 'blue': printBlue(text), 49 | 'warn': printWarning(text), 50 | 'fail': printFail(text) 51 | }.get(color, 'blue') 52 | 53 | 54 | def retrieve_links(url): 55 | """Given a url, fetch all hyperlinks on that page and return them as a list""" 56 | 57 | page_source = get_source(url) 58 | 59 | linkList = [] # pun! 60 | for link in BeautifulSoup(page_source, parseOnlyThese=SoupStrainer('a')): 61 | if link.has_key('href'): 62 | linkList.append(link['href']) 63 | 64 | return linkList 65 | 66 | 67 | def scanner(url): 68 | """Look for S3 bucket urls 69 | Check their permissions and print their security setting 70 | """ 71 | 72 | if globalBaseUrl in url: 73 | if url not in globalLinkList: 74 | # add the current url in the global links pool 75 | globalLinkList.append(url) 76 | 77 | global s3 78 | 79 | sys.stdout.write(printScreen("[>]Current webpage: " + url + "\n", "blue")) 80 | 81 | page_source = get_source(url) 82 | 83 | reg = re.compile('((?:https*)(?::\\/{2}[\\.\\w-]+\\.amazonaws.com)(?:[\\/|\\.]?)(?:[^\\s"]*))') 84 | 85 | # return if empty page 86 | if page_source == None: 87 | return 88 | 89 | for bucket in re.findall(reg, page_source): 90 | 91 | sys.stdout.write(printScreen("[*]Found " + bucket + "\n", "blue")) 92 | 93 | # we don't need the complete URL. Just the root of the bucket 94 | bucketUrl = bucket.split('com/')[0] + 'com/' # TODO this should be a regex 95 | 96 | # grab the username 97 | # https://abhn.s3.amazonaws.com/randomstring ==> abhn 98 | if "https" in bucketUrl: 99 | bucketName = bucketUrl.split('.s3')[0].split('https://')[1] 100 | else: 101 | bucketName = bucketUrl.split('.s3')[0].split('http://')[1] 102 | 103 | bucket = s3.Bucket(bucketName) 104 | 105 | sys.stdout.write(printScreen("[>]Testing " + bucketName + "\t", "blue")) 106 | 107 | # flags 108 | readFlag = 0 109 | writeFlag = 0 110 | fullControlFlag = 0 111 | secureFlag = 0 112 | # READ :- Any authenticated AWS user can read 113 | # WRITE :- Any authenticated AWS user ca 114 | # FULL CONTROL :- Any authenticated AWS user can read/write/delete 115 | # ClientError :- AccessDenied (Bucket is secure) 116 | try: 117 | acl = bucket.Acl() 118 | for grant in acl.grants: 119 | if grant['Grantee']['Type'] == "Group" and grant['Permission'] == "READ": 120 | readFlag = 1 121 | elif grant['Grantee']['Type'] == "Group" and grant['Permission'] == "WRITE": 122 | 123 | writeFlag = 1 124 | elif grant['Grantee']['Type'] == "Group" and grant['Permission'] == "FULL_CONTROL": 125 | fullControlFlag = 1 126 | else: 127 | pass 128 | 129 | # evaluate 130 | if readFlag and not writeFlag and not fullControlFlag: 131 | sys.stdout.write(printScreen("[Insecure - Read]", "fail")) 132 | elif readFlag and writeFlag and not fullControlFlag: 133 | sys.stdout.write(printScreen("[Insecure - Read+Write]", "fail")) 134 | elif fullControlFlag: 135 | sys.stdout.write(printScreen("[Insecure - Full Control]", "fail")) 136 | else: 137 | sys.stdout.write(printScreen("[Not Public]", "green")) 138 | sys.stdout.write('\n') 139 | 140 | except botocore.exceptions.ClientError as e: 141 | if e.response["Error"]["Code"] == "NoSuchBucket": 142 | sys.stdout.write(printScreen("[No Such Bucket. Takeover?]\n", "fail")) 143 | else: 144 | sys.stdout.write(printScreen("[" + e.response["Error"]["Code"] + "]\n", "green")) 145 | 146 | 147 | 148 | 149 | def get_source(url): 150 | """Return the source of the supplied url argument""" 151 | 152 | http = httplib2.Http() 153 | try: 154 | status, response = http.request(url, 155 | headers={'User-Agent':' Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0'}) 156 | if status.status == 200: 157 | return response 158 | else: 159 | return None 160 | except httplib2.HttpLib2Error as e: 161 | return None 162 | 163 | 164 | def driver(url): 165 | """Scan the current url, retrieve all hyperlinks and then scan those pages recursively""" 166 | 167 | # maintain a list of links from the current page 168 | currList = [] 169 | 170 | page_source = get_source(url) 171 | if page_source != None: 172 | links = retrieve_links(url) 173 | for link in links: 174 | if len(link) > 0: 175 | # we hit a relative link 176 | if globalBaseUrl not in link and link[0] == '/': 177 | link = globalBaseUrl + link 178 | scanner(link) 179 | currList.append(link) 180 | else: 181 | scanner(link) 182 | currList.append(link) 183 | else: 184 | continue 185 | for link in currList: 186 | driver(link) 187 | else: 188 | sys.stdout.write(printScreen("[x]Empty response. Skipping\n", "warn")) 189 | 190 | 191 | def initiator(globalBaseUrl): 192 | """take a url and set up s3 auth. Then call the driver""" 193 | 194 | global s3 195 | 196 | # alternate way to authenticate in else. 197 | # use what you prefer 198 | if True: 199 | access_key = os.environ.get('AWS_ACCESS_KEY_ID') 200 | secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') 201 | 202 | if access_key is None or secret_key is None: 203 | print printWarning("""No access credentials available. 204 | Please export your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. 205 | Details: http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html 206 | """) 207 | sys.exit(0) 208 | 209 | s3 = boto3.resource('s3', config=Config(signature_version='s3v4')) 210 | 211 | else: 212 | # If you prefer to supply the credentials here, 213 | # make sure you flip the if condition to False 214 | # and subsitiute the necessary data :) 215 | s3 = boto3.resource('s3', 216 | aws_access_key_id=ACCESS_ID, 217 | aws_secret_access_key=ACCESS_KEY, 218 | config=Config(signature_version='s3v4') 219 | ) 220 | 221 | print printScreen("[>]Initiating...", "blue") 222 | print printScreen("[>]Press Ctrl+C to terminate script", "blue") 223 | 224 | scanner(globalBaseUrl) 225 | driver(globalBaseUrl) 226 | 227 | def main(): 228 | parser = OptionParser(usage="$ python ./%prog [-u] url", version="%prog 1.0") 229 | 230 | parser.add_option("-u", "--url", dest="url", 231 | help="url to scan") 232 | parser.add_option("-d", action="store_true", dest="debug", 233 | help="turn on debug messages") 234 | 235 | (options, args) = parser.parse_args() 236 | 237 | if options.url == None: 238 | parser.print_help() 239 | exit(0) 240 | 241 | # debug switch 242 | if not options.debug: 243 | # show no traceback. Only exception 244 | sys.tracebacklimit = 0 245 | 246 | global globalBaseUrl 247 | globalBaseUrl = options.url 248 | 249 | # initiate 250 | initiator(globalBaseUrl) 251 | 252 | if __name__ == '__main__': 253 | main() 254 | --------------------------------------------------------------------------------