mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-12 02:06:59 +00:00
Multiple Features (Scan timeout, Firebase Remote Config, Search Scans) (#2441)
Support time out for SAST and Binary scans Search by MD5, package name, file name and app name. Search REST API + docs + tests Firebase remote config check [FEATURE] Add support for Firebase Remote Config information #2429 autopep8
This commit is contained in:
+2
-1
@@ -24,4 +24,5 @@ mobsf/downloads
|
||||
mobsf/uploads
|
||||
mobsf/debug.log
|
||||
mobsf/secret
|
||||
mobsf/StaticAnalyzer/test_files/
|
||||
mobsf/StaticAnalyzer/test_files/
|
||||
TODO.md
|
||||
@@ -78,3 +78,4 @@ mobsf/secret
|
||||
mobsf/StaticAnalyzer/migrations
|
||||
mobsf/MobSF/windows_vm_priv_key.asc
|
||||
mobsf/setup_done.txt
|
||||
TODO.md
|
||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
||||
|
||||
from mobsf.MobSF.utils import (
|
||||
append_scan_status,
|
||||
run_with_timeout,
|
||||
settings_enabled,
|
||||
)
|
||||
|
||||
@@ -49,13 +50,28 @@ def apkid_analysis(checksum, apk_file):
|
||||
)
|
||||
rules = options.rules_manager.load()
|
||||
scanner = Scanner(rules, options)
|
||||
res = scanner.scan_file(apk_file)
|
||||
findings = {}
|
||||
res = None
|
||||
try:
|
||||
findings = output._build_json_output(res)['files']
|
||||
res = run_with_timeout(
|
||||
scanner.scan_file,
|
||||
settings.BINARY_ANALYSIS_TIMEOUT,
|
||||
apk_file)
|
||||
except Exception as e:
|
||||
msg = 'APKID scan timed out'
|
||||
logger.error(msg)
|
||||
append_scan_status(
|
||||
checksum,
|
||||
msg,
|
||||
str(e))
|
||||
try:
|
||||
if res:
|
||||
findings = output._build_json_output(res)['files']
|
||||
except AttributeError:
|
||||
# apkid >= 2.0.3
|
||||
try:
|
||||
findings = output.build_json_output(res)['files']
|
||||
if res:
|
||||
findings = output.build_json_output(res)['files']
|
||||
except AttributeError:
|
||||
msg = (
|
||||
'yara-python dependency required by '
|
||||
@@ -66,7 +82,6 @@ def apkid_analysis(checksum, apk_file):
|
||||
checksum,
|
||||
msg,
|
||||
'Missing dependency')
|
||||
findings = {}
|
||||
sanitized = {}
|
||||
for item in findings:
|
||||
filename = item['filename']
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ from mobsf.install.windows.setup import windows_config_local
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VERSION = '4.1.0'
|
||||
VERSION = '4.1.1'
|
||||
BANNER = r"""
|
||||
__ __ _ ____ _____ _ _ _
|
||||
| \/ | ___ | |__/ ___|| ___|_ _| || | / |
|
||||
|
||||
@@ -330,7 +330,9 @@ LOGGING = {
|
||||
},
|
||||
},
|
||||
}
|
||||
JADX_TIMEOUT = int(os.getenv('MOBSF_JADX_TIMEOUT', 1800))
|
||||
JADX_TIMEOUT = int(os.getenv('MOBSF_JADX_TIMEOUT', 1000))
|
||||
SAST_TIMEOUT = int(os.getenv('MOBSF_SAST_TIMEOUT', 1000))
|
||||
BINARY_ANALYSIS_TIMEOUT = int(os.getenv('MOBSF_BINARY_ANALYSIS_TIMEOUT', 600))
|
||||
DISABLE_AUTHENTICATION = os.getenv('MOBSF_DISABLE_AUTHENTICATION')
|
||||
RATELIMIT = os.getenv('MOBSF_RATELIMIT', '7/m')
|
||||
USE_X_FORWARDED_HOST = bool(
|
||||
|
||||
+1
-1
@@ -85,6 +85,7 @@ urlpatterns = [
|
||||
# Static Analysis
|
||||
re_path(r'^api/v1/upload$', api_sz.api_upload),
|
||||
re_path(r'^api/v1/scan$', api_sz.api_scan),
|
||||
re_path(r'^api/v1/search$', api_sz.api_search),
|
||||
re_path(r'^api/v1/scan_logs$', api_sz.api_scan_logs),
|
||||
re_path(r'^api/v1/delete_scan$', api_sz.api_delete_scan),
|
||||
re_path(r'^api/v1/download_pdf$', api_sz.api_pdf_report),
|
||||
@@ -198,7 +199,6 @@ if settings.API_ONLY == '0':
|
||||
re_path(r'^search$', home.search),
|
||||
re_path(r'^status/$', home.scan_status, name='status'),
|
||||
re_path(r'^error/$', home.error, name='error'),
|
||||
re_path(r'^not_found/$', home.not_found),
|
||||
re_path(r'^zip_format/$', home.zip_format),
|
||||
re_path(r'^dynamic_analysis/$', home.dynamic_analysis, name='dynamic'),
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ URL_REGEX = re.compile(
|
||||
re.UNICODE)
|
||||
EMAIL_REGEX = re.compile(r'[\w+.-]{1,20}@[\w-]{1,20}\.[\w]{2,10}')
|
||||
USERNAME_REGEX = re.compile(r'^\w[\w\-\@\.]{1,35}$')
|
||||
GOOGLE_API_KEY_REGEX = re.compile(r'AIza[0-9A-Za-z-_]{35}$')
|
||||
GOOGLE_APP_ID_REGEX = re.compile(r'\d{1,2}:\d{1,50}:android:[a-f0-9]{1,50}')
|
||||
|
||||
|
||||
class Color(object):
|
||||
@@ -947,3 +949,28 @@ def get_scan_logs(checksum):
|
||||
msg = 'Fetching scan logs from the DB failed.'
|
||||
logger.exception(msg)
|
||||
return []
|
||||
|
||||
|
||||
class TaskTimeoutError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def run_with_timeout(func, limit, *args, **kwargs):
|
||||
def run_func(result, *args, **kwargs):
|
||||
result.append(func(*args, **kwargs))
|
||||
|
||||
result = []
|
||||
thread = threading.Thread(
|
||||
target=run_func,
|
||||
args=(result, *args),
|
||||
kwargs=kwargs)
|
||||
thread.start()
|
||||
thread.join(limit)
|
||||
|
||||
if thread.is_alive():
|
||||
msg = (f'function <{func.__name__}> '
|
||||
f'timed out after {limit} seconds')
|
||||
raise TaskTimeoutError(msg)
|
||||
if result:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
@@ -12,7 +12,12 @@ from mobsf.MobSF.utils import (
|
||||
is_md5,
|
||||
)
|
||||
from mobsf.MobSF.views.helpers import request_method
|
||||
from mobsf.MobSF.views.home import RecentScans, Upload, delete_scan
|
||||
from mobsf.MobSF.views.home import (
|
||||
RecentScans,
|
||||
Upload,
|
||||
delete_scan,
|
||||
search,
|
||||
)
|
||||
from mobsf.MobSF.views.api.api_middleware import make_api_response
|
||||
from mobsf.StaticAnalyzer.views.android.views import view_source
|
||||
from mobsf.StaticAnalyzer.views.android.static_analyzer import static_analyzer
|
||||
@@ -180,6 +185,21 @@ def api_json_report(request):
|
||||
return response
|
||||
|
||||
|
||||
@request_method(['POST'])
|
||||
@csrf_exempt
|
||||
def api_search(request):
|
||||
"""Search by checksum or text."""
|
||||
if 'query' not in request.POST:
|
||||
return make_api_response(
|
||||
{'error': 'Missing Parameters'}, 422)
|
||||
resp = search(request, api=True)
|
||||
if 'checksum' in resp:
|
||||
request.POST = {'hash': resp['checksum']}
|
||||
return api_json_report(request)
|
||||
elif 'error' in resp:
|
||||
return make_api_response(resp, 404)
|
||||
|
||||
|
||||
@request_method(['POST'])
|
||||
@csrf_exempt
|
||||
def api_view_source(request):
|
||||
|
||||
+40
-27
@@ -22,6 +22,7 @@ from django.template.defaulttags import register
|
||||
|
||||
from mobsf.MobSF.forms import FormUtil, UploadFileForm
|
||||
from mobsf.MobSF.utils import (
|
||||
MD5_REGEX,
|
||||
api_key,
|
||||
get_md5,
|
||||
is_dir_exists,
|
||||
@@ -244,16 +245,6 @@ def zip_format(request):
|
||||
return render(request, template, context)
|
||||
|
||||
|
||||
def not_found(request, *args):
|
||||
"""Not Found Route."""
|
||||
context = {
|
||||
'title': 'Not Found',
|
||||
'version': settings.MOBSF_VER,
|
||||
}
|
||||
template = 'general/not_found.html'
|
||||
return render(request, template, context)
|
||||
|
||||
|
||||
@login_required
|
||||
def dynamic_analysis(request):
|
||||
"""Dynamic Analysis Landing."""
|
||||
@@ -337,18 +328,43 @@ def download_apk(request):
|
||||
|
||||
|
||||
@login_required
|
||||
def search(request):
|
||||
"""Search Scan by MD5 Route."""
|
||||
md5 = request.GET['md5']
|
||||
if re.match('[0-9a-f]{32}', md5):
|
||||
db_obj = RecentScansDB.objects.filter(MD5=md5)
|
||||
if db_obj.exists():
|
||||
e = db_obj[0]
|
||||
url = f'/{e.ANALYZER}/{e.MD5}/'
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect('/not_found/')
|
||||
return print_n_send_error_response(request, 'Invalid Scan Hash')
|
||||
def search(request, api=False):
|
||||
"""Search scan by checksum or text."""
|
||||
if request.method == 'POST':
|
||||
query = request.POST['query']
|
||||
else:
|
||||
query = request.GET['query']
|
||||
|
||||
if not query:
|
||||
msg = 'No search query provided.'
|
||||
return print_n_send_error_response(request, msg, api)
|
||||
|
||||
checksum = query if re.match(MD5_REGEX, query) else find_checksum(query)
|
||||
|
||||
if checksum and re.match(MD5_REGEX, checksum):
|
||||
db_obj = RecentScansDB.objects.filter(MD5=checksum).first()
|
||||
if db_obj:
|
||||
url = f'/{db_obj.ANALYZER}/{db_obj.MD5}/'
|
||||
if api:
|
||||
return {'checksum': db_obj.MD5}
|
||||
else:
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
msg = 'You can search by MD5, app name, package name, or file name.'
|
||||
return print_n_send_error_response(request, msg, api, 'Scan not found')
|
||||
|
||||
|
||||
def find_checksum(query):
|
||||
"""Get the first matching checksum from the database."""
|
||||
search_fields = ['FILE_NAME', 'PACKAGE_NAME', 'APP_NAME']
|
||||
|
||||
for field in search_fields:
|
||||
result = RecentScansDB.objects.filter(
|
||||
**{f'{field}__icontains': query}).first()
|
||||
if result:
|
||||
return result.MD5
|
||||
|
||||
return None
|
||||
|
||||
# AJAX
|
||||
|
||||
@@ -453,7 +469,7 @@ def delete_scan(request, api=False):
|
||||
else:
|
||||
md5_hash = request.POST['md5']
|
||||
data = {'deleted': 'scan hash not found'}
|
||||
if re.match('[0-9a-f]{32}', md5_hash):
|
||||
if re.match(MD5_REGEX, md5_hash):
|
||||
# Delete DB Entries
|
||||
scan = RecentScansDB.objects.filter(MD5=md5_hash)
|
||||
if scan.exists():
|
||||
@@ -485,10 +501,7 @@ def delete_scan(request, api=False):
|
||||
except Exception as exp:
|
||||
msg = str(exp)
|
||||
exp_doc = exp.__doc__
|
||||
if api:
|
||||
return print_n_send_error_response(request, msg, True, exp_doc)
|
||||
else:
|
||||
return print_n_send_error_response(request, msg, False, exp_doc)
|
||||
return print_n_send_error_response(request, msg, api, exp_doc)
|
||||
|
||||
|
||||
class RecentScans(object):
|
||||
|
||||
@@ -117,7 +117,7 @@ def static_analysis_test():
|
||||
logger.info(resp.content)
|
||||
return True
|
||||
|
||||
# Search by MD5
|
||||
# Search by MD5 and text
|
||||
if platform.system() in ['Darwin', 'Linux']:
|
||||
scan_md5s = ['02e7989c457ab67eb514a8328779f256',
|
||||
'82ab8b2193b3cfb1c737e3a786be363a',
|
||||
@@ -133,18 +133,23 @@ def static_analysis_test():
|
||||
'57bb5be0ea44a755ada4a93885c3825e',
|
||||
'8179b557433835827a70510584f3143e',
|
||||
'7b0a23bffc80bac05739ea1af898daad']
|
||||
# Search by text
|
||||
queries = [
|
||||
'diva',
|
||||
'webview',
|
||||
]
|
||||
logger.info('Running Search test')
|
||||
for scan_md5 in scan_md5s:
|
||||
url = '/search?md5={}'.format(scan_md5)
|
||||
for q in scan_md5s + queries:
|
||||
url = f'/search?query={q}'
|
||||
resp = http_client.get(url, follow=True)
|
||||
assert (resp.status_code == 200)
|
||||
if resp.status_code == 200:
|
||||
logger.info('[OK] Search by MD5 test passed for %s', scan_md5)
|
||||
logger.info('[OK] Search by query test passed for %s', q)
|
||||
else:
|
||||
logger.error('Search by MD5 test failed for %s', scan_md5)
|
||||
logger.error('Search by query test failed for %s', q)
|
||||
logger.info(resp.content)
|
||||
return True
|
||||
logger.info('[OK] Search by MD5 tests completed')
|
||||
logger.info('[OK] Search by MD5 and text tests completed')
|
||||
|
||||
# Deleting Scan Results
|
||||
logger.info('Running Delete Scan Results test')
|
||||
@@ -256,6 +261,18 @@ def api_test():
|
||||
logger.error('Scan Logs API test: %s', upl['hash'])
|
||||
return True
|
||||
logger.info('[OK] Static Analysis API test completed')
|
||||
# Search API Tests
|
||||
logger.info('Running Search API tests')
|
||||
for term in ['diva', 'webview', '52c50ae824e329ba8b5b7a0f523efffe']:
|
||||
resp = http_client.post(
|
||||
'/api/v1/search',
|
||||
{'query': term},
|
||||
HTTP_AUTHORIZATION=auth)
|
||||
if resp.status_code == 200:
|
||||
logger.info('[OK] Search API test: %s', term)
|
||||
else:
|
||||
logger.error('Search API test: %s', term)
|
||||
return True
|
||||
# PDF Tests
|
||||
logger.info('Running PDF Generation API Test')
|
||||
if platform.system() in ['Darwin', 'Linux']:
|
||||
|
||||
@@ -130,11 +130,12 @@ def _dump_additional_attributes(additional_attributes):
|
||||
def _dump_digests_or_signatures(digests_or_sigs):
|
||||
|
||||
infos = ""
|
||||
for i,dos in enumerate(digests_or_sigs):
|
||||
for i, dos in enumerate(digests_or_sigs):
|
||||
|
||||
infos += "\n"
|
||||
infos += " [%d]\n" % i
|
||||
infos += " - Signature Id : %s\n" % APK._APK_SIG_ALGO_IDS.get(dos[0], hex(dos[0]))
|
||||
infos += " - Signature Id : %s\n" % APK._APK_SIG_ALGO_IDS.get(
|
||||
dos[0], hex(dos[0]))
|
||||
infos += " - Digest: %s" % binascii.hexlify(dos[1])
|
||||
|
||||
return infos
|
||||
@@ -149,20 +150,22 @@ class APKV2SignedData:
|
||||
def __init__(self):
|
||||
self._bytes = None
|
||||
self.digests = None
|
||||
self.certificates = None
|
||||
self.certificates = None
|
||||
self.additional_attributes = None
|
||||
|
||||
def __str__(self):
|
||||
|
||||
certs_infos = ""
|
||||
|
||||
for i,cert in enumerate(self.certificates):
|
||||
for i, cert in enumerate(self.certificates):
|
||||
x509_cert = x509.Certificate.load(cert)
|
||||
|
||||
certs_infos += "\n"
|
||||
certs_infos += " [%d]\n" % i
|
||||
certs_infos += " - Issuer: %s\n" % get_certificate_name_string(x509_cert.issuer, short=True)
|
||||
certs_infos += " - Subject: %s\n" % get_certificate_name_string(x509_cert.subject, short=True)
|
||||
certs_infos += " - Issuer: %s\n" % get_certificate_name_string(
|
||||
x509_cert.issuer, short=True)
|
||||
certs_infos += " - Subject: %s\n" % get_certificate_name_string(
|
||||
x509_cert.subject, short=True)
|
||||
certs_infos += " - Serial Number: %s\n" % hex(x509_cert.serial_number)
|
||||
certs_infos += " - Hash Algorithm: %s\n" % x509_cert.hash_algo
|
||||
certs_infos += " - Signature Algorithm: %s\n" % x509_cert.signature_algo
|
||||
@@ -170,7 +173,8 @@ class APKV2SignedData:
|
||||
certs_infos += " - Valid not after: %s" % x509_cert['tbs_certificate']['validity']['not_after'].native
|
||||
|
||||
return "\n".join([
|
||||
'additional_attributes : {}'.format(_dump_additional_attributes(self.additional_attributes)),
|
||||
'additional_attributes : {}'.format(
|
||||
_dump_additional_attributes(self.additional_attributes)),
|
||||
'digests : {}'.format(_dump_digests_or_signatures(self.digests)),
|
||||
'certificates : {}'.format(certs_infos),
|
||||
])
|
||||
@@ -262,13 +266,15 @@ class APK:
|
||||
_APK_SIG_ATTR_V2_STRIPPING_PROTECTION = 0xbeeff00d
|
||||
|
||||
_APK_SIG_ALGO_IDS = {
|
||||
0x0101 : "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc",
|
||||
0x0102 : "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc",
|
||||
0x0103 : "RSASSA-PKCS1-v1_5 with SHA2-256 digest.", # This is for build systems which require deterministic signatures.
|
||||
0x0104 : "RSASSA-PKCS1-v1_5 with SHA2-512 digest.", # This is for build systems which require deterministic signatures.
|
||||
0x0201 : "ECDSA with SHA2-256 digest",
|
||||
0x0202 : "ECDSA with SHA2-512 digest",
|
||||
0x0301 : "DSA with SHA2-256 digest",
|
||||
0x0101: "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc",
|
||||
0x0102: "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc",
|
||||
# This is for build systems which require deterministic signatures.
|
||||
0x0103: "RSASSA-PKCS1-v1_5 with SHA2-256 digest.",
|
||||
# This is for build systems which require deterministic signatures.
|
||||
0x0104: "RSASSA-PKCS1-v1_5 with SHA2-512 digest.",
|
||||
0x0201: "ECDSA with SHA2-256 digest",
|
||||
0x0202: "ECDSA with SHA2-512 digest",
|
||||
0x0301: "DSA with SHA2-256 digest",
|
||||
}
|
||||
|
||||
__no_magic = False
|
||||
@@ -296,7 +302,8 @@ class APK:
|
||||
|
||||
"""
|
||||
if magic_file:
|
||||
logger.warning("You set magic_file but this parameter is actually unused. You should remove it.")
|
||||
logger.warning(
|
||||
"You set magic_file but this parameter is actually unused. You should remove it.")
|
||||
|
||||
self.filename = filename
|
||||
|
||||
@@ -345,7 +352,8 @@ class APK:
|
||||
# we could print the filename here, but there are zip which are so broken
|
||||
# That the filename is either very very long or does not make any sense.
|
||||
# Thus we do not do it, the user might find out by using other tools.
|
||||
raise BrokenAPKError("The APK is probably broken: testzip returned an error.")
|
||||
raise BrokenAPKError(
|
||||
"The APK is probably broken: testzip returned an error.")
|
||||
|
||||
if not skip_analysis:
|
||||
self._apk_analysis()
|
||||
@@ -375,24 +383,30 @@ class APK:
|
||||
ap = AXMLPrinter(manifest_data)
|
||||
|
||||
if not ap.is_valid():
|
||||
logger.error("Error while parsing AndroidManifest.xml - is the file valid?")
|
||||
logger.error(
|
||||
"Error while parsing AndroidManifest.xml - is the file valid?")
|
||||
return
|
||||
|
||||
self.axml[i] = ap
|
||||
self.xml[i] = self.axml[i].get_xml_obj()
|
||||
|
||||
if self.axml[i].is_packed():
|
||||
logger.warning("XML Seems to be packed, operations on the AndroidManifest.xml might fail.")
|
||||
logger.warning(
|
||||
"XML Seems to be packed, operations on the AndroidManifest.xml might fail.")
|
||||
|
||||
if self.xml[i] is not None:
|
||||
if self.xml[i].tag != "manifest":
|
||||
logger.error("AndroidManifest.xml does not start with a <manifest> tag! Is this a valid APK?")
|
||||
logger.error(
|
||||
"AndroidManifest.xml does not start with a <manifest> tag! Is this a valid APK?")
|
||||
return
|
||||
|
||||
self.package = self.get_attribute_value("manifest", "package")
|
||||
self.androidversion["Code"] = self.get_attribute_value("manifest", "versionCode")
|
||||
self.androidversion["Name"] = self.get_attribute_value("manifest", "versionName")
|
||||
permission = list(self.get_all_attribute_value("uses-permission", "name"))
|
||||
self.androidversion["Code"] = self.get_attribute_value(
|
||||
"manifest", "versionCode")
|
||||
self.androidversion["Name"] = self.get_attribute_value(
|
||||
"manifest", "versionName")
|
||||
permission = list(self.get_all_attribute_value(
|
||||
"uses-permission", "name"))
|
||||
self.permissions = list(set(self.permissions + permission))
|
||||
|
||||
for uses_permission in self.find_tags("uses-permission"):
|
||||
@@ -480,7 +494,8 @@ class APK:
|
||||
try:
|
||||
maxSdkVersion = int(self.get_value_from_tag(item, "maxSdkVersion"))
|
||||
except ValueError:
|
||||
logger.warning(str(maxSdkVersion) + ' is not a valid value for <uses-permission> maxSdkVersion')
|
||||
logger.warning(str(maxSdkVersion)
|
||||
+ ' is not a valid value for <uses-permission> maxSdkVersion')
|
||||
except TypeError:
|
||||
pass
|
||||
return maxSdkVersion
|
||||
@@ -526,12 +541,14 @@ class APK:
|
||||
|
||||
# FIXME: would need to use _format_value inside get_attribute_value for each returned name!
|
||||
# For example, as the activity name might be foobar.foo.bar but inside the activity it is only .bar
|
||||
app_name = self.get_attribute_value('activity', 'label', name=main_activity_name)
|
||||
app_name = self.get_attribute_value(
|
||||
'activity', 'label', name=main_activity_name)
|
||||
|
||||
if app_name is None:
|
||||
# No App name set
|
||||
# TODO return packagename instead?
|
||||
logger.warning("It looks like that no app name is set for the main activity!")
|
||||
logger.warning(
|
||||
"It looks like that no app name is set for the main activity!")
|
||||
return ""
|
||||
|
||||
if app_name.startswith("@"):
|
||||
@@ -549,11 +566,12 @@ class APK:
|
||||
# TODO: we can not resolve this, as we lack framework-res.apk
|
||||
# one exception would be when parsing framework-res.apk directly.
|
||||
logger.warning("Resource ID with android package name encountered! "
|
||||
"Will not resolve, framework-res.apk would be required.")
|
||||
"Will not resolve, framework-res.apk would be required.")
|
||||
return app_name
|
||||
else:
|
||||
# TODO should look this up, might be in the resources
|
||||
logger.warning("Resource ID with Package name '{}' encountered! Will not resolve".format(package))
|
||||
logger.warning(
|
||||
"Resource ID with Package name '{}' encountered! Will not resolve".format(package))
|
||||
return app_name
|
||||
|
||||
try:
|
||||
@@ -624,7 +642,8 @@ class APK:
|
||||
app_icon = "@%x" % res_id
|
||||
|
||||
if not app_icon:
|
||||
res_id = res_parser.get_res_id_by_key(self.package, 'drawable', 'ic_launcher')
|
||||
res_id = res_parser.get_res_id_by_key(
|
||||
self.package, 'drawable', 'ic_launcher')
|
||||
if res_id:
|
||||
app_icon = "@%x" % res_id
|
||||
|
||||
@@ -726,7 +745,7 @@ class APK:
|
||||
except AttributeError:
|
||||
self.__no_magic = True
|
||||
logger.warning("Not the correct Magic library was found on your "
|
||||
"system. Please install python-magic or python-magic-bin!")
|
||||
"system. Please install python-magic or python-magic-bin!")
|
||||
return default
|
||||
|
||||
try:
|
||||
@@ -793,9 +812,10 @@ class APK:
|
||||
self.files_crc32[filename] = crc32(buffer)
|
||||
if self.files_crc32[filename] != self.zip.infolist()[filename].crc32_of_uncompressed_data:
|
||||
logger.error("File '{}' has different CRC32 after unpacking! "
|
||||
"Declared: {:08x}, Calculated: {:08x}".format(filename,
|
||||
self.zip.infolist()[filename].crc32_of_uncompressed_data,
|
||||
self.files_crc32[filename]))
|
||||
"Declared: {:08x}, Calculated: {:08x}".format(filename,
|
||||
self.zip.infolist()[
|
||||
filename].crc32_of_uncompressed_data,
|
||||
self.files_crc32[filename]))
|
||||
return buffer
|
||||
|
||||
def get_files_crc32(self):
|
||||
@@ -986,7 +1006,7 @@ class APK:
|
||||
if value:
|
||||
# If value is still None, the attribute could not be found, thus is not present
|
||||
logger.warning("Failed to get the attribute '{}' on tag '{}' with namespace. "
|
||||
"But found the same attribute without namespace!".format(attribute, tag.tag))
|
||||
"But found the same attribute without namespace!".format(attribute, tag.tag))
|
||||
return value
|
||||
|
||||
def find_tags(self, tag_name, **attribute_filter):
|
||||
@@ -1070,7 +1090,7 @@ class APK:
|
||||
if self.xml[i] is None:
|
||||
continue
|
||||
activities_and_aliases = self.xml[i].findall(".//activity") + \
|
||||
self.xml[i].findall(".//activity-alias")
|
||||
self.xml[i].findall(".//activity-alias")
|
||||
|
||||
for item in activities_and_aliases:
|
||||
# Some applications have more than one MAIN activity.
|
||||
@@ -1139,8 +1159,8 @@ class APK:
|
||||
for alias in self.find_tags('activity-alias'):
|
||||
activity_alias = {}
|
||||
for attribute in ['name', 'targetActivity']:
|
||||
value = (alias.get(attribute) or
|
||||
alias.get(self._ns(attribute)))
|
||||
value = (alias.get(attribute)
|
||||
or alias.get(self._ns(attribute)))
|
||||
if not value:
|
||||
continue
|
||||
activity_alias[attribute] = self._format_value(value)
|
||||
@@ -1205,7 +1225,8 @@ class APK:
|
||||
:param name: the `android:name` of the parent item, e.g. activity name
|
||||
:returns: a dictionary with the keys `action` and `category` containing the `android:name` of those items
|
||||
"""
|
||||
attributes = {"action": ["name"], "category": ["name"], "data": ['scheme', 'host', 'port', 'path', 'pathPattern', 'pathPrefix', 'mimeType']}
|
||||
attributes = {"action": ["name"], "category": ["name"], "data": [
|
||||
'scheme', 'host', 'port', 'path', 'pathPattern', 'pathPrefix', 'mimeType']}
|
||||
|
||||
d = {}
|
||||
for element in attributes.keys():
|
||||
@@ -1218,7 +1239,7 @@ class APK:
|
||||
for sitem in item.findall(".//intent-filter"):
|
||||
for element in d.keys():
|
||||
for ssitem in sitem.findall(element):
|
||||
if element == 'data': # multiple attributes
|
||||
if element == 'data': # multiple attributes
|
||||
values = {}
|
||||
for attribute in attributes[element]:
|
||||
value = ssitem.get(self._ns(attribute))
|
||||
@@ -1288,7 +1309,7 @@ class APK:
|
||||
implied.append([READ_PHONE_STATE, None])
|
||||
|
||||
if (WRITE_EXTERNAL_STORAGE in self.permissions or implied_WRITE_EXTERNAL_STORAGE) \
|
||||
and READ_EXTERNAL_STORAGE not in self.permissions:
|
||||
and READ_EXTERNAL_STORAGE not in self.permissions:
|
||||
maxSdkVersion = None
|
||||
for name, version in self.uses_permissions:
|
||||
if name == WRITE_EXTERNAL_STORAGE:
|
||||
@@ -1298,10 +1319,10 @@ class APK:
|
||||
|
||||
if target_sdk_version < 16:
|
||||
if READ_CONTACTS in self.permissions \
|
||||
and READ_CALL_LOG not in self.permissions:
|
||||
and READ_CALL_LOG not in self.permissions:
|
||||
implied.append([READ_CALL_LOG, None])
|
||||
if WRITE_CONTACTS in self.permissions \
|
||||
and WRITE_CALL_LOG not in self.permissions:
|
||||
and WRITE_CALL_LOG not in self.permissions:
|
||||
implied.append([WRITE_CALL_LOG, None])
|
||||
|
||||
return implied
|
||||
@@ -1317,8 +1338,8 @@ class APK:
|
||||
filled_permissions = permissions.copy()
|
||||
for permission in filled_permissions:
|
||||
protection_level, label, description = filled_permissions[permission]
|
||||
if ((not label or not description)
|
||||
and permission in self.permission_module_min_sdk):
|
||||
if ((not label or not description)
|
||||
and permission in self.permission_module_min_sdk):
|
||||
x = self.permission_module_min_sdk[permission]
|
||||
protection_level = self._update_permission_protection_level(
|
||||
x['protectionLevel'], min_sdk)
|
||||
@@ -1327,7 +1348,7 @@ class APK:
|
||||
else:
|
||||
filled_permissions[permission] = [
|
||||
self._update_permission_protection_level(
|
||||
protection_level, target_sdk),
|
||||
protection_level, target_sdk),
|
||||
label, description]
|
||||
return filled_permissions
|
||||
|
||||
@@ -1697,7 +1718,7 @@ class APK:
|
||||
if r == self._PK_END_OF_CENTRAL_DIR:
|
||||
# Read central dir
|
||||
this_disk, disk_central, this_entries, total_entries, \
|
||||
size_central, offset_central = unpack('<HHHHII', f.read(16))
|
||||
size_central, offset_central = unpack('<HHHHII', f.read(16))
|
||||
# TODO according to the standard we need to check if the
|
||||
# end of central directory is the last item in the zip file
|
||||
# TODO We also need to check if the central dir is exactly
|
||||
@@ -1705,9 +1726,11 @@ class APK:
|
||||
|
||||
# These things should not happen for APKs
|
||||
if this_disk != 0:
|
||||
logger.warning("This is a multi disk ZIP! Attempting to process its signature anyway!")
|
||||
logger.warning(
|
||||
"This is a multi disk ZIP! Attempting to process its signature anyway!")
|
||||
if disk_central != 0:
|
||||
logger.warning("This is a multi disk ZIP! Attempting to process its signature anyway!")
|
||||
logger.warning(
|
||||
"This is a multi disk ZIP! Attempting to process its signature anyway!")
|
||||
break
|
||||
f.seek(-4, io.SEEK_CUR)
|
||||
|
||||
@@ -1750,7 +1773,6 @@ class APK:
|
||||
if self._APK_SIG_KEY_V3_SIGNATURE in self._v2_blocks:
|
||||
self._is_signed_v3 = True
|
||||
|
||||
|
||||
def parse_v3_signing_block(self):
|
||||
"""
|
||||
Parse the V2 signing block and extract all features
|
||||
@@ -1800,7 +1822,6 @@ class APK:
|
||||
raw_digests = signed_data.read(len_digests)
|
||||
digests = self.parse_signatures_or_digests(raw_digests)
|
||||
|
||||
|
||||
# Certs
|
||||
certs = []
|
||||
len_certs = self.read_uint32_le(signed_data)
|
||||
@@ -1841,7 +1862,7 @@ class APK:
|
||||
publickey = block.read(len_publickey)
|
||||
|
||||
signer = APKV3Signer()
|
||||
signer._bytes = view[off_signer:off_signer+size_signer]
|
||||
signer._bytes = view[off_signer:off_signer + size_signer]
|
||||
signer.signed_data = signed_data_object
|
||||
signer.signatures = sigs
|
||||
signer.public_key = publickey
|
||||
@@ -1925,7 +1946,7 @@ class APK:
|
||||
publickey = block.read(len_publickey)
|
||||
|
||||
signer = APKV2Signer()
|
||||
signer._bytes = view[off_signer:off_signer+size_signer]
|
||||
signer._bytes = view[off_signer:off_signer + size_signer]
|
||||
signer.signed_data = signed_data_object
|
||||
signer.signatures = sigs
|
||||
signer.public_key = publickey
|
||||
@@ -1997,14 +2018,14 @@ class APK:
|
||||
Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found
|
||||
in the v3 signing block.
|
||||
"""
|
||||
return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v3()]
|
||||
return [keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v3()]
|
||||
|
||||
def get_public_keys_v2(self):
|
||||
"""
|
||||
Return a list of :class:`asn1crypto.keys.PublicKeyInfo` which are found
|
||||
in the v2 signing block.
|
||||
"""
|
||||
return [ keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v2()]
|
||||
return [keys.PublicKeyInfo.load(pkey) for pkey in self.get_public_keys_der_v2()]
|
||||
|
||||
def get_certificates_v3(self):
|
||||
"""
|
||||
@@ -2013,7 +2034,7 @@ class APK:
|
||||
Note that we simply extract all certificates regardless of the signer.
|
||||
Therefore this is just a list of all certificates found in all signers.
|
||||
"""
|
||||
return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v3()]
|
||||
return [x509.Certificate.load(cert) for cert in self.get_certificates_der_v3()]
|
||||
|
||||
def get_certificates_v2(self):
|
||||
"""
|
||||
@@ -2022,7 +2043,7 @@ class APK:
|
||||
Note that we simply extract all certificates regardless of the signer.
|
||||
Therefore this is just a list of all certificates found in all signers.
|
||||
"""
|
||||
return [ x509.Certificate.load(cert) for cert in self.get_certificates_der_v2()]
|
||||
return [x509.Certificate.load(cert) for cert in self.get_certificates_der_v2()]
|
||||
|
||||
def get_certificates_v1(self):
|
||||
"""
|
||||
@@ -2077,7 +2098,8 @@ class APK:
|
||||
if "{}.SF".format(i.rsplit(".", 1)[0]) in self.get_files():
|
||||
signatures.append(i)
|
||||
else:
|
||||
logger.warning("v1 signature file {} missing .SF file - Partial signature!".format(i))
|
||||
logger.warning(
|
||||
"v1 signature file {} missing .SF file - Partial signature!".format(i))
|
||||
|
||||
return signatures
|
||||
|
||||
|
||||
@@ -24,23 +24,23 @@ RES_STRING_POOL_TYPE = 0x0001
|
||||
RES_TABLE_TYPE = 0x0002
|
||||
RES_XML_TYPE = 0x0003
|
||||
|
||||
RES_XML_FIRST_CHUNK_TYPE = 0x0100
|
||||
RES_XML_START_NAMESPACE_TYPE = 0x0100
|
||||
RES_XML_END_NAMESPACE_TYPE = 0x0101
|
||||
RES_XML_START_ELEMENT_TYPE = 0x0102
|
||||
RES_XML_END_ELEMENT_TYPE = 0x0103
|
||||
RES_XML_CDATA_TYPE = 0x0104
|
||||
RES_XML_LAST_CHUNK_TYPE = 0x017f
|
||||
RES_XML_FIRST_CHUNK_TYPE = 0x0100
|
||||
RES_XML_START_NAMESPACE_TYPE = 0x0100
|
||||
RES_XML_END_NAMESPACE_TYPE = 0x0101
|
||||
RES_XML_START_ELEMENT_TYPE = 0x0102
|
||||
RES_XML_END_ELEMENT_TYPE = 0x0103
|
||||
RES_XML_CDATA_TYPE = 0x0104
|
||||
RES_XML_LAST_CHUNK_TYPE = 0x017f
|
||||
|
||||
RES_XML_RESOURCE_MAP_TYPE = 0x0180
|
||||
RES_XML_RESOURCE_MAP_TYPE = 0x0180
|
||||
|
||||
RES_TABLE_PACKAGE_TYPE = 0x0200
|
||||
RES_TABLE_TYPE_TYPE = 0x0201
|
||||
RES_TABLE_TYPE_SPEC_TYPE = 0x0202
|
||||
RES_TABLE_LIBRARY_TYPE = 0x0203
|
||||
RES_TABLE_OVERLAYABLE_TYPE = 0x0204
|
||||
RES_TABLE_PACKAGE_TYPE = 0x0200
|
||||
RES_TABLE_TYPE_TYPE = 0x0201
|
||||
RES_TABLE_TYPE_SPEC_TYPE = 0x0202
|
||||
RES_TABLE_LIBRARY_TYPE = 0x0203
|
||||
RES_TABLE_OVERLAYABLE_TYPE = 0x0204
|
||||
RES_TABLE_OVERLAYABLE_POLICY_TYPE = 0x0205
|
||||
RES_TABLE_STAGED_ALIAS_TYPE = 0x0206
|
||||
RES_TABLE_STAGED_ALIAS_TYPE = 0x0206
|
||||
# Flags in the STRING Section
|
||||
SORTED_FLAG = 1 << 0
|
||||
UTF8_FLAG = 1 << 8
|
||||
@@ -104,6 +104,7 @@ class StringBlock:
|
||||
|
||||
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#436
|
||||
"""
|
||||
|
||||
def __init__(self, buff, header):
|
||||
"""
|
||||
:param buff: buffer which holds the string block
|
||||
@@ -126,7 +127,8 @@ class StringBlock:
|
||||
self.stringsOffset = unpack('<I', buff.read(4))[0]
|
||||
# check if the stringCount is correct
|
||||
if (self.stringsOffset - (self.styleCount * 4 + 28)) / 4 != self.stringCount:
|
||||
self.stringCount = int((self.stringsOffset - (self.styleCount * 4 + 28)) / 4)
|
||||
self.stringCount = int(
|
||||
(self.stringsOffset - (self.styleCount * 4 + 28)) / 4)
|
||||
|
||||
# style_pool_offset
|
||||
# The styles offset is counted as well from the beginning of the string section
|
||||
@@ -135,7 +137,7 @@ class StringBlock:
|
||||
# Check if they supplied a stylesOffset even if the count is 0:
|
||||
if self.styleCount == 0 and self.stylesOffset > 0:
|
||||
logger.info("Styles Offset given, but styleCount is zero. "
|
||||
"This is not a problem but could indicate packers.")
|
||||
"This is not a problem but could indicate packers.")
|
||||
|
||||
self.m_stringOffsets = []
|
||||
self.m_styleOffsets = []
|
||||
@@ -246,7 +248,8 @@ class StringBlock:
|
||||
data = self.m_charbuff[offset: offset + encoded_bytes]
|
||||
|
||||
if self.m_charbuff[offset + encoded_bytes] != 0:
|
||||
raise ResParserError("UTF-8 String is not null terminated! At offset={}".format(offset))
|
||||
raise ResParserError(
|
||||
"UTF-8 String is not null terminated! At offset={}".format(offset))
|
||||
|
||||
return self._decode_bytes(data, 'utf-8', str_len)
|
||||
|
||||
@@ -266,7 +269,8 @@ class StringBlock:
|
||||
data = self.m_charbuff[offset: offset + encoded_bytes]
|
||||
|
||||
if self.m_charbuff[offset + encoded_bytes:offset + encoded_bytes + 2] != b"\x00\x00":
|
||||
raise ResParserError("UTF-16 String is not null terminated! At offset={}".format(offset))
|
||||
raise ResParserError(
|
||||
"UTF-16 String is not null terminated! At offset={}".format(offset))
|
||||
|
||||
return self._decode_bytes(data, 'utf-16', str_len)
|
||||
|
||||
@@ -319,9 +323,11 @@ class StringBlock:
|
||||
|
||||
# These are true asserts, as the size should never be less than the values
|
||||
if sizeof_char == 1:
|
||||
assert length <= 0x7FFF, "length of UTF-8 string is too large! At offset={}".format(offset)
|
||||
assert length <= 0x7FFF, "length of UTF-8 string is too large! At offset={}".format(
|
||||
offset)
|
||||
else:
|
||||
assert length <= 0x7FFFFFFF, "length of UTF-16 string is too large! At offset={}".format(offset)
|
||||
assert length <= 0x7FFFFFFF, "length of UTF-16 string is too large! At offset={}".format(
|
||||
offset)
|
||||
|
||||
return length, size
|
||||
|
||||
@@ -376,6 +382,7 @@ class AXMLParser:
|
||||
|
||||
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#563
|
||||
"""
|
||||
|
||||
def __init__(self, raw_buff):
|
||||
logger.debug("AXMLParser")
|
||||
|
||||
@@ -388,14 +395,16 @@ class AXMLParser:
|
||||
|
||||
# Minimum is a single ARSCHeader, which would be a strange edge case...
|
||||
if self.buff_size < 8:
|
||||
logger.error("Filesize is too small to be a valid AXML file! Filesize: {}".format(self.buff_size))
|
||||
logger.error(
|
||||
"Filesize is too small to be a valid AXML file! Filesize: {}".format(self.buff_size))
|
||||
self._valid = False
|
||||
return
|
||||
|
||||
# This would be even stranger, if an AXML file is larger than 4GB...
|
||||
# But this is not possible as the maximum chunk size is a unsigned 4 byte int.
|
||||
if self.buff_size > 0xFFFFFFFF:
|
||||
logger.error("Filesize is too large to be a valid AXML file! Filesize: {}".format(self.buff_size))
|
||||
logger.error(
|
||||
"Filesize is too large to be a valid AXML file! Filesize: {}".format(self.buff_size))
|
||||
self._valid = False
|
||||
return
|
||||
|
||||
@@ -412,15 +421,18 @@ class AXMLParser:
|
||||
if axml_header.header_size == 28024:
|
||||
# Can be a common error: the file is not an AXML but a plain XML
|
||||
# The file will then usually start with '<?xm' / '3C 3F 78 6D'
|
||||
logger.warning("Header size is 28024! Are you trying to parse a plain XML file?")
|
||||
logger.warning(
|
||||
"Header size is 28024! Are you trying to parse a plain XML file?")
|
||||
|
||||
if axml_header.header_size != 8:
|
||||
logger.error("This does not look like an AXML file. header size does not equal 8! header size = {}".format(axml_header.header_size))
|
||||
logger.error("This does not look like an AXML file. header size does not equal 8! header size = {}".format(
|
||||
axml_header.header_size))
|
||||
self._valid = False
|
||||
return
|
||||
|
||||
if self.filesize > self.buff_size:
|
||||
logger.error("This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format(self.filesize, self.buff_size))
|
||||
logger.error("This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format(
|
||||
self.filesize, self.buff_size))
|
||||
self._valid = False
|
||||
return
|
||||
|
||||
@@ -428,7 +440,7 @@ class AXMLParser:
|
||||
# The file can still be parsed up to the point where the chunk should end.
|
||||
self.axml_tampered = True
|
||||
logger.warning("Declared filesize ({}) is smaller than total file size ({}). "
|
||||
"Was something appended to the file? Trying to parse it anyways.".format(self.filesize, self.buff.size()))
|
||||
"Was something appended to the file? Trying to parse it anyways.".format(self.filesize, self.buff.size()))
|
||||
|
||||
# Not that severe of an error, we have plenty files where this is not
|
||||
# set correctly
|
||||
@@ -448,7 +460,8 @@ class AXMLParser:
|
||||
return
|
||||
|
||||
if header.header_size != 0x1C:
|
||||
logger.error("This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format(header.header_size))
|
||||
logger.error("This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format(
|
||||
header.header_size))
|
||||
self._valid = False
|
||||
return
|
||||
|
||||
@@ -530,14 +543,15 @@ class AXMLParser:
|
||||
# h.size is the size of the whole chunk including the header.
|
||||
# We read already 8 bytes of the header, thus we need to
|
||||
# subtract them.
|
||||
logger.error("Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format(h.type, h.size))
|
||||
logger.error(
|
||||
"Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format(h.type, h.size))
|
||||
self.buff.seek(h.end)
|
||||
continue
|
||||
|
||||
# Check that we read a correct header
|
||||
if h.header_size != 0x10:
|
||||
logger.error("XML Resource Type Chunk header size does not match 16! " \
|
||||
"At chunk type 0x{:04x}, declared header size=0x{:04x}, chunk size=0x{:04x}".format(h.type, h.header_size, h.size))
|
||||
logger.error("XML Resource Type Chunk header size does not match 16! "
|
||||
"At chunk type 0x{:04x}, declared header size=0x{:04x}, chunk size=0x{:04x}".format(h.type, h.header_size, h.size))
|
||||
self.buff.seek(h.end)
|
||||
continue
|
||||
|
||||
@@ -548,7 +562,8 @@ class AXMLParser:
|
||||
self.m_comment_index, = unpack('<L', self.buff.read(4))
|
||||
|
||||
if self.m_comment_index != 0xFFFFFFFF and h.type in [RES_XML_START_NAMESPACE_TYPE, RES_XML_END_NAMESPACE_TYPE]:
|
||||
logger.warning("Unhandled Comment at namespace chunk: '{}'".format(self.sb[self.m_comment_index]))
|
||||
logger.warning("Unhandled Comment at namespace chunk: '{}'".format(
|
||||
self.sb[self.m_comment_index]))
|
||||
|
||||
if h.type == RES_XML_START_NAMESPACE_TYPE:
|
||||
prefix, = unpack('<L', self.buff.read(4))
|
||||
@@ -557,15 +572,16 @@ class AXMLParser:
|
||||
s_prefix = self.sb[prefix]
|
||||
s_uri = self.sb[uri]
|
||||
|
||||
logger.debug("Start of Namespace mapping: prefix {}: '{}' --> uri {}: '{}'".format(prefix, s_prefix, uri, s_uri))
|
||||
logger.debug(
|
||||
"Start of Namespace mapping: prefix {}: '{}' --> uri {}: '{}'".format(prefix, s_prefix, uri, s_uri))
|
||||
|
||||
if s_uri == '':
|
||||
logger.warning("Namespace prefix '{}' resolves to empty URI. "
|
||||
"This might be a packer.".format(s_prefix))
|
||||
"This might be a packer.".format(s_prefix))
|
||||
|
||||
if (prefix, uri) in self.namespaces:
|
||||
logger.debug("Namespace mapping ({}, {}) already seen! "
|
||||
"This is usually not a problem but could indicate packers or broken AXML compilers.".format(prefix, uri))
|
||||
"This is usually not a problem but could indicate packers or broken AXML compilers.".format(prefix, uri))
|
||||
self.namespaces.append((prefix, uri))
|
||||
|
||||
# We can continue with the next chunk, as we store the namespace
|
||||
@@ -582,7 +598,7 @@ class AXMLParser:
|
||||
self.namespaces.remove((prefix, uri))
|
||||
else:
|
||||
logger.warning("Reached a NAMESPACE_END without having the namespace stored before? "
|
||||
"Prefix ID: {}, URI ID: {}".format(prefix, uri))
|
||||
"Prefix ID: {}, URI ID: {}".format(prefix, uri))
|
||||
|
||||
# We can continue with the next chunk, as we store the namespace
|
||||
# mappings for each tag
|
||||
@@ -661,17 +677,18 @@ class AXMLParser:
|
||||
size, res0, dataType, data = unpack("<HBBL", self.buff.read(8))
|
||||
|
||||
logger.debug("found a CDATA Chunk: "
|
||||
"index={: 6d}, size={: 4d}, res0={: 4d}, dataType={: 4d}, data={: 4d}".format(self.m_name,
|
||||
size,
|
||||
res0,
|
||||
dataType,
|
||||
data))
|
||||
"index={: 6d}, size={: 4d}, res0={: 4d}, dataType={: 4d}, data={: 4d}".format(self.m_name,
|
||||
size,
|
||||
res0,
|
||||
dataType,
|
||||
data))
|
||||
|
||||
self.m_event = TEXT
|
||||
break
|
||||
|
||||
# Still here? Looks like we read an unknown XML header, try to skip it...
|
||||
logger.warning("Unknown XML Chunk: 0x{:04x}, skipping {} bytes.".format(h.type, h.size))
|
||||
logger.warning(
|
||||
"Unknown XML Chunk: 0x{:04x}, skipping {} bytes.".format(h.type, h.size))
|
||||
self.buff.seek(h.end)
|
||||
|
||||
@property
|
||||
@@ -829,7 +846,8 @@ class AXMLParser:
|
||||
if not res or res == ":":
|
||||
attr = self.m_resourceIDs[name]
|
||||
if attr in public.SYSTEM_RESOURCES['attributes']['inverse']:
|
||||
res = 'android:' + public.SYSTEM_RESOURCES['attributes']['inverse'][attr]
|
||||
res = 'android:' + \
|
||||
public.SYSTEM_RESOURCES['attributes']['inverse'][attr]
|
||||
else:
|
||||
# Attach the HEX Number, so for multiple missing attributes we do not run
|
||||
# into problems.
|
||||
@@ -891,10 +909,10 @@ def format_value(_type, _data, lookup_string=lambda ix: "<string>"):
|
||||
|
||||
# Function to prepend android prefix for attributes/references from the
|
||||
# android library
|
||||
fmt_package = lambda x: "android:" if x >> 24 == 1 else ""
|
||||
def fmt_package(x): return "android:" if x >> 24 == 1 else ""
|
||||
|
||||
# Function to represent integers
|
||||
fmt_int = lambda x: (0x7FFFFFFF & x) - 0x80000000 if x > 0x7FFFFFFF else x
|
||||
def fmt_int(x): return (0x7FFFFFFF & x) - 0x80000000 if x > 0x7FFFFFFF else x
|
||||
|
||||
if _type == TYPE_STRING:
|
||||
return lookup_string(_data)
|
||||
@@ -962,11 +980,13 @@ class AXMLPrinter:
|
||||
comment = self.axml.comment
|
||||
if comment:
|
||||
if self.root is None:
|
||||
logger.warning("Can not attach comment with content '{}' without root!".format(comment))
|
||||
logger.warning(
|
||||
"Can not attach comment with content '{}' without root!".format(comment))
|
||||
else:
|
||||
cur[-1].append(etree.Comment(comment))
|
||||
|
||||
logger.debug("START_TAG: {} (line={})".format(tag, self.axml.m_lineNumber))
|
||||
logger.debug("START_TAG: {} (line={})".format(
|
||||
tag, self.axml.m_lineNumber))
|
||||
elem = etree.Element(tag, nsmap=self.axml.nsmap)
|
||||
|
||||
for i in range(self.axml.getAttributeCount()):
|
||||
@@ -974,9 +994,11 @@ class AXMLPrinter:
|
||||
uri, name = self._fix_name(uri, self.axml.getAttributeName(i))
|
||||
value = self._fix_value(self._get_attribute_value(i))
|
||||
|
||||
logger.debug("found an attribute: {}{}='{}'".format(uri, name, value.encode("utf-8")))
|
||||
logger.debug("found an attribute: {}{}='{}'".format(
|
||||
uri, name, value.encode("utf-8")))
|
||||
if "{}{}".format(uri, name) in elem.attrib:
|
||||
logger.warning("Duplicate attribute '{}{}'! Will overwrite!".format(uri, name))
|
||||
logger.warning(
|
||||
"Duplicate attribute '{}{}'! Will overwrite!".format(uri, name))
|
||||
elem.set("{}{}".format(uri, name), value)
|
||||
|
||||
if self.root is None:
|
||||
@@ -984,20 +1006,23 @@ class AXMLPrinter:
|
||||
else:
|
||||
if not cur:
|
||||
# looks like we lost the root?
|
||||
logger.error("No more elements available to attach to! Is the XML malformed?")
|
||||
logger.error(
|
||||
"No more elements available to attach to! Is the XML malformed?")
|
||||
break
|
||||
cur[-1].append(elem)
|
||||
cur.append(elem)
|
||||
|
||||
if _type == END_TAG:
|
||||
if not cur:
|
||||
logger.warning("Too many END_TAG! No more elements available to attach to!")
|
||||
logger.warning(
|
||||
"Too many END_TAG! No more elements available to attach to!")
|
||||
|
||||
name = self.axml.name
|
||||
uri = self._print_namespace(self.axml.namespace)
|
||||
tag = "{}{}".format(uri, name)
|
||||
if cur[-1].tag != tag:
|
||||
logger.warning("Closing tag '{}' does not match current stack! At line number: {}. Is the XML malformed?".format(self.axml.name, self.axml.m_lineNumber))
|
||||
logger.warning("Closing tag '{}' does not match current stack! At line number: {}. Is the XML malformed?".format(
|
||||
self.axml.name, self.axml.m_lineNumber))
|
||||
cur.pop()
|
||||
if _type == TEXT:
|
||||
logger.debug("TEXT for {}".format(cur[-1]))
|
||||
@@ -1005,7 +1030,8 @@ class AXMLPrinter:
|
||||
if _type == END_DOCUMENT:
|
||||
# Check if all namespace mappings are closed
|
||||
if len(self.axml.namespaces) > 0:
|
||||
logger.warning("Not all namespace mappings were closed! Malformed AXML?")
|
||||
logger.warning(
|
||||
"Not all namespace mappings were closed! Malformed AXML?")
|
||||
break
|
||||
|
||||
def get_buff(self):
|
||||
@@ -1089,12 +1115,13 @@ class AXMLPrinter:
|
||||
"""
|
||||
if not name[0].isalpha() and name[0] != "_":
|
||||
logger.warning("Invalid start for name '{}'. "
|
||||
"XML name must start with a letter.".format(name))
|
||||
"XML name must start with a letter.".format(name))
|
||||
self.packerwarning = True
|
||||
name = "_{}".format(name)
|
||||
if name.startswith("android:") and prefix == '' and 'android' in self.axml.nsmap:
|
||||
# Seems be a common thing...
|
||||
logger.info("Name '{}' starts with 'android:' prefix but 'android' is a known prefix. Replacing prefix.".format(name))
|
||||
logger.info(
|
||||
"Name '{}' starts with 'android:' prefix but 'android' is a known prefix. Replacing prefix.".format(name))
|
||||
prefix = self._print_namespace(self.axml.nsmap['android'])
|
||||
name = name[len("android:"):]
|
||||
# It looks like this is some kind of packer... Not sure though.
|
||||
@@ -1103,13 +1130,14 @@ class AXMLPrinter:
|
||||
self.packerwarning = True
|
||||
embedded_prefix, new_name = name.split(":", 1)
|
||||
if embedded_prefix in self.axml.nsmap:
|
||||
logger.info("Prefix '{}' is in namespace mapping, assume that it is a prefix.")
|
||||
logger.info(
|
||||
"Prefix '{}' is in namespace mapping, assume that it is a prefix.")
|
||||
prefix = self._print_namespace(self.axml.nsmap[embedded_prefix])
|
||||
name = new_name
|
||||
else:
|
||||
# Print out an extra warning
|
||||
logger.warning("Confused: name contains a unknown namespace prefix: '{}'. "
|
||||
"This is either a broken AXML file or some attempt to break stuff.".format(name))
|
||||
"This is either a broken AXML file or some attempt to break stuff.".format(name))
|
||||
if not re.match(r"^[a-zA-Z0-9._-]*$", name):
|
||||
logger.warning("Name '{}' contains invalid characters!".format(name))
|
||||
self.packerwarning = True
|
||||
@@ -1129,16 +1157,18 @@ class AXMLPrinter:
|
||||
:return: the cleaned value
|
||||
"""
|
||||
if not self.__charrange or not self.__replacement:
|
||||
self.__charrange = re.compile('^[\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]*$')
|
||||
self.__replacement = re.compile('[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]')
|
||||
self.__charrange = re.compile(
|
||||
'^[\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]*$')
|
||||
self.__replacement = re.compile(
|
||||
'[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]')
|
||||
|
||||
# Reading string until \x00. This is the same as aapt does.
|
||||
if "\x00" in value:
|
||||
self.packerwarning = True
|
||||
logger.warning("Null byte found in attribute value at position {}: "
|
||||
"Value(hex): '{}'".format(
|
||||
value.find("\x00"),
|
||||
binascii.hexlify(value.encode("utf-8"))))
|
||||
"Value(hex): '{}'".format(
|
||||
value.find("\x00"),
|
||||
binascii.hexlify(value.encode("utf-8"))))
|
||||
value = value[:value.find("\x00")]
|
||||
|
||||
if not self.__charrange.match(value):
|
||||
@@ -1154,7 +1184,8 @@ class AXMLPrinter:
|
||||
|
||||
# See http://aospxref.com/android-13.0.0_r3/xref/frameworks/native/include/android/configuration.h#56
|
||||
|
||||
ACONFIGURATION_ORIENTATION_ANY = 0x0000
|
||||
|
||||
ACONFIGURATION_ORIENTATION_ANY = 0x0000
|
||||
ACONFIGURATION_ORIENTATION_PORT = 0x0001
|
||||
ACONFIGURATION_ORIENTATION_LAND = 0x0002
|
||||
ACONFIGURATION_ORIENTATION_SQUARE = 0x0003
|
||||
@@ -1172,11 +1203,11 @@ ACONFIGURATION_DENSITY_XXHIGH = 480
|
||||
ACONFIGURATION_DENSITY_XXXHIGH = 640
|
||||
ACONFIGURATION_DENSITY_ANY = 0xfffe
|
||||
ACONFIGURATION_DENSITY_NONE = 0xffff
|
||||
ACONFIGURATION_KEYBOARD_ANY = 0x0000
|
||||
ACONFIGURATION_KEYBOARD_ANY = 0x0000
|
||||
ACONFIGURATION_KEYBOARD_NOKEYS = 0x0001
|
||||
ACONFIGURATION_KEYBOARD_QWERTY = 0x0002
|
||||
ACONFIGURATION_KEYBOARD_12KEY = 0x0003
|
||||
ACONFIGURATION_NAVIGATION_ANY = 0x0000
|
||||
ACONFIGURATION_NAVIGATION_ANY = 0x0000
|
||||
ACONFIGURATION_NAVIGATION_NONAV = 0x0001
|
||||
ACONFIGURATION_NAVIGATION_DPAD = 0x0002
|
||||
ACONFIGURATION_NAVIGATION_TRACKBALL = 0x0003
|
||||
@@ -1357,6 +1388,7 @@ class ARSCParser:
|
||||
Each package is a chunk of type RES_TABLE_PACKAGE_TYPE.
|
||||
It contains again many more chunks.
|
||||
"""
|
||||
|
||||
def __init__(self, raw_buff):
|
||||
"""
|
||||
:param bytes raw_buff: the raw bytes of the file
|
||||
@@ -1365,7 +1397,8 @@ class ARSCParser:
|
||||
self.buff_size = self.buff.raw.getbuffer().nbytes
|
||||
|
||||
if self.buff_size < 8 or self.buff_size > 0xFFFFFFFF:
|
||||
raise ResParserError("Invalid file size {} for a resources.arsc file!".format(self.buff.size()))
|
||||
raise ResParserError(
|
||||
"Invalid file size {} for a resources.arsc file!".format(self.buff.size()))
|
||||
|
||||
self.analyzed = False
|
||||
self._resolved_strings = None
|
||||
@@ -1381,22 +1414,27 @@ class ARSCParser:
|
||||
|
||||
# More sanity checks...
|
||||
if self.header.header_size != 12:
|
||||
logger.warning("The ResTable_header has an unexpected header size! Expected 12 bytes, got {}.".format(self.header.header_size))
|
||||
logger.warning("The ResTable_header has an unexpected header size! Expected 12 bytes, got {}.".format(
|
||||
self.header.header_size))
|
||||
|
||||
if self.header.size > self.buff_size:
|
||||
raise ResParserError("The file seems to be truncated. Refuse to parse the file! Filesize: {}, declared size: {}".format(self.buff_size, self.header.size))
|
||||
raise ResParserError("The file seems to be truncated. Refuse to parse the file! Filesize: {}, declared size: {}".format(
|
||||
self.buff_size, self.header.size))
|
||||
|
||||
if self.header.size < self.buff_size:
|
||||
logger.warning("The Resource file seems to have data appended to it. Filesize: {}, declared size: {}".format(self.buff_size, self.header.size))
|
||||
logger.warning("The Resource file seems to have data appended to it. Filesize: {}, declared size: {}".format(
|
||||
self.buff_size, self.header.size))
|
||||
|
||||
# The ResTable_header contains the packageCount, i.e. the number of ResTable_package
|
||||
self.packageCount = unpack('<I', self.buff.read(4))[0]
|
||||
|
||||
# Even more sanity checks...
|
||||
if self.packageCount < 1:
|
||||
logger.warning("The number of packages is smaller than one. There should be at least one package!")
|
||||
logger.warning(
|
||||
"The number of packages is smaller than one. There should be at least one package!")
|
||||
|
||||
logger.debug("Parsed ResTable_header with {} package(s) inside.".format(self.packageCount))
|
||||
logger.debug(
|
||||
"Parsed ResTable_header with {} package(s) inside.".format(self.packageCount))
|
||||
|
||||
# skip to the start of the first chunk's data, skipping trailing header bytes (there should be none)
|
||||
self.buff.seek(self.header.start + self.header.header_size)
|
||||
@@ -1408,32 +1446,39 @@ class ARSCParser:
|
||||
|
||||
if res_header.end > self.header.end:
|
||||
# this inner chunk crosses the boundary of the table chunk
|
||||
logger.warning("Invalid chunk found! It is larger than the outer chunk: %s", res_header)
|
||||
logger.warning(
|
||||
"Invalid chunk found! It is larger than the outer chunk: %s", res_header)
|
||||
break
|
||||
|
||||
if res_header.type == RES_STRING_POOL_TYPE:
|
||||
# There should be only one StringPool per resource table.
|
||||
if self.stringpool_main:
|
||||
logger.warning("Already found a ResStringPool_header, but there should be only one! Will not parse the Pool again.")
|
||||
logger.warning(
|
||||
"Already found a ResStringPool_header, but there should be only one! Will not parse the Pool again.")
|
||||
else:
|
||||
self.stringpool_main = StringBlock(self.buff, res_header)
|
||||
logger.debug("Found the main string pool: %s", self.stringpool_main)
|
||||
|
||||
elif res_header.type == RES_TABLE_PACKAGE_TYPE:
|
||||
if len(self.packages) > self.packageCount:
|
||||
raise ResParserError("Got more packages ({}) than expected ({})".format(len(self.packages), self.packageCount))
|
||||
raise ResParserError("Got more packages ({}) than expected ({})".format(
|
||||
len(self.packages), self.packageCount))
|
||||
|
||||
current_package = ARSCResTablePackage(self.buff, res_header)
|
||||
package_name = current_package.get_name()
|
||||
|
||||
# After the Header, we have the resource type symbol table
|
||||
self.buff.seek(current_package.header.start + current_package.typeStrings)
|
||||
type_sp_header = ARSCHeader(self.buff, expected_type=RES_STRING_POOL_TYPE)
|
||||
self.buff.seek(current_package.header.start
|
||||
+ current_package.typeStrings)
|
||||
type_sp_header = ARSCHeader(
|
||||
self.buff, expected_type=RES_STRING_POOL_TYPE)
|
||||
mTableStrings = StringBlock(self.buff, type_sp_header)
|
||||
|
||||
# Next, we should have the resource key symbol table
|
||||
self.buff.seek(current_package.header.start + current_package.keyStrings)
|
||||
key_sp_header = ARSCHeader(self.buff, expected_type=RES_STRING_POOL_TYPE)
|
||||
self.buff.seek(current_package.header.start
|
||||
+ current_package.keyStrings)
|
||||
key_sp_header = ARSCHeader(
|
||||
self.buff, expected_type=RES_STRING_POOL_TYPE)
|
||||
mKeyStrings = StringBlock(self.buff, key_sp_header)
|
||||
|
||||
# Add them to the dict of read packages
|
||||
@@ -1441,7 +1486,8 @@ class ARSCParser:
|
||||
self.packages[package_name].append(mTableStrings)
|
||||
self.packages[package_name].append(mKeyStrings)
|
||||
|
||||
pc = PackageContext(current_package, self.stringpool_main, mTableStrings, mKeyStrings)
|
||||
pc = PackageContext(
|
||||
current_package, self.stringpool_main, mTableStrings, mKeyStrings)
|
||||
logger.debug("Constructed a PackageContext: %s", pc)
|
||||
|
||||
# skip to the first header in this table package chunk
|
||||
@@ -1450,14 +1496,18 @@ class ARSCParser:
|
||||
# this looks more like we want: (???)
|
||||
# FIXME it looks like that the two string pools we have read might not be concatenated to each other,
|
||||
# thus jumping to the sum of the sizes might not be correct...
|
||||
next_idx = res_header.start + res_header.header_size + type_sp_header.size + key_sp_header.size
|
||||
next_idx = res_header.start + res_header.header_size + \
|
||||
type_sp_header.size + key_sp_header.size
|
||||
|
||||
if next_idx != self.buff.tell():
|
||||
# If this happens, we have a testfile ;)
|
||||
logger.error("This looks like an odd resources.arsc file!")
|
||||
logger.error("Please report this error including the file you have parsed!")
|
||||
logger.error("next_idx = {}, current buffer position = {}".format(next_idx, self.buff.tell()))
|
||||
logger.error("Please open a issue at https://github.com/androguard/androguard/issues")
|
||||
logger.error(
|
||||
"Please report this error including the file you have parsed!")
|
||||
logger.error("next_idx = {}, current buffer position = {}".format(
|
||||
next_idx, self.buff.tell()))
|
||||
logger.error(
|
||||
"Please open a issue at https://github.com/androguard/androguard/issues")
|
||||
logger.error("Thank you!")
|
||||
|
||||
self.buff.seek(next_idx)
|
||||
@@ -1473,21 +1523,24 @@ class ARSCParser:
|
||||
self.packages[package_name].append(pkg_chunk_header)
|
||||
|
||||
if pkg_chunk_header.type == RES_TABLE_TYPE_SPEC_TYPE:
|
||||
self.packages[package_name].append(ARSCResTypeSpec(self.buff, pc))
|
||||
self.packages[package_name].append(
|
||||
ARSCResTypeSpec(self.buff, pc))
|
||||
|
||||
elif pkg_chunk_header.type == RES_TABLE_TYPE_TYPE:
|
||||
# Parse a RES_TABLE_TYPE
|
||||
# http://androidxref.com/9.0.0_r3/xref/frameworks/base/tools/aapt2/format/binary/BinaryResourceParser.cpp#311
|
||||
a_res_type = ARSCResType(self.buff, pc)
|
||||
self.packages[package_name].append(a_res_type)
|
||||
self.resource_configs[package_name][a_res_type].add(a_res_type.config)
|
||||
self.resource_configs[package_name][a_res_type].add(
|
||||
a_res_type.config)
|
||||
|
||||
logger.debug("Config: {}".format(a_res_type.config))
|
||||
|
||||
entries = []
|
||||
for i in range(0, a_res_type.entryCount):
|
||||
current_package.mResId = current_package.mResId & 0xffff0000 | i
|
||||
entries.append((unpack('<i', self.buff.read(4))[0], current_package.mResId))
|
||||
entries.append((unpack('<i', self.buff.read(4))[
|
||||
0], current_package.mResId))
|
||||
|
||||
self.packages[package_name].append(entries)
|
||||
|
||||
@@ -1508,7 +1561,8 @@ class ARSCParser:
|
||||
logger.warning("RES_TABLE_LIBRARY_TYPE chunk is not supported")
|
||||
else:
|
||||
# Unknown / not-handled chunk type
|
||||
logger.warning("Unknown chunk type encountered inside RES_TABLE_PACKAGE: %s", pkg_chunk_header)
|
||||
logger.warning(
|
||||
"Unknown chunk type encountered inside RES_TABLE_PACKAGE: %s", pkg_chunk_header)
|
||||
|
||||
# skip to the next chunk
|
||||
self.buff.seek(pkg_chunk_header.end)
|
||||
@@ -1537,7 +1591,8 @@ class ARSCParser:
|
||||
|
||||
locale = a_res_type.config.get_language_and_region()
|
||||
|
||||
c_value = self.values[package_name].setdefault(locale, {"public": []})
|
||||
c_value = self.values[package_name].setdefault(
|
||||
locale, {"public": []})
|
||||
|
||||
entries = self.packages[package_name][nb + 2]
|
||||
nb_i = 0
|
||||
@@ -1546,7 +1601,8 @@ class ARSCParser:
|
||||
ate = self.packages[package_name][nb + 3 + nb_i]
|
||||
|
||||
self.resource_values[ate.mResId][a_res_type.config] = ate
|
||||
self.resource_keys[package_name][a_res_type.get_type()][ate.get_value()] = ate.mResId
|
||||
self.resource_keys[package_name][a_res_type.get_type(
|
||||
)][ate.get_value()] = ate.mResId
|
||||
|
||||
if ate.get_index() != -1:
|
||||
c_value["public"].append(
|
||||
@@ -1738,7 +1794,8 @@ class ARSCParser:
|
||||
buff += '<resources>\n'
|
||||
try:
|
||||
for i in self.values[package_name][locale]["string"]:
|
||||
buff += '<string name="{}">{}</string>\n'.format(i[0], escape(i[1]))
|
||||
buff += '<string name="{}">{}</string>\n'.format(
|
||||
i[0], escape(i[1]))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -1772,7 +1829,7 @@ class ARSCParser:
|
||||
buff += '<item type="id" name="%s"/>\n' % (i[0])
|
||||
else:
|
||||
buff += '<item type="id" name="{}">{}</item>\n'.format(i[0],
|
||||
escape(i[1]))
|
||||
escape(i[1]))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -1905,6 +1962,7 @@ class ARSCParser:
|
||||
Resolves resources by ID and configuration.
|
||||
This resolver deals with complex resources as well as with references.
|
||||
"""
|
||||
|
||||
def __init__(self, android_resources, config=None):
|
||||
"""
|
||||
:param ARSCParser android_resources: A resource parser
|
||||
@@ -1965,10 +2023,12 @@ class ARSCParser:
|
||||
# Infinite loop detection:
|
||||
# TODO should this stay here or should be detect the loop much earlier?
|
||||
if res_id == parent.mResId:
|
||||
logger.warning("Infinite loop detected at resource item {}. It references itself!".format(parent))
|
||||
logger.warning(
|
||||
"Infinite loop detected at resource item {}. It references itself!".format(parent))
|
||||
return
|
||||
|
||||
self._resolve_into_result(result, item.get_data(), self.wanted_config)
|
||||
self._resolve_into_result(
|
||||
result, item.get_data(), self.wanted_config)
|
||||
else:
|
||||
if complex_:
|
||||
result.append(item.format_value())
|
||||
@@ -2054,7 +2114,8 @@ class ARSCParser:
|
||||
raise ValueError("'rid' must be an int")
|
||||
|
||||
if rid not in self.resource_values:
|
||||
logger.warning("The requested rid '0x{:08x}' could not be found in the list of resources.".format(rid))
|
||||
logger.warning(
|
||||
"The requested rid '0x{:08x}' could not be found in the list of resources.".format(rid))
|
||||
return []
|
||||
|
||||
res_options = self.resource_values[rid]
|
||||
@@ -2062,7 +2123,8 @@ class ARSCParser:
|
||||
if config in res_options:
|
||||
return [(config, res_options[config])]
|
||||
elif fallback and config == ARSCResTableConfig.default_config():
|
||||
logger.warning("No default resource config could be found for the given rid '0x{:08x}', using fallback!".format(rid))
|
||||
logger.warning(
|
||||
"No default resource config could be found for the given rid '0x{:08x}', using fallback!".format(rid))
|
||||
return [list(self.resource_values[rid].items())[0]]
|
||||
else:
|
||||
return []
|
||||
@@ -2096,7 +2158,7 @@ class ARSCParser:
|
||||
|
||||
for res_type, configs in list(self.resource_configs[package_name].items()):
|
||||
if res_type.get_package_name() == package_name and (
|
||||
type_name is None or res_type.get_type() == type_name):
|
||||
type_name is None or res_type.get_type() == type_name):
|
||||
result[res_type.get_type()].extend(configs)
|
||||
|
||||
return result
|
||||
@@ -2116,7 +2178,8 @@ class ARSCParser:
|
||||
"""
|
||||
|
||||
if not name.startswith('@'):
|
||||
raise ValueError("Not a valid resource ID, must start with @: '{}'".format(name))
|
||||
raise ValueError(
|
||||
"Not a valid resource ID, must start with @: '{}'".format(name))
|
||||
|
||||
# remove @
|
||||
name = name[1:]
|
||||
@@ -2128,7 +2191,8 @@ class ARSCParser:
|
||||
res_id = name
|
||||
|
||||
if len(res_id) != 8:
|
||||
raise ValueError("Numerical ID is not 8 characters long: '{}'".format(res_id))
|
||||
raise ValueError(
|
||||
"Numerical ID is not 8 characters long: '{}'".format(res_id))
|
||||
|
||||
try:
|
||||
return int(res_id, 16), package
|
||||
@@ -2229,13 +2293,15 @@ class ARSCHeader:
|
||||
self.start = buff.tell()
|
||||
# Make sure we do not read over the buffer:
|
||||
if buff.raw.getbuffer().nbytes < self.start + self.SIZE:
|
||||
raise ResParserError("Can not read over the buffer size! Offset={}".format(self.start))
|
||||
raise ResParserError(
|
||||
"Can not read over the buffer size! Offset={}".format(self.start))
|
||||
|
||||
# Checking for dummy data between elements
|
||||
if possible_types:
|
||||
while True:
|
||||
cur_pos = buff.tell()
|
||||
self._type, self._header_size, self._size = unpack('<HHL', buff.read(self.SIZE))
|
||||
self._type, self._header_size, self._size = unpack(
|
||||
'<HHL', buff.read(self.SIZE))
|
||||
|
||||
# cases where packers set the EndNamespace with zero size: check we are the end and add the prefix + uri
|
||||
if self._size < self.SIZE and (buff.raw.getbuffer().nbytes == cur_pos + self._header_size + 4 + 4):
|
||||
@@ -2248,10 +2314,12 @@ class ARSCHeader:
|
||||
buff.read(1)
|
||||
logger.warning("Appears that dummy data are found between elements!")
|
||||
else:
|
||||
self._type, self._header_size, self._size = unpack('<HHL', buff.read(self.SIZE))
|
||||
self._type, self._header_size, self._size = unpack(
|
||||
'<HHL', buff.read(self.SIZE))
|
||||
|
||||
if expected_type and self._type != expected_type:
|
||||
raise ResParserError("Header type is not equal the expected type: Got 0x{:04x}, wanted 0x{:04x}".format(self._type, expected_type))
|
||||
raise ResParserError("Header type is not equal the expected type: Got 0x{:04x}, wanted 0x{:04x}".format(
|
||||
self._type, expected_type))
|
||||
|
||||
# Assert that the read data will fit into the chunk.
|
||||
# The total size must be equal or larger than the header size
|
||||
@@ -2315,6 +2383,7 @@ class ARSCResTablePackage:
|
||||
|
||||
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#861
|
||||
"""
|
||||
|
||||
def __init__(self, buff, header):
|
||||
self.header = header
|
||||
self.start = buff.tell()
|
||||
@@ -2336,6 +2405,7 @@ class ARSCResTypeSpec:
|
||||
"""
|
||||
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1327
|
||||
"""
|
||||
|
||||
def __init__(self, buff, parent=None):
|
||||
self.start = buff.tell()
|
||||
self.parent = parent
|
||||
@@ -2356,6 +2426,7 @@ class ARSCResTypeSpec:
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
class ARSCResType:
|
||||
"""
|
||||
This is a `ResTable_type` without it's `ResChunk_header`.
|
||||
@@ -2363,6 +2434,7 @@ class ARSCResType:
|
||||
|
||||
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1364
|
||||
"""
|
||||
|
||||
def __init__(self, buff, parent=None):
|
||||
self.start = buff.tell()
|
||||
self.parent = parent
|
||||
@@ -2463,7 +2535,8 @@ class ARSCResTableConfig:
|
||||
# uint16_t smallestScreenWidthDp
|
||||
self.screenConfig, = unpack('<I', buff.read(4))
|
||||
else:
|
||||
logger.debug("This file does not have a screenConfig! size={}".format(self.size))
|
||||
logger.debug(
|
||||
"This file does not have a screenConfig! size={}".format(self.size))
|
||||
self.screenConfig = 0
|
||||
|
||||
if self.size >= 36:
|
||||
@@ -2472,7 +2545,8 @@ class ARSCResTableConfig:
|
||||
# uint16_t screenHeightDp
|
||||
self.screenSizeDp, = unpack('<I', buff.read(4))
|
||||
else:
|
||||
logger.debug("This file does not have a screenSizeDp! size={}".format(self.size))
|
||||
logger.debug(
|
||||
"This file does not have a screenSizeDp! size={}".format(self.size))
|
||||
self.screenSizeDp = 0
|
||||
|
||||
if self.size >= 40:
|
||||
@@ -2488,7 +2562,8 @@ class ARSCResTableConfig:
|
||||
# uint16_t screenConfigPad2
|
||||
self.screenConfig2, = unpack("<I", buff.read(4))
|
||||
else:
|
||||
logger.debug("This file does not have a screenConfig2! size={}".format(self.size))
|
||||
logger.debug(
|
||||
"This file does not have a screenConfig2! size={}".format(self.size))
|
||||
self.screenConfig2 = 0
|
||||
|
||||
self.exceedingSize = self.size - (buff.tell() - self.start)
|
||||
@@ -2562,8 +2637,10 @@ class ARSCResTableConfig:
|
||||
:return:
|
||||
"""
|
||||
if self.locale != 0:
|
||||
_language = self._unpack_language_or_region([self.locale & 0xff, (self.locale & 0xff00) >> 8, ], ord('a'))
|
||||
_region = self._unpack_language_or_region([(self.locale & 0xff0000) >> 16, (self.locale & 0xff000000) >> 24, ], ord('0'))
|
||||
_language = self._unpack_language_or_region(
|
||||
[self.locale & 0xff, (self.locale & 0xff00) >> 8, ], ord('a'))
|
||||
_region = self._unpack_language_or_region(
|
||||
[(self.locale & 0xff0000) >> 16, (self.locale & 0xff000000) >> 24, ], ord('0'))
|
||||
return (_language + "-r" + _region) if _region else _language
|
||||
return "\x00\x00"
|
||||
|
||||
@@ -2916,6 +2993,7 @@ class ARSCComplex:
|
||||
See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1485 for `ResTable_map_entry`
|
||||
and http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1498 for `ResTable_map`
|
||||
"""
|
||||
|
||||
def __init__(self, buff, parent=None):
|
||||
self.start = buff.tell()
|
||||
self.parent = parent
|
||||
@@ -2928,7 +3006,8 @@ class ARSCComplex:
|
||||
# these are structs of ResTable_ref and Res_value
|
||||
# ResTable_ref is a uint32_t.
|
||||
for i in range(0, self.count):
|
||||
self.items.append((unpack('<I', buff.read(4))[0], ARSCResStringPoolRef(buff, self.parent)))
|
||||
self.items.append((unpack('<I', buff.read(4))[
|
||||
0], ARSCResStringPoolRef(buff, self.parent)))
|
||||
|
||||
def __repr__(self):
|
||||
return "<ARSCComplex idx='0x{:08x}' parent='{}' count='{}'>".format(self.start, self.id_parent, self.count)
|
||||
@@ -2941,6 +3020,7 @@ class ARSCResStringPoolRef:
|
||||
|
||||
See: http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#262
|
||||
"""
|
||||
|
||||
def __init__(self, buff, parent=None):
|
||||
self.start = buff.tell()
|
||||
self.parent = parent
|
||||
@@ -3013,4 +3093,4 @@ def get_arsc_info(arscobj):
|
||||
buff += "\t\t\t" + i + "\n"
|
||||
except AttributeError:
|
||||
pass
|
||||
return buff
|
||||
return buff
|
||||
|
||||
@@ -20,7 +20,8 @@ if _public_res is None:
|
||||
_public_res[_type] = {}
|
||||
_public_res[_type][_name] = _id
|
||||
else:
|
||||
raise Exception("need to copy the sdk/platforms/android-?/data/res/values/public.xml here")
|
||||
raise Exception(
|
||||
"need to copy the sdk/platforms/android-?/data/res/values/public.xml here")
|
||||
|
||||
SYSTEM_RESOURCES = {
|
||||
"attributes": {
|
||||
|
||||
@@ -65,6 +65,7 @@ class EndOfCentralDirectoryRecord:
|
||||
"""
|
||||
A class to provide details about the end of central directory record.
|
||||
"""
|
||||
|
||||
def __init__(self, signature, number_of_this_disk, disk_where_central_directory_starts,
|
||||
number_of_central_directory_records_on_this_disk,
|
||||
total_number_of_central_directory_records, size_of_central_directory,
|
||||
@@ -104,24 +105,29 @@ class EndOfCentralDirectoryRecord:
|
||||
chunk = apk_file.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
signature_offset = chunk.rfind(b'\x50\x4b\x05\x06') # end of Central Directory File Header signature
|
||||
# end of Central Directory File Header signature
|
||||
signature_offset = chunk.rfind(b'\x50\x4b\x05\x06')
|
||||
if signature_offset != -1:
|
||||
eo_central_directory_offset = position + signature_offset
|
||||
break # Found End of central directory record (EOCD) signature
|
||||
offset += chunk_size
|
||||
if signature_offset == -1:
|
||||
raise ValueError("End of central directory record (EOCD) signature not found")
|
||||
raise ValueError(
|
||||
"End of central directory record (EOCD) signature not found")
|
||||
apk_file.seek(eo_central_directory_offset)
|
||||
|
||||
signature = apk_file.read(4)
|
||||
number_of_this_disk = struct.unpack('<H', apk_file.read(2))[0]
|
||||
disk_where_central_directory_starts = struct.unpack('<H', apk_file.read(2))[0]
|
||||
number_of_central_directory_records_on_this_disk = struct.unpack('<H', apk_file.read(2))[0]
|
||||
total_number_of_central_directory_records = struct.unpack('<H', apk_file.read(2))[0]
|
||||
number_of_central_directory_records_on_this_disk = struct.unpack('<H', apk_file.read(2))[
|
||||
0]
|
||||
total_number_of_central_directory_records = struct.unpack('<H', apk_file.read(2))[
|
||||
0]
|
||||
size_of_central_directory = struct.unpack('<I', apk_file.read(4))[0]
|
||||
offset_of_start_of_central_directory = struct.unpack('<I', apk_file.read(4))[0]
|
||||
comment_length = struct.unpack('<H', apk_file.read(2))[0]
|
||||
comment = struct.unpack(f'<{comment_length}s', apk_file.read(comment_length))[0].decode('utf-8', 'ignore')
|
||||
comment = struct.unpack(f'<{comment_length}s', apk_file.read(comment_length))[
|
||||
0].decode('utf-8', 'ignore')
|
||||
return cls(
|
||||
signature,
|
||||
number_of_this_disk,
|
||||
@@ -170,6 +176,7 @@ class CentralDirectoryEntry:
|
||||
"""
|
||||
A class representing each entry in the central directory.
|
||||
"""
|
||||
|
||||
def __init__(self, version_made_by, version_needed_to_extract, general_purpose_bit_flag,
|
||||
compression_method, file_last_modification_time, file_last_modification_date,
|
||||
crc32_of_uncompressed_data, compressed_size, uncompressed_size, file_name_length,
|
||||
@@ -245,6 +252,7 @@ class CentralDirectory:
|
||||
The CentralDirectory containing all the CentralDirectoryEntry entries discovered.
|
||||
The entries are listed as a dictionary where the filename is the key.
|
||||
"""
|
||||
|
||||
def __init__(self, entries):
|
||||
self.entries = entries
|
||||
|
||||
@@ -266,7 +274,8 @@ class CentralDirectory:
|
||||
eocd = EndOfCentralDirectoryRecord.parse(apk_file)
|
||||
apk_file.seek(eocd.offset_of_start_of_central_directory)
|
||||
if apk_file.tell() != eocd.offset_of_start_of_central_directory:
|
||||
raise ValueError(f"Failed to find the offset for the central directory within the file!")
|
||||
raise ValueError(
|
||||
f"Failed to find the offset for the central directory within the file!")
|
||||
|
||||
central_directory_entries = {}
|
||||
while True:
|
||||
@@ -289,8 +298,10 @@ class CentralDirectory:
|
||||
disk_number_where_file_starts = struct.unpack('<H', apk_file.read(2))[0]
|
||||
internal_file_attributes = struct.unpack('<H', apk_file.read(2))[0]
|
||||
external_file_attributes = struct.unpack('<I', apk_file.read(4))[0]
|
||||
relative_offset_of_local_file_header = struct.unpack('<I', apk_file.read(4))[0]
|
||||
filename = struct.unpack(f'<{file_name_length}s', apk_file.read(file_name_length))[0].decode('utf-8')
|
||||
relative_offset_of_local_file_header = struct.unpack('<I', apk_file.read(4))[
|
||||
0]
|
||||
filename = struct.unpack(f'<{file_name_length}s', apk_file.read(file_name_length))[
|
||||
0].decode('utf-8')
|
||||
extra_field = struct.unpack(f'<{extra_field_length}s', apk_file.read(extra_field_length))[0].decode('utf-8',
|
||||
'ignore')
|
||||
file_comment = struct.unpack(f'<{file_comment_length}s', apk_file.read(file_comment_length))[0].decode(
|
||||
@@ -339,6 +350,7 @@ class LocalHeaderRecord:
|
||||
"""
|
||||
The local header for each entry discovered.
|
||||
"""
|
||||
|
||||
def __init__(self, version_needed_to_extract, general_purpose_bit_flag,
|
||||
compression_method, file_last_modification_time, file_last_modification_date,
|
||||
crc32_of_uncompressed_data, compressed_size, uncompressed_size, file_name_length,
|
||||
@@ -386,7 +398,8 @@ class LocalHeaderRecord:
|
||||
uncompressed_size = struct.unpack('<I', apk_file.read(4))[0]
|
||||
file_name_length = struct.unpack('<H', apk_file.read(2))[0]
|
||||
extra_field_length = struct.unpack('<H', apk_file.read(2))[0]
|
||||
filename = struct.unpack(f'<{file_name_length}s', apk_file.read(file_name_length))[0].decode('utf-8')
|
||||
filename = struct.unpack(f'<{file_name_length}s', apk_file.read(file_name_length))[
|
||||
0].decode('utf-8')
|
||||
extra_field = struct.unpack(f'<{extra_field_length}s', apk_file.read(extra_field_length))[0].decode('utf-8',
|
||||
'ignore')
|
||||
return cls(
|
||||
@@ -434,6 +447,7 @@ class ZipEntry:
|
||||
"""
|
||||
Is the actual APK represented as a composition of the previous classes, which are: the EndOfCentralDirectoryRecord, the CentralDirectory and a dictionary of values of LocalHeaderRecord.
|
||||
"""
|
||||
|
||||
def __init__(self, zip_bytes, eocd: EndOfCentralDirectoryRecord, central_directory: CentralDirectory,
|
||||
local_headers: Dict[str, LocalHeaderRecord]):
|
||||
self.zip = zip_bytes
|
||||
@@ -462,7 +476,8 @@ class ZipEntry:
|
||||
central_directory = CentralDirectory.parse(apk_file, eocd)
|
||||
local_headers = {}
|
||||
for entry in central_directory.entries:
|
||||
local_header_entry = LocalHeaderRecord.parse(apk_file, central_directory.entries[entry])
|
||||
local_header_entry = LocalHeaderRecord.parse(
|
||||
apk_file, central_directory.entries[entry])
|
||||
local_headers[local_header_entry.filename] = local_header_entry
|
||||
return cls(apk_file, eocd, central_directory, local_headers)
|
||||
|
||||
@@ -486,7 +501,8 @@ class ZipEntry:
|
||||
if not eocd or not central_directory:
|
||||
eocd = EndOfCentralDirectoryRecord.parse(apk_file)
|
||||
central_directory = CentralDirectory.parse(apk_file, eocd)
|
||||
local_header = {filename: LocalHeaderRecord.parse(apk_file, central_directory.entries[filename])}
|
||||
local_header = {filename: LocalHeaderRecord.parse(
|
||||
apk_file, central_directory.entries[filename])}
|
||||
return cls(apk_file, eocd, central_directory, local_header)
|
||||
|
||||
def to_dict(self):
|
||||
@@ -514,7 +530,8 @@ class ZipEntry:
|
||||
if filename in self.central_directory.entries:
|
||||
return self.central_directory.entries[filename].to_dict()
|
||||
else:
|
||||
raise KeyError(f"Key: {filename} was not found within the central directory entries!")
|
||||
raise KeyError(
|
||||
f"Key: {filename} was not found within the central directory entries!")
|
||||
|
||||
def get_local_header_dict(self, filename):
|
||||
"""
|
||||
@@ -528,7 +545,8 @@ class ZipEntry:
|
||||
if filename in self.local_headers:
|
||||
return self.local_headers[filename].to_dict()
|
||||
else:
|
||||
raise KeyError(f"Key: {filename} was not found within the local headers list!")
|
||||
raise KeyError(
|
||||
f"Key: {filename} was not found within the local headers list!")
|
||||
|
||||
def read(self, name, save: bool = False):
|
||||
"""
|
||||
|
||||
@@ -14,11 +14,13 @@ from mobsf.MobSF.utils import (
|
||||
print_n_send_error_response,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
firebase_analysis,
|
||||
get_avg_cvss,
|
||||
hash_gen,
|
||||
unzip,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.firebase import (
|
||||
firebase_analysis,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.appsec import (
|
||||
get_android_dashboard,
|
||||
)
|
||||
@@ -215,7 +217,7 @@ def common_analysis(request, app_dic, rescan, api, analysis_type):
|
||||
# Firebase DB Check
|
||||
code_an_dic['firebase'] = firebase_analysis(
|
||||
checksum,
|
||||
code_an_dic['urls_list'])
|
||||
code_an_dic)
|
||||
# Domain Extraction and Malware Check
|
||||
code_an_dic['domains'] = MalwareDomainCheck().scan(
|
||||
checksum,
|
||||
|
||||
@@ -79,6 +79,7 @@ def assetlinks_check(act_name, well_knowns):
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def _check_url(host, w_url):
|
||||
try:
|
||||
iden = 'sha256_cert_fingerprints'
|
||||
@@ -87,10 +88,10 @@ def _check_url(host, w_url):
|
||||
status_code = 0
|
||||
|
||||
r = requests.get(w_url,
|
||||
allow_redirects=False,
|
||||
proxies=proxies,
|
||||
verify=verify,
|
||||
timeout=5)
|
||||
allow_redirects=False,
|
||||
proxies=proxies,
|
||||
verify=verify,
|
||||
timeout=5)
|
||||
|
||||
status_code = r.status_code
|
||||
if status_code == 302:
|
||||
@@ -106,7 +107,7 @@ def _check_url(host, w_url):
|
||||
|
||||
except Exception:
|
||||
logger.error(f'Well Known Assetlinks Check for URL: {w_url}')
|
||||
return {'url': w_url,
|
||||
return {'url': w_url,
|
||||
'host': host,
|
||||
'status_code': None,
|
||||
'status': False}
|
||||
@@ -153,9 +154,9 @@ def get_browsable_activities(node, ns):
|
||||
path_patterns.append(path_pattern)
|
||||
# Collect possible well-known paths
|
||||
if (scheme
|
||||
and scheme in ('http', 'https')
|
||||
and host
|
||||
and host != '*'):
|
||||
and scheme in ('http', 'https')
|
||||
and host
|
||||
and host != '*'):
|
||||
host = host.replace('*.', '').replace('#', '')
|
||||
if not valid_host(host):
|
||||
continue
|
||||
|
||||
@@ -12,10 +12,12 @@ from mobsf.MobSF.utils import (
|
||||
print_n_send_error_response,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
firebase_analysis,
|
||||
get_symbols,
|
||||
hash_gen,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.firebase import (
|
||||
firebase_analysis,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.binary.lib_analysis import (
|
||||
library_analysis,
|
||||
)
|
||||
@@ -134,7 +136,7 @@ def so_analysis(request, app_dic, rescan, api):
|
||||
# Firebase DB Check
|
||||
code_an_dic['firebase'] = firebase_analysis(
|
||||
checksum,
|
||||
code_an_dic['urls_list'])
|
||||
code_an_dic)
|
||||
# Domain Extraction and Malware Check
|
||||
code_an_dic['domains'] = MalwareDomainCheck().scan(
|
||||
checksum,
|
||||
|
||||
@@ -84,11 +84,13 @@ from mobsf.StaticAnalyzer.views.android.so import (
|
||||
so_analysis,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
firebase_analysis,
|
||||
get_avg_cvss,
|
||||
hash_gen,
|
||||
unzip,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.firebase import (
|
||||
firebase_analysis,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.appsec import (
|
||||
get_android_dashboard,
|
||||
)
|
||||
@@ -313,7 +315,7 @@ def static_analyzer(request, checksum, api=False):
|
||||
# Firebase DB Check
|
||||
code_an_dic['firebase'] = firebase_analysis(
|
||||
checksum,
|
||||
code_an_dic['urls_list'])
|
||||
code_an_dic)
|
||||
# Domain Extraction and Malware Check
|
||||
code_an_dic['domains'] = MalwareDomainCheck().scan(
|
||||
checksum,
|
||||
@@ -492,7 +494,7 @@ def static_analyzer(request, checksum, api=False):
|
||||
# Firebase DB Check
|
||||
code_an_dic['firebase'] = firebase_analysis(
|
||||
checksum,
|
||||
code_an_dic['urls_list'])
|
||||
code_an_dic)
|
||||
# Domain Extraction and Malware Check
|
||||
code_an_dic['domains'] = MalwareDomainCheck().scan(
|
||||
checksum,
|
||||
|
||||
@@ -12,6 +12,8 @@ from mobsf.StaticAnalyzer.views.common.entropy import (
|
||||
get_entropies,
|
||||
)
|
||||
from mobsf.MobSF.utils import (
|
||||
GOOGLE_API_KEY_REGEX,
|
||||
GOOGLE_APP_ID_REGEX,
|
||||
append_scan_status,
|
||||
get_android_src_dir,
|
||||
)
|
||||
@@ -55,6 +57,7 @@ def strings_from_apk(checksum, apk):
|
||||
urls = []
|
||||
urls_nf = []
|
||||
emails_nf = []
|
||||
firebase_creds = {}
|
||||
try:
|
||||
msg = 'Extracting String data from APK'
|
||||
logger.info(msg)
|
||||
@@ -68,6 +71,12 @@ def strings_from_apk(checksum, apk):
|
||||
if not res_string:
|
||||
continue
|
||||
for duo in res_string:
|
||||
if (duo[0] == 'google_api_key'
|
||||
and GOOGLE_API_KEY_REGEX.match(duo[1])):
|
||||
firebase_creds['google_api_key'] = duo[1]
|
||||
if (duo[0] == 'google_app_id'
|
||||
and GOOGLE_APP_ID_REGEX.match(duo[1])):
|
||||
firebase_creds['google_app_id'] = duo[1]
|
||||
cap_str = '"' + duo[0] + '" : "' + duo[1] + '"'
|
||||
# Extract possible secret holding keys
|
||||
if is_secret_key(duo[0] + '"') and ' ' not in duo[1]:
|
||||
@@ -86,6 +95,7 @@ def strings_from_apk(checksum, apk):
|
||||
'urls_nf': urls_nf,
|
||||
'emails_nf': emails_nf,
|
||||
'secrets': secrets,
|
||||
'firebase_creds': firebase_creds,
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +133,7 @@ def get_strings_metadata(
|
||||
if apk:
|
||||
# APK
|
||||
apk_res = strings_from_apk(checksum, apk)
|
||||
code_dic['firebase_creds'] = apk_res['firebase_creds']
|
||||
strings['strings_apk_res'] = apk_res['strings']
|
||||
urls_list.extend(apk_res['urls_list'])
|
||||
urls_n_files.extend(apk_res['urls_nf'])
|
||||
|
||||
@@ -31,10 +31,12 @@ from mobsf.StaticAnalyzer.views.ios.strings import (
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
ar_extract,
|
||||
firebase_analysis,
|
||||
get_symbols,
|
||||
hash_gen,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.firebase import (
|
||||
firebase_analysis,
|
||||
)
|
||||
from mobsf.MalwareAnalyzer.views.MalwareDomainCheck import (
|
||||
MalwareDomainCheck,
|
||||
)
|
||||
@@ -166,7 +168,7 @@ def a_analysis(request, app_dict, rescan, api):
|
||||
code_dict['code_anal'] = {}
|
||||
code_dict['firebase'] = firebase_analysis(
|
||||
checksum,
|
||||
code_dict['urls_list'])
|
||||
code_dict)
|
||||
code_dict['trackers'] = trackers
|
||||
context = save_get_ctx(
|
||||
app_dict,
|
||||
|
||||
@@ -123,15 +123,11 @@ def common_fields(findings, data):
|
||||
})
|
||||
# Firebase
|
||||
for fb in data['firebase_urls']:
|
||||
if fb['open']:
|
||||
fdb = fb['url']
|
||||
findings['high'].append({
|
||||
'title': 'Firebase DB is exposed publicly.',
|
||||
'description': (
|
||||
f'The Firebase database at {fdb} is exposed'
|
||||
' to internet without any authentication'),
|
||||
'section': 'firebase',
|
||||
})
|
||||
findings[fb['severity']].append({
|
||||
'title': fb['title'],
|
||||
'description': fb['description'],
|
||||
'section': 'firebase',
|
||||
})
|
||||
# Trackers
|
||||
if 'trackers' in data['trackers']:
|
||||
findings['total_trackers'] = data['trackers']['total_trackers']
|
||||
|
||||
@@ -8,6 +8,11 @@ import lief
|
||||
from mobsf.StaticAnalyzer.views.common.binary.strings import (
|
||||
strings_on_binary,
|
||||
)
|
||||
from mobsf.MobSF.utils import (
|
||||
run_with_timeout,
|
||||
)
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
NA = 'Not Applicable'
|
||||
@@ -32,7 +37,10 @@ class ELFChecksec:
|
||||
def __init__(self, elf_file, so_rel):
|
||||
self.elf_path = elf_file.as_posix()
|
||||
self.elf_rel = so_rel
|
||||
self.elf = lief.parse(self.elf_path)
|
||||
self.elf = run_with_timeout(
|
||||
lief.parse,
|
||||
settings.BINARY_ANALYSIS_TIMEOUT,
|
||||
self.elf_path)
|
||||
|
||||
def checksec(self):
|
||||
elf_dict = {}
|
||||
|
||||
@@ -27,6 +27,9 @@ def library_analysis(checksum, src, arch):
|
||||
f'{arch}_analysis': [],
|
||||
f'{arch}_strings': [],
|
||||
f'{arch}_symbols': [],
|
||||
'framework_analysis': [],
|
||||
'framework_strings': [],
|
||||
'framework_symbols': [],
|
||||
}
|
||||
try:
|
||||
if arch == 'macho':
|
||||
@@ -79,9 +82,6 @@ def library_analysis(checksum, src, arch):
|
||||
rel_path: symbols})
|
||||
if ext == '*.dylib':
|
||||
# Do Framework Analysis for iOS
|
||||
res['framework_analysis'] = []
|
||||
res['framework_strings'] = []
|
||||
res['framework_symbols'] = []
|
||||
frameworks_analysis(checksum, src, base_dir, res)
|
||||
if res['framework_strings']:
|
||||
res[f'{arch}_strings'].extend(
|
||||
|
||||
@@ -9,6 +9,11 @@ import lief
|
||||
from mobsf.StaticAnalyzer.views.common.binary.strings import (
|
||||
strings_on_binary,
|
||||
)
|
||||
from mobsf.MobSF.utils import (
|
||||
run_with_timeout,
|
||||
)
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def objdump_is_debug_symbol_stripped(macho_file):
|
||||
@@ -28,7 +33,10 @@ class MachOChecksec:
|
||||
self.macho_name = rel_path
|
||||
else:
|
||||
self.macho_name = macho.name
|
||||
self.macho = lief.parse(self.macho_path)
|
||||
self.macho = run_with_timeout(
|
||||
lief.parse,
|
||||
settings.BINARY_ANALYSIS_TIMEOUT,
|
||||
self.macho_path)
|
||||
|
||||
def checksec(self):
|
||||
macho_dict = {}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Firebase Analysis."""
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from mobsf.MobSF.utils import (
|
||||
append_scan_status,
|
||||
upstream_proxy,
|
||||
valid_host,
|
||||
)
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
HIGH = 'high'
|
||||
WARNING = 'warning'
|
||||
INFO = 'info'
|
||||
SECURE = 'secure'
|
||||
FIREBASE_FINDINGS = {
|
||||
'firebase_db_open': {
|
||||
'title': 'Open Firebase database',
|
||||
'severity': HIGH,
|
||||
'description': (
|
||||
'The Firebase database at %s is exposed'
|
||||
' to internet without any authentication'),
|
||||
},
|
||||
'firebase_db_exists': {
|
||||
'title': 'App talks to a Firebase database',
|
||||
'severity': INFO,
|
||||
'description': 'The app talks to Firebase database at %s',
|
||||
},
|
||||
'firebase_remote_config_enabled': {
|
||||
'title': 'Firebase Remote Config enabled',
|
||||
'severity': WARNING,
|
||||
'description': (
|
||||
'The Firebase Remote Config at %s is enabled.'
|
||||
' Ensure that the configurations are not sensitive.'
|
||||
' This is indicated by the response:\n\n%s'),
|
||||
},
|
||||
'firebase_remote_config_disabled': {
|
||||
'title': 'Firebase Remote Config disabled',
|
||||
'severity': SECURE,
|
||||
'description': (
|
||||
'Firebase Remote Config is disabled for %s.',
|
||||
' This is indicated by the response:\n\n%s'),
|
||||
},
|
||||
'firebase_remote_config_failed': {
|
||||
'title': 'Firebase Remote Config check failed',
|
||||
'description': (
|
||||
'Failed to check for Firebase Remote Config.'
|
||||
' Please verify this manually. Error:\n\n%s'),
|
||||
'severity': INFO,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def firebase_analysis(checksum, code_an_dic):
|
||||
"""Firebase Analysis."""
|
||||
urls = list(set(code_an_dic['urls_list']))
|
||||
findings = []
|
||||
finds = None
|
||||
logger.info('Starting Firebase Analysis')
|
||||
# Check for Firebase Database
|
||||
logger.info('Looking for Firebase URL(s)')
|
||||
for url in urls:
|
||||
if 'firebaseio.com' not in url:
|
||||
continue
|
||||
returl, is_open = open_firebase(checksum, url)
|
||||
if is_open:
|
||||
item = FIREBASE_FINDINGS['firebase_db_open']
|
||||
item['description'] = item['description'] % returl
|
||||
findings.append(item)
|
||||
else:
|
||||
item = FIREBASE_FINDINGS['firebase_db_exists']
|
||||
item['description'] = item['description'] % returl
|
||||
findings.append(item)
|
||||
# Check for Firebase Remote Config
|
||||
firebase_creds = code_an_dic.get('firebase_creds')
|
||||
finds = firebase_remote_config(checksum, firebase_creds)
|
||||
if finds:
|
||||
findings.extend(finds)
|
||||
return findings
|
||||
|
||||
|
||||
def open_firebase(checksum, url):
|
||||
# Detect Open Firebase Database
|
||||
try:
|
||||
invalid = 'Invalid Firebase URL'
|
||||
if not valid_host(url):
|
||||
logger.warning(invalid)
|
||||
return url, False
|
||||
purl = urlparse(url)
|
||||
if not purl.netloc.endswith('firebaseio.com'):
|
||||
logger.warning(invalid)
|
||||
return url, False
|
||||
base_url = '{}://{}/.json'.format(purl.scheme, purl.netloc)
|
||||
proxies, verify = upstream_proxy('https')
|
||||
headers = {
|
||||
'User-Agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)'
|
||||
' AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/39.0.2171.95 Safari/537.36')}
|
||||
resp = requests.get(base_url, headers=headers,
|
||||
proxies=proxies, verify=verify,
|
||||
allow_redirects=False)
|
||||
if resp.status_code == 200:
|
||||
return base_url, True
|
||||
except Exception as exp:
|
||||
msg = 'Open Firebase DB detection failed'
|
||||
logger.warning(msg)
|
||||
append_scan_status(checksum, msg, repr(exp))
|
||||
return url, False
|
||||
|
||||
|
||||
def firebase_remote_config(checksum, creds):
|
||||
"""Check for Firebase Remote Config."""
|
||||
url = None
|
||||
findings = []
|
||||
try:
|
||||
if not creds:
|
||||
return None
|
||||
google_api_key = creds.get('google_api_key')
|
||||
google_app_id = creds.get('google_app_id')
|
||||
|
||||
if not (google_app_id and google_api_key):
|
||||
return None
|
||||
|
||||
project_id = google_app_id.split(':')[1]
|
||||
if not project_id.isnumeric():
|
||||
return None
|
||||
|
||||
# https://docs.emergetools.com/docs/firebase-remote-config-api-exposed
|
||||
# https://blog.deesee.xyz/android/automation/2019/08/03/
|
||||
# firebase-remote-config-dump.html
|
||||
logger.info('Checking for Firebase Remote Config')
|
||||
url = (f'https://firebaseremoteconfig.googleapis.com/v1/projects/'
|
||||
f'{project_id}/namespaces/firebase:fetch?key={google_api_key}')
|
||||
|
||||
body = {
|
||||
'appId': google_app_id,
|
||||
'appInstanceId': 'required_but_unused_value',
|
||||
}
|
||||
|
||||
proxies, verify = upstream_proxy('https')
|
||||
response = requests.post(
|
||||
url,
|
||||
timeout=4,
|
||||
json=body,
|
||||
proxies=proxies,
|
||||
verify=verify,
|
||||
allow_redirects=False)
|
||||
|
||||
if response.status_code == 200:
|
||||
resp = response.json()
|
||||
if resp.get('state') == 'NO_TEMPLATE':
|
||||
item = FIREBASE_FINDINGS['firebase_remote_config_disabled']
|
||||
item['description'] = item['description'] % (url, resp)
|
||||
findings.append(item)
|
||||
else:
|
||||
item = FIREBASE_FINDINGS['firebase_remote_config_enabled']
|
||||
item['description'] = item['description'] % (url, resp)
|
||||
findings.append(item)
|
||||
else:
|
||||
item = FIREBASE_FINDINGS['firebase_remote_config_disabled']
|
||||
response_msg = f'The response code is {response.status_code}'
|
||||
item['description'] = item['description'] % (url, response_msg)
|
||||
findings.append(item)
|
||||
except Exception as exp:
|
||||
msg = 'Failed to check for Firebase Remote Config'
|
||||
logger.warning(msg)
|
||||
append_scan_status(checksum, msg, repr(exp))
|
||||
item = FIREBASE_FINDINGS['firebase_remote_config_failed']
|
||||
item['description'] = item['description'] % repr(exp)
|
||||
findings.append(item)
|
||||
return findings
|
||||
@@ -13,11 +13,8 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
from urllib.parse import urlparse
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
import arpy
|
||||
|
||||
from django.utils.html import escape
|
||||
@@ -33,8 +30,6 @@ from mobsf.MobSF.utils import (
|
||||
is_path_traversal,
|
||||
is_safe_path,
|
||||
print_n_send_error_response,
|
||||
upstream_proxy,
|
||||
valid_host,
|
||||
)
|
||||
from mobsf.MobSF.views.scanning import (
|
||||
add_to_recent_scan,
|
||||
@@ -291,49 +286,6 @@ def get_avg_cvss(findings):
|
||||
return avg_cvss
|
||||
|
||||
|
||||
def open_firebase(checksum, url):
|
||||
# Detect Open Firebase Database
|
||||
try:
|
||||
invalid = 'Invalid Firebase URL'
|
||||
if not valid_host(url):
|
||||
logger.warning(invalid)
|
||||
return url, False
|
||||
purl = urlparse(url)
|
||||
if not purl.netloc.endswith('firebaseio.com'):
|
||||
logger.warning(invalid)
|
||||
return url, False
|
||||
base_url = '{}://{}/.json'.format(purl.scheme, purl.netloc)
|
||||
proxies, verify = upstream_proxy('https')
|
||||
headers = {
|
||||
'User-Agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)'
|
||||
' AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/39.0.2171.95 Safari/537.36')}
|
||||
resp = requests.get(base_url, headers=headers,
|
||||
proxies=proxies, verify=verify,
|
||||
allow_redirects=False)
|
||||
if resp.status_code == 200:
|
||||
return base_url, True
|
||||
except Exception as exp:
|
||||
msg = 'Open Firebase DB detection failed'
|
||||
logger.warning(msg)
|
||||
append_scan_status(checksum, msg, repr(exp))
|
||||
return url, False
|
||||
|
||||
|
||||
def firebase_analysis(checksum, urls):
|
||||
# Detect Firebase URL
|
||||
firebase_db = []
|
||||
logger.info('Detecting Firebase URL(s)')
|
||||
for url in urls:
|
||||
if 'firebaseio.com' not in url:
|
||||
continue
|
||||
returl, is_open = open_firebase(checksum, url)
|
||||
fbdic = {'url': returl, 'open': is_open}
|
||||
if fbdic not in firebase_db:
|
||||
firebase_db.append(fbdic)
|
||||
return firebase_db
|
||||
|
||||
|
||||
def find_java_source_folder(base_folder: Path):
|
||||
# Find the correct java/kotlin source folder for APK/source zip
|
||||
# Returns a Tuple of - (SRC_PATH, SRC_TYPE, SRC_SYNTAX)
|
||||
|
||||
@@ -56,20 +56,23 @@ def get_bin_info(bin_file):
|
||||
|
||||
|
||||
def ipa_macho_analysis(binary):
|
||||
data = {
|
||||
'checksec': {},
|
||||
'symbols': [],
|
||||
'libraries': [],
|
||||
}
|
||||
try:
|
||||
logger.info('Running MachO Analysis on: %s', binary.name)
|
||||
cs = MachOChecksec(binary)
|
||||
chksec = cs.checksec()
|
||||
symbols = cs.get_symbols()
|
||||
libs = cs.get_libraries()
|
||||
return {
|
||||
'checksec': chksec,
|
||||
'symbols': symbols,
|
||||
'libraries': libs,
|
||||
}
|
||||
data['checksec'] = chksec
|
||||
data['symbols'] = symbols
|
||||
data['libraries'] = libs
|
||||
except Exception:
|
||||
logger.exception('Running MachO Analysis')
|
||||
return {}
|
||||
return data
|
||||
|
||||
|
||||
def binary_analysis(checksum, src, tools_dir, app_dir, executable_name):
|
||||
|
||||
@@ -32,10 +32,12 @@ from mobsf.StaticAnalyzer.views.ios.strings import (
|
||||
get_strings_metadata,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
firebase_analysis,
|
||||
get_symbols,
|
||||
hash_gen,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.firebase import (
|
||||
firebase_analysis,
|
||||
)
|
||||
from mobsf.MalwareAnalyzer.views.MalwareDomainCheck import (
|
||||
MalwareDomainCheck,
|
||||
)
|
||||
@@ -148,7 +150,7 @@ def dylib_analysis(request, app_dict, rescan, api):
|
||||
code_dict['code_anal'] = {}
|
||||
code_dict['firebase'] = firebase_analysis(
|
||||
checksum,
|
||||
code_dict['urls_list'])
|
||||
code_dict)
|
||||
code_dict['trackers'] = trackers
|
||||
context = save_get_ctx(
|
||||
app_dict,
|
||||
|
||||
@@ -50,12 +50,14 @@ from mobsf.StaticAnalyzer.views.common.a import (
|
||||
a_analysis,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.shared_func import (
|
||||
firebase_analysis,
|
||||
get_avg_cvss,
|
||||
hash_gen,
|
||||
strings_and_entropies,
|
||||
unzip,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.firebase import (
|
||||
firebase_analysis,
|
||||
)
|
||||
from mobsf.StaticAnalyzer.views.common.appsec import (
|
||||
get_ios_dashboard,
|
||||
)
|
||||
@@ -225,7 +227,7 @@ def static_analyzer_ios(request, checksum, api=False):
|
||||
code_dict['code_anal'] = {}
|
||||
code_dict['firebase'] = firebase_analysis(
|
||||
checksum,
|
||||
code_dict['urls_list'])
|
||||
code_dict)
|
||||
code_dict['trackers'] = trackers
|
||||
context = save_get_ctx(
|
||||
app_dict,
|
||||
@@ -303,7 +305,7 @@ def static_analyzer_ios(request, checksum, api=False):
|
||||
# Firebase DB Check
|
||||
code_analysis_dic['firebase'] = firebase_analysis(
|
||||
checksum,
|
||||
list(set(code_analysis_dic['urls_list'])))
|
||||
code_analysis_dic)
|
||||
# Extract Trackers from Domains
|
||||
trk = Trackers.Trackers(
|
||||
checksum,
|
||||
|
||||
@@ -4,8 +4,11 @@ import logging
|
||||
|
||||
from libsast import Scanner
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from mobsf.MobSF.utils import (
|
||||
append_scan_status,
|
||||
run_with_timeout,
|
||||
settings_enabled,
|
||||
)
|
||||
|
||||
@@ -21,8 +24,8 @@ def scan(checksum, rule, extensions, paths, ignore_paths=None):
|
||||
'ignore_paths': ignore_paths,
|
||||
'show_progress': False}
|
||||
scanner = Scanner(options, paths)
|
||||
res = scanner.scan()
|
||||
if res:
|
||||
res = run_with_timeout(scanner.scan, settings.SAST_TIMEOUT)
|
||||
if res and res.get('pattern_matcher'):
|
||||
return format_findings(res['pattern_matcher'], paths[0])
|
||||
except Exception as exp:
|
||||
msg = 'libsast scan failed'
|
||||
@@ -48,8 +51,8 @@ def niap_scan(checksum, rule, extensions, paths, apath, ignore_paths=None):
|
||||
'ignore_paths': ignore_paths,
|
||||
'show_progress': False}
|
||||
scanner = Scanner(options, paths)
|
||||
res = scanner.scan()
|
||||
if res:
|
||||
res = run_with_timeout(scanner.scan, settings.SAST_TIMEOUT)
|
||||
if res and res.get('choice_matcher'):
|
||||
return res['choice_matcher']
|
||||
except Exception as exp:
|
||||
msg = 'NIAP Analyzer Failed'
|
||||
|
||||
+442451
-342836
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,7 @@
|
||||
<!-- SEARCH FORM -->
|
||||
<form class="form-inline ml-3" action="/search" method="GET">
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control form-control-navbar" id="navbar-search-input" type="search" placeholder="Search MD5" name="md5" aria-label="Search">
|
||||
<input class="form-control form-control-navbar" id="navbar-search-input" type="search" placeholder="Search" name="query" aria-label="Search">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-navbar" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<!-- SEARCH FORM -->
|
||||
<form class="form-inline ml-3" action="/search" method="GET">
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control form-control-navbar" id="navbar-search-input" type="search" placeholder="Search MD5" name="md5" aria-label="Search">
|
||||
<input class="form-control form-control-navbar" id="navbar-search-input" type="search" placeholder="Search" name="query" aria-label="Search">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-navbar" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<li><code>api/v1/upload</code> - <a href="#upload-file-api">Upload a File</a></li>
|
||||
<li><code>api/v1/scan</code> - <a href="#scan-file-api">Scan a File</a></li>
|
||||
<li><code>api/v1/scan_logs</code> - <a href="#scan-logs-api">Display Live Scan Logs</a></li>
|
||||
<li><code>api/v1/search</code> - <a href="#search-api">Search a Scan</a></li>
|
||||
<li><code>api/v1/scans</code> - <a href="#recent-scans-api">Display Recent Scans</a></li>
|
||||
<li><code>api/v1/delete_scan</code> - <a href="#delete-scan-api">Delete a Scan</a></li>
|
||||
<li><code>api/v1/scorecard</code> - <a href="#scorecard-api">App Scorecard</a></li>
|
||||
@@ -358,6 +359,87 @@
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2><a id="search-api"></a><strong>Search API</strong></h2>
|
||||
<p>API for querying scan results. You can search using an MD5 checksum, app name, package name, or file name. The API returns the closest match based on your search term.</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong>URL:</strong> <code>/api/v1/search</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Method:</strong> <code>POST</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Header:</strong> <code>Authorization: <api_key></code> <strong>Or</strong> <code>X-Mobsf-Api-Key: <api_key></code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Data Params</strong></p>
|
||||
</li>
|
||||
</ul>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param Name</th>
|
||||
<th>Param Value</th>
|
||||
<th>Required</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>query</td>
|
||||
<td>hash of the scan or text</td>
|
||||
<td>Yes</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong>Success Response:</strong></p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Code:</strong> <code>200</code><br />
|
||||
<strong>Content-Type:</strong> <code>application/json; charset=utf-8</code> <br />
|
||||
<strong>Content:</strong> <code>JSON Contents</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Error Response:</strong></p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Code:</strong> <code>404 Not Found</code> or <code>405 Method Not Allowed</code> or <code>422 Unprocessable Entity</code><br />
|
||||
<strong>Content-Type:</strong> <code>application/json; charset=utf-8</code><br />
|
||||
<strong>Content:</strong> <code>{"error": <You can search by MD5, app name, package name, or file name.> }</code>
|
||||
</li>
|
||||
</ul>
|
||||
<p>OR</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Code:</strong> <code>401 Unauthorized</code><br />
|
||||
<strong>Content-Type:</strong> <code>application/json; charset=utf-8</code><br />
|
||||
<strong>Content:</strong> <code>{"error": "You are unauthorized to make this request." }</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Sample Call:</strong></p>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>curl -X POST --url http://localhost:8000/api/v1/search --data "query=com.foo.bar" -H "Authorization: {{ api_key}}"
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
<p>OR</p>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>curl -X POST --url http://localhost:8000/api/v1/search --data "query=82ab8b2193b3cfb1c737e3a786be363a" -H "X-Mobsf-Api-Key: {{ api_key}}"
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
|
||||
<h2><a id="delete-scan-api"></a><strong>Delete Scan API</strong></h2>
|
||||
<p>API to delete scan results.</p>
|
||||
<ul>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{% endif %}
|
||||
<a class="nav-link">
|
||||
<form action="/search" method="GET">
|
||||
<input name="md5" type="text" class="form-control form-control-sm" placeholder="Search MD5">
|
||||
<input name="query" type="text" class="form-control form-control-sm" placeholder="Search">
|
||||
</form>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{% extends "base/base_layout.html" %}
|
||||
{% block sidebar_option %}
|
||||
sidebar-collapse
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="content-wrapper">
|
||||
<div class="content-header">
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3>Scan hash not found!</h3>
|
||||
<p class="lead">We couldn't find a scan matching that MD5 hash.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -749,6 +749,38 @@
|
||||
{% endif %}
|
||||
-->
|
||||
|
||||
{% if firebase_urls %}
|
||||
<h2><i class="fas fa-database"></i> FIREBASE DATABASES ANALYSIS</h2>
|
||||
<table class="basic">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TITLE</th>
|
||||
<th>SEVERITY</th>
|
||||
<th>DESCRIPTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for find in firebase_urls %}
|
||||
<tr>
|
||||
<td>{{ find.title }}</td>
|
||||
<td>
|
||||
{% if find.severity == 'high' %}
|
||||
<span class="danger">high</span>
|
||||
{% elif find.severity == 'secure' %}
|
||||
<span class="success">secure</span>
|
||||
{% elif find.severity == 'warning' %}
|
||||
<span class="warning">warning</span>
|
||||
{% elif find.severity == 'info' %}
|
||||
<span class="info">info</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ find.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if malware_permissions %}
|
||||
<h2><i class="fa fa-braille"></i> ABUSED PERMISSIONS</h2>
|
||||
|
||||
@@ -884,37 +916,6 @@
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if firebase_urls %}
|
||||
<h2><i class="fas fa-database"></i> FIREBASE DATABASES</h2>
|
||||
<table class="basic">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIREBASE URL</th>
|
||||
<th>DETAILS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in firebase_urls %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ item.url }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.open %}
|
||||
<span class="danger">high</span></br>Firebase DB is exposed publicly.
|
||||
{% else %}
|
||||
<span class="info">info</span></br>App talks to a Firebase Database.
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% if emails %}
|
||||
<h2><i class="fas fa-envelope"></i> EMAILS</h2>
|
||||
<table class="basic">
|
||||
|
||||
@@ -793,6 +793,37 @@
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if firebase_urls %}
|
||||
<h2><i class="fas fa-database"></i> FIREBASE DATABASES ANALYSIS</h2>
|
||||
<table class="basic">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TITLE</th>
|
||||
<th>SEVERITY</th>
|
||||
<th>DESCRIPTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for find in firebase_urls %}
|
||||
<tr>
|
||||
<td>{{ find.title }}</td>
|
||||
<td>
|
||||
{% if find.severity == 'high' %}
|
||||
<span class="danger">high</span>
|
||||
{% elif find.severity == 'secure' %}
|
||||
<span class="success">secure</span>
|
||||
{% elif find.severity == 'warning' %}
|
||||
<span class="warning">warning</span>
|
||||
{% elif find.severity == 'info' %}
|
||||
<span class="info">info</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ find.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if domains %}
|
||||
|
||||
<h2><i class="fa fa-exclamation"></i> OFAC SANCTIONED COUNTRIES</h2>
|
||||
@@ -892,37 +923,6 @@
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if firebase_urls %}
|
||||
<h2><i class="fas fa-database"></i> FIREBASE DATABASES</h2>
|
||||
<table class="basic">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIREBASE URL</th>
|
||||
<th>DETAILS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in firebase_urls %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ item.url }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.open %}
|
||||
<span class="danger">high</span></br>Firebase DB is exposed publicly.
|
||||
{% else %}
|
||||
<span class="info">info</span></br>App talks to a Firebase Database.
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% if emails %}
|
||||
<h2><i class="fas fa-envelope"></i> EMAILS</h2>
|
||||
<table class="basic">
|
||||
|
||||
@@ -167,6 +167,12 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a href="#firebase" class="nav-link">
|
||||
<i class="fas fa-database nav-icon"></i>
|
||||
<p>Firebase Analysis</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item has-treeview">
|
||||
@@ -235,12 +241,6 @@
|
||||
<p>URLs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#firebase" class="nav-link">
|
||||
<i class="fas fa-database nav-icon"></i>
|
||||
<p>Firebase DB</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#emails" class="nav-link">
|
||||
<i class="fas fa-envelope nav-icon"></i>
|
||||
@@ -1568,6 +1568,54 @@
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end file analysis ================================== -->
|
||||
<a id="firebase" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong><i class="fas fa-database"></i> FIREBASE DATABASE ANALYSIS</strong>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table id="table_firebase" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TITLE</th>
|
||||
<th>SEVERITY</th>
|
||||
<th>DESCRIPTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for find in firebase_urls %}
|
||||
<tr>
|
||||
<td>{{ find.title }}</td>
|
||||
<td>
|
||||
{% if find.severity == 'high' %}
|
||||
<span class="badge bg-danger">high</span>
|
||||
{% elif find.severity == 'secure' %}
|
||||
<span class="badge bg-success">secure</span>
|
||||
{% elif find.severity == 'warning' %}
|
||||
<span class="badge bg-warning">warning</span>
|
||||
{% elif find.severity == 'info' %}
|
||||
<span class="badge bg-info">info</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ find.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end firebase db analysis ================================== -->
|
||||
|
||||
<a id="apkid" class="anchor"></a>
|
||||
<section class="content">
|
||||
@@ -1975,53 +2023,6 @@
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end urls ================================== -->
|
||||
<a id="firebase" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong><i class="fas fa-database"></i> FIREBASE DATABASE</strong>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
{% if firebase_urls %}
|
||||
<table id="table_firebase" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIREBASE URL</th>
|
||||
<th>DETAILS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for item in firebase_urls %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ item.url }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.open %}
|
||||
<span class="badge bg-danger">high</span><br/>Firebase Database is exposed publicly.
|
||||
{% else %}
|
||||
<span class="badge bg-primary">info</span><br/>App talks to a Firebase database.
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end firebase db ================================== -->
|
||||
<a id="emails" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
|
||||
@@ -119,6 +119,12 @@
|
||||
<p>File Analysis</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#firebase" class="nav-link">
|
||||
<i class="fas fa-database nav-icon"></i>
|
||||
<p>Firebase Analysis</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item has-treeview">
|
||||
@@ -165,12 +171,6 @@
|
||||
<p>URLs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#firebase" class="nav-link">
|
||||
<i class="fas fa-database nav-icon"></i>
|
||||
<p>Firebase DB</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#emails" class="nav-link">
|
||||
<i class="fas fa-envelope nav-icon"></i>
|
||||
@@ -1218,6 +1218,55 @@
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end file analysis ================================== -->
|
||||
<a id="firebase" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong><i class="fas fa-database"></i> FIREBASE DATABASE ANALYSIS</strong>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table id="table_firebase" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TITLE</th>
|
||||
<th>SEVERITY</th>
|
||||
<th>DESCRIPTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for find in firebase_urls %}
|
||||
<tr>
|
||||
<td>{{ find.title }}</td>
|
||||
<td>
|
||||
{% if find.severity == 'high' %}
|
||||
<span class="badge bg-danger">high</span>
|
||||
{% elif find.severity == 'secure' %}
|
||||
<span class="badge bg-success">secure</span>
|
||||
{% elif find.severity == 'warning' %}
|
||||
<span class="badge bg-warning">warning</span>
|
||||
{% elif find.severity == 'info' %}
|
||||
<span class="badge bg-info">info</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ find.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end firebase db analysis ================================== -->
|
||||
|
||||
<a id="malware_permissions" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
@@ -1440,53 +1489,6 @@
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end urls ================================== -->
|
||||
<a id="firebase" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong><i class="fas fa-database"></i> FIREBASE DATABASE</strong>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
{% if firebase_urls %}
|
||||
<table id="table_firebase" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIREBASE URL</th>
|
||||
<th>DETAILS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for item in firebase_urls %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ item.url }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.open %}
|
||||
<span class="badge bg-danger">high</span><br/>Firebase Database is exposed publicly.
|
||||
{% else %}
|
||||
<span class="badge bg-primary">info</span><br/>App talks to a Firebase database.
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end firebase db ================================== -->
|
||||
<a id="emails" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
|
||||
@@ -141,6 +141,12 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a href="#firebase" class="nav-link">
|
||||
<i class="fas fa-database nav-icon"></i>
|
||||
<p>Firebase Analysis</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item has-treeview">
|
||||
@@ -191,12 +197,6 @@
|
||||
<p>URLs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#firebase" class="nav-link">
|
||||
<i class="fas fa-database nav-icon"></i>
|
||||
<p>Firebase DB</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#emails" class="nav-link">
|
||||
<i class="fas fa-envelope nav-icon"></i>
|
||||
@@ -1232,6 +1232,54 @@
|
||||
</section>
|
||||
<!-- ===========================end file analysis ================================== -->
|
||||
{% endif %}
|
||||
<a id="firebase" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong><i class="fas fa-database"></i> FIREBASE DATABASE ANALYSIS</strong>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table id="table_firebase" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TITLE</th>
|
||||
<th>SEVERITY</th>
|
||||
<th>DESCRIPTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for find in firebase_urls %}
|
||||
<tr>
|
||||
<td>{{ find.title }}</td>
|
||||
<td>
|
||||
{% if find.severity == 'high' %}
|
||||
<span class="badge bg-danger">high</span>
|
||||
{% elif find.severity == 'secure' %}
|
||||
<span class="badge bg-success">secure</span>
|
||||
{% elif find.severity == 'warning' %}
|
||||
<span class="badge bg-warning">warning</span>
|
||||
{% elif find.severity == 'info' %}
|
||||
<span class="badge bg-info">info</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ find.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end firebase db analysis ================================== -->
|
||||
{% if virus_total %}
|
||||
<a id="virus_total" class="anchor"></a>
|
||||
<section class="content">
|
||||
@@ -1447,53 +1495,6 @@
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end urls ================================== -->
|
||||
<a id="firebase" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong><i class="fas fa-database"></i> FIREBASE DATABASE</strong>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
{% if firebase_urls %}
|
||||
<table id="table_firebase" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIREBASE URL</th>
|
||||
<th>DETAILS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for item in firebase_urls %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ item.url }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.open %}
|
||||
<span class="badge bg-danger">high</span><br/>Firebase Database is exposed publicly.
|
||||
{% else %}
|
||||
<span class="badge bg-primary">info</span><br/>App talks to a Firebase database.
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end firebase db ================================== -->
|
||||
<a id="emails" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
|
||||
@@ -114,6 +114,12 @@
|
||||
<p>File Analysis</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#firebase" class="nav-link">
|
||||
<i class="fas fa-database nav-icon"></i>
|
||||
<p>Firebase Analysis</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item has-treeview">
|
||||
@@ -154,12 +160,6 @@
|
||||
<p>URLs</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#firebase" class="nav-link">
|
||||
<i class="fas fa-database nav-icon"></i>
|
||||
<p>Firebase DB</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#emails" class="nav-link">
|
||||
<i class="fas fa-envelope nav-icon"></i>
|
||||
@@ -854,6 +854,54 @@
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end file analysis ================================== -->
|
||||
<a id="firebase" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong><i class="fas fa-database"></i> FIREBASE DATABASE ANALYSIS</strong>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table id="table_firebase" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TITLE</th>
|
||||
<th>SEVERITY</th>
|
||||
<th>DESCRIPTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for find in firebase_urls %}
|
||||
<tr>
|
||||
<td>{{ find.title }}</td>
|
||||
<td>
|
||||
{% if find.severity == 'high' %}
|
||||
<span class="badge bg-danger">high</span>
|
||||
{% elif find.severity == 'secure' %}
|
||||
<span class="badge bg-success">secure</span>
|
||||
{% elif find.severity == 'warning' %}
|
||||
<span class="badge bg-warning">warning</span>
|
||||
{% elif find.severity == 'info' %}
|
||||
<span class="badge bg-info">info</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ find.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end firebase db analysis ================================== -->
|
||||
<style>
|
||||
#chartdiv {
|
||||
width: 100%;
|
||||
@@ -1017,55 +1065,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end urls ================================== -->
|
||||
<a id="firebase" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong><i class="fas fa-database"></i> FIREBASE DATABASE</strong>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
{% if firebase_urls %}
|
||||
<table id="table_firebase" class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIREBASE URL</th>
|
||||
<th>DETAILS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for item in firebase_urls %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ item.url }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.open %}
|
||||
<span class="badge bg-danger">high</span><br/>Firebase Database is exposed publicly.
|
||||
{% else %}
|
||||
<span class="badge bg-primary">info</span><br/>App talks to a Firebase database.
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.card -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===========================end firebase db ================================== -->
|
||||
<a id="emails" class="anchor"></a>
|
||||
<!-- ===========================end urls ================================== -->
|
||||
<a id="emails" class="anchor"></a>
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
Generated
+7
-7
@@ -2420,13 +2420,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.5"
|
||||
version = "4.66.6"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"},
|
||||
{file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"},
|
||||
{file = "tqdm-4.66.6-py3-none-any.whl", hash = "sha256:223e8b5359c2efc4b30555531f09e9f2f3589bcd7fdd389271191031b49b7a63"},
|
||||
{file = "tqdm-4.66.6.tar.gz", hash = "sha256:4bdd694238bef1485ce839d67967ab50af8f9272aab687c0d7702a01da0be090"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2647,13 +2647,13 @@ watchdog = ["watchdog (>=2.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "whitenoise"
|
||||
version = "6.7.0"
|
||||
version = "6.8.1"
|
||||
description = "Radically simplified static file serving for WSGI applications"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "whitenoise-6.7.0-py3-none-any.whl", hash = "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6"},
|
||||
{file = "whitenoise-6.7.0.tar.gz", hash = "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636"},
|
||||
{file = "whitenoise-6.8.1-py3-none-any.whl", hash = "sha256:196ba04ca0a80f4a3f99f88381864f218a28b5fb5b44d29feea484d501fa0ba3"},
|
||||
{file = "whitenoise-6.8.1.tar.gz", hash = "sha256:11042f39f1dcfbb3814726b9364703af6901706582d988e96494cfefdc3a89e2"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "mobsf"
|
||||
version = "4.1.0"
|
||||
version = "4.1.1"
|
||||
description = "Mobile Security Framework (MobSF) is an automated, all-in-one mobile application (Android/iOS/Windows) pen-testing, malware analysis and security assessment framework capable of performing static and dynamic analysis."
|
||||
keywords = ["mobsf", "mobile security framework", "mobile security", "security tool", "static analysis", "dynamic analysis", "malware analysis"]
|
||||
authors = ["Ajin Abraham <[email protected]>"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = py310, py311
|
||||
envlist = py310, py311, p312
|
||||
skipsdist = True
|
||||
isolated_build = true
|
||||
|
||||
@@ -17,6 +17,7 @@ commands =
|
||||
[testenv:lint]
|
||||
skip_install = true
|
||||
deps =
|
||||
autopep8
|
||||
pydocstyle
|
||||
flake8
|
||||
flake8-broken-line
|
||||
@@ -35,6 +36,7 @@ deps =
|
||||
radon
|
||||
codespell
|
||||
commands =
|
||||
autopep8 . --recursive --in-place
|
||||
flake8 {posargs}
|
||||
codespell --ignore-words-list="doubleclick,dout,ne,upto,lief,afile" \
|
||||
--skip="./mobsf/StaticAnalyzer/tools/*,./mobsf/signatures/*,./mobsf/StaticAnalyzer/views/android/rules/*,.*.map,*.js,*.svg,./build/*,./.tox/*,./venv/*,./.git/*,./poetry.lock"
|
||||
@@ -98,3 +100,4 @@ ignore =
|
||||
# remove # flake8: noqa from manifest_analysis.py, should be fixed with R701
|
||||
max-complexity = 42
|
||||
radon_max_cc = 10
|
||||
max-line-length = 88
|
||||
Reference in New Issue
Block a user