mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-11 22:50:09 +00:00
* Harden path traversal checks for Windows root-relative escapes. Reject POSIX/Windows absolute paths in is_path_traversal and require post-join containment at icon, network-config, and archive extraction sinks. Co-authored-by: Cursor <[email protected]> * Restrict network security config names to a basename. CodeQL flagged the manifest-derived join; reduce the resource name first so path construction cannot escape xml_dir. Co-authored-by: Cursor <[email protected]> * Use os.path.basename for CodeQL-recognized path sanitization. Path.name did not clear py/path-injection taint; basename plus existing containment checks keep the network-config read inside xml_dir. Co-authored-by: Cursor <[email protected]> * Drop CodeQL-appeasement basename sanitization from network config reads. is_safe_path after join already rejects traversal, absolute, and Windows root-relative names; CodeQL py/path-injection is a false positive. Co-authored-by: Cursor <[email protected]> * Bound androguard ZIP member decompression Use the configured per-file ZIP limit for apkinspector reads and cap actual raw-DEFLATE output. Keep decompression-limit failures out of the tampered-entry fallback so malicious compressed data cannot be treated as stored bytes. Co-authored-by: Cursor <[email protected]> * Reject inactive SAML users before ACS group updates. Existing accounts mapped from a valid IdP assertion should not have groups replaced or be passed to login() when is_active is False. Co-authored-by: Cursor <[email protected]> * Return the ACS error page for inactive users instead of raising Exception. Co-authored-by: Cursor <[email protected]> * Fix iOS plist-derived path traversal Select and validate one contained IPA app bundle, reject unsafe plist executable values, and guard icon and Mach-O inputs before access. Harden equivalent source icon, plist, library, and dynamic icon paths. Co-authored-by: Cursor <[email protected]> * correct version * fix CSRF in dynamic analysis actions Co-authored-by: Ajin Abraham <[email protected]> * document HTTPTools host header SSRF advisory Co-authored-by: Ajin Abraham <[email protected]> --------- Co-authored-by: Cursor <[email protected]>
349 lines
12 KiB
Python
Executable File
349 lines
12 KiB
Python
Executable File
# -*- coding: utf_8 -*-
|
|
"""Android Dynamic Analysis."""
|
|
import logging
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
from json import dump
|
|
|
|
from shelljob import proc
|
|
|
|
|
|
from django.http import (HttpResponseRedirect,
|
|
StreamingHttpResponse)
|
|
from django.conf import settings
|
|
from django.shortcuts import render
|
|
from django.db.models import ObjectDoesNotExist
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from mobsf.DynamicAnalyzer.views.android.environment import (
|
|
ANDROID_API_SUPPORTED,
|
|
Environment,
|
|
)
|
|
from mobsf.DynamicAnalyzer.views.android.operations import (
|
|
get_package_name,
|
|
)
|
|
from mobsf.DynamicAnalyzer.tools.webproxy import (
|
|
get_http_tools_url,
|
|
start_httptools_ui,
|
|
stop_httptools,
|
|
)
|
|
from mobsf.MobSF.utils import (
|
|
get_android_dm_exception_msg,
|
|
get_config_loc,
|
|
get_device,
|
|
get_proxy_ip,
|
|
is_md5,
|
|
print_n_send_error_response,
|
|
python_dict,
|
|
python_list,
|
|
strict_package_check,
|
|
)
|
|
from mobsf.MobSF.views.scanning import add_to_recent_scan
|
|
from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid
|
|
from mobsf.MobSF.views.authentication import (
|
|
login_required,
|
|
)
|
|
from mobsf.MobSF.views.authorization import (
|
|
Permissions,
|
|
permission_required,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
def android_dynamic_analysis(request, api=False):
|
|
"""Android Dynamic Analysis Entry point."""
|
|
try:
|
|
scan_apps = []
|
|
device_packages = {}
|
|
and_ver = None
|
|
and_sdk = None
|
|
apks = StaticAnalyzerAndroid.objects.filter(
|
|
APP_TYPE='apk')
|
|
|
|
for apk in reversed(apks):
|
|
|
|
logcat = Path(settings.UPLD_DIR) / apk.MD5 / 'logcat.txt'
|
|
temp_dict = {
|
|
'ICON_PATH': apk.ICON_PATH,
|
|
'MD5': apk.MD5,
|
|
'APP_NAME': apk.APP_NAME,
|
|
'VERSION_NAME': apk.VERSION_NAME,
|
|
'FILE_NAME': apk.FILE_NAME,
|
|
'PACKAGE_NAME': apk.PACKAGE_NAME,
|
|
'DYNAMIC_REPORT_EXISTS': logcat.exists(),
|
|
}
|
|
scan_apps.append(temp_dict)
|
|
try:
|
|
identifier = get_device()
|
|
except Exception:
|
|
return print_n_send_error_response(
|
|
request, get_android_dm_exception_msg(), api)
|
|
try:
|
|
if identifier:
|
|
env = Environment(identifier)
|
|
env.connect()
|
|
device_packages = env.get_device_packages()
|
|
if device_packages:
|
|
pkg_file = Path(settings.DWD_DIR) / 'packages.json'
|
|
with pkg_file.open('w', encoding='utf-8') as target:
|
|
dump(device_packages, target)
|
|
and_ver = env.get_android_version()
|
|
and_sdk = env.get_android_sdk()
|
|
except Exception:
|
|
pass
|
|
context = {'apps': scan_apps,
|
|
'identifier': identifier,
|
|
'android_version': and_ver,
|
|
'android_sdk': and_sdk,
|
|
'android_supported': ANDROID_API_SUPPORTED,
|
|
'proxy_ip': get_proxy_ip(identifier),
|
|
'proxy_port': settings.PROXY_PORT,
|
|
'settings_loc': get_config_loc(),
|
|
'device_packages': device_packages,
|
|
'title': 'MobSF Dynamic Analysis',
|
|
'version': settings.MOBSF_VER}
|
|
if api:
|
|
return context
|
|
template = 'dynamic_analysis/android/dynamic_analysis.html'
|
|
return render(request, template, context)
|
|
except Exception as exp:
|
|
logger.exception('Dynamic Analysis')
|
|
return print_n_send_error_response(request, exp, api)
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def dynamic_analyzer(request, checksum, api=False):
|
|
"""Android Dynamic Analyzer Environment."""
|
|
try:
|
|
identifier = None
|
|
activities = None
|
|
deeplinks = None
|
|
exported_activities = None
|
|
reinstall = request.POST.get('re_install', '1')
|
|
install = request.POST.get('install', '1')
|
|
if not is_md5(checksum):
|
|
# We need this check since checksum is not validated
|
|
# in REST API
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Invalid Hash',
|
|
api)
|
|
package = get_package_name(checksum)
|
|
if not package:
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Cannot get package name from checksum',
|
|
api)
|
|
logger.info('Creating Dynamic Analysis Environment for %s', package)
|
|
try:
|
|
identifier = get_device()
|
|
except Exception:
|
|
return print_n_send_error_response(
|
|
request, get_android_dm_exception_msg(), api)
|
|
|
|
# Get activities from the static analyzer results
|
|
try:
|
|
static_android_db = StaticAnalyzerAndroid.objects.get(
|
|
MD5=checksum)
|
|
exported_activities = python_list(
|
|
static_android_db.EXPORTED_ACTIVITIES)
|
|
activities = python_list(
|
|
static_android_db.ACTIVITIES)
|
|
deeplinks = python_dict(
|
|
static_android_db.BROWSABLE_ACTIVITIES)
|
|
except ObjectDoesNotExist:
|
|
logger.warning(
|
|
'Failed to get Activities/Deeplinks. '
|
|
'Static Analysis not completed for the app.')
|
|
env = Environment(identifier)
|
|
if not env.connect_n_mount():
|
|
msg = 'Cannot Connect to ' + identifier
|
|
return print_n_send_error_response(request, msg, api)
|
|
version = env.get_android_version()
|
|
logger.info('Android Version identified as %s', version)
|
|
xposed_first_run = False
|
|
if not env.is_mobsfyied(version):
|
|
msg = ('This Android instance is not MobSFyed/Outdated.\n'
|
|
'MobSFying the android runtime environment')
|
|
logger.warning(msg)
|
|
if not env.mobsfy_init():
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Failed to MobSFy the instance',
|
|
api)
|
|
if version < 5:
|
|
# Start Clipboard monitor
|
|
env.start_clipmon()
|
|
xposed_first_run = True
|
|
if xposed_first_run:
|
|
msg = ('Have you MobSFyed the instance before'
|
|
' attempting Dynamic Analysis?'
|
|
' Install Framework for Xposed.'
|
|
' Restart the device and enable'
|
|
' all Xposed modules. And finally'
|
|
' restart the device once again.')
|
|
return print_n_send_error_response(request, msg, api)
|
|
# Clean up previous analysis
|
|
env.dz_cleanup(checksum)
|
|
# Configure Web Proxy
|
|
env.configure_proxy(package, request)
|
|
# Supported in Android 5+
|
|
env.enable_adb_reverse_tcp(version)
|
|
# Apply Global Proxy to device
|
|
env.set_global_proxy(version)
|
|
if install == '1':
|
|
# Install APK
|
|
apk_path = Path(settings.UPLD_DIR) / checksum / f'{checksum}.apk'
|
|
status, output = env.install_apk(
|
|
apk_path.as_posix(),
|
|
package,
|
|
reinstall)
|
|
if not status:
|
|
# Unset Proxy
|
|
env.unset_global_proxy()
|
|
msg = (f'This APK cannot be installed. Is this APK '
|
|
f'compatible the Android VM/Emulator?\n{output}')
|
|
return print_n_send_error_response(
|
|
request,
|
|
msg,
|
|
api)
|
|
logger.info('Testing Environment is Ready!')
|
|
context = {'package': package,
|
|
'hash': checksum,
|
|
'android_version': version,
|
|
'version': settings.MOBSF_VER,
|
|
'activities': activities,
|
|
'exported_activities': exported_activities,
|
|
'deeplinks': deeplinks,
|
|
'title': 'Dynamic Analyzer'}
|
|
template = 'dynamic_analysis/android/dynamic_analyzer.html'
|
|
if api:
|
|
return context
|
|
return render(request, template, context)
|
|
except Exception:
|
|
logger.exception('Dynamic Analyzer')
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Dynamic Analysis Failed.',
|
|
api)
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def httptools_start(request):
|
|
"""Start httprools UI."""
|
|
logger.info('Starting httptools Web UI')
|
|
try:
|
|
httptools_url = get_http_tools_url(request)
|
|
stop_httptools(httptools_url)
|
|
start_httptools_ui(settings.PROXY_PORT)
|
|
time.sleep(3)
|
|
logger.info('httptools UI started')
|
|
if request.POST['project']:
|
|
project = request.POST['project']
|
|
else:
|
|
project = ''
|
|
url = f'{httptools_url}/dashboard/{project}'
|
|
return HttpResponseRedirect(url)
|
|
except Exception:
|
|
logger.exception('Starting httptools Web UI')
|
|
err = 'Error Starting httptools UI'
|
|
return print_n_send_error_response(request, err)
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['GET', 'POST'])
|
|
def logcat(request, api=False):
|
|
logger.info('Starting Logcat streaming')
|
|
try:
|
|
if request.method == 'GET':
|
|
pkg = request.GET.get('package')
|
|
if pkg and strict_package_check(pkg):
|
|
template = 'dynamic_analysis/android/logcat.html'
|
|
return render(request, template, {'package': pkg})
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Invalid package name',
|
|
api)
|
|
if api:
|
|
app_pkg = request.POST['package']
|
|
else:
|
|
app_pkg = request.POST.get('app_package')
|
|
if app_pkg:
|
|
if not strict_package_check(app_pkg):
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Invalid package name',
|
|
api)
|
|
adb = os.environ['MOBSF_ADB']
|
|
g = proc.Group()
|
|
g.run([adb, 'logcat', app_pkg + ':V', '*:*'])
|
|
|
|
def read_process():
|
|
while g.is_pending():
|
|
lines = g.readlines()
|
|
for _, line in lines:
|
|
yield 'data:{}\n\n'.format(line)
|
|
return StreamingHttpResponse(read_process(),
|
|
content_type='text/event-stream')
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Invalid parameters',
|
|
api)
|
|
except Exception:
|
|
logger.exception('Logcat Streaming')
|
|
err = 'Error in Logcat streaming'
|
|
return print_n_send_error_response(request, err, api)
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def trigger_static_analysis(request, checksum):
|
|
"""On device APK Static Analysis."""
|
|
try:
|
|
identifier = None
|
|
if not is_md5(checksum):
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Invalid MD5')
|
|
package = get_package_name(checksum)
|
|
if not package:
|
|
return print_n_send_error_response(
|
|
request,
|
|
'Cannot get package name from checksum')
|
|
try:
|
|
identifier = get_device()
|
|
except Exception:
|
|
err = 'Cannot connect to Android Runtime'
|
|
return print_n_send_error_response(request, err)
|
|
env = Environment(identifier)
|
|
scan_type = env.get_apk(checksum, package)
|
|
if not scan_type:
|
|
err = 'Failed to download APK file'
|
|
return print_n_send_error_response(request, err)
|
|
file_name = f'{package}.apk'
|
|
if scan_type == 'apks':
|
|
file_name = f'{file_name}s'
|
|
data = {
|
|
'analyzer': 'static_analyzer',
|
|
'status': 'success',
|
|
'hash': checksum,
|
|
'scan_type': scan_type,
|
|
'file_name': file_name,
|
|
}
|
|
add_to_recent_scan(data)
|
|
return HttpResponseRedirect(f'/static_analyzer/{checksum}/')
|
|
except Exception:
|
|
msg = 'On device APK Static Analysis'
|
|
logger.exception(msg)
|
|
return print_n_send_error_response(request, msg)
|