diff --git a/mobsf/MobSF/init.py b/mobsf/MobSF/init.py index 1597fab395..b41dc746df 100644 --- a/mobsf/MobSF/init.py +++ b/mobsf/MobSF/init.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -VERSION = '4.0.1' +VERSION = '4.0.2' BANNER = """ __ __ _ ____ _____ _ _ ___ | \/ | ___ | |__/ ___|| ___|_ _| || | / _ \ diff --git a/mobsf/MobSF/security.py b/mobsf/MobSF/security.py index b3dd4c958f..3e50fe4b66 100644 --- a/mobsf/MobSF/security.py +++ b/mobsf/MobSF/security.py @@ -104,6 +104,7 @@ def get_executable_hashes(): settings.JTOOL_BINARY, settings.CLASSDUMP_BINARY, settings.CLASSDUMP_SWIFT_BINARY, + getattr(settings, 'BUNDLE_TOOL', ''), ] for ubin in user_defined_bins: if ubin: @@ -130,9 +131,9 @@ def store_exec_hashes_at_first_run(): hashes['signature'] = signature EXECUTABLE_HASH_MAP = hashes except Exception: - logger.warning('Cannot calculate executable hashes, ' - 'disabling runtime executable ' - 'tampering detection') + logger.exception('Cannot calculate executable hashes, ' + 'disabling runtime executable ' + 'tampering detection') def subprocess_hook(oldfunc, *args, **kwargs): @@ -149,6 +150,7 @@ def subprocess_hook(oldfunc, *args, **kwargs): for arg in agmtz: if arg.endswith('.jar'): exec2 = Path(arg).as_posix() + break if '/' in exec1 or '\\' in exec1: exec1 = Path(exec1).as_posix() else: diff --git a/mobsf/MobSF/settings.py b/mobsf/MobSF/settings.py index 68d8ef5995..276b61500b 100644 --- a/mobsf/MobSF/settings.py +++ b/mobsf/MobSF/settings.py @@ -86,6 +86,7 @@ 'application/x-zip-compressed', 'binary/octet-stream', 'application/java-archive', + 'application/x-authorware-bin', ] IPA_MIME = [ 'application/iphone', @@ -107,7 +108,13 @@ 'application/vns.ms-appx', 'application/x-zip-compressed', ] - +# Supported File Extensions +ANDROID_EXTS = ( + 'apk', 'xapk', 'apks', 'zip', + 'aab', 'so', 'jar', 'aar', +) +IOS_EXTS = ('ipa', 'dylib', 'a') +WINDOWS_EXTS = ('appx',) # REST API only mode # Set MOBSF_API_ONLY to 1 to enable REST API only mode # In this mode, web UI related urls are disabled. @@ -420,6 +427,7 @@ """ # Android 3P Tools + BUNDLE_TOOL = os.getenv('MOBSF_BUNDLE_TOOL', '') JADX_BINARY = os.getenv('MOBSF_JADX_BINARY', '') BACKSMALI_BINARY = os.getenv('MOBSF_BACKSMALI_BINARY', '') VD2SVG_BINARY = os.getenv('MOBSF_VD2SVG_BINARY', '') diff --git a/mobsf/MobSF/views/api/api_static_analysis.py b/mobsf/MobSF/views/api/api_static_analysis.py index bf3bcaaf4a..3aa824afe3 100755 --- a/mobsf/MobSF/views/api/api_static_analysis.py +++ b/mobsf/MobSF/views/api/api_static_analysis.py @@ -2,6 +2,7 @@ """MobSF REST API V 1.""" from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt +from django.conf import settings from mobsf.StaticAnalyzer.models import ( RecentScansDB, @@ -66,7 +67,7 @@ def api_scan(request): {'error': 'The file is not uploaded/available'}, 500) scan_type = robj[0].SCAN_TYPE # APK, Source Code (Android/iOS) ZIP, SO, JAR, AAR - if scan_type in {'xapk', 'apk', 'apks', 'zip', 'so', 'jar', 'aar'}: + if scan_type in settings.ANDROID_EXTS: resp = static_analyzer(request, checksum, True) if 'type' in resp: resp = static_analyzer_ios(request, checksum, True) @@ -75,14 +76,14 @@ def api_scan(request): else: response = make_api_response(resp, 200) # IPA - elif scan_type in {'ipa', 'dylib', 'a'}: + elif scan_type in settings.IOS_EXTS: resp = static_analyzer_ios(request, checksum, True) if 'error' in resp: response = make_api_response(resp, 500) else: response = make_api_response(resp, 200) # APPX - elif scan_type == 'appx': + elif scan_type in settings.WINDOWS_EXTS: resp = windows.staticanalyzer_windows(request, checksum, True) if 'error' in resp: response = make_api_response(resp, 500) diff --git a/mobsf/MobSF/views/helpers.py b/mobsf/MobSF/views/helpers.py index 102c2904eb..0bd0f93f42 100644 --- a/mobsf/MobSF/views/helpers.py +++ b/mobsf/MobSF/views/helpers.py @@ -44,6 +44,7 @@ def is_allow_file(self): or self.is_ipa() or self.is_appx() or self.is_apks() + or self.is_aab() or self.is_jar() or self.is_aar()): return True @@ -57,6 +58,10 @@ def is_xapk(self): return (self.file_type in settings.APK_MIME and self.file_name_lower.endswith('.xapk')) + def is_aab(self): + return (self.file_type in settings.APK_MIME + and self.file_name_lower.endswith('.aab')) + def is_apk(self): return (self.file_type in settings.APK_MIME and self.file_name_lower.endswith('.apk')) diff --git a/mobsf/MobSF/views/home.py b/mobsf/MobSF/views/home.py index 852fb58182..f3a79cd32b 100755 --- a/mobsf/MobSF/views/home.py +++ b/mobsf/MobSF/views/home.py @@ -61,9 +61,13 @@ def index(request): + settings.IPA_MIME + settings.ZIP_MIME + settings.APPX_MIME) + exts = (settings.ANDROID_EXTS + + settings.IOS_EXTS + + settings.WINDOWS_EXTS) context = { 'version': settings.MOBSF_VER, 'mimes': mimes, + 'exts': '|'.join(exts), } template = 'general/home.html' return render(request, template, context) @@ -154,6 +158,8 @@ def upload(self): return scanning.scan_xapk() elif self.file_type.is_apks(): return scanning.scan_apks() + elif self.file_type.is_aab(): + return scanning.scan_aab() elif self.file_type.is_jar(): return scanning.scan_jar() elif self.file_type.is_aar(): diff --git a/mobsf/MobSF/views/scanning.py b/mobsf/MobSF/views/scanning.py index 9ef47ec512..590cf15c55 100644 --- a/mobsf/MobSF/views/scanning.py +++ b/mobsf/MobSF/views/scanning.py @@ -98,6 +98,15 @@ def scan_apks(self): logger.info('Performing Static Analysis of Android Split APK') return self.data + def scan_aab(self): + """Android App Bundle.""" + md5 = handle_uploaded_file(self.file, '.aab') + self.data['hash'] = md5 + self.data['scan_type'] = 'aab' + add_to_recent_scan(self.data) + logger.info('Performing Static Analysis of Android App Bundle') + return self.data + def scan_jar(self): """Java JAR file.""" md5 = handle_uploaded_file(self.file, '.jar') diff --git a/mobsf/StaticAnalyzer/tests.py b/mobsf/StaticAnalyzer/tests.py index 19e9ab7395..aeee70840a 100755 --- a/mobsf/StaticAnalyzer/tests.py +++ b/mobsf/StaticAnalyzer/tests.py @@ -14,17 +14,7 @@ RESCAN = False # Set RESCAN to True if Static Analyzer Code is modified -EXTS = ( - '.xapk', - '.apk', - '.ipa', - '.appx', - '.zip', - '.a', - '.so', - '.dylib', - '.aar', - '.jar') +EXTS = settings.ANDROID_EXTS + settings.IOS_EXTS + settings.WINDOWS_EXTS def static_analysis_test(): diff --git a/mobsf/StaticAnalyzer/tools/bundletool-all-1.16.0.jar b/mobsf/StaticAnalyzer/tools/bundletool-all-1.16.0.jar new file mode 100644 index 0000000000..150d59eba4 Binary files /dev/null and b/mobsf/StaticAnalyzer/tools/bundletool-all-1.16.0.jar differ diff --git a/mobsf/StaticAnalyzer/views/android/static_analyzer.py b/mobsf/StaticAnalyzer/views/android/static_analyzer.py index 4a0ec76304..d7a15860b5 100755 --- a/mobsf/StaticAnalyzer/views/android/static_analyzer.py +++ b/mobsf/StaticAnalyzer/views/android/static_analyzer.py @@ -71,6 +71,7 @@ get_strings_metadata, ) from mobsf.StaticAnalyzer.views.android.xapk import ( + handle_aab, handle_split_apk, handle_xapk, ) @@ -131,12 +132,9 @@ def static_analyzer(request, checksum, api=False): api) typ = robj[0].SCAN_TYPE filename = robj[0].FILE_NAME - allowed_exts = ( - '.apk', '.xapk', '.zip', '.apks', - '.jar', '.aar', '.so') - allowed_typ = [i.replace('.', '') for i in allowed_exts] + allowed_exts = tuple(f'.{i}' for i in settings.ANDROID_EXTS) if (not filename.lower().endswith(allowed_exts) - or typ not in allowed_typ): + or typ not in settings.ANDROID_EXTS): return print_n_send_error_response( request, 'Invalid file extension or file type', @@ -163,6 +161,11 @@ def static_analyzer(request, checksum, api=False): if not handle_split_apk(app_dic): raise Exception('Invalid Split APK File') typ = 'apk' + elif typ == 'aab': + # Convert AAB to APK + if not handle_aab(app_dic): + raise Exception('Invalid AAB File') + typ = 'apk' if typ == 'apk': app_dic['app_file'] = app_dic['md5'] + '.apk' # NEW FILENAME app_dic['app_path'] = ( @@ -504,6 +507,7 @@ def static_analyzer(request, checksum, api=False): err = ('Only APK, JAR, AAR, SO and Zipped ' 'Android/iOS Source code supported now!') logger.error(err) + raise Exception(err) except Exception as excep: logger.exception('Error Performing Static Analysis') msg = str(excep) diff --git a/mobsf/StaticAnalyzer/views/android/xapk.py b/mobsf/StaticAnalyzer/views/android/xapk.py index 243c1db104..d5b02422d2 100644 --- a/mobsf/StaticAnalyzer/views/android/xapk.py +++ b/mobsf/StaticAnalyzer/views/android/xapk.py @@ -1,11 +1,19 @@ # -*- coding: utf_8 -*- """Handle XAPK File.""" import logging +import subprocess from json import load from shutil import move +from pathlib import Path + +from django.conf import settings from mobsf.StaticAnalyzer.views.common.shared_func import unzip -from mobsf.MobSF.utils import is_safe_path +from mobsf.MobSF.utils import ( + find_java_binary, + is_file_exists, + is_safe_path, +) logger = logging.getLogger(__name__) @@ -47,11 +55,62 @@ def handle_split_apk(app_dic): manifest = app_dic['app_dir'] / 'AndroidManifest.xml' if manifest.exists(): return True - files = unzip(apks.as_posix(), app_dic['app_dir']) - for apk in files: - if not apk.startswith('config.') and apk.endswith('.apk'): - full_path = app_dic['app_dir'] / apk - if is_safe_path(app_dic['app_dir'], full_path): - move(full_path, apks) + for apk in unzip(apks.as_posix(), app_dic['app_dir']): + full_path = app_dic['app_dir'] / apk + safe_path = is_safe_path(app_dic['app_dir'], full_path) + if (not apk.startswith('config.') + and apk.endswith('.apk') + and safe_path): + move(full_path, apks) + return True + return None + + +def handle_aab(app_dic): + """Convert AAB to APK using bundletool.""" + try: + checksum = app_dic['md5'] + aab_path = app_dic['app_dir'] / f'{checksum}.aab' + apks = aab_path.with_suffix('.apks') + apk = aab_path.with_suffix('.apk') + tools_dir = app_dic['tools_dir'] + # Check if previously converted + manifest = app_dic['app_dir'] / 'AndroidManifest.xml' + if manifest.exists(): + return True + logger.info('Converting AAB to APK') + if (getattr(settings, 'BUNDLE_TOOL', '') + and len(settings.BUNDLE_TOOL) > 0 + and is_file_exists(settings.BUNDLE_TOOL)): + bundletool = settings.BUNDLE_TOOL + else: + bundletool = Path(tools_dir) / 'bundletool-all-1.16.0.jar' + bundletool = bundletool.as_posix() + args = [ + find_java_binary(), + '-jar', + bundletool, + 'build-apks', + f'--bundle={aab_path.as_posix()}', + f'--output={apks.as_posix()}', + '--mode=universal', + ] + if not apks.exists() and aab_path.exists(): + # Convert AAB to APKS + subprocess.run(args, timeout=300) + # Remove AAB + aab_path.unlink() + # Extract APK from APKS + for apk_file in unzip(apks.as_posix(), app_dic['app_dir']): + full_path = app_dic['app_dir'] / apk_file + safe_path = is_safe_path(app_dic['app_dir'], full_path) + if apk_file == 'universal.apk' and safe_path: + move(full_path, apk) + apks.unlink() return True + raise Exception('Unable to convert AAB to APK') + except subprocess.TimeoutExpired: + logger.warning('Converting AAB to APK timed out') + except Exception: + logger.exception('Converting AAB to APK') return None diff --git a/mobsf/StaticAnalyzer/views/common/shared_func.py b/mobsf/StaticAnalyzer/views/common/shared_func.py index c1f1e14b53..a8c90bd854 100755 --- a/mobsf/StaticAnalyzer/views/common/shared_func.py +++ b/mobsf/StaticAnalyzer/views/common/shared_func.py @@ -411,12 +411,12 @@ def scan_library(request, checksum): return print_n_send_error_response(request, msg) with open(sfile, 'rb') as f: libchecksum = handle_uploaded_file(f, ext) - if ext in ('.ipa', '.dylib', '.a'): + if ext in [f'.{i}' for i in settings.IOS_EXTS]: static_analyzer = 'static_analyzer_ios' elif ext == '.appx': # Not applicable, but still set it static_analyzer = 'windows_static_analyzer' - elif ext in ('.zip', '.so', '.jar', '.aar', '.apk', '.xapk'): + elif ext in [f'.{i}' for i in settings.ANDROID_EXTS]: static_analyzer = 'static_analyzer' else: msg = 'Extension not supported' diff --git a/mobsf/StaticAnalyzer/views/ios/static_analyzer.py b/mobsf/StaticAnalyzer/views/ios/static_analyzer.py index c4e4bea4c7..638c880217 100755 --- a/mobsf/StaticAnalyzer/views/ios/static_analyzer.py +++ b/mobsf/StaticAnalyzer/views/ios/static_analyzer.py @@ -105,10 +105,11 @@ def static_analyzer_ios(request, checksum, api=False): if file_type == 'dylib' and not Path(filename).suffix: # Force dylib extension on Frameworks filename = f'{filename}.dylib' - allowed_exts = ('ios', '.ipa', '.zip', '.dylib', '.a') - allowed_typ = [i.replace('.', '') for i in allowed_exts] + ios_exts = tuple(f'.{i}' for i in settings.IOS_EXTS) + allowed_exts = ios_exts + ('.zip', 'ios') + allowed_types = settings.IOS_EXTS + ('zip', 'ios') if (not filename.lower().endswith(allowed_exts) - or file_type not in allowed_typ): + or file_type not in allowed_types): return print_n_send_error_response( request, 'Invalid file extension or file type', diff --git a/mobsf/StaticAnalyzer/views/windows/windows.py b/mobsf/StaticAnalyzer/views/windows/windows.py index 98697105df..97e11a202e 100755 --- a/mobsf/StaticAnalyzer/views/windows/windows.py +++ b/mobsf/StaticAnalyzer/views/windows/windows.py @@ -91,7 +91,7 @@ def staticanalyzer_windows(request, checksum, api=False): api) typ = robj[0].SCAN_TYPE filename = robj[0].FILE_NAME - if typ != 'appx': + if typ not in settings.WINDOWS_EXTS: return print_n_send_error_response( request, 'File type not supported', diff --git a/mobsf/templates/general/home.html b/mobsf/templates/general/home.html index e273d9268f..59def1caae 100644 --- a/mobsf/templates/general/home.html +++ b/mobsf/templates/general/home.html @@ -215,10 +215,10 @@
Static Report - {% if '.apk' == e.FILE_NAME|slice:"-4:" or '.xapk' == e.FILE_NAME|slice:"-5:" or '.apks' == e.FILE_NAME|slice:"-5:"%} +
Static Report + {% if '.apk' == e.FILE_NAME|slice:"-4:" or '.xapk' == e.FILE_NAME|slice:"-5:" or '.apks' == e.FILE_NAME|slice:"-5:" or '.aab' == e.FILE_NAME|slice:"-4:" %} Dynamic Report {% elif '.ipa' == e.FILE_NAME|slice:"-4:" %} {% if e.PACKAGE_NAME %} @@ -86,6 +86,7 @@