mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-11 22:50:09 +00:00
Security Fixes [GHSA-8j49-mmcx-4mp5] Arbitrary File Read via Path Traversal in ZIP/APK Icon Extraction The android:icon attribute from an APK manifest was interpolated into file paths without validation, allowing a crafted APK to read arbitrary files from the server. Fixed by adding is_path_traversal() and is_safe_path() guards in find_icon_path_zip. Dependency bump included. [GHSA-3p54-567p-2wpr] CSRF Checks Not Enforced After Django Middleware Migration The migration from the deprecated MIDDLEWARE_CLASSES to MIDDLEWARE omitted CsrfViewMiddleware, SecurityMiddleware, and XFrameOptionsMiddleware, leaving CSRF, HSTS, and clickjacking protections silently disabled. All three have been restored to the active MIDDLEWARE tuple. The now-dead MIDDLEWARE_CLASSES block has been removed to prevent future confusion. [GHSA-x768-8642-mmq9] Zip Bomb Denial of Service via Per-File Size Limit Bypass The per-file size check in ZIP extraction logged a warning on oversized members but was missing a continue, allowing files exceeding ZIP_MAX_UNCOMPRESSED_FILE_SIZE to be extracted anyway as long as the aggregate limit was not reached. Fixed by adding the missing continue. [GHSA-95px-34x5-p37h] SSRF Port Restriction Bypass in assetlinks_check valid_host() was called with only the bare hostname, allowing android:port to be appended afterward without going through port validation, bypassing the HTTP/HTTPS-only restriction. Fixed with a two-layer approach: primary port allowlist check before URL assembly in get_browsable_activities, and a defence-in-depth re-check in _check_url. Hardening The hand-rolled safe_extract used os.path.abspath (symlink-blind) instead of os.path.realpath, creating a TOCTOU window where a symlink could redirect extraction outside the destination directory. Replaced with Python 3.12's tarfile.extractall(filter='data') (PEP 706), which rejects symlinks, hardlinks, absolute paths, and traversal members per-member before extraction. A robust realpath-based fallback is included for older Python versions. Both download_app_data call sites in the iOS dynamic analyser now wrap app_container and tarfile with shlex.quote when building the SSH tar command, as defensive coding hygiene. Refactoring is_path_traversal, is_safe_path, clean_filename, cmd_injection_check, is_pipe_or_link, and is_attack_pattern were scattered across utils.py and shared.py. All have been moved to mobsf/MobSF/security.py as the single authoritative location for security primitives. Developer Tooling AGENTS.md / CLAUDE.md - AI agent guidelines Added a coding-standards document for AI coding agents (Cursor, Claude, Codex) covering MobSF-specific secure-by-default patterns: path traversal guards, archive extraction safety, Django middleware active-tuple hygiene, split-validation anti-patterns (SSRF port bypass), guard completeness (continue/return/raise after every security check), and Django-specific security features (form validators, decorators, middleware). CLAUDE.md is a symlink to AGENTS.md.
333 lines
9.9 KiB
Python
333 lines
9.9 KiB
Python
# -*- coding: utf_8 -*-
|
|
"""Dynamic Analyzer Operations."""
|
|
import json
|
|
import logging
|
|
import os
|
|
import random
|
|
import shlex
|
|
import subprocess
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
from django.conf import settings
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from mobsf.DynamicAnalyzer.views.common.shared import (
|
|
invalid_params,
|
|
send_response,
|
|
)
|
|
from mobsf.MobSF.security import is_attack_pattern
|
|
from mobsf.DynamicAnalyzer.views.android.environment import (
|
|
Environment,
|
|
)
|
|
from mobsf.MobSF.security import cmd_injection_check
|
|
from mobsf.MobSF.utils import (
|
|
docker_translate_localhost,
|
|
get_adb,
|
|
get_device,
|
|
is_md5,
|
|
is_number,
|
|
)
|
|
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__)
|
|
|
|
|
|
# Helpers
|
|
|
|
def get_package_name(checksum):
|
|
"""Get Package Name from DB or Device."""
|
|
try:
|
|
static_android_db = StaticAnalyzerAndroid.objects.get(
|
|
MD5=checksum)
|
|
return static_android_db.PACKAGE_NAME
|
|
except Exception:
|
|
pkg_file = Path(settings.DWD_DIR) / 'packages.json'
|
|
if not pkg_file.exists():
|
|
return None
|
|
with pkg_file.open(encoding='utf-8') as src:
|
|
packages = json.load(src)
|
|
if packages.get(checksum):
|
|
return packages[checksum][0]
|
|
return None
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def mobsfy(request, api=False):
|
|
"""Configure Instance for Dynamic Analysis."""
|
|
logger.info('MobSFying Android instance')
|
|
data = {}
|
|
msg = 'Connection failed'
|
|
try:
|
|
identifier = request.POST['identifier']
|
|
if cmd_injection_check(identifier):
|
|
# Additional Check, not required
|
|
data = {
|
|
'status': 'failed',
|
|
'message': 'Command Injection Detected',
|
|
}
|
|
return send_response(data, api)
|
|
identifier = docker_translate_localhost(identifier)
|
|
create_env = Environment(identifier)
|
|
if not create_env.connect_n_mount():
|
|
data = {'status': 'failed', 'message': msg}
|
|
return send_response(data, api)
|
|
version = create_env.mobsfy_init()
|
|
if not version:
|
|
data = {'status': 'failed', 'message': msg}
|
|
return send_response(data, api)
|
|
else:
|
|
data = {'status': 'ok', 'android_version': version}
|
|
except Exception as exp:
|
|
logger.exception('MobSFying Android instance failed')
|
|
data = {'status': 'failed', 'message': str(exp)}
|
|
return send_response(data, api)
|
|
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def execute_adb(request, api=False):
|
|
"""Execute ADB Commands."""
|
|
data = {'status': 'ok', 'message': ''}
|
|
cmd = request.POST['cmd']
|
|
if cmd:
|
|
args = [get_adb(),
|
|
'-s',
|
|
get_device()]
|
|
try:
|
|
proc = subprocess.Popen(
|
|
args + cmd.split(' '), # lgtm [py/command-line-injection]
|
|
stdout=subprocess.PIPE, # Expected, cmd execute inside VM/AVD
|
|
stderr=subprocess.PIPE)
|
|
stdout, stderr = proc.communicate()
|
|
except Exception:
|
|
logger.exception('Executing ADB Commands')
|
|
if stdout or stderr:
|
|
out = stdout or stderr
|
|
out = out.decode('utf8', 'ignore')
|
|
else:
|
|
out = ''
|
|
data = {'status': 'ok', 'message': out}
|
|
return send_response(data, api)
|
|
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def get_component(request):
|
|
"""Get Android Component."""
|
|
data = {}
|
|
try:
|
|
env = Environment()
|
|
comp = request.POST['component']
|
|
bin_hash = request.POST['hash']
|
|
if is_attack_pattern(comp) or not is_md5(bin_hash):
|
|
return invalid_params()
|
|
comp = env.android_component(bin_hash, comp)
|
|
data = {'status': 'ok', 'message': comp}
|
|
except Exception as exp:
|
|
logger.exception('Getting Android Component')
|
|
data = {'status': 'failed', 'message': str(exp)}
|
|
return send_response(data)
|
|
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def run_apk(request):
|
|
"""Run Android APK."""
|
|
data = {}
|
|
try:
|
|
env = Environment()
|
|
md5_hash = request.POST['hash']
|
|
if not is_md5(md5_hash):
|
|
return invalid_params()
|
|
pkg = get_package_name(md5_hash)
|
|
if not pkg:
|
|
return invalid_params()
|
|
env.run_app(pkg)
|
|
data = {'status': 'ok'}
|
|
except Exception as exp:
|
|
logger.exception('Running the App')
|
|
data = {'status': 'failed', 'message': str(exp)}
|
|
return send_response(data)
|
|
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def take_screenshot(request, api=False):
|
|
"""Take Screenshot."""
|
|
logger.info('Taking screenshot')
|
|
data = {}
|
|
try:
|
|
env = Environment()
|
|
bin_hash = request.POST['hash']
|
|
if not is_md5(bin_hash):
|
|
return invalid_params(api)
|
|
data = {}
|
|
rand_int = random.randint(1, 1000000)
|
|
screen_dir = os.path.join(settings.UPLD_DIR,
|
|
bin_hash + '/screenshots-apk/')
|
|
if not os.path.exists(screen_dir):
|
|
os.makedirs(screen_dir)
|
|
outile = '{}screenshot-{}.png'.format(
|
|
screen_dir,
|
|
str(rand_int))
|
|
env.screen_shot(outile)
|
|
logger.info('Screenshot captured')
|
|
data = {'status': 'ok'}
|
|
except Exception as exp:
|
|
logger.exception('Taking screenshot')
|
|
data = {'status': 'failed', 'message': str(exp)}
|
|
return send_response(data, api)
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def screen_cast(request):
|
|
"""ScreenCast."""
|
|
data = {
|
|
'status': 'failed',
|
|
'message': 'Failed to stream screen'}
|
|
try:
|
|
env = Environment()
|
|
b64dat = env.screen_stream()
|
|
data = {
|
|
'status': 'ok',
|
|
'message': f'data:image/png;base64,{b64dat}'}
|
|
except Exception as exp:
|
|
logger.exception('Screen streaming')
|
|
data['message'] = str(exp)
|
|
return send_response(data)
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def touch(request):
|
|
"""Sending Touch/Swipe/Text Events."""
|
|
data = {
|
|
'status': 'failed',
|
|
'message': '',
|
|
}
|
|
try:
|
|
env = Environment()
|
|
x = request.POST['x']
|
|
y = request.POST['y']
|
|
event = request.POST['event']
|
|
max_x = request.POST.get('max_x', 0)
|
|
max_y = request.POST.get('max_y', 0)
|
|
|
|
if event == 'text':
|
|
args = ['text', shlex.quote(x)]
|
|
else:
|
|
if (not is_number(x)
|
|
or not is_number(y)
|
|
or not is_number(max_x)
|
|
or not is_number(max_y)):
|
|
return data
|
|
# Should not be greater than max screen size
|
|
swipe_x = str(min(int(float(x)) + 500, int(float(max_x))))
|
|
swipe_y = str(min(int(float(y)) + 500, int(float(max_y))))
|
|
|
|
if event == 'enter':
|
|
args = ['keyevent', '66']
|
|
elif event == 'backspace':
|
|
args = ['keyevent', '67']
|
|
elif event == 'left':
|
|
args = ['keyevent', '21']
|
|
elif event == 'right':
|
|
args = ['keyevent', '22']
|
|
elif event == 'swipe_up':
|
|
args = ['swipe', x, y, x, swipe_y]
|
|
elif event == 'swipe_down':
|
|
args = ['swipe', x, swipe_y, x, y]
|
|
elif event == 'swipe_left':
|
|
args = ['swipe', x, y, swipe_x, y]
|
|
elif event == 'swipe_right':
|
|
args = ['swipe', swipe_x, y, x, y]
|
|
else:
|
|
args = ['tap', x, y]
|
|
threading.Thread(target=env.adb_command,
|
|
args=(['input'] + args, True),
|
|
daemon=True).start()
|
|
data = {'status': 'ok'}
|
|
except Exception as exp:
|
|
logger.exception('Sending Touchscreen Events')
|
|
data['message'] = str(exp)
|
|
return send_response(data)
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def mobsf_ca(request, api=False):
|
|
"""Install and Remove MobSF Proxy RootCA."""
|
|
data = {}
|
|
try:
|
|
env = Environment()
|
|
action = request.POST['action']
|
|
if action == 'install':
|
|
env.install_mobsf_ca(action)
|
|
data = {'status': 'ok', 'message': 'installed'}
|
|
elif action == 'remove':
|
|
env.install_mobsf_ca(action)
|
|
data = {'status': 'ok', 'message': 'removed'}
|
|
else:
|
|
data = {'status': 'failed',
|
|
'message': 'Action not supported'}
|
|
except Exception as exp:
|
|
logger.exception('MobSF RootCA Handler')
|
|
data = {'status': 'failed', 'message': str(exp)}
|
|
return send_response(data, api)
|
|
# AJAX
|
|
|
|
|
|
@login_required
|
|
@permission_required(Permissions.SCAN)
|
|
@require_http_methods(['POST'])
|
|
def global_proxy(request, api=False):
|
|
"""Set/unset global proxy."""
|
|
data = {}
|
|
try:
|
|
env = Environment()
|
|
version = env.get_android_version()
|
|
action = request.POST['action']
|
|
if action == 'set':
|
|
env.set_global_proxy(version)
|
|
data = {'status': 'ok', 'message': 'set'}
|
|
elif action == 'unset':
|
|
env.unset_global_proxy()
|
|
data = {'status': 'ok', 'message': 'unset'}
|
|
else:
|
|
data = {'status': 'failed',
|
|
'message': 'Action not supported'}
|
|
except Exception as exp:
|
|
logger.exception('MobSF Global Proxy')
|
|
data = {'status': 'failed', 'message': str(exp)}
|
|
return send_response(data, api)
|