mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-12 02:06:59 +00:00
Security Fixes [GHSA-8j49-mmcx-4mp5] Arbitrary File Read via Path Traversal in ZIP/APK Icon Extraction The android:icon attribute from an APK manifest was interpolated into file paths without validation, allowing a crafted APK to read arbitrary files from the server. Fixed by adding is_path_traversal() and is_safe_path() guards in find_icon_path_zip. Dependency bump included. [GHSA-3p54-567p-2wpr] CSRF Checks Not Enforced After Django Middleware Migration The migration from the deprecated MIDDLEWARE_CLASSES to MIDDLEWARE omitted CsrfViewMiddleware, SecurityMiddleware, and XFrameOptionsMiddleware, leaving CSRF, HSTS, and clickjacking protections silently disabled. All three have been restored to the active MIDDLEWARE tuple. The now-dead MIDDLEWARE_CLASSES block has been removed to prevent future confusion. [GHSA-x768-8642-mmq9] Zip Bomb Denial of Service via Per-File Size Limit Bypass The per-file size check in ZIP extraction logged a warning on oversized members but was missing a continue, allowing files exceeding ZIP_MAX_UNCOMPRESSED_FILE_SIZE to be extracted anyway as long as the aggregate limit was not reached. Fixed by adding the missing continue. [GHSA-95px-34x5-p37h] SSRF Port Restriction Bypass in assetlinks_check valid_host() was called with only the bare hostname, allowing android:port to be appended afterward without going through port validation, bypassing the HTTP/HTTPS-only restriction. Fixed with a two-layer approach: primary port allowlist check before URL assembly in get_browsable_activities, and a defence-in-depth re-check in _check_url. Hardening The hand-rolled safe_extract used os.path.abspath (symlink-blind) instead of os.path.realpath, creating a TOCTOU window where a symlink could redirect extraction outside the destination directory. Replaced with Python 3.12's tarfile.extractall(filter='data') (PEP 706), which rejects symlinks, hardlinks, absolute paths, and traversal members per-member before extraction. A robust realpath-based fallback is included for older Python versions. Both download_app_data call sites in the iOS dynamic analyser now wrap app_container and tarfile with shlex.quote when building the SSH tar command, as defensive coding hygiene. Refactoring is_path_traversal, is_safe_path, clean_filename, cmd_injection_check, is_pipe_or_link, and is_attack_pattern were scattered across utils.py and shared.py. All have been moved to mobsf/MobSF/security.py as the single authoritative location for security primitives. Developer Tooling AGENTS.md / CLAUDE.md - AI agent guidelines Added a coding-standards document for AI coding agents (Cursor, Claude, Codex) covering MobSF-specific secure-by-default patterns: path traversal guards, archive extraction safety, Django middleware active-tuple hygiene, split-validation anti-patterns (SSRF port bypass), guard completeness (continue/return/raise after every security check), and Django-specific security features (form validators, decorators, middleware). CLAUDE.md is a symlink to AGENTS.md.
876 lines
51 KiB
Python
Executable File
876 lines
51 KiB
Python
Executable File
# -*- coding: utf_8 -*-
|
|
# flake8: noqa
|
|
"""Module for android manifest analysis."""
|
|
import logging
|
|
from urllib.parse import urlparse
|
|
|
|
import requests
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from django.utils.html import escape
|
|
|
|
from mobsf.MobSF.utils import (
|
|
append_scan_status,
|
|
is_number,
|
|
upstream_proxy,
|
|
)
|
|
from mobsf.MobSF.security import valid_host
|
|
from mobsf.StaticAnalyzer.views.android import (
|
|
network_security,
|
|
)
|
|
from mobsf.StaticAnalyzer.views.android.kb import (
|
|
android_manifest_desc,
|
|
)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
ANDROID_4_2_LEVEL = 17
|
|
ANDROID_5_0_LEVEL = 21
|
|
ANDROID_8_0_LEVEL = 26
|
|
ANDROID_9_0_LEVEL = 28
|
|
ANDROID_10_0_LEVEL = 29
|
|
ANDROID_MANIFEST_FILE = 'AndroidManifest.xml'
|
|
WELL_KNOWN_PATH = '/.well-known/assetlinks.json'
|
|
|
|
ANDROID_API_LEVEL_MAP = {
|
|
'1': '1.0',
|
|
'2': '1.1',
|
|
'3': '1.5',
|
|
'4': '1.6',
|
|
'5': '2.0-2.1',
|
|
'8': '2.2-2.2.3',
|
|
'9': '2.3-2.3.2',
|
|
'10': '2.3.3-2.3.7',
|
|
'11': '3.0',
|
|
'12': '3.1',
|
|
'13': '3.2-3.2.6',
|
|
'14': '4.0-4.0.2',
|
|
'15': '4.0.3-4.0.4',
|
|
'16': '4.1-4.1.2',
|
|
'17': '4.2-4.2.2',
|
|
'18': '4.3-4.3.1',
|
|
'19': '4.4-4.4.4',
|
|
'20': '4.4W-4.4W.2',
|
|
'21': '5.0-5.0.2',
|
|
'22': '5.1-5.1.1',
|
|
'23': '6.0-6.0.1',
|
|
'24': '7.0',
|
|
'25': '7.1-7.1.2',
|
|
'26': '8.0',
|
|
'27': '8.1',
|
|
'28': '9',
|
|
'29': '10',
|
|
'30': '11',
|
|
'31': '12',
|
|
'32': '12L',
|
|
'33': '13',
|
|
'34': '14',
|
|
'35': '15',
|
|
'36': '16',
|
|
}
|
|
|
|
|
|
def escape_manifest_attribute(value):
|
|
"""Escape manifest attributes."""
|
|
if not value:
|
|
return value
|
|
return escape(value)
|
|
|
|
|
|
def assetlinks_check(act_name, well_knowns):
|
|
"""Well known assetlink check."""
|
|
findings = []
|
|
|
|
with ThreadPoolExecutor() as executor:
|
|
futures = []
|
|
for w_url, host in well_knowns.items():
|
|
logger.info(
|
|
'App Link Assetlinks Check - [%s] %s', act_name, host)
|
|
futures.append(
|
|
executor.submit(_check_url, host, w_url))
|
|
for future in futures:
|
|
findings.append(future.result())
|
|
|
|
return findings
|
|
|
|
|
|
def _check_url(host, w_url):
|
|
"""Check for the presence of Assetlinks URL."""
|
|
try:
|
|
iden = 'sha256_cert_fingerprints'
|
|
proxies, verify = upstream_proxy('https')
|
|
status = False
|
|
status_code = 0
|
|
|
|
urls = {w_url}
|
|
if w_url.startswith('http://'):
|
|
# Upgrade http to https
|
|
urls.add(f'https://{w_url[7:]}')
|
|
|
|
for url in urls:
|
|
# Validate the fully-assembled URL — path, port, query, and params.
|
|
purl = urlparse(url)
|
|
if (purl.path != WELL_KNOWN_PATH
|
|
or len(purl.query) > 0
|
|
or len(purl.params) > 0):
|
|
logger.warning('Invalid Assetlinks URL: %s', url)
|
|
continue
|
|
if purl.port and purl.port not in (80, 443):
|
|
logger.warning(
|
|
'Non-standard port in assetlinks URL rejected: %s', url)
|
|
continue
|
|
r = requests.get(url,
|
|
timeout=5,
|
|
allow_redirects=False,
|
|
proxies=proxies,
|
|
verify=verify)
|
|
|
|
status_code = r.status_code
|
|
if (str(status_code).startswith('2') and iden in str(r.json())):
|
|
status = True
|
|
break
|
|
if status_code in (301, 302):
|
|
logger.warning('Status Code: [%d], Redirecting to '
|
|
'a different URL, skipping check!', status_code)
|
|
return {'url': w_url,
|
|
'host': host,
|
|
'status_code': status_code,
|
|
'status': status}
|
|
|
|
except Exception:
|
|
logger.error(f'Well Known Assetlinks Check for URL: {w_url}')
|
|
return {'url': w_url,
|
|
'host': host,
|
|
'status_code': None,
|
|
'status': False}
|
|
|
|
|
|
def get_browsable_activities(node, ns):
|
|
"""Get Browsable Activities."""
|
|
try:
|
|
browse_dic = {}
|
|
schemes = []
|
|
mime_types = []
|
|
hosts = []
|
|
ports = []
|
|
paths = []
|
|
path_prefixs = []
|
|
path_patterns = []
|
|
well_known = {}
|
|
catg = node.getElementsByTagName('category')
|
|
for cat in catg:
|
|
if cat.getAttribute(f'{ns}:name') == 'android.intent.category.BROWSABLE':
|
|
data_tag = node.getElementsByTagName('data')
|
|
for data in data_tag:
|
|
scheme = data.getAttribute(f'{ns}:scheme')
|
|
if scheme and scheme not in schemes:
|
|
schemes.append(scheme)
|
|
mime = data.getAttribute(f'{ns}:mimeType')
|
|
if mime and mime not in mime_types:
|
|
mime_types.append(mime)
|
|
host = data.getAttribute(f'{ns}:host')
|
|
if host and host not in hosts:
|
|
hosts.append(host)
|
|
port = data.getAttribute(f'{ns}:port')
|
|
if port and port not in ports:
|
|
ports.append(port)
|
|
path = data.getAttribute(f'{ns}:path')
|
|
if path and path not in paths:
|
|
paths.append(path)
|
|
path_prefix = data.getAttribute(f'{ns}:pathPrefix')
|
|
if path_prefix and path_prefix not in path_prefixs:
|
|
path_prefixs.append(path_prefix)
|
|
path_pattern = data.getAttribute(f'{ns}:pathPattern')
|
|
if path_pattern and path_pattern not in path_patterns:
|
|
path_patterns.append(path_pattern)
|
|
# Collect possible well-known paths
|
|
if (scheme
|
|
and scheme in ('http', 'https')
|
|
and host
|
|
and host != '*'):
|
|
host = host.replace('*.', '').replace('#', '')
|
|
if not valid_host(host):
|
|
logger.warning('Invalid Host: %s', host)
|
|
continue
|
|
shost = f'{scheme}://{host}'
|
|
if port and is_number(port):
|
|
if int(port) not in (80, 443):
|
|
logger.warning(
|
|
'Non-standard port rejected in assetlinks '
|
|
'check (port %s bypasses valid_host): %s',
|
|
port, host)
|
|
continue
|
|
c_url = f'{shost}:{port}{WELL_KNOWN_PATH}'
|
|
else:
|
|
c_url = f'{shost}{WELL_KNOWN_PATH}'
|
|
well_known[c_url] = shost
|
|
schemes = [scheme + '://' for scheme in schemes]
|
|
browse_dic['schemes'] = schemes
|
|
browse_dic['mime_types'] = mime_types
|
|
browse_dic['hosts'] = hosts
|
|
browse_dic['ports'] = ports
|
|
browse_dic['paths'] = paths
|
|
browse_dic['path_prefixs'] = path_prefixs
|
|
browse_dic['path_patterns'] = path_patterns
|
|
browse_dic['browsable'] = bool(browse_dic['schemes'])
|
|
browse_dic['well_known'] = well_known
|
|
return browse_dic
|
|
except Exception:
|
|
logger.exception('Getting Browsable Activities')
|
|
|
|
|
|
def manifest_analysis(app_dic, man_data_dic):
|
|
"""Analyse manifest file."""
|
|
# pylint: disable=C0301
|
|
checksum = app_dic['md5']
|
|
mfxml = app_dic['manifest_parsed_xml']
|
|
ns = app_dic['manifest_namespace']
|
|
src_type = app_dic['zipped']
|
|
app_dir = app_dic['app_dir']
|
|
try:
|
|
msg = 'Manifest Analysis Started'
|
|
logger.info(msg)
|
|
append_scan_status(checksum, msg)
|
|
exp_count = dict.fromkeys(['act', 'ser', 'bro', 'cnt'], 0)
|
|
applications = mfxml.getElementsByTagName('application')
|
|
data_tag = mfxml.getElementsByTagName('data')
|
|
intents = mfxml.getElementsByTagName('intent-filter')
|
|
actions = mfxml.getElementsByTagName('action')
|
|
granturipermissions = mfxml.getElementsByTagName(
|
|
'grant-uri-permission')
|
|
permissions = mfxml.getElementsByTagName('permission')
|
|
ret_value = []
|
|
ret_list = []
|
|
exported = []
|
|
browsable_activities = {}
|
|
permission_dict = {}
|
|
do_netsec = False
|
|
debuggable = False
|
|
# PERMISSION
|
|
for permission in permissions:
|
|
if permission.getAttribute(f'{ns}:protectionLevel'):
|
|
protectionlevel = permission.getAttribute(
|
|
f'{ns}:protectionLevel')
|
|
if protectionlevel == '0x00000000':
|
|
protectionlevel = 'normal'
|
|
elif protectionlevel == '0x00000001':
|
|
protectionlevel = 'dangerous'
|
|
elif protectionlevel == '0x00000002':
|
|
protectionlevel = 'signature'
|
|
elif protectionlevel == '0x00000003':
|
|
protectionlevel = 'signatureOrSystem'
|
|
|
|
permission_dict[permission.getAttribute(
|
|
f'{ns}:name')] = protectionlevel
|
|
elif permission.getAttribute(f'{ns}:name'):
|
|
permission_dict[permission.getAttribute(
|
|
f'{ns}:name')] = 'normal'
|
|
# GENERAL
|
|
if man_data_dic['min_sdk'] and int(man_data_dic['min_sdk']) < ANDROID_8_0_LEVEL:
|
|
minsdk = man_data_dic.get('min_sdk')
|
|
android_version = ANDROID_API_LEVEL_MAP.get(minsdk, 'XX')
|
|
ret_list.append(('vulnerable_os_version', (android_version, minsdk,), ()))
|
|
elif man_data_dic['min_sdk'] and int(man_data_dic['min_sdk']) < ANDROID_10_0_LEVEL:
|
|
minsdk = man_data_dic.get('min_sdk')
|
|
android_version = ANDROID_API_LEVEL_MAP.get(minsdk, 'XX')
|
|
ret_list.append(('vulnerable_os_version2', (android_version, minsdk,), ()))
|
|
# APPLICATIONS
|
|
# Handle multiple application tags in AAR
|
|
backupDisabled = False
|
|
for application in applications:
|
|
# Esteve 23.07.2016 - begin - identify permission at the
|
|
# application level
|
|
if application.getAttribute(f'{ns}:permission'):
|
|
perm_appl_level_exists = True
|
|
perm_appl_level = application.getAttribute(
|
|
f'{ns}:permission')
|
|
else:
|
|
perm_appl_level_exists = False
|
|
# End
|
|
if application.getAttribute(f'{ns}:usesCleartextTraffic') == 'true':
|
|
ret_list.append(('clear_text_traffic', (), ()))
|
|
if application.getAttribute(f'{ns}:directBootAware') == 'true':
|
|
ret_list.append(('direct_boot_aware', (), ()))
|
|
if application.getAttribute(f'{ns}:networkSecurityConfig'):
|
|
item = application.getAttribute(f'{ns}:networkSecurityConfig')
|
|
ret_list.append(('has_network_security', (item,), ()))
|
|
do_netsec = item
|
|
if application.getAttribute(f'{ns}:debuggable') == 'true':
|
|
ret_list.append(('app_is_debuggable', (), ()))
|
|
debuggable = True
|
|
if application.getAttribute(f'{ns}:allowBackup') == 'true':
|
|
ret_list.append(('app_allowbackup', (), ()))
|
|
elif application.getAttribute(f'{ns}:allowBackup') == 'false':
|
|
backupDisabled = True
|
|
else:
|
|
if not backupDisabled:
|
|
ret_list.append(('allowbackup_not_set', (), ()))
|
|
if application.getAttribute(f'{ns}:testOnly') == 'true':
|
|
ret_list.append(('app_in_test_mode', (), ()))
|
|
for node in application.childNodes:
|
|
an_or_a = ''
|
|
if node.nodeName == 'activity':
|
|
itemname = 'Activity'
|
|
cnt_id = 'act'
|
|
an_or_a = 'n'
|
|
elif node.nodeName == 'activity-alias':
|
|
itemname = 'Activity-Alias'
|
|
cnt_id = 'act'
|
|
an_or_a = 'n'
|
|
elif node.nodeName == 'provider':
|
|
itemname = 'Content Provider'
|
|
cnt_id = 'cnt'
|
|
elif node.nodeName == 'receiver':
|
|
itemname = 'Broadcast Receiver'
|
|
cnt_id = 'bro'
|
|
elif node.nodeName == 'service':
|
|
itemname = 'Service'
|
|
cnt_id = 'ser'
|
|
else:
|
|
itemname = 'NIL'
|
|
item = ''
|
|
# Checks for Activities
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
item = node.getAttribute(f'{ns}:name')
|
|
item = escape_manifest_attribute(item)
|
|
# Browsable Activities
|
|
browse_dic = get_browsable_activities(node, ns)
|
|
if browse_dic['browsable']:
|
|
browsable_activities[node.getAttribute(
|
|
f'{ns}:name')] = browse_dic
|
|
for finding in assetlinks_check(item, browse_dic['well_known']):
|
|
if not finding['status']:
|
|
ret_list.append(('well_known_assetlinks',
|
|
(item, finding['host']),
|
|
(finding['url'],
|
|
finding['status_code'])))
|
|
|
|
# Task Affinity
|
|
task_affinity = node.getAttribute(f'{ns}:taskAffinity')
|
|
if (task_affinity):
|
|
ret_list.append(('task_affinity_set', (item,), ()))
|
|
|
|
# LaunchMode
|
|
try:
|
|
affected_sdk = int(
|
|
man_data_dic['min_sdk']) < ANDROID_5_0_LEVEL
|
|
except Exception:
|
|
# in case min_sdk is not defined we assume vulnerability
|
|
affected_sdk = True
|
|
launchmode = node.getAttribute(f'{ns}:launchMode')
|
|
modes = ('singleTask', 'singleInstance')
|
|
if (affected_sdk
|
|
and launchmode in modes):
|
|
ret_list.append(('non_standard_launchmode', (item,), ()))
|
|
|
|
# Android Task Hijacking or StrandHogg 1.0
|
|
try:
|
|
target_sdk = int(man_data_dic['target_sdk'])
|
|
except Exception:
|
|
try:
|
|
target_sdk = int(man_data_dic['min_sdk'])
|
|
except Exception:
|
|
target_sdk = ANDROID_8_0_LEVEL
|
|
if (target_sdk < ANDROID_9_0_LEVEL
|
|
and launchmode == 'singleTask'):
|
|
ret_list.append(('task_hijacking', (item,), (target_sdk,)))
|
|
|
|
# Android StrandHogg 2.0
|
|
exported_act = node.getAttribute(f'{ns}:exported')
|
|
if (target_sdk < ANDROID_10_0_LEVEL
|
|
and exported_act == 'true'
|
|
and (launchmode != 'singleInstance' or task_affinity != '')):
|
|
ret_list.append(('task_hijacking2', (item,), (target_sdk,)))
|
|
|
|
# Exported Check
|
|
item = ''
|
|
is_inf = False
|
|
is_perm_exist = False
|
|
# Esteve 23.07.2016 - begin - initialise variables to identify
|
|
# the existence of a permission at the component level that
|
|
# matches a permission at the manifest level
|
|
prot_level_exist = False
|
|
protlevel = ''
|
|
# End
|
|
if itemname != 'NIL':
|
|
if node.getAttribute(f'{ns}:exported') == 'true':
|
|
perm = ''
|
|
item = node.getAttribute(f'{ns}:name')
|
|
item = escape_manifest_attribute(item)
|
|
if node.getAttribute(f'{ns}:permission'):
|
|
# permission exists
|
|
perm = ('<strong>Permission: </strong>'
|
|
+ node.getAttribute(f'{ns}:permission'))
|
|
is_perm_exist = True
|
|
if item != man_data_dic['mainactivity']:
|
|
if is_perm_exist:
|
|
prot = ''
|
|
if node.getAttribute(f'{ns}:permission') in permission_dict:
|
|
prot = ('</br><strong>protectionLevel: </strong>'
|
|
+ permission_dict[node.getAttribute(f'{ns}:permission')])
|
|
# Esteve 23.07.2016 - begin - take into account protection level of the permission when claiming that a component is protected by it;
|
|
# - the permission might not be defined in the application being analysed, if so, the protection level is not known;
|
|
# - activities (or activity-alias) that are exported and have an unknown or normal or dangerous protection level are
|
|
# included in the EXPORTED data structure for further treatment; components in this situation are also
|
|
# counted as exported.
|
|
prot_level_exist = True
|
|
protlevel = permission_dict[
|
|
node.getAttribute(f'{ns}:permission')]
|
|
if prot_level_exist:
|
|
if protlevel == 'normal':
|
|
ret_list.append(
|
|
('exported_protected_permission_normal', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'dangerous':
|
|
ret_list.append(
|
|
('exported_protected_permission_dangerous', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'signature':
|
|
ret_list.append(
|
|
('exported_protected_permission_signature', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
elif protlevel == 'signatureOrSystem':
|
|
ret_list.append(
|
|
('exported_protected_permission_signatureorsystem', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
else:
|
|
ret_list.append(
|
|
('exported_protected_permission_not_defined', (itemname, item, perm), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[cnt_id] + 1
|
|
# Esteve 23.07.2016 - end
|
|
else:
|
|
# Esteve 24.07.2016 - begin - At this point, we are dealing with components that do not have a permission neither at the component level nor at the
|
|
# application level. As they are exported, they
|
|
# are not protected.
|
|
if perm_appl_level_exists is False:
|
|
ret_list.append(
|
|
('explicitly_exported', (itemname, item), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[cnt_id] + 1
|
|
# Esteve 24.07.2016 - end
|
|
# Esteve 24.07.2016 - begin - At this point, we are dealing with components that have a permission at the application level, but not at the component
|
|
# level. Two options are possible:
|
|
# 1) The permission is defined at the manifest level, which allows us to differentiate the level of protection as
|
|
# we did just above for permissions specified at the component level.
|
|
# 2) The permission is not defined at the manifest level, which means the protection level is unknown, as it is not
|
|
# defined in the analysed application.
|
|
else:
|
|
perm = '<strong>Permission: </strong>' + perm_appl_level
|
|
prot = ''
|
|
if perm_appl_level in permission_dict:
|
|
prot = ('</br><strong>protectionLevel: </strong>'
|
|
+ permission_dict[perm_appl_level])
|
|
prot_level_exist = True
|
|
protlevel = permission_dict[
|
|
perm_appl_level]
|
|
if prot_level_exist:
|
|
if protlevel == 'normal':
|
|
ret_list.append(
|
|
('exported_protected_permission_normal_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'dangerous':
|
|
ret_list.append(
|
|
('exported_protected_permission_dangerous_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'signature':
|
|
ret_list.append(
|
|
('exported_protected_permission', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
elif protlevel == 'signatureOrSystem':
|
|
ret_list.append(
|
|
('exported_protected_permission_signatureorsystem_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
else:
|
|
ret_list.append(
|
|
('exported_protected_permission_app_level', (itemname, item, perm), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
# Esteve 24.07.2016 - end
|
|
|
|
elif node.getAttribute(f'{ns}:exported') != 'false':
|
|
# Check for Implicitly Exported
|
|
# Logic to support intent-filter
|
|
intentfilters = node.childNodes
|
|
for i in intentfilters:
|
|
inf = i.nodeName
|
|
if inf == 'intent-filter':
|
|
is_inf = True
|
|
if is_inf:
|
|
item = node.getAttribute(f'{ns}:name')
|
|
item = escape_manifest_attribute(item)
|
|
if node.getAttribute(f'{ns}:permission'):
|
|
# permission exists
|
|
perm = ('<strong>Permission: </strong>'
|
|
+ node.getAttribute(f'{ns}:permission'))
|
|
is_perm_exist = True
|
|
if item != man_data_dic['mainactivity']:
|
|
if is_perm_exist:
|
|
prot = ''
|
|
if node.getAttribute(f'{ns}:permission') in permission_dict:
|
|
prot = ('</br><strong>protectionLevel: </strong>'
|
|
+ permission_dict[node.getAttribute(f'{ns}:permission')])
|
|
# Esteve 24.07.2016 - begin - take into account protection level of the permission when claiming that a component is protected by it;
|
|
# - the permission might not be defined in the application being analysed, if so, the protection level is not known;
|
|
# - activities (or activity-alias) that are exported and have an unknown or normal or dangerous protection level are
|
|
# included in the EXPORTED data structure for further treatment; components in this situation are also
|
|
# counted as exported.
|
|
prot_level_exist = True
|
|
protlevel = permission_dict[
|
|
node.getAttribute(f'{ns}:permission')]
|
|
if prot_level_exist:
|
|
if protlevel == 'normal':
|
|
ret_list.append(
|
|
('exported_protected_permission_normal', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'dangerous':
|
|
ret_list.append(
|
|
('exported_protected_permission_dangerous', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'signature':
|
|
ret_list.append(
|
|
('exported_protected_permission_signature', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
elif protlevel == 'signatureOrSystem':
|
|
ret_list.append(
|
|
('exported_protected_permission_signatureorsystem', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
else:
|
|
ret_list.append(
|
|
('exported_protected_permission_not_defined', (itemname, item, perm), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
# Esteve 24.07.2016 - end
|
|
else:
|
|
# Esteve 24.07.2016 - begin - At this point, we are dealing with components that do not have a permission neither at the component level nor at the
|
|
# application level. As they are exported,
|
|
# they are not protected.
|
|
if perm_appl_level_exists is False:
|
|
ret_list.append(
|
|
('exported_intent_filter_exists', (itemname, item), (an_or_a, itemname, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
# Esteve 24.07.2016 - end
|
|
# Esteve 24.07.2016 - begin - At this point, we are dealing with components that have a permission at the application level, but not at the component
|
|
# level. Two options are possible:
|
|
# 1) The permission is defined at the manifest level, which allows us to differentiate the level of protection as
|
|
# we did just above for permissions specified at the component level.
|
|
# 2) The permission is not defined at the manifest level, which means the protection level is unknown, as it is not
|
|
# defined in the analysed application.
|
|
else:
|
|
perm = '<strong>Permission: </strong>' + perm_appl_level
|
|
prot = ''
|
|
if perm_appl_level in permission_dict:
|
|
prot = ('</br><strong>protectionLevel: </strong>'
|
|
+ permission_dict[perm_appl_level])
|
|
prot_level_exist = True
|
|
protlevel = permission_dict[
|
|
perm_appl_level]
|
|
if prot_level_exist:
|
|
if protlevel == 'normal':
|
|
ret_list.append(
|
|
('exported_protected_permission_normal_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'dangerous':
|
|
ret_list.append(
|
|
('exported_protected_permission_dangerous_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'signature':
|
|
ret_list.append(
|
|
('exported_protected_permission', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
elif protlevel == 'signatureOrSystem':
|
|
ret_list.append(
|
|
('exported_protected_permission_signatureorsystem_app_level', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
else:
|
|
ret_list.append(
|
|
('exported_protected_permission_app_level', (itemname, item, perm), (an_or_a, itemname)))
|
|
if itemname in ['Activity', 'Activity-Alias']:
|
|
exported.append(item)
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
# Esteve 24.07.2016 - end
|
|
# Esteve 29.07.2016 - begin The component is not explicitly exported (android:exported is not 'true'). It is not implicitly exported either (it does not
|
|
# make use of an intent filter). Despite that, it could still be exported by default, if it is a content provider and the android:targetSdkVersion
|
|
# is older than 17 (Jelly Bean, Android version 4.2). This is true regardless of the system's API level.
|
|
# Finally, it must also be taken into account that, if the minSdkVersion is greater or equal than 17, this check is unnecessary, because the
|
|
# app will not be run on a system where the
|
|
# system's API level is below 17.
|
|
else:
|
|
if man_data_dic['min_sdk'] and man_data_dic['target_sdk'] and int(man_data_dic['min_sdk']) < ANDROID_4_2_LEVEL:
|
|
if itemname == 'Content Provider' and int(man_data_dic['target_sdk']) < ANDROID_4_2_LEVEL:
|
|
perm = ''
|
|
item = node.getAttribute(f'{ns}:name')
|
|
item = escape_manifest_attribute(item)
|
|
if node.getAttribute(f'{ns}:permission'):
|
|
# permission exists
|
|
perm = ('<strong>Permission: </strong>'
|
|
+ node.getAttribute(f'{ns}:permission'))
|
|
is_perm_exist = True
|
|
if is_perm_exist:
|
|
prot = ''
|
|
if node.getAttribute(f'{ns}:permission') in permission_dict:
|
|
prot = ('</br><strong>protectionLevel: </strong>'
|
|
+ permission_dict[node.getAttribute(f'{ns}:permission')])
|
|
prot_level_exist = True
|
|
protlevel = permission_dict[
|
|
node.getAttribute(f'{ns}:permission')]
|
|
if prot_level_exist:
|
|
if protlevel == 'normal':
|
|
ret_list.append(
|
|
('exported_provider_normal', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'dangerous':
|
|
ret_list.append(
|
|
('exported_provider_danger', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'signature':
|
|
ret_list.append(
|
|
('exported_provider_signature', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
elif protlevel == 'signatureOrSystem':
|
|
ret_list.append(
|
|
('exported_provider_signatureorsystem', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
else:
|
|
ret_list.append(
|
|
('exported_provider_unknown', (itemname, item, perm), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
else:
|
|
if perm_appl_level_exists is False:
|
|
ret_list.append(
|
|
('exported_provider', (itemname, item), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
else:
|
|
perm = '<strong>Permission: </strong>' + perm_appl_level
|
|
prot = ''
|
|
if perm_appl_level in permission_dict:
|
|
prot = ('</br><strong>protectionLevel: </strong>'
|
|
+ permission_dict[perm_appl_level])
|
|
prot_level_exist = True
|
|
protlevel = permission_dict[
|
|
perm_appl_level]
|
|
if prot_level_exist:
|
|
if protlevel == 'normal':
|
|
ret_list.append(
|
|
('exported_provider_normal_app', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'dangerous':
|
|
ret_list.append(
|
|
('exported_provider_danger_appl', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'signature':
|
|
ret_list.append(
|
|
('exported_provider_signature_appl', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
elif protlevel == 'signatureOrSystem':
|
|
ret_list.append(
|
|
('exported_provider_signatureorsystem_app', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
else:
|
|
ret_list.append(
|
|
('exported_provider_unknown_app', (itemname, item, perm), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
# Esteve 29.07.2016 - end
|
|
# Esteve 08.08.2016 - begin - If the content provider does not target an API version lower than 17, it could still be exported by default, depending
|
|
# on the API version of the platform. If it was below 17, the content
|
|
# provider would be exported by default.
|
|
else:
|
|
if itemname == 'Content Provider' and int(man_data_dic['target_sdk']) >= 17:
|
|
perm = ''
|
|
item = node.getAttribute(
|
|
f'{ns}:name')
|
|
item = escape_manifest_attribute(item)
|
|
if node.getAttribute(f'{ns}:permission'):
|
|
# permission exists
|
|
perm = ('<strong>Permission: </strong>'
|
|
+ node.getAttribute(f'{ns}:permission'))
|
|
is_perm_exist = True
|
|
if is_perm_exist:
|
|
prot = ''
|
|
if node.getAttribute(f'{ns}:permission') in permission_dict:
|
|
prot = ('</br><strong>protectionLevel: </strong>'
|
|
+ permission_dict[node.getAttribute(f'{ns}:permission')])
|
|
prot_level_exist = True
|
|
protlevel = permission_dict[
|
|
node.getAttribute(f'{ns}:permission')]
|
|
if prot_level_exist:
|
|
if protlevel == 'normal':
|
|
ret_list.append(
|
|
('exported_provider_normal_new', (itemname, item, perm + prot), (itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
if protlevel == 'dangerous':
|
|
ret_list.append(
|
|
('exported_provider_danger_new', (itemname, item, perm + prot), (itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
if protlevel == 'signature':
|
|
ret_list.append(
|
|
('exported_provider_signature_new', (itemname, item, perm + prot), (itemname)))
|
|
if protlevel == 'signatureOrSystem':
|
|
ret_list.append(
|
|
('exported_provider_signatureorsystem_new', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
else:
|
|
ret_list.append(
|
|
('exported_provider_unknown_new', (itemname, item, perm), (itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
else:
|
|
if perm_appl_level_exists is False:
|
|
ret_list.append(
|
|
('exported_provider_2', (itemname, item), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
else:
|
|
perm = '<strong>Permission: </strong>' + perm_appl_level
|
|
prot = ''
|
|
if perm_appl_level in permission_dict:
|
|
prot = ('</br><strong>protectionLevel: </strong>'
|
|
+ permission_dict[perm_appl_level])
|
|
prot_level_exist = True
|
|
protlevel = permission_dict[
|
|
perm_appl_level]
|
|
if prot_level_exist:
|
|
if protlevel == 'normal':
|
|
ret_list.append(
|
|
('exported_provider_normal_app_new', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'dangerous':
|
|
ret_list.append(
|
|
('exported_provider_danger_app_new', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
elif protlevel == 'signature':
|
|
ret_list.append(
|
|
('exported_provider_signature_app_new', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
elif protlevel == 'signatureOrSystem':
|
|
ret_list.append(
|
|
('exported_provider_signatureorsystem_app_new', (itemname, item, perm + prot), (an_or_a, itemname)))
|
|
else:
|
|
ret_list.append(
|
|
('exported_provider_unknown_app_new', (itemname, item, perm), (an_or_a, itemname)))
|
|
exp_count[cnt_id] = exp_count[
|
|
cnt_id] + 1
|
|
# Esteve 08.08.2016 - end
|
|
|
|
# GRANT-URI-PERMISSIONS
|
|
for granturi in granturipermissions:
|
|
if granturi.getAttribute(f'{ns}:pathPrefix') == '/':
|
|
ret_list.append(
|
|
('improper_provider_permission', ('pathPrefix=/',), ()))
|
|
elif granturi.getAttribute(f'{ns}:path') == '/':
|
|
ret_list.append(('improper_provider_permission', ('path=/',), ()))
|
|
elif granturi.getAttribute(f'{ns}:pathPattern') == '*':
|
|
ret_list.append(('improper_provider_permission', ('path=*',), ()))
|
|
# DATA
|
|
for data in data_tag:
|
|
if data.getAttribute(f'{ns}:scheme') == 'android_secret_code':
|
|
xmlhost = data.getAttribute(f'{ns}:host')
|
|
xmlhost = escape_manifest_attribute(xmlhost)
|
|
ret_list.append(('dialer_code_found', (xmlhost,), ()))
|
|
|
|
elif data.getAttribute(f'{ns}:port'):
|
|
dataport = data.getAttribute(f'{ns}:port')
|
|
dataport = escape_manifest_attribute(dataport)
|
|
ret_list.append(('sms_receiver_port_found', (dataport,), ()))
|
|
# INTENTS
|
|
processed_priorities = {}
|
|
for intent in intents:
|
|
if intent.getAttribute(f'{ns}:priority').isdigit():
|
|
value = intent.getAttribute(f'{ns}:priority')
|
|
if int(value) > 100:
|
|
if value not in processed_priorities:
|
|
processed_priorities[value] = 1
|
|
else:
|
|
processed_priorities[value] += 1
|
|
for priority, count in processed_priorities.items():
|
|
ret_list.append(
|
|
('high_intent_priority_found', (priority, count,), ()))
|
|
# ACTIONS
|
|
for action in actions:
|
|
if action.getAttribute(f'{ns}:priority').isdigit():
|
|
value = action.getAttribute(f'{ns}:priority')
|
|
if int(value) > 100:
|
|
ret_list.append(
|
|
('high_action_priority_found', (value,), ()))
|
|
for a_key, t_name, t_desc in ret_list:
|
|
a_template = android_manifest_desc.MANIFEST_DESC.get(a_key)
|
|
if a_template:
|
|
ret_value.append({
|
|
'rule': a_key,
|
|
'title': a_template['title'] % t_name,
|
|
'severity': a_template['level'],
|
|
'description': a_template['description'] % t_desc,
|
|
'name': a_template['name'] % t_name,
|
|
'component': t_name,
|
|
})
|
|
else:
|
|
logger.warning("No template found for key '%s'", a_key)
|
|
|
|
for category in man_data_dic['categories']:
|
|
if category == 'android.intent.category.LAUNCHER':
|
|
break
|
|
|
|
permissions = {}
|
|
for k, permission in man_data_dic['perm'].items():
|
|
permissions[k] = (
|
|
{
|
|
'status': permission[0],
|
|
'info': permission[1],
|
|
'description': permission[2],
|
|
})
|
|
# Prepare return dict
|
|
exported_comp = {
|
|
'exported_activities': exp_count['act'],
|
|
'exported_services': exp_count['ser'],
|
|
'exported_receivers': exp_count['bro'],
|
|
'exported_providers': exp_count['cnt'],
|
|
}
|
|
man_an_dic = {
|
|
'manifest_anal': ret_value,
|
|
'exported_act': exported,
|
|
'exported_cnt': exported_comp,
|
|
'browsable_activities': browsable_activities,
|
|
'permissions': permissions,
|
|
'network_security': network_security.analysis(
|
|
checksum,
|
|
app_dir,
|
|
do_netsec,
|
|
debuggable,
|
|
src_type),
|
|
}
|
|
return man_an_dic
|
|
except Exception as exp:
|
|
msg = 'Error Performing Manifest Analysis'
|
|
logger.exception(msg)
|
|
append_scan_status(checksum, msg, repr(exp))
|