diff --git a/android/tests/apikeys.py b/android/tests/apikeys.py index 106393f..1633718 100644 --- a/android/tests/apikeys.py +++ b/android/tests/apikeys.py @@ -4,6 +4,7 @@ import re import xml.etree.ElementTree as ET +API_GOOGLE_SEVERITY = 2.5 API_DEFAULT_SEVERITY = 6.5 API_DETAILS = "details" @@ -11,9 +12,9 @@ API_SEVERITY = "severity" common_api_keys = { - "google_api_key": { API_SEVERITY: API_DEFAULT_SEVERITY, API_DETAILS: "Google API Key found", API_DESCRIPTION: "Google API Key" }, - "google_maps_key": { API_SEVERITY: API_DEFAULT_SEVERITY, API_DETAILS: "Google Maps Key found", API_DESCRIPTION: "Google Maps Key" }, - "google_crash_reporting_api_key": { API_SEVERITY: API_DEFAULT_SEVERITY, API_DETAILS: "Google Crash Report found", API_DESCRIPTION: "Google Crash Report API Key" }, + "google_api_key": { API_SEVERITY: API_GOOGLE_SEVERITY, API_DETAILS: "Google API Key found", API_DESCRIPTION: "Google API Key" }, + "google_maps_key": { API_SEVERITY: API_GOOGLE_SEVERITY, API_DETAILS: "Google Maps Key found", API_DESCRIPTION: "Google Maps Key" }, + "google_crash_reporting_api_key": { API_SEVERITY: API_GOOGLE_SEVERITY, API_DETAILS: "Google Crash Report found", API_DESCRIPTION: "Google Crash Report API Key" }, "seed_crypto_keystore_password": { API_SEVERITY: API_DEFAULT_SEVERITY, API_DETAILS: "Seed Crypto Keystore Password found", API_DESCRIPTION: "Seed Crypto Keystore Password" }, "seed_crypto_privatekey_alias": { API_SEVERITY: API_DEFAULT_SEVERITY, API_DETAILS: "Seed Crypto Privatekey Alias found", API_DESCRIPTION: "Seed Crypto Privatekey Alias" }, "seed_crypto_privatekey_password": { API_SEVERITY: API_DEFAULT_SEVERITY, API_DETAILS: "Seed Crypto Privatekey Password found", API_DESCRIPTION: "Seed Crypto Privatekey Password" }, @@ -32,7 +33,6 @@ def is_hex(value): def run_tests(apk, pipes, u, rzh, au): file = os.path.join("res", "values", "strings.xml") manifest = os.path.join(apk.apktool, file) - apk.extra.add_text_file(manifest) root = ET.parse(manifest).getroot() tags = root.findall("string") for tag in tags: @@ -50,28 +50,31 @@ def run_tests(apk, pipes, u, rzh, au): descrip = "Easily discoverable of {} ({}: {}) embedded inside {}".format(common_api_keys[key][API_DESCRIPTION], key, value, file) elif ("api_key" in lkey or "apikey" in lkey) and " " not in value: details = "Insecure storage of a generic API key in application resource." - descrip = "Easily discoverable of API key ({}: {}) embedded inside {}".format(key, value, file) + descrip = "Easily discoverable API key ({}: {}) embedded inside {}".format(key, value, file) elif ("privatekey" in lkey or "private_key" in lkey) and " " not in value: details = "Insecure storage of a generic Private Key in application resource." - descrip = "Easily discoverable of Private Key ({}: {}) embedded inside {}".format(key, value, file) + descrip = "Easily discoverable Private Key ({}: {}) embedded inside {}".format(key, value, file) elif "secret" in lkey and " " not in value: details = "Insecure storage of a generic Secret in application resource." - descrip = "Easily discoverable of Secret ({}: {}) embedded inside {}".format(key, value, file) + descrip = "Easily discoverable Secret ({}: {}) embedded inside {}".format(key, value, file) elif ("appkey" in lkey or "app_key" in lkey) and " " not in value: details = "Insecure storage of a generic Application Key in application resource." - descrip = "Easily discoverable of Application Key ({}: {}) embedded inside {}".format(key, value, file) + descrip = "Easily discoverable Application Key ({}: {}) embedded inside {}".format(key, value, file) elif "password" in lkey and (is_base64(value) or is_uuid(value) or is_hex(value)): details = "Insecure storage of a generic Password in application resource." - descrip = "Easily discoverable of Password ({}: {}) embedded inside {}".format(key, value, file) + descrip = "Easily discoverable Password ({}: {}) embedded inside {}".format(key, value, file) elif "token" in lkey and (is_base64(value) or is_uuid(value) or is_hex(value)): details = "Insecure storage of a generic Token in application resource." - descrip = "Easily discoverable of Token ({}: {}) embedded inside {}".format(key, value, file) + descrip = "Easily discoverable Token ({}: {}) embedded inside {}".format(key, value, file) elif "seed" in lkey and (is_base64(value) or is_uuid(value) or is_hex(value)): details = "Insecure storage of a generic Seed in application resource." - descrip = "Easily discoverable of Seed ({}: {}) embedded inside {}".format(key, value, file) + descrip = "Easily discoverable Seed ({}: {}) embedded inside {}".format(key, value, file) + elif "nonce" in lkey and (is_base64(value) or is_uuid(value) or is_hex(value)): + details = "Insecure storage of a generic Nonce in application resource." + descrip = "Easily discoverable Nonce ({}: {}) embedded inside {}".format(key, value, file) if len(descrip) > 0 and len(details) > 0: u.test(apk, False, details, descrip, severity) def name_test(): - return "Detection insecure API secrets values" \ No newline at end of file + return "Detection insecure API secrets values" diff --git a/android/tests/firebaseio.py b/android/tests/firebaseio.py new file mode 100644 index 0000000..b51d0f4 --- /dev/null +++ b/android/tests/firebaseio.py @@ -0,0 +1,77 @@ +## fufluns - Copyright 2021 - deroad + +import os +import xml.etree.ElementTree as ET +import urllib3 +import re + +urllib3.disable_warnings() + +## CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N +SEVERITY = 7.1 +DESCRIPTION = "Firebase Real-time Databases contains sensitive information on their users, including their email addresses, usernames, passwords, phone numbers, full names, chat messages and location data." +NO_NETWORK = "No network connection. Please check the network connectivity." +FIREBASE_REGEX = r'https*://(.+?)\.firebaseio.com' + +def run_tests(apk, rzs, u, rzh, au): + projects = [] + misconfigured = [] + + file = os.path.join("res", "values", "strings.xml") + manifest = os.path.join(apk.apktool, file) + root = ET.parse(manifest).getroot() + tags = root.findall("string") + + for tag in tags: + if tag.text == None: + continue + project = re.findall(FIREBASE_REGEX, tag.text.strip()) + if len(project) > 0: + projects.extend(project) + + apk.logger.notify("Found {} firebaseio.com projects.".format(len(projects))) + + http = urllib3.PoolManager() + for project in projects: + url = 'https://{}.firebaseio.com/.json'.format(project) + try: + resp = http.request('GET', url) + if resp.status == 200: + misconfigured.append(project) + apk.logger.warning("[XX] https://{}.firebaseio.com/ is insecure.".format(project)) + elif resp.status == 401: + apk.logger.info("[OK] https://{}.firebaseio.com is secure.".format(project)) + elif resp.status == 404: + apk.logger.notify("[--] https://{}.firebaseio.com/ was not found.".format(project)) + except urllib3.exceptions.NewConnectionError: + apk.logger.error(NO_NETWORK) + return + except urllib3.exceptions.MaxRetryError: + apk.logger.error(NO_NETWORK) + return + url = 'https://firestore.googleapis.com/v1/projects/{}/databases/(default)/'.format(project) + try: + resp = http.request('GET', url) + if resp.status == 200: + misconfigured.append(project) + apk.logger.warning("[XX] https://firestore.googleapis.com/v1/projects/{}/databases/(default)/ is insecure.".format(project)) + elif resp.status == 401: + apk.logger.info("[OK] https://firestore.googleapis.com/v1/projects/{}/databases/(default)/ is secure.".format(project)) + elif resp.status == 404: + apk.logger.notify("[--] https://firestore.googleapis.com/v1/projects/{}/databases/(default)/ was not found.".format(project)) + except urllib3.exceptions.NewConnectionError: + apk.logger.error(NO_NETWORK) + return + except urllib3.exceptions.MaxRetryError: + apk.logger.error(NO_NETWORK) + return + + verb = "found" + if len(misconfigured) < 1: + verb = "not found" + + msg = "Misconfigured firebaseio instance {}.".format(verb) + u.test(apk, len(misconfigured) < 1, msg, DESCRIPTION, SEVERITY) + +def name_test(): + return "Misconfigured Firebaseio Instance" \ No newline at end of file diff --git a/android/tests/manifest.py b/android/tests/manifest.py deleted file mode 100644 index bc558c9..0000000 --- a/android/tests/manifest.py +++ /dev/null @@ -1,11 +0,0 @@ -## fufluns - Copyright 2019-2021 - deroad - -import os - -def run_tests(apk, pipes, u, rzh, au): - manifest = os.path.join(apk.apktool, "AndroidManifest.xml") - apk.extra.add_text_file(manifest) - apk.logger.notify("AndroidManifest found.") - -def name_test(): - return "Dumping AndroidManifest.xml" \ No newline at end of file diff --git a/android/tests/manifest_flags.py b/android/tests/manifest_flags.py index 482ced9..ba67ac4 100644 --- a/android/tests/manifest_flags.py +++ b/android/tests/manifest_flags.py @@ -33,14 +33,15 @@ def find_any(apk, u, root, keys, attval, keywords, issue, descr, severity): if value in keywords: found += 1 if found > 0: - u.test(apk, False, DEBUGGABLE_APP_ISSUE, DEBUGGABLE_APP_DESCRIPTION, DEBUGGABLE_APP_SEVERITY) + u.test(apk, False, issue, descr, severity) def run_tests(apk, pipes, u, rzh, au): manifest = os.path.join(apk.apktool, "AndroidManifest.xml") root = ET.parse(manifest).getroot() - find_any(apk, u, root, DEBUGGABLE_APP_KEYS, "debuggable" , TRUES, DEBUGGABLE_APP_ISSUE, DEBUGGABLE_APP_DESCRIPTION, DEBUGGABLE_APP_SEVERITY) - find_any(apk, u, root, EXPORT_RCV_KEYS , "exported" , TRUES, EXPORT_RCV_ISSUE , EXPORT_RCV_DESCRIPTION , EXPORT_RCV_SEVERITY ) - find_any(apk, u, root, BACKUP_APP_KEYS , "allowBackup", TRUES, BACKUP_APP_ISSUE , BACKUP_APP_DESCRIPTION , BACKUP_APP_SEVERITY ) + find_any(apk, u, root, DEBUGGABLE_APP_KEYS, "debuggable" , TRUES, DEBUGGABLE_APP_ISSUE, DEBUGGABLE_APP_DESCRIPTION, DEBUGGABLE_APP_SEVERITY) + find_any(apk, u, root, EXPORT_RCV_KEYS , "exported" , TRUES, EXPORT_RCV_ISSUE , EXPORT_RCV_DESCRIPTION , EXPORT_RCV_SEVERITY ) + find_any(apk, u, root, BACKUP_APP_KEYS , "allowBackup" , TRUES, BACKUP_APP_ISSUE , BACKUP_APP_DESCRIPTION , BACKUP_APP_SEVERITY ) + find_any(apk, u, root, BACKUP_APP_KEYS , "fullBackupOnly", TRUES, BACKUP_APP_ISSUE , BACKUP_APP_DESCRIPTION , BACKUP_APP_SEVERITY ) def name_test(): return "Detection interesting tag flags in AndroidManifest.xml" diff --git a/android/tests/resources.py b/android/tests/resources.py new file mode 100644 index 0000000..04c1210 --- /dev/null +++ b/android/tests/resources.py @@ -0,0 +1,48 @@ +## fufluns - Copyright 2021 - deroad + +import glob +import os + +SKIP_FILES = [ + '/res/anim', + '/res/color', + '/res/colour', + '/res/drawable', + '/res/font', + '/res/layout', + '/res/menu', + '/res/mipmap', + # langs usually are `/res/values-en` `/res/values-fr`.. + '/res/values-', + # fullpaths + '/original/AndroidManifest.xml', + '/res/values/colors.xml', + '/res/values/dimens.xml', + '/res/values/drawables.xml', + '/res/values/styles.xml' +] + +def can_skip(file): + for prefix in SKIP_FILES: + if file.startswith(prefix): + return True + return False + +def run_tests(apk, rz, u, rzh, au): + files = glob.glob(os.path.join(apk.apktool, "**", "*.xml"), recursive=True) + dirlen = len(apk.apktool) + resources = 0 + for file in files: + fpath = file[dirlen:] + + if can_skip(fpath): + continue + + resources += 1 + with open(file, 'r', errors='replace') as fp: + text = "".join(fp.readlines()) + apk.extra.add(fpath, text) + apk.logger.notify("Found {} resources.".format(resources)) + +def name_test(): + return "Application resources" diff --git a/android/tests/strings.py b/android/tests/strings.py index 979b9df..02a8f0b 100644 --- a/android/tests/strings.py +++ b/android/tests/strings.py @@ -1,6 +1,6 @@ ## fufluns - Copyright 2019-2021 - deroad -import re +import re, base64 STRINGS_SIGNATURES = [ ':"', @@ -19,6 +19,56 @@ ] JAVA_REGEX = r'(L([a-zA-Z\d\/\$_\-]+)(([a-zA-Z\d\.<>\$]+)?(\(\)|\([\[a-zA-Z\d\/\$_\-;]+\))([\[a-zA-Z\d\/\$_\-;]+|[\[ZBSCIJFDV]))?)' +HEX_REGEX = r'^[A-Fa-f0-9]{5,}$' +BASE64_REGEX = r'^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$' +BADB64_REGEX = r'^[A-Za-z$+]+$' + +KNOWN_BADB64 = [ + '0123456789abcdef', + '0123456789ABCDEF', + '0123456789ABCDEFGHJKMNPQRSTVWXYZ', + '0123456789ABCDEFGHIJKLMNOPQRSTUV', + '0123456789ABCDEFGHIJKLMNOPQRSTUVZ', + '0oO1iIlLAaBbCcDdEeFfGgHhJjKkMmNnPpQqRrSsTtVvWwXxYyZz', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + 'AES/CBC/NoPadding', + 'AES/CBC/PKCS5Padding', + 'AES/ECB/NoPadding', + 'AES/ECB/PKCS5Padding', + 'DES/CBC/NoPadding', + 'DES/CBC/PKCS5Padding', + 'DES/ECB/NoPadding', + 'DES/ECB/PKCS5Padding', + 'DESede/CBC/NoPadding', + 'DESede/CBC/PKCS5Padding', + 'DESede/ECB/NoPadding', + 'DESede/ECB/PKCS5Padding', + 'RSA/ECB/PKCS1Padding', + 'RSA/ECB/OAEPWithSHA-1AndMGF1Padding', + 'RSA/ECB/OAEPWithSHA-256AndMGF1Padding' +] + +def is_hex(value): + if value in KNOWN_BADB64: + return False + return re.match(HEX_REGEX, value, flags=re.M) + +def is_base64(value): + if value in KNOWN_BADB64: + return False + found = re.search(BASE64_REGEX, value, flags=re.M) + if found and not re.match(BADB64_REGEX, value, flags=re.M): + found = found.group(0) + try: + decoded = base64.b64decode(found.encode('ascii')) + return len(decoded) > 0 + except Exception: + pass + return False class ContextStrings(object): def __init__(self, apk, utils): @@ -28,18 +78,29 @@ def __init__(self, apk, utils): self.file = '' self.found = [] - def add(self, offset, value): + def add(self, offset, value, stype="String"): if value not in self.found: self.found.append(value) - self.apk.strings.add(self.file, "String", offset, value) + self.apk.strings.add(self.file, stype, offset, value) def size(self): return len(self.found) def find_strings(offset, string, ctx): + if re.search(JAVA_REGEX, string, flags=re.M): + return None + + if is_hex(string): + ctx.add(offset, string, "hex") + return None + + if len(string) > 4 and is_base64(string): + ctx.add(offset, string, "base64") + return None + ustring = string.strip().upper() for key in STRINGS_SIGNATURES: - if key.upper() in ustring and not (re.search(JAVA_REGEX, string)): + if key.upper() in ustring: ctx.add(offset, string) return None @@ -52,4 +113,4 @@ def run_tests(apk, pipes, u, rzh, au): apk.logger.info("[OK] No interesting strings signatures found") def name_test(): - return "Detection of interesting string signatures" \ No newline at end of file + return "Detection of interesting string signatures" diff --git a/ios/tests/apikeys.py b/ios/tests/apikeys.py index 9dac022..bd429cc 100644 --- a/ios/tests/apikeys.py +++ b/ios/tests/apikeys.py @@ -4,7 +4,12 @@ import os import re +API_GOOGLE_FILE = "GoogleService-Info.plist" +API_GOOGLE_PROVIDER = "Google" +API_GOOGLE_SEVERITY = 2.5 + API_DEFAULT_SEVERITY = 6.5 +API_DEFAULT_PROVIDER = "generic" API_DETAILS = "details" API_DESCRIPTION = "description" @@ -35,8 +40,14 @@ def test_recursive(ipa, u, d, file): return for key in d: severity = API_DEFAULT_SEVERITY + provider = API_DEFAULT_PROVIDER details = "" descrip = "" + + if file.endswith(API_GOOGLE_FILE): + severity = API_GOOGLE_SEVERITY + provider = API_GOOGLE_PROVIDER + lkey = key.lower() if key in common_api_keys: continue @@ -47,26 +58,30 @@ def test_recursive(ipa, u, d, file): elif not isinstance(value, str): continue elif ("api_key" in lkey or "apikey" in lkey) and " " not in value: - details = "Insecure storage of a generic API key in application resource." - descrip = "Easily discoverable of API key ({}: {}) embedded inside {}".format(key, value, file) + details = "Insecure storage of a {} API key in application resource.".format(provider) + descrip = "Easily discoverable API key ({}: {}) embedded inside {}".format(key, value, file) elif ("privatekey" in lkey or "private_key" in lkey) and " " not in value: - details = "Insecure storage of a generic Private Key in application resource." - descrip = "Easily discoverable of Private Key ({}: {}) embedded inside {}".format(key, value, file) + details = "Insecure storage of a {} Private Key in application resource.".format(provider) + descrip = "Easily discoverable Private Key ({}: {}) embedded inside {}".format(key, value, file) elif "secret" in lkey and " " not in value: - details = "Insecure storage of a generic Secret in application resource." - descrip = "Easily discoverable of Secret ({}: {}) embedded inside {}".format(key, value, file) + details = "Insecure storage of a {} Secret in application resource.".format(provider) + descrip = "Easily discoverable Secret ({}: {}) embedded inside {}".format(key, value, file) elif ("appkey" in lkey or "app_key" in lkey) and " " not in value: - details = "Insecure storage of a generic Application Key in application resource." - descrip = "Easily discoverable of Application Key ({}: {}) embedded inside {}".format(key, value, file) + details = "Insecure storage of a {} Application Key in application resource.".format(provider) + descrip = "Easily discoverable Application Key ({}: {}) embedded inside {}".format(key, value, file) elif "password" in lkey and (is_base64(value) or is_uuid(value) or is_hex(value)): - details = "Insecure storage of a generic Password in application resource." - descrip = "Easily discoverable of Password ({}: {}) embedded inside {}".format(key, value, file) + details = "Insecure storage of a {} Password in application resource.".format(provider) + descrip = "Easily discoverable Password ({}: {}) embedded inside {}".format(key, value, file) elif "token" in lkey and (is_base64(value) or is_uuid(value) or is_hex(value)): - details = "Insecure storage of a generic Token in application resource." - descrip = "Easily discoverable of Token ({}: {}) embedded inside {}".format(key, value, file) + details = "Insecure storage of a {} Token in application resource.".format(provider) + descrip = "Easily discoverable Token ({}: {}) embedded inside {}".format(key, value, file) elif "seed" in lkey and (is_base64(value) or is_uuid(value) or is_hex(value)): - details = "Insecure storage of a generic Seed in application resource." - descrip = "Easily discoverable of Seed ({}: {}) embedded inside {}".format(key, value, file) + details = "Insecure storage of a {} Seed in application resource.".format(provider) + descrip = "Easily discoverable Seed ({}: {}) embedded inside {}".format(key, value, file) + elif "nonce" in lkey and (is_base64(value) or is_uuid(value) or is_hex(value)): + details = "Insecure storage of a {} Nonce in application resource.".format(provider) + descrip = "Easily discoverable Nonce ({}: {}) embedded inside {}".format(key, value, file) + if len(descrip) > 0 and len(details) > 0: u.test(ipa, False, details, descrip, severity) diff --git a/ios/tests/apptransportsecurity.py b/ios/tests/apptransportsecurity.py index 0b01f50..a907c56 100644 --- a/ios/tests/apptransportsecurity.py +++ b/ios/tests/apptransportsecurity.py @@ -37,6 +37,11 @@ def run_tests(ipa, pipe, u, rzh): if len(tmp) > 0: plist = u.load_plist(tmp[0]) + # check for TrustKit + if u.dk(plist, "TSKConfiguration") != None: + ipa.logger.notify("TrustKit was found, so App Transport Security config is ignored.") + return + tmp = u.dk(plist, "NSAppTransportSecurity.NSExceptionDomains", {}) for domain in tmp.keys(): domain_msg = domain diff --git a/ios/tests/compiler.py b/ios/tests/compiler.py index 6559bf3..b088af0 100644 --- a/ios/tests/compiler.py +++ b/ios/tests/compiler.py @@ -14,8 +14,8 @@ def run_tests(ipa, pipe, u, rzh): u.test(ipa, rzh.has_import(pipe, LIST_STACK_GUARD), "Stack smashing protection missing (-fstack-protector-all)", DESC_STACK_GUARD, SVRT_STACK_GUARD) - u.test(ipa, rzh.has_import(pipe, LIST_OBJC_ARC ), "Objective-C automatic reference counting (-fobjc-arc)", DESC_OBJC_ARC, SVRT_OBJC_ARC) + u.test(ipa, rzh.has_import(pipe, LIST_OBJC_ARC ), "Objective-C automatic reference counting is missing (-fobjc-arc)", DESC_OBJC_ARC, SVRT_OBJC_ARC) u.test(ipa, rzh.has_info (pipe, "pic" ), "Full ASLR support is missing (-pie)", DESC_PIE, SVRT_PIE) def name_test(): - return "Detection Compiler flags" \ No newline at end of file + return "Detection Compiler flags" diff --git a/ios/tests/firebaseio.py b/ios/tests/firebaseio.py new file mode 100644 index 0000000..834d373 --- /dev/null +++ b/ios/tests/firebaseio.py @@ -0,0 +1,100 @@ +## fufluns - Copyright 2021 - deroad + +import glob +import os +import urllib3 +import re + +urllib3.disable_warnings() + +## CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N +SEVERITY = 7.1 +DESCRIPTION = "Firebase Real-time Databases contains sensitive information on their users, including their email addresses, usernames, passwords, phone numbers, full names, chat messages and location data." +NO_NETWORK = "No network connection. Please check the network connectivity." +FIREBASE_REGEX = rb'https*://(.+?)\.firebaseio.com' + +def iterate_object(obj): + projects = [] + if isinstance(obj, list): + for value in obj: + if isinstance(value, (list, dict)): + projects.extend(iterate_object(value)) + continue + elif not isinstance(value, str): + continue + value = value.encode('utf-8') + project = re.findall(FIREBASE_REGEX, value) + if len(project) > 0: + projects.extend(project) + else: + for key in obj: + value = obj[key] + if isinstance(value, (list, dict)): + projects.extend(iterate_object(value)) + continue + elif not isinstance(value, str): + continue + value = value.encode('utf-8') + project = re.findall(FIREBASE_REGEX, value) + if len(project) > 0: + projects.extend(project) + return projects + +def run_tests(ipa, rz, u, rzh): + projects = [] + misconfigured = [] + plists = glob.glob(os.path.join(ipa.directory, "**", "*.plist"), recursive=True) + for file in plists: + plist = u.load_plist(file) + projects.extend(iterate_object(plist)) + + projects = list(filter(lambda x: len(x) > 0, projects)) + + ipa.logger.notify("Found {} firebaseio.com projects.".format(len(projects))) + + if len(projects) > 0: + http = urllib3.PoolManager() + for project in projects: + project = project.decode('utf-8') + url = 'https://{}.firebaseio.com/.json'.format(project) + try: + resp = http.request('GET', url) + if resp.status == 200: + misconfigured.append(project) + ipa.logger.warning("[XX] https://{}.firebaseio.com/ is insecure.".format(project)) + elif resp.status == 401: + ipa.logger.info("[OK] https://{}.firebaseio.com/ is secure.".format(project)) + elif resp.status == 404: + ipa.logger.notify("[--] https://{}.firebaseio.com/ was not found.".format(project)) + except urllib3.exceptions.NewConnectionError: + ipa.logger.error(NO_NETWORK) + return + except urllib3.exceptions.MaxRetryError: + ipa.logger.error(NO_NETWORK) + return + url = 'https://firestore.googleapis.com/v1/projects/{}/databases/(default)/'.format(project) + try: + resp = http.request('GET', url) + if resp.status == 200: + misconfigured.append(project) + ipa.logger.warning("[XX] https://firestore.googleapis.com/v1/projects/{}/databases/(default)/ is insecure.".format(project)) + elif resp.status == 401: + ipa.logger.info("[OK] https://firestore.googleapis.com/v1/projects/{}/databases/(default)/ is secure.".format(project)) + elif resp.status == 404: + ipa.logger.notify("[--] https://firestore.googleapis.com/v1/projects/{}/databases/(default)/ was not found.".format(project)) + except urllib3.exceptions.NewConnectionError: + ipa.logger.error(NO_NETWORK) + return + except urllib3.exceptions.MaxRetryError: + ipa.logger.error(NO_NETWORK) + return + + verb = "found" + if len(misconfigured) < 1: + verb = "not found" + + msg = "Misconfigured firebaseio instance {}.".format(verb) + u.test(ipa, len(misconfigured) < 1, msg, DESCRIPTION, SEVERITY) + +def name_test(): + return "Misconfigured Firebaseio Instance" \ No newline at end of file diff --git a/ios/tests/strings.py b/ios/tests/strings.py index 27af820..837b509 100644 --- a/ios/tests/strings.py +++ b/ios/tests/strings.py @@ -1,5 +1,7 @@ ## fufluns - Copyright 2019-2021 - deroad +import re, base64 + STRINGS_SIGNATURES = [ ':"', ': "', @@ -16,6 +18,83 @@ 'sha256', ] +HEX_REGEX = r'^[A-Fa-f0-9]{5,}$' +BASE64_REGEX = r'^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$' +BADB64_REGEX = r'^[A-Za-z$+]+$|^[A-Za-z/+]+$' + +KNOWN_BADB64 = [ + '0123456789abcdef', + '0123456789ABCDEF', + '0123456789ABCDEFGHJKMNPQRSTVWXYZ', + '0123456789ABCDEFGHIJKLMNOPQRSTUV', + '0123456789ABCDEFGHIJKLMNOPQRSTUVZ', + '0oO1iIlLAaBbCcDdEeFfGgHhJjKkMmNnPpQqRrSsTtVvWwXxYyZz', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + 'getInt32', + 'getInt64', + 'GPBInt32DoubleDictionary', + 'GPBInt32ObjectDictionary', + 'GPBInt32UInt32Dictionary', + 'GPBInt32UInt64Dictionary', + 'GPBInt64DoubleDictionary', + 'GPBInt64ObjectDictionary', + 'GPBInt64UInt32Dictionary', + 'GPBInt64UInt64Dictionary', + 'GPBStringInt32Dictionary', + 'GPBStringInt64Dictionary', + 'GPBUInt32FloatDictionary', + 'GPBUInt32Int32Dictionary', + 'GPBUInt32Int64Dictionary', + 'GPBUInt64FloatDictionary', + 'GPBUInt64Int32Dictionary', + 'GPBUInt64Int64Dictionary', + 'ISO8601DateFormatter', + 'readSFixed32', + 'readSFixed64', + 'St9exception', + 'SyntaxProto2', + 'SyntaxProto3', + 'TypeSfixed32', + 'TypeSfixed64', + 'gost2001', + 'gost94cc', + 'hmacWithSHA1', + 'md2WithRSAEncryption', + 'md4WithRSAEncryption', + 'md5WithRSAEncryption', + 'ripemd160WithRSA', + 'x500UniqueIdentifier', + 'h7BadExpectedAccessE', + '0JSExecutorE', + '11ColumnNoVecEEE', + '4JSBigStdStringE', + '6InstanceCallbackEEE', + '6RowSumI', + '8MessageQueueThreadE', + '8RowNoVecEEE', + '9BaseValueEE' + 'N2cv13BaseRowFilterE', + 'N2cv6RowSumIddEE', + 'N2cv6RowSumIfdEE', + 'N2cv6RowSumIhdEE', + 'N2cv6RowSumIhiEE', + 'N2cv6RowSumIiiEE', + 'N2cv6RowSumIsdEE', + 'N2cv6RowSumIsiEE', + 'N2cv6RowSumItdEE', + 'N2cv6RowSumItiEE', + 'N5folly22OptionalEmptyExceptionE', + 'N8facebook3jsi15InstrumentationE', + 'N8facebook5react17JSBigBufferStringE', + 'N8facebook5react17JSExecutorFactoryE', + 'N8facebook5react17JSModulesUnbundleE', + 'N8facebook5react17RAMBundleRegistryE', +] + class ContextStrings(object): def __init__(self, ipa, utils, file): super(ContextStrings, self).__init__() @@ -24,15 +103,41 @@ def __init__(self, ipa, utils, file): self.file = file self.found = [] - def add(self, offset, value): + def add(self, offset, value, stype="String"): if value not in self.found: self.found.append(value) - self.ipa.strings.add(self.file, "String", offset, value) + self.ipa.strings.add(self.file, stype, offset, value) def size(self): return len(self.found) +def is_hex(value): + if value in KNOWN_BADB64: + return False + return re.match(HEX_REGEX, value, flags=re.M) + +def is_base64(value): + if value in KNOWN_BADB64: + return False + found = re.search(BASE64_REGEX, value, flags=re.M) + if found and not re.match(BADB64_REGEX, value, flags=re.M): + found = found.group(0) + try: + decoded = base64.b64decode(found.encode('ascii')) + return len(decoded) > 0 + except Exception: + pass + return False + def find_strings(offset, string, ctx): + if is_hex(string): + ctx.add(offset, string, "hex") + return None + + if len(string) > 4 and is_base64(string): + ctx.add(offset, string, "base64") + return None + ustring = string.strip().upper() for key in STRINGS_SIGNATURES: if key.upper() in ustring: @@ -46,4 +151,4 @@ def run_tests(ipa, pipe, u, rzh): ipa.logger.info("[OK] No interesting strings signatures found") def name_test(): - return "Detection of interesting string signatures" \ No newline at end of file + return "Detection of interesting string signatures" diff --git a/ios/tests/trustkit.py b/ios/tests/trustkit.py new file mode 100644 index 0000000..6c59240 --- /dev/null +++ b/ios/tests/trustkit.py @@ -0,0 +1,69 @@ +## fufluns - Copyright 2021 - deroad + +import glob +import os + +SEVERITY="severity" +DESCRIPTION="description" + +ISSU_NSURLSWIZZLING = "TrustKit swizzling on NSURL is disabled" +DESC_NSURLSWIZZLING = "Swizzling allows enabling pinning within an App without having to find every instance of NSURLConnection or NSURLSession delegates (best practice from docs)." +SVRT_NSURLSWIZZLING = 4.2 + +ISSU_NODOMAINS = "TrustKit No Pinned Domains" +DESC_NODOMAINS = "There are no pinned domains in the TrustKit configuration (maybe they are hardcoded in the code)." +SVRT_NODOMAINS = 8.2 + +ISSU_NOSUBDOMS = "TrustKit Pinning is not applied to subdomains on {}" +DESC_NOSUBDOMS = "TrustKit will not check pinning for all the subdomains of the specified domain." +SVRT_NOSUBDOMS = 4.3 + +ISSU_NOENFORCE = "TrustKit Pinning is not enforced on {}" +DESC_NOENFORCE = "TrustKit will not block SSL connections that caused a pin or certificate validation error." +SVRT_NOENFORCE = 8.2 + +ISSU_NOKEYHASHES = "TrustKit Public Key Hashes missing on {}" +DESC_NOKEYHASHES = "TrustKit will not be able to verify the certificate chain received from the server." +SVRT_NOKEYHASHES = 8.2 + +ISSU_NOBACKUPKEY = "TrustKit No Backup Public Key Hash on {}" +DESC_NOBACKUPKEY = "TrustKit documentation suggests to always provide at least one backup pin to prevent accidental blocking." +SVRT_NOBACKUPKEY = 8.2 + + +def run_tests(ipa, rz, u, rzh): + tmp = [f for f in glob.glob(os.path.join(ipa.directory, "Payload", "*", "Info.plist"), recursive=True)] + plist = {} + if len(tmp) > 0: + plist = u.load_plist(tmp[0]) + + if u.dk(plist, "TSKConfiguration", None) == None: + ipa.logger.notify("TrustKit not found") + return + + ## Allows Arbitrary Loads + b = u.dk(plist, "TSKConfiguration.TSKSwizzleNetworkDelegates") + if b is not None: + u.test(ipa, not b, ISSU_NSURLSWIZZLING, DESC_NSURLSWIZZLING, SVRT_NSURLSWIZZLING) + + domains = u.dk(plist, "TSKConfiguration.TSKPinnedDomains", {}) + if len(domains.keys()) < 1: + u.test(ipa, False, ISSU_NODOMAINS, DESC_NODOMAINS, SVRT_NODOMAINS) + return + + for domain in domains: + config = domains[domain] + b = u.dk(config, "TSKIncludeSubdomains", False) + u.test(ipa, b, ISSU_NOSUBDOMS.format(domain), DESC_NOSUBDOMS, SVRT_NOSUBDOMS) + + b = u.dk(config, "TSKEnforcePinning", True) + u.test(ipa, b, ISSU_NOENFORCE.format(domain), DESC_NOENFORCE, SVRT_NOENFORCE) + + a = u.dk(config, "TSKPublicKeyHashes", []) + if len(a) < 1: + u.test(ipa, False, ISSU_NOKEYHASHES.format(domain), DESC_NOKEYHASHES, SVRT_NOKEYHASHES) + elif len(a) < 2: + u.test(ipa, False, ISSU_NOBACKUPKEY.format(domain), DESC_NOBACKUPKEY, SVRT_NOBACKUPKEY) + +def name_test(): + return "Detection TrustKit (Certificate Pinning)" \ No newline at end of file diff --git a/report.py b/report.py index 9c5c980..5cb2a9d 100644 --- a/report.py +++ b/report.py @@ -171,7 +171,7 @@ def add(self, key, string): def add_text_file(self, filename): basename = os.path.basename(filename) - with open(filename, "r") as fp: + with open(filename, 'r', errors='replace') as fp: self.add(basename, "".join(fp.readlines())) def json(self): diff --git a/requirements.txt b/requirements.txt index 417e2b8..8a5a9d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ tornado rzpipe +urllib3 diff --git a/web.py b/web.py index f9707b5..8782fdf 100644 --- a/web.py +++ b/web.py @@ -70,6 +70,6 @@ def __init__(self, core, listen=8080, proto="http", debug=False): self.app = make_app({'debug': debug}, dict(core=core)) def run(self): - self.app.listen(self.listen) + self.app.listen(self.listen, max_buffer_size=1073741274) # 1GB max print("Server available at {proto}://localhost:{port}".format(proto=self.proto, port=self.listen)) tornado.ioloop.IOLoop.instance().start() \ No newline at end of file