|
| 1 | +from typing import Tuple |
| 2 | +from io import StringIO |
| 3 | +from argparse import ArgumentParser, FileType |
| 4 | +import requests |
| 5 | +import json |
| 6 | +import sys |
| 7 | + |
| 8 | +common_endpoints = [ |
| 9 | + "", |
| 10 | + "users", |
| 11 | + # TODO - users/$uid - extract uid from JWT? |
| 12 | + "groups", |
| 13 | + "messages", |
| 14 | + "posts", |
| 15 | + "chats", |
| 16 | +] |
| 17 | + |
| 18 | +# TODO - attempt using different values to check value filters |
| 19 | +value_attempts = [ |
| 20 | + "1", |
| 21 | + "0", |
| 22 | + "-1", |
| 23 | + "1566889891" |
| 24 | + "true", |
| 25 | + "false", |
| 26 | + "a", |
| 27 | + "null", |
| 28 | + "-JSOpn9ZC54A4P4RoqVa", |
| 29 | + "591dd66c-ffb0-4f7c-80f2-13345066a159" |
| 30 | +] |
| 31 | + |
| 32 | + |
| 33 | +def try_endpoint(args, endpoint: str) -> Tuple[bool, bool, str]: |
| 34 | + url = 'https://{}.firebaseio.com/{}.json'.format(args.code, endpoint) |
| 35 | + if args.auth is not None: |
| 36 | + url += '?auth={}'.format(args.auth) |
| 37 | + |
| 38 | + read_r = requests.get(url) |
| 39 | + # POST nothing to test write access to avoid overwriting data |
| 40 | + write_r = requests.post(url, data="null", headers={'Content-type': 'application/json'}) |
| 41 | + data = read_r.json() if read_r.status_code != 401 else None |
| 42 | + print(data) |
| 43 | + return read_r.status_code != 401, write_r.status_code != 401, data |
| 44 | + |
| 45 | + |
| 46 | +# from https://stackoverflow.com/a/7205107/2683545 |
| 47 | +def merge(a, b, path=None): |
| 48 | + "merges b into a" |
| 49 | + if path is None: path = [] |
| 50 | + for key in b: |
| 51 | + if key in a: |
| 52 | + if isinstance(a[key], dict) and isinstance(b[key], dict): |
| 53 | + merge(a[key], b[key], path + [str(key)]) |
| 54 | + elif a[key] == b[key]: |
| 55 | + pass # same leaf value |
| 56 | + else: |
| 57 | + raise Exception('Conflict at %s' % '.'.join(path + [str(key)])) |
| 58 | + else: |
| 59 | + a[key] = b[key] |
| 60 | + return a |
| 61 | + |
| 62 | + |
| 63 | +def scan_sites(args): |
| 64 | + firebase_sites = args.sites.read().splitlines() |
| 65 | + output = {} |
| 66 | + for site in firebase_sites: |
| 67 | + print("Scanning site {}...".format(site)) |
| 68 | + arg_endpoints = args.endpoints.read().splitlines() |
| 69 | + endpoint_list = common_endpoints + arg_endpoints |
| 70 | + endpoint_info = {} |
| 71 | + db_dump = {} |
| 72 | + for endpoint in endpoint_list: |
| 73 | + dump_path = endpoint.split("/") |
| 74 | + read_success, write_success, data = try_endpoint(args, endpoint) |
| 75 | + if data is not None: |
| 76 | + for part in dump_path: |
| 77 | + if part not in db_dump: |
| 78 | + db_dump[part] = {} |
| 79 | + if isinstance(data, dict): |
| 80 | + db_dump[part] = merge(db_dump[part], data) |
| 81 | + else: |
| 82 | + db_dump[part] = data |
| 83 | + if read_success or write_success: |
| 84 | + endpoint_info[endpoint] = {"read": read_success, "write": write_success} |
| 85 | + output[site] = {"info": endpoint_info, "dump": db_dump} |
| 86 | + args.out.write(json.dumps(output)) |
| 87 | + args.out.close() |
| 88 | + |
| 89 | + |
| 90 | +def parse_args(): |
| 91 | + parser = ArgumentParser() |
| 92 | + parser.add_argument('sites', help="File containing list of firebase sites to scan [code].firebaseio.com", type=FileType('r')) |
| 93 | + # parser.add_argument('--methods', help="A list of HTTP methods to try", nargs="*", default=firebase_http_methods, choices=firebase_http_methods) |
| 94 | + parser.add_argument('--auth', help="An optional auth token to use") |
| 95 | + parser.add_argument('--endpoints', help="A list of known endpoints to check", nargs='?', type=FileType('r'), default=StringIO("")) |
| 96 | + parser.add_argument('--dirty', help="Should we modify the db to find more writable locations?", action="store_true", default=False) |
| 97 | + parser.add_argument('--out', help="A file to dump info to", nargs='?', type=FileType('w'), default=sys.stdout) |
| 98 | + args = parser.parse_args() |
| 99 | + scan_sites(args) |
| 100 | + |
| 101 | + |
| 102 | +if __name__ == '__main__': |
| 103 | + parse_args() |
0 commit comments