Skip to content

Commit

Permalink
[HOTFIX] Support AAB with MobSF, Convert AAB to APK, Fixes #2387 (#2391)
Browse files Browse the repository at this point in the history
* AAB to APK conversion
* relative urls fix for recent scan
  • Loading branch information
ajinabraham committed May 25, 2024
1 parent e357823 commit 680ca5d
Show file tree
Hide file tree
Showing 17 changed files with 130 additions and 44 deletions.
2 changes: 1 addition & 1 deletion mobsf/MobSF/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

logger = logging.getLogger(__name__)

VERSION = '4.0.1'
VERSION = '4.0.2'
BANNER = """
__ __ _ ____ _____ _ _ ___
| \/ | ___ | |__/ ___|| ___|_ _| || | / _ \
Expand Down
8 changes: 5 additions & 3 deletions mobsf/MobSF/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand All @@ -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:
Expand Down
10 changes: 9 additions & 1 deletion mobsf/MobSF/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
'application/x-zip-compressed',
'binary/octet-stream',
'application/java-archive',
'application/x-authorware-bin',
]
IPA_MIME = [
'application/iphone',
Expand All @@ -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.
Expand Down Expand Up @@ -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', '')
Expand Down
7 changes: 4 additions & 3 deletions mobsf/MobSF/views/api/api_static_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions mobsf/MobSF/views/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'))
Expand Down
6 changes: 6 additions & 0 deletions mobsf/MobSF/views/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down
9 changes: 9 additions & 0 deletions mobsf/MobSF/views/scanning.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
12 changes: 1 addition & 11 deletions mobsf/StaticAnalyzer/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Binary file not shown.
14 changes: 9 additions & 5 deletions mobsf/StaticAnalyzer/views/android/static_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
get_strings_metadata,
)
from mobsf.StaticAnalyzer.views.android.xapk import (
handle_aab,
handle_split_apk,
handle_xapk,
)
Expand Down Expand Up @@ -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',
Expand All @@ -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'] = (
Expand Down Expand Up @@ -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)
Expand Down
73 changes: 66 additions & 7 deletions mobsf/StaticAnalyzer/views/android/xapk.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions mobsf/StaticAnalyzer/views/common/shared_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
7 changes: 4 additions & 3 deletions mobsf/StaticAnalyzer/views/ios/static_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion mobsf/StaticAnalyzer/views/windows/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions mobsf/templates/general/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,10 @@ <h6> <a href="{% url 'recent' %}">RECENT SCANS</a> | <a href="{% url 'dynamic'
// Is valid file extensions
function isValidExt(file_name){
var val = file_name.toLowerCase();
var regex = new RegExp("^(.{1,300}?)\.(ipa|apk|apks|xapk|jar|aar|so|dylib|a|zip|appx)$");
var regex = new RegExp("^(.{1,300}?)\.({{exts}})$");
val = val.replace(/^.*[\\\/]/, '');
if (!(regex.test(val))) {
_('status').innerText = "MobSF only supports APK, APKS, XAPK, JAR, AAR, SO, IPA, DYLIB, A, ZIP, and APPX files.";
_('status').innerText = "MobSF only supports APK, APKS, XAPK, AAB, JAR, AAR, SO, IPA, DYLIB, A, ZIP, and APPX files.";
return false;
}
return true;
Expand Down
9 changes: 5 additions & 4 deletions mobsf/templates/general/recent.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ <h3 class="box-title"><i class="fa fa-rocket"></i> Recent Scans</h3>
{% endif %}
{% endif %}
{% endif %}
<p><a class="btn btn-primary btn-sm" href="../{{ e.ANALYZER }}/{{e.MD5}}/"><i class="fas fa-eye"></i> Static Report</a>
{% if '.apk' == e.FILE_NAME|slice:"-4:" or '.xapk' == e.FILE_NAME|slice:"-5:" or '.apks' == e.FILE_NAME|slice:"-5:"%}
<p><a class="btn btn-primary btn-sm" href="../../../{{ e.ANALYZER }}/{{e.MD5}}/"><i class="fas fa-eye"></i> Static Report</a>
{% 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:" %}
<a class="btn btn-success btn-sm {% if not e.DYNAMIC_REPORT_EXISTS %}disabled{% endif %}" href="{% url "dynamic_report" checksum=e.MD5 %}"><i class="fa fa-mobile"></i> Dynamic Report</a>
{% elif '.ipa' == e.FILE_NAME|slice:"-4:" %}
{% if e.PACKAGE_NAME %}
Expand All @@ -86,6 +86,7 @@ <h3 class="box-title"><i class="fa fa-rocket"></i> Recent Scans</h3>
{% if '.apk' == e.FILE_NAME|slice:"-4:"%}<i class="fab fa-android fa-3x"></i>
{% elif '.xapk' == e.FILE_NAME|slice:"-5:"%}<i class="fab fa-android fa-3x"></i>
{% elif '.apks' == e.FILE_NAME|slice:"-5:"%}<i class="fab fa-android fa-3x"></i>
{% elif '.aab' == e.FILE_NAME|slice:"-4:"%}<i class="fab fa-android fa-3x"></i>
{% elif '.jar' == e.FILE_NAME|slice:"-4:"%}<i class="fab fa-java fa-3x"></i>
{% elif '.aar' == e.FILE_NAME|slice:"-4:"%}<i class="fas fa-table fa-3x"></i>
{% elif '.so' == e.FILE_NAME|slice:"-3:"%}<i class="fa fa-th-large fa-3x"></i>
Expand All @@ -100,9 +101,9 @@ <h3 class="box-title"><i class="fa fa-rocket"></i> Recent Scans</h3>
<td>{{ e.TIMESTAMP }}</td>
<td><p>
<a class="btn btn-outline-primary btn-sm" href="{% url "pdf" checksum=e.MD5%}"><i class="fas fa-file-pdf"></i></a>
<a class="btn btn-outline-info btn-sm" href="../{{ e.ANALYZER }}/{{e.MD5}}/?rescan=1"><i class="fas fa-sync-alt"></i></a>
<a class="btn btn-outline-info btn-sm" href="../../../{{ e.ANALYZER }}/{{e.MD5}}/?rescan=1"><i class="fas fa-sync-alt"></i></a>
</p>
{% if '.apk' == e.FILE_NAME|slice:"-4:" or '.xapk' == e.FILE_NAME|slice:"-5:" or '.apks' == e.FILE_NAME|slice:"-5:"%}
{% 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:"%}
<p><a class="diffButton btn btn-warning btn-sm" id="{{ e.MD5 }}_{{ e.FILE_NAME }}"><i class="fas fa-not-equal"></i> Diff or Compare</a>
</p>
{% endif %}
Expand Down
Loading

0 comments on commit 680ca5d

Please sign in to comment.