Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More improvements for Android and iOS #9

Merged
merged 14 commits into from
Jun 12, 2021
27 changes: 15 additions & 12 deletions android/tests/apikeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
import re
import xml.etree.ElementTree as ET

API_GOOGLE_SEVERITY = 2.5
API_DEFAULT_SEVERITY = 6.5

API_DETAILS = "details"
API_DESCRIPTION = "description"
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" },
Expand All @@ -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:
Expand All @@ -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"
return "Detection insecure API secrets values"
77 changes: 77 additions & 0 deletions android/tests/firebaseio.py
Original file line number Diff line number Diff line change
@@ -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"
11 changes: 0 additions & 11 deletions android/tests/manifest.py

This file was deleted.

9 changes: 5 additions & 4 deletions android/tests/manifest_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
48 changes: 48 additions & 0 deletions android/tests/resources.py
Original file line number Diff line number Diff line change
@@ -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"
71 changes: 66 additions & 5 deletions android/tests/strings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## fufluns - Copyright 2019-2021 - deroad

import re
import re, base64

STRINGS_SIGNATURES = [
':"',
Expand All @@ -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):
Expand All @@ -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

Expand All @@ -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"
return "Detection of interesting string signatures"
Loading