Files
Mobile-Security-Framework-M…/mobsf/DynamicAnalyzer/views/android/dynamic_analyzer.py
T
Ajin AbrahamandCursor a23ff75ec0 Fix CSRF in dynamic analysis actions (#2679)
* 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]>
2026-09-20 16:36:10 -07:00

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)