From 5ce7e237af1f06edf4adfeb8f830ecc730343a1a Mon Sep 17 00:00:00 2001
From: Ajin Abraham
Date: Thu, 28 Nov 2024 17:26:50 -0800
Subject: [PATCH] [4.2.8] Multiple APK Analysis improvements, general Code QA &
bug fixes (#2470)
* Dockerfile QA
* Add sdk-build-tools to Docker image
* Replace biplist with plistlib std lib
* Fixed a bug in iOS pbxproj parsing
* Added support for APK parsing with aapt2/aapt
* Use aapt/aapt2 as a fallback for APK parsing, files listing and string extraction
* Added "started at" to Scan task queue model #2463
* Tasks List API to return string status #2464
* Replaced all minidom calls with defusedxml.minidom
* Code QA on android manifest data extraction and parsing
* Improved android file analysis
* Improved android manifest data extraction
* Improved android icon file extraction
* Improved android app name extraction
* Improved android appstore package details extraction
* Android string extraction to fallback on aapt2 strings
* APK analysis arguments refactor
* Handle packed APKs, refactor unzip to handle malformed APK files
* Handle reserved filename conflict during ZIP extraction
* Explicit Zipslip handling during ZIP extraction
* Graceful files extraction on unzip failure
* Removed bail out and continue analysis
* Moved androguard parsing to the start of static analysis
* AndroidManifest.xml fallback from apktool to androguard during extraction and parsing
* Updated Tasks UI to show started at
---
Dockerfile | 5 +-
mobsf/DynamicAnalyzer/views/common/device.py | 8 +-
mobsf/MobSF/init.py | 2 +-
mobsf/MobSF/security.py | 29 ++-
mobsf/MobSF/settings.py | 2 +
mobsf/MobSF/utils.py | 29 +++
mobsf/StaticAnalyzer/models.py | 1 +
.../tools/androguard4/resources/public.py | 5 +-
mobsf/StaticAnalyzer/views/android/aapt.py | 151 +++++++++++
mobsf/StaticAnalyzer/views/android/apk.py | 169 +++++-------
mobsf/StaticAnalyzer/views/android/app.py | 140 +++++++---
.../views/android/cert_analysis.py | 55 ++--
.../views/android/code_analysis.py | 59 ++---
.../StaticAnalyzer/views/android/converter.py | 35 +++
.../views/android/db_interaction.py | 4 +-
.../views/android/icon_analysis.py | 117 ++++-----
mobsf/StaticAnalyzer/views/android/jar_aar.py | 57 +---
.../views/android/manifest_analysis.py | 9 +-
.../views/android/manifest_utils.py | 245 +++++++++---------
.../views/android/network_security.py | 5 +-
.../StaticAnalyzer/views/android/playstore.py | 16 +-
mobsf/StaticAnalyzer/views/android/so.py | 14 +-
mobsf/StaticAnalyzer/views/android/strings.py | 72 ++---
.../views/android/views/manifest_view.py | 27 +-
mobsf/StaticAnalyzer/views/common/appsec.py | 2 +-
.../StaticAnalyzer/views/common/async_task.py | 10 +-
.../views/common/shared_func.py | 142 ++++++++--
mobsf/StaticAnalyzer/views/ios/ipa.py | 3 +
.../views/ios/plist_analysis.py | 20 +-
.../views/ios/views/view_source.py | 15 +-
mobsf/templates/base/base_layout.html | 10 +
mobsf/templates/general/tasks.html | 108 +++++---
.../android_binary_analysis.html | 6 +-
.../android_source_analysis.html | 6 +-
.../static_analysis/ios_binary_analysis.html | 14 +-
.../static_analysis/ios_source_analysis.html | 2 +
.../windows_binary_analysis.html | 2 +
poetry.lock | 23 +-
pyproject.toml | 4 +-
scripts/dependencies.sh | 4 +
40 files changed, 1029 insertions(+), 598 deletions(-)
create mode 100644 mobsf/StaticAnalyzer/views/android/aapt.py
diff --git a/Dockerfile b/Dockerfile
index 30da480eeb..1ae2880009 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,10 +20,6 @@ ENV DEBIAN_FRONTEND=noninteractive \
USER_ID=9901 \
MOBSF_PLATFORM=docker \
MOBSF_ADB_BINARY=/usr/bin/adb \
- JDK_FILE=openjdk-22.0.2_linux-x64_bin.tar.gz \
- JDK_FILE_ARM=openjdk-22.0.2_linux-aarch64_bin.tar.gz \
- WKH_FILE=wkhtmltox_0.12.6.1-3.bookworm_amd64.deb \
- WKH_FILE_ARM=wkhtmltox_0.12.6.1-3.bookworm_arm64.deb \
JAVA_HOME=/jdk-22.0.2 \
PATH=/jdk-22.0.2/bin:/root/.local/bin:$PATH \
DJANGO_SUPERUSER_USERNAME=mobsf \
@@ -32,6 +28,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
# See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run
RUN apt update -y && \
apt install -y --no-install-recommends \
+ android-sdk-build-tools \
android-tools-adb \
build-essential \
curl \
diff --git a/mobsf/DynamicAnalyzer/views/common/device.py b/mobsf/DynamicAnalyzer/views/common/device.py
index 144400d7ee..2340f4cd3a 100644
--- a/mobsf/DynamicAnalyzer/views/common/device.py
+++ b/mobsf/DynamicAnalyzer/views/common/device.py
@@ -19,11 +19,11 @@
read_sqlite,
)
-from biplist import (
- writePlistToString,
+from plistlib import (
+ FMT_XML,
+ dumps,
)
-
logger = logging.getLogger(__name__)
@@ -57,7 +57,7 @@ def view_file(request, api=False):
return print_n_send_error_response(request, err, api)
dat = sfile.read_text('ISO-8859-1')
if fil.endswith('.plist') and dat.startswith('bplist0'):
- dat = writePlistToString(dat).decode('utf-8', 'ignore')
+ dat = dumps(dat, fmt=FMT_XML).decode('utf-8', 'ignore')
if fil.endswith(('.xml', '.plist')) and typ in ['xml', 'plist']:
rtyp = 'xml'
elif typ == 'db':
diff --git a/mobsf/MobSF/init.py b/mobsf/MobSF/init.py
index 14b6d8abde..812a34818f 100644
--- a/mobsf/MobSF/init.py
+++ b/mobsf/MobSF/init.py
@@ -18,7 +18,7 @@
logger = logging.getLogger(__name__)
-VERSION = '4.2.7'
+VERSION = '4.2.8'
BANNER = r"""
__ __ _ ____ _____ _ _ ____
| \/ | ___ | |__/ ___|| ___|_ _| || | |___ \
diff --git a/mobsf/MobSF/security.py b/mobsf/MobSF/security.py
index 243b7207c9..3687c4faf4 100644
--- a/mobsf/MobSF/security.py
+++ b/mobsf/MobSF/security.py
@@ -6,10 +6,11 @@
import sys
from shutil import which
from pathlib import Path
+from platform import system
from concurrent.futures import ThreadPoolExecutor
-
from mobsf.MobSF.utils import (
+ find_aapt,
find_java_binary,
gen_sha256_hash,
get_adb,
@@ -72,9 +73,21 @@ def get_executable_hashes():
downloaded_tools,
manage_py,
]
+ aapt = 'aapt'
+ aapt2 = 'aapt2'
+ if system() == 'Windows':
+ aapt = 'aapt.exe'
+ aapt2 = 'aapt2.exe'
+ aapts = [find_aapt(aapt), find_aapt(aapt2)]
+ exec_loc.extend(Path(a) for a in aapts if a)
# External binaries used directly by MobSF
system_bins = [
+ 'aapt',
+ 'aapt.exe',
+ 'aapt2',
+ 'aapt2.exe',
'adb',
+ 'adb.exe',
'which',
'wkhtmltopdf',
'httptools',
@@ -110,6 +123,8 @@ def get_executable_hashes():
settings.CLASSDUMP_BINARY,
settings.CLASSDUMP_SWIFT_BINARY,
getattr(settings, 'BUNDLE_TOOL', ''),
+ getattr(settings, 'AAPT2_BINARY', ''),
+ getattr(settings, 'AAPT_BINARY', ''),
]
for ubin in user_defined_bins:
if ubin:
@@ -222,3 +237,15 @@ def sanitize_filename(filename):
# Remove leading and trailing underscores
safe_filename = safe_filename.strip('_')
return safe_filename
+
+
+def sanitize_for_logging(filename: str, max_length: int = 255) -> str:
+ """Sanitize a filename to prevent log injection."""
+ # Remove newline, carriage return, and other risky characters
+ filename = filename.replace('\n', '_').replace('\r', '_').replace('\t', '_')
+
+ # Allow only safe characters (alphanumeric, underscore, dash, and period)
+ filename = re.sub(r'[^a-zA-Z0-9._-]', '_', filename)
+
+ # Truncate filename to the maximum allowed length
+ return filename[:max_length]
diff --git a/mobsf/MobSF/settings.py b/mobsf/MobSF/settings.py
index e2df638000..e1c708f245 100644
--- a/mobsf/MobSF/settings.py
+++ b/mobsf/MobSF/settings.py
@@ -462,6 +462,8 @@
VD2SVG_BINARY = os.getenv('MOBSF_VD2SVG_BINARY', '')
APKTOOL_BINARY = os.getenv('MOBSF_APKTOOL_BINARY', '')
ADB_BINARY = os.getenv('MOBSF_ADB_BINARY', '')
+ AAPT2_BINARY = os.getenv('MOBSF_AAPT2_BINARY', '')
+ AAPT_BINARY = os.getenv('MOBSF_AAPT_BINARY', '')
# iOS 3P Tools
JTOOL_BINARY = os.getenv('MOBSF_JTOOL_BINARY', '')
diff --git a/mobsf/MobSF/utils.py b/mobsf/MobSF/utils.py
index 1cc2122d47..b6a4f53202 100755
--- a/mobsf/MobSF/utils.py
+++ b/mobsf/MobSF/utils.py
@@ -184,6 +184,32 @@ def find_java_binary():
return 'java'
+def find_aapt(tool_name):
+ """Find the specified tool (aapt or aapt2)."""
+ # Check system PATH for the tool
+ tool_path = shutil.which(tool_name)
+ if tool_path:
+ return tool_path
+
+ # Check common Android SDK locations
+ home_dir = Path.home() # Get the user's home directory
+ sdk_paths = [
+ home_dir / 'Library' / 'Android' / 'sdk', # macOS
+ home_dir / 'Android' / 'Sdk', # Linux
+ home_dir / 'AppData' / 'Local' / 'Android' / 'Sdk', # Windows
+ ]
+
+ for sdk_path in sdk_paths:
+ build_tools_path = sdk_path / 'build-tools'
+ if build_tools_path.exists():
+ for version in sorted(build_tools_path.iterdir(), reverse=True):
+ tool_path = version / tool_name
+ if tool_path.exists():
+ return str(tool_path)
+
+ return None
+
+
def print_n_send_error_response(request,
msg,
api=False,
@@ -667,6 +693,8 @@ def common_check(instance_id):
def is_path_traversal(user_input):
"""Check for path traversal."""
+ if not user_input:
+ return False
if (('../' in user_input)
or ('%2e%2e' in user_input)
or ('..' in user_input)
@@ -836,6 +864,7 @@ def get_android_dm_exception_msg():
def get_android_src_dir(app_dir, typ):
"""Get Android source code location."""
+ src = None
if typ == 'apk':
src = app_dir / 'java_source'
elif typ == 'studio':
diff --git a/mobsf/StaticAnalyzer/models.py b/mobsf/StaticAnalyzer/models.py
index bc7a22f55d..349653b9dc 100755
--- a/mobsf/StaticAnalyzer/models.py
+++ b/mobsf/StaticAnalyzer/models.py
@@ -177,6 +177,7 @@ class EnqueuedTask(models.Model):
file_name = models.CharField(max_length=255)
created_at = models.DateTimeField(default=timezone.now)
status = models.CharField(max_length=255, default='Enqueued')
+ started_at = models.DateTimeField(null=True)
completed_at = models.DateTimeField(null=True)
app_name = models.CharField(max_length=255, default='')
diff --git a/mobsf/StaticAnalyzer/tools/androguard4/resources/public.py b/mobsf/StaticAnalyzer/tools/androguard4/resources/public.py
index 7b909ac856..42903ea919 100644
--- a/mobsf/StaticAnalyzer/tools/androguard4/resources/public.py
+++ b/mobsf/StaticAnalyzer/tools/androguard4/resources/public.py
@@ -1,7 +1,8 @@
# -*- coding: utf_8 -*-
# flake8: noqa
import os
-from xml.dom import minidom
+
+from defusedxml.minidom import parseString
_public_res = None
# copy the newest sdk/platforms/android-?/data/res/values/public.xml here
@@ -11,7 +12,7 @@
xmlfile = os.path.join(root, "public.xml")
if os.path.isfile(xmlfile):
with open(xmlfile, "r") as fp:
- _xml = minidom.parseString(fp.read())
+ _xml = parseString(fp.read())
for element in _xml.getElementsByTagName("public"):
_type = element.getAttribute('type')
_name = element.getAttribute('name')
diff --git a/mobsf/StaticAnalyzer/views/android/aapt.py b/mobsf/StaticAnalyzer/views/android/aapt.py
new file mode 100644
index 0000000000..3163a4830c
--- /dev/null
+++ b/mobsf/StaticAnalyzer/views/android/aapt.py
@@ -0,0 +1,151 @@
+# -*- coding: utf_8 -*-
+"""Use aapt2 to extract APK features."""
+import re
+import logging
+import subprocess
+from platform import system
+from pathlib import Path
+
+from django.conf import settings
+
+from mobsf.MobSF.utils import (
+ find_aapt,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class AndroidAAPT:
+
+ def __init__(self, apk_path):
+ self.aapt2_path = None
+ self.aapt_path = None
+ self.apk_path = apk_path
+ self.data = {
+ 'permissions': [],
+ 'uses_features': {},
+ 'package': None,
+ 'application_label': None,
+ 'application_icon': None,
+ 'launchable_activity': None,
+ 'min_sdk_version': None,
+ 'target_sdk_version': None,
+ }
+
+ # Check for custom AAPT2 path in settings
+ if (getattr(settings, 'AAPT2_BINARY', '')
+ and len(settings.AAPT2_BINARY) > 0
+ and Path(settings.AAPT2_BINARY).exists()):
+ self.aapt2_path = settings.AAPT2_BINARY
+ else:
+ aapt2 = 'aapt2.exe' if system() == 'Windows' else 'aapt2'
+ self.aapt2_path = find_aapt(aapt2)
+
+ # Check for custom AAPT path in settings
+ if (getattr(settings, 'AAPT_BINARY', '')
+ and len(settings.AAPT_BINARY) > 0
+ and Path(settings.AAPT_BINARY).exists()):
+ self.aapt_path = settings.AAPT_BINARY
+ else:
+ aapt = 'aapt.exe' if system() == 'Windows' else 'aapt'
+ self.aapt_path = find_aapt(aapt)
+
+ # Ensure both aapt and aapt2 are found
+ if not (self.aapt2_path and self.aapt_path):
+ raise FileNotFoundError('aapt and aapt2 found')
+
+ def _execute_command(self, args):
+ try:
+ out = subprocess.check_output(
+ args,
+ stderr=subprocess.STDOUT)
+ return out.decode('utf-8', errors='ignore')
+ except subprocess.CalledProcessError as e:
+ logger.warning(e.output)
+ return None
+
+ def _get_strings(self, output):
+ # Regex to match strings while ignoring paths (strings without slashes)
+ pattern = r'String #[\d]+ : ([^\/\n]+)'
+ matches = re.findall(pattern, output)
+ # Strip whitespace and return the extracted strings
+ return [match.strip() for match in matches]
+
+ def _parse_badging(self, output):
+ # Match the package information
+ package_match = re.search(r'package: name=\'([\w\.]+)\'', output)
+ if package_match:
+ self.data['package'] = package_match.group(1)
+
+ # Match permissions
+ permissions = re.findall(r'uses-permission: name=\'([\w\.]+)\'', output)
+ if permissions:
+ self.data['permissions'] = permissions
+
+ # Match minSdkVersion
+ min_sdk_match = re.search(r'minSdkVersion:\'(\d+)\'', output)
+ if min_sdk_match:
+ self.data['min_sdk_version'] = min_sdk_match.group(1)
+
+ # Match targetSdkVersion
+ target_sdk_match = re.search(r'targetSdkVersion:\'(\d+)\'', output)
+ if target_sdk_match:
+ self.data['target_sdk_version'] = target_sdk_match.group(1)
+
+ # Match application label
+ label_match = re.search(r'application-label(?:-[\w\-]+)?:\'([^\']+)\'', output)
+ if label_match:
+ self.data['application_label'] = label_match.group(1)
+
+ # Match application icon
+ icon_match = re.search(r'application:.*icon=\'([^\']+)\'', output)
+ if icon_match:
+ self.data['application_icon'] = icon_match.group(1)
+
+ # Match launchable activity
+ activity_match = re.search(r'launchable-activity: name=\'([\w\.]+)\'', output)
+ if activity_match:
+ self.data['launchable_activity'] = activity_match.group(1)
+
+ # Match used features
+ features = {}
+ feature_matches = re.findall(
+ (r'(uses-feature(?:-not-required)?|uses-implied-feature): '
+ r'name=\'([\w\.]+)\'(?: reason=\'([^\']+)\')?'),
+ output,
+ )
+ for feature_type, feature_name, reason in feature_matches:
+ features[feature_name] = {
+ 'type': feature_type,
+ # e.g., 'uses-feature',
+ # 'uses-feature-not-required',
+ # 'uses-implied-feature'
+ 'reason': reason if reason else 'No reason provided',
+ }
+ self.data['uses_features'] = features
+
+ return self.data
+
+ def get_apk_files(self):
+ """List all files in the APK."""
+ output = self._execute_command(
+ [self.aapt_path, 'list', self.apk_path])
+ if output:
+ return output.splitlines()
+ return []
+
+ def get_apk_strings(self):
+ """Extract strings from the APK."""
+ output = self._execute_command(
+ [self.aapt2_path, 'dump', 'strings', self.apk_path])
+ if output:
+ return self._get_strings(output)
+ return []
+
+ def get_apk_features(self):
+ """Extract features from the APK."""
+ output = self._execute_command(
+ [self.aapt2_path, 'dump', 'badging', self.apk_path])
+ if output:
+ return self._parse_badging(output)
+ return self.data
diff --git a/mobsf/StaticAnalyzer/views/android/apk.py b/mobsf/StaticAnalyzer/views/android/apk.py
index 5555e64a0c..57b4450bc2 100644
--- a/mobsf/StaticAnalyzer/views/android/apk.py
+++ b/mobsf/StaticAnalyzer/views/android/apk.py
@@ -1,4 +1,4 @@
-
+# -*- coding: utf_8 -*-
"""Android APK and Source Analysis."""
import logging
import shutil
@@ -29,8 +29,9 @@
library_analysis,
)
from mobsf.StaticAnalyzer.views.android.app import (
- get_app_name,
- parse_apk,
+ aapt_parse,
+ androguard_parse,
+ get_apk_name,
)
from mobsf.StaticAnalyzer.views.android.cert_analysis import (
cert_info,
@@ -53,10 +54,12 @@
manifest_analysis,
)
from mobsf.StaticAnalyzer.views.android.manifest_utils import (
- get_manifest,
- manifest_data,
+ extract_manifest_data,
+ get_parsed_manifest,
+)
+from mobsf.StaticAnalyzer.views.android.playstore import (
+ get_app_details,
)
-from mobsf.StaticAnalyzer.views.android.playstore import get_app_details
from mobsf.StaticAnalyzer.views.android.strings import (
get_strings_metadata,
)
@@ -73,6 +76,7 @@
)
from mobsf.StaticAnalyzer.views.common.async_task import (
async_analysis,
+ enqueued_task_init,
update_enqueued_task,
)
from mobsf.MobSF.views.authorization import (
@@ -96,39 +100,27 @@ def get_size_and_hashes(app_dic):
app_dic['sha1'], app_dic['sha256'] = hash_gen(app_dic['md5'], app_dic['app_path'])
-def get_manifest_data(checksum, app_dic, andro_apk=None):
+def get_manifest_data(app_dic):
"""Get Manifest Data."""
- # Manifest XML
- mani_file, ns, mani_xml = get_manifest(
- checksum,
- app_dic['app_path'],
- app_dic['app_dir'],
- app_dic['tools_dir'],
- app_dic['zipped'],
- andro_apk,
- )
- app_dic['manifest_file'] = mani_file
- app_dic['parsed_xml'] = mani_xml
+ # Manifest XML parsed
+ get_parsed_manifest(app_dic)
+ # Populates manifest_file, manifest_namespace, manifest_parsed_xml
+
# Manifest data extraction
- man_data = manifest_data(
- checksum,
- app_dic['parsed_xml'],
- ns)
+ man_data_dic = extract_manifest_data(app_dic)
# Manifest Analysis
- man_analysis = manifest_analysis(
- checksum,
- app_dic['parsed_xml'],
- ns,
- man_data,
- app_dic['zipped'],
- app_dic['app_dir'])
- return man_data, man_analysis
+ man_analysis = manifest_analysis(app_dic, man_data_dic)
+ return man_data_dic, man_analysis
-def print_scan_subject(checksum, app_dic, man_data):
+def print_scan_subject(app_dic, man_data):
"""Log scan subject."""
- app_name = app_dic['real_name']
- pkg_name = man_data['packagename']
+ checksum = app_dic['md5']
+ app_name = app_dic.get('real_name')
+ pkg_name = man_data.get('packagename')
+ pkg_name2 = app_dic.get('apk_features', {}).get('package')
+ if not pkg_name:
+ pkg_name = pkg_name2
subject = 'Android App'
if app_name and pkg_name:
subject = f'{app_name} ({pkg_name})'
@@ -141,7 +133,13 @@ def print_scan_subject(checksum, app_dic, man_data):
append_scan_status(checksum, msg)
if subject == 'Failed':
subject = f'({subject})'
- return subject
+ app_dic['subject'] = subject
+
+
+def clean_up(app_dic):
+ """Clean up for pickling."""
+ app_dic['androguard_apk'] = None
+ app_dic['androguard_apk_resources'] = None
def apk_analysis_task(checksum, app_dic, rescan, queue=False):
@@ -150,64 +148,42 @@ def apk_analysis_task(checksum, app_dic, rescan, queue=False):
try:
if queue:
settings.ASYNC_ANALYSIS = True
+ enqueued_task_init(checksum)
append_scan_status(checksum, 'init')
get_size_and_hashes(app_dic)
msg = 'Extracting APK'
logger.info(msg)
append_scan_status(checksum, msg)
+ app_dic['zipped'] = 'apk'
+ # Extract APK and get files
app_dic['files'] = unzip(
checksum,
app_dic['app_path'],
app_dic['app_dir'])
- logger.info('APK Extracted')
- if not app_dic['files']:
- # Can't Analyze APK, bail out.
- msg = 'APK file is invalid or corrupt'
- logger.error(msg)
- append_scan_status(checksum, msg)
- if queue:
- return update_enqueued_task(
- checksum, 'Failed', msg)
- return context, msg
- app_dic['zipped'] = 'apk'
- app_dic['certz'] = get_hardcoded_cert_keystore(
- checksum,
- app_dic['files'])
- # Parse APK with Androguard4
- andro_apk = parse_apk(
- checksum,
- app_dic['app_path'])
+ # Extract APK data with Androguard
+ androguard_parse(app_dic)
+ # Populates androguard_apk, androguard_manifest_xml, androguard_string_resources
+ # Extract APK data with AAPT/AAPT2
+ aapt_parse(app_dic) # Populates apk_features, files, apk_strings
+ get_hardcoded_cert_keystore(app_dic) # Populates file_analysis
# Manifest Data
- man_data, man_analysis = get_manifest_data(
- checksum,
- app_dic,
- andro_apk)
+ man_data_dic, man_analysis = get_manifest_data(app_dic)
# Get App name
- app_dic['real_name'] = get_app_name(
- andro_apk,
- app_dic['app_dir'],
- True)
- # Print scan subject
- subject = print_scan_subject(checksum, app_dic, man_data)
- app_dic['playstore'] = get_app_details(
- checksum,
- man_data['packagename'])
+ get_apk_name(app_dic) # Populates real_name
+ print_scan_subject(app_dic, man_data_dic) # Populate subject
+ get_app_details(app_dic, man_data_dic) # Populates playstore
# Malware Permission check
mal_perms = permissions.check_malware_permission(
checksum,
- man_data['perm'])
+ man_data_dic['perm'])
man_analysis['malware_permissions'] = mal_perms
# Get icon
- # apktool should run before this
- get_icon_apk(andro_apk, app_dic)
+ get_icon_apk(app_dic) # Populates icon_path
elf_dict = library_analysis(
checksum,
app_dic['app_dir'],
'elf')
- cert_dic = cert_info(
- andro_apk,
- app_dic,
- man_data)
+ cert_dic = cert_info(app_dic, man_data_dic)
apkid_results = apkid.apkid_analysis(
checksum,
app_dic['app_path'])
@@ -229,14 +205,11 @@ def apk_analysis_task(checksum, app_dic, rescan, queue=False):
app_dic['app_dir'],
app_dic['zipped'],
app_dic['manifest_file'],
- man_data['perm'])
+ man_data_dic['perm'])
# Get the strings and metadata
get_strings_metadata(
- checksum,
- andro_apk,
- app_dic['app_dir'],
+ app_dic,
elf_dict['elf_strings'],
- app_dic['zipped'],
['.java'],
code_an_dic)
# Firebase DB Check
@@ -249,7 +222,7 @@ def apk_analysis_task(checksum, app_dic, rescan, queue=False):
code_an_dic['urls_list'])
context = save_get_ctx(
app_dic,
- man_data,
+ man_data_dic,
man_analysis,
code_an_dic,
cert_dic,
@@ -260,13 +233,16 @@ def apk_analysis_task(checksum, app_dic, rescan, queue=False):
)
if queue:
return update_enqueued_task(
- checksum, subject, 'Success')
+ checksum, app_dic['subject'], 'Success')
return context, None
except Exception as exp:
if queue:
return update_enqueued_task(
checksum, 'Failed', repr(exp))
return context, repr(exp)
+ finally:
+ # Clean up
+ clean_up(app_dic)
def generate_dynamic_context(request, app_dic, checksum, context, api):
@@ -312,6 +288,7 @@ def src_analysis_task(checksum, app_dic, rescan, pro_type, queue=False):
try:
if queue:
settings.ASYNC_ANALYSIS = True
+ enqueued_task_init(checksum)
cert_dic = {
'certificate_info': '',
'certificate_status': '',
@@ -321,45 +298,33 @@ def src_analysis_task(checksum, app_dic, rescan, pro_type, queue=False):
app_dic['secrets'] = []
# Above fields are only available for APK and not ZIP
app_dic['zipped'] = pro_type
- app_dic['certz'] = get_hardcoded_cert_keystore(
- checksum,
- app_dic['files'])
+ get_hardcoded_cert_keystore(app_dic)
# Manifest Data
- man_data, man_analysis = get_manifest_data(
- checksum,
- app_dic)
+ man_data_dic, man_analysis = get_manifest_data(app_dic)
# Get app name
- app_dic['real_name'] = get_app_name(
- None,
- app_dic['app_dir'],
- False)
+ get_apk_name(app_dic)
# Print scan subject
- subject = print_scan_subject(checksum, app_dic, man_data)
- app_dic['playstore'] = get_app_details(
- checksum,
- man_data['packagename'])
+ print_scan_subject(app_dic, man_data_dic)
+ get_app_details(app_dic, man_data_dic)
# Malware Permission check
mal_perms = permissions.check_malware_permission(
checksum,
- man_data['perm'])
+ man_data_dic['perm'])
man_analysis['malware_permissions'] = mal_perms
# Get icon
get_icon_from_src(
app_dic,
- man_data['icons'])
+ man_data_dic['icons'])
code_an_dic = code_analysis(
checksum,
app_dic['app_dir'],
app_dic['zipped'],
app_dic['manifest_file'],
- man_data['perm'])
+ man_data_dic['perm'])
# Get the strings and metadata
get_strings_metadata(
- checksum,
- None,
- app_dic['app_dir'],
+ app_dic,
None,
- app_dic['zipped'],
['.java', '.kt'],
code_an_dic)
# Firebase DB Check
@@ -378,7 +343,7 @@ def src_analysis_task(checksum, app_dic, rescan, pro_type, queue=False):
code_an_dic['domains'], [])
context = save_get_ctx(
app_dic,
- man_data,
+ man_data_dic,
man_analysis,
code_an_dic,
cert_dic,
@@ -389,7 +354,7 @@ def src_analysis_task(checksum, app_dic, rescan, pro_type, queue=False):
)
if queue:
return update_enqueued_task(
- checksum, subject, 'Success')
+ checksum, app_dic['subject'], 'Success')
except Exception as exp:
if queue:
return update_enqueued_task(
diff --git a/mobsf/StaticAnalyzer/views/android/app.py b/mobsf/StaticAnalyzer/views/android/app.py
index 11e8a32e6f..5a2f22eafc 100644
--- a/mobsf/StaticAnalyzer/views/android/app.py
+++ b/mobsf/StaticAnalyzer/views/android/app.py
@@ -8,6 +8,9 @@
from mobsf.StaticAnalyzer.tools.androguard4 import (
apk,
)
+from mobsf.StaticAnalyzer.views.android import (
+ aapt,
+)
from mobsf.MobSF.utils import (
append_scan_status,
)
@@ -15,53 +18,122 @@
logger = logging.getLogger(__name__)
-def parse_apk(checksum, app_path):
- """Androguard4 APK."""
+def aapt_parse(app_dict):
+ """Extract features from APK using aapt/aapt2."""
+ checksum = app_dict['md5']
+ app_dict['apk_features'] = {}
+ app_dict['apk_strings'] = []
+ try:
+ msg = 'Extracting APK features using aapt/aapt2'
+ logger.info(msg)
+ append_scan_status(checksum, msg)
+ aapt_obj = aapt.AndroidAAPT(app_dict['app_path'])
+ app_dict['apk_features'] = aapt_obj.get_apk_features()
+ if not app_dict.get('files'):
+ app_dict['files'] = aapt_obj.get_apk_files()
+ app_dict['apk_strings'] = aapt_obj.get_apk_strings()
+ except FileNotFoundError:
+ msg = 'aapt and aapt2 not found, skipping APK feature extraction'
+ logger.warning(msg)
+ append_scan_status(checksum, msg)
+ except Exception as exp:
+ msg = 'Failed to extract APK features using aapt/aapt2'
+ logger.warning(msg)
+ append_scan_status(checksum, msg, repr(exp))
+
+
+def androguard_parse(app_dict):
+ """Extract features from APK using androguard."""
+ checksum = app_dict['md5']
+ app_dict['androguard_apk'] = None
+ app_dict['androguard_manifest_xml'] = None
+ app_dict['androguard_apk_resources'] = None
+ app_dict['androguard_apk_name'] = None
+ app_dict['androguard_apk_icon'] = None
try:
msg = 'Parsing APK with androguard'
logger.info(msg)
append_scan_status(checksum, msg)
- return apk.APK(app_path)
+ a = apk.APK(app_dict['app_path'])
+ if not a:
+ msg = 'Failed to parse APK with androguard'
+ logger.warning(msg)
+ append_scan_status(checksum, msg)
+ return
+ app_dict['androguard_apk'] = a
+ try:
+ app_dict['androguard_apk_name'] = a.get_app_name()
+ except Exception as exp:
+ msg = 'Failed to get app name with androguard'
+ logger.warning(msg)
+ append_scan_status(checksum, msg, repr(exp))
+ try:
+ app_dict['androguard_apk_icon'] = a.get_app_icon(max_dpi=0xFFFE - 1)
+ except Exception as exp:
+ msg = 'Failed to get app icon with androguard'
+ logger.warning(msg)
+ append_scan_status(checksum, msg, repr(exp))
+ try:
+ xml = a.get_android_manifest_axml().get_xml()
+ app_dict['androguard_manifest_xml'] = xml
+ except Exception as exp:
+ msg = 'Failed to parse AndroidManifest.xml with androguard'
+ logger.warning(msg)
+ append_scan_status(checksum, msg, repr(exp))
+ try:
+ app_dict['androguard_apk_resources'] = a.get_android_resources()
+ except Exception as exp:
+ msg = 'Failed to parse resources with androguard'
+ logger.warning(msg)
+ append_scan_status(checksum, msg, repr(exp))
except Exception as exp:
msg = 'Failed to parse APK with androguard'
+ logger.error(msg)
append_scan_status(checksum, msg, repr(exp))
- return None
-def get_app_name(a, app_dir, is_apk):
+def get_apk_name(app_dic):
"""Get app name."""
- base = Path(app_dir)
- if is_apk:
- if a:
- # Parsed Androguard APK Object
- try:
- app_name = a.get_app_name()
- if app_name:
- return app_name
- except Exception:
- logger.warning('Failed to get app name from parsed APK object')
- # Look for app_name in values folder.
- val = base / 'apktool_out' / 'res' / 'values'
- if val.exists():
- try:
- return get_app_name_from_values_folder(val.as_posix())
- except Exception:
- logger.error('Failed to get app name from values folder')
+ real_name = ''
+ base = Path(app_dic['app_dir'])
+
+ # Check if it's an APK and try to retrieve the app name
+ if app_dic.get('androguard_apk_name') or app_dic.get('apk_features'):
+ app_name = (
+ app_dic.get('androguard_apk_name')
+ or app_dic.get('apk_features', {}).get('application_label')
+ )
+ if app_name:
+ real_name = app_name
+ else:
+ # Fallback: Look for app_name in the values folder
+ values_path = base / 'apktool_out' / 'res' / 'values'
+ if values_path.exists():
+ try:
+ real_name = get_app_name_from_values_folder(values_path.as_posix())
+ except Exception:
+ logger.error('Failed to get app name from values folder')
+
+ # Check if it's source code and try to retrieve the app name
else:
- # For source code
try:
- strings_path = base / 'app' / 'src' / 'main' / 'res' / 'values'
- eclipse_path = base / 'res' / 'values'
- if strings_path.exists():
- return get_app_name_from_values_folder(
- strings_path.as_posix())
- elif eclipse_path.exists():
- return get_app_name_from_values_folder(
- eclipse_path.as_posix())
+ # Check paths for values folders
+ paths_to_check = [
+ base / 'app' / 'src' / 'main' / 'res' / 'values',
+ base / 'res' / 'values',
+ ]
+ for path in paths_to_check:
+ if path.exists():
+ real_name = get_app_name_from_values_folder(path.as_posix())
+ break
except Exception:
- logger.error('Failed to get app name')
- logger.warning('Cannot find app name')
- return ''
+ logger.error('Failed to get app name from source code')
+
+ if not real_name:
+ logger.warning('Cannot find app name')
+
+ # Update the app dictionary
+ app_dic['real_name'] = real_name
def get_app_name_from_values_folder(values_dir):
diff --git a/mobsf/StaticAnalyzer/views/android/cert_analysis.py b/mobsf/StaticAnalyzer/views/android/cert_analysis.py
index a5d7b18786..fae996e85f 100755
--- a/mobsf/StaticAnalyzer/views/android/cert_analysis.py
+++ b/mobsf/StaticAnalyzer/views/android/cert_analysis.py
@@ -42,15 +42,20 @@
}
-def get_hardcoded_cert_keystore(checksum, files):
+def get_hardcoded_cert_keystore(app_dic):
"""Returns the hardcoded certificate keystore."""
+ app_dic['file_analysis'] = []
+ checksum = app_dic['md5']
try:
+ files = app_dic.get('files') or app_dic.get('apk_files')
msg = 'Getting Hardcoded Certificates/Keystores'
logger.info(msg)
append_scan_status(checksum, msg)
findings = []
certz = []
key_store = []
+ if not files:
+ return
for file_name in files:
if '.' not in file_name:
continue
@@ -66,7 +71,7 @@ def get_hardcoded_cert_keystore(checksum, files):
if key_store:
desc = 'Hardcoded Keystore found.'
findings.append({'finding': desc, 'files': key_store})
- return findings
+ app_dic['file_analysis'] = findings
except Exception as exp:
msg = 'Getting Hardcoded Certificates/Keystores'
append_scan_status(checksum, msg, repr(exp))
@@ -183,26 +188,29 @@ def apksigtool_cert(checksum, apk_path, tools_dir):
av1 = True
else:
av1 = False
- _, sig_block = extract_v2_sig(apk_path)
- for pair in parse_apk_signing_block(sig_block).pairs:
- b = pair.value
- if isinstance(b, APKSignatureSchemeBlock):
- signed = True
- for signer in b.signers:
- av2 = b.is_v2()
- av3 = b.is_v3()
- if b.is_v3():
- min_sdk = signer.min_sdk
- certs_no = len(signer.signed_data.certificates)
- for cert in signer.signed_data.certificates:
- d = get_cert_details(cert.data)
- for i in d:
- if i not in certs:
- certs.append(i)
- p = get_pub_key_details(signer.public_key.data)
- for j in p:
- if j not in pub_keys:
- pub_keys.append(j)
+ try:
+ _, sig_block = extract_v2_sig(apk_path)
+ for pair in parse_apk_signing_block(sig_block).pairs:
+ b = pair.value
+ if isinstance(b, APKSignatureSchemeBlock):
+ signed = True
+ for signer in b.signers:
+ av2 = b.is_v2()
+ av3 = b.is_v3()
+ if b.is_v3():
+ min_sdk = signer.min_sdk
+ certs_no = len(signer.signed_data.certificates)
+ for cert in signer.signed_data.certificates:
+ d = get_cert_details(cert.data)
+ for i in d:
+ if i not in certs:
+ certs.append(i)
+ p = get_pub_key_details(signer.public_key.data)
+ for j in p:
+ if j not in pub_keys:
+ pub_keys.append(j)
+ except Exception:
+ logger.warning('Failed to get signature versions with apksigtool')
if signed:
certlist.append('Binary is signed')
@@ -289,12 +297,13 @@ def get_cert_data(checksum, a, app_path, tools_dir):
}
-def cert_info(a, app_dic, man_dict):
+def cert_info(app_dic, man_dict):
"""Return certificate information."""
try:
msg = 'Reading Code Signing Certificate'
logger.info(msg)
append_scan_status(app_dic['md5'], msg)
+ a = app_dic.get('androguard_apk')
manifestfile = None
manidat = ''
files = []
diff --git a/mobsf/StaticAnalyzer/views/android/code_analysis.py b/mobsf/StaticAnalyzer/views/android/code_analysis.py
index c2cc7f4326..27aa5a0e28 100755
--- a/mobsf/StaticAnalyzer/views/android/code_analysis.py
+++ b/mobsf/StaticAnalyzer/views/android/code_analysis.py
@@ -71,6 +71,17 @@ def permission_transform(perm_mappings):
def code_analysis(checksum, app_dir, typ, manifest_file, android_permissions):
"""Perform the code analysis."""
+ result = {
+ 'api': {},
+ 'behaviour': {},
+ 'perm_mappings': {},
+ 'findings': {},
+ 'niap': {},
+ 'urls_list': [],
+ 'urls': [],
+ 'emails': [],
+ 'sbom': {},
+ }
try:
root = Path(settings.BASE_DIR) / 'StaticAnalyzer' / 'views'
and_rules = root / 'android' / 'rules'
@@ -78,15 +89,6 @@ def code_analysis(checksum, app_dir, typ, manifest_file, android_permissions):
api_rules = and_rules / 'android_apis.yaml'
perm_rules = and_rules / 'android_permissions.yaml'
niap_rules = and_rules / 'android_niap.yaml'
- code_findings = {}
- api_findings = {}
- perm_mappings = {}
- behaviour_findings = {}
- niap_findings = {}
- email_n_file = []
- url_n_file = []
- url_list = []
- sbom = {}
app_dir = Path(app_dir)
src = get_android_src_dir(app_dir, typ).as_posix() + '/'
skp = settings.SKIP_CLASS_PATH
@@ -104,13 +106,14 @@ def code_analysis(checksum, app_dir, typ, manifest_file, android_permissions):
file_data = sast.read_files()
# SBOM Analysis
- sbom = sbom_analysis.sbom(app_dir, file_data)
+ result['sbom'] = sbom_analysis.sbom(app_dir, file_data)
msg = 'Android SBOM Analysis Completed'
logger.info(msg)
append_scan_status(checksum, msg)
# Code Analysis
- code_findings = sast.run_rules(file_data, code_rules.as_posix())
+ result['findings'] = sast.run_rules(
+ file_data, code_rules.as_posix())
msg = 'Android SAST Completed'
logger.info(msg)
append_scan_status(checksum, msg)
@@ -120,19 +123,21 @@ def code_analysis(checksum, app_dir, typ, manifest_file, android_permissions):
logger.info(msg)
append_scan_status(checksum, msg)
sast = SastEngine(options, src)
- api_findings = sast.run_rules(file_data, api_rules.as_posix())
+ result['api'] = sast.run_rules(
+ file_data, api_rules.as_posix())
msg = 'Android API Analysis Completed'
logger.info(msg)
append_scan_status(checksum, msg)
# Permission Mapping
- rule_file = get_perm_rules(checksum, perm_rules, android_permissions)
+ rule_file = get_perm_rules(
+ checksum, perm_rules, android_permissions)
if rule_file:
msg = 'Android Permission Mapping Started'
logger.info(msg)
append_scan_status(checksum, msg)
sast = SastEngine(options, src)
- perm_mappings = permission_transform(
+ result['perm_mappings'] = permission_transform(
sast.run_rules(file_data, rule_file.name))
msg = 'Android Permission Mapping Completed'
logger.info(msg)
@@ -141,7 +146,7 @@ def code_analysis(checksum, app_dir, typ, manifest_file, android_permissions):
# Behavior Analysis
sast = SastEngine(options, src)
- behaviour_findings = behaviour_analysis.analyze(
+ result['behaviour'] = behaviour_analysis.analyze(
checksum, sast, file_data)
# NIAP Scan
@@ -151,13 +156,14 @@ def code_analysis(checksum, app_dir, typ, manifest_file, android_permissions):
append_scan_status(checksum, msg)
niap_options = {
'choice_rules': niap_rules.as_posix(),
- 'alternative_path': manifest_file if manifest_file else '',
+ 'alternative_path': str(manifest_file) if manifest_file else '',
'choice_extensions': {'.java', '.xml'},
'ignore_paths': skp,
}
cengine = ChoiceEngine(niap_options, src)
file_data = cengine.read_files()
- niap_findings = cengine.run_rules(file_data, niap_rules.as_posix())
+ result['niap'] = cengine.run_rules(
+ file_data, niap_rules.as_posix())
msg = 'NIAP Analysis Completed'
logger.info(msg)
append_scan_status(checksum, msg)
@@ -182,25 +188,14 @@ def code_analysis(checksum, app_dir, typ, manifest_file, android_permissions):
relative_java_path = pfile.as_posix().replace(src, '')
urls, urls_nf, emails_nf = url_n_email_extract(
content, relative_java_path)
- url_list.extend(urls)
- url_n_file.extend(urls_nf)
- email_n_file.extend(emails_nf)
+ result['urls_list'].extend(urls)
+ result['urls'].extend(urls_nf)
+ result['emails'].extend(emails_nf)
msg = 'Email and URL Extraction Completed'
logger.info(msg)
append_scan_status(checksum, msg)
- code_an_dic = {
- 'api': api_findings,
- 'behaviour': behaviour_findings,
- 'perm_mappings': perm_mappings,
- 'findings': code_findings,
- 'niap': niap_findings,
- 'urls_list': url_list,
- 'urls': url_n_file,
- 'emails': email_n_file,
- 'sbom': sbom,
- }
- return code_an_dic
except Exception as exp:
msg = 'Failed to perform code analysis'
logger.exception(msg)
append_scan_status(checksum, msg, repr(exp))
+ return result
diff --git a/mobsf/StaticAnalyzer/views/android/converter.py b/mobsf/StaticAnalyzer/views/android/converter.py
index 531414f426..9f0144e256 100755
--- a/mobsf/StaticAnalyzer/views/android/converter.py
+++ b/mobsf/StaticAnalyzer/views/android/converter.py
@@ -10,6 +10,7 @@
import threading
import stat
from pathlib import Path
+from tempfile import gettempdir
from django.conf import settings
@@ -153,3 +154,37 @@ def run_jadx(arguments):
msg = 'Decompiling with JADX failed'
logger.exception(msg)
append_scan_status(checksum, msg, repr(exp))
+
+
+def run_apktool(app_path, app_dir, tools_dir):
+ """Get readable AndroidManifest.xml from APK."""
+ try:
+ if (len(settings.APKTOOL_BINARY) > 0
+ and Path(settings.APKTOOL_BINARY).exists()):
+ apktool_path = Path(settings.APKTOOL_BINARY)
+ else:
+ apktool_path = tools_dir / 'apktool_2.10.0.jar'
+
+ # Prepare output directory and manifest file paths
+ output_dir = app_dir / 'apktool_out'
+ # Run apktool to extract AndroidManifest.xml
+ args = [find_java_binary(),
+ '-jar',
+ '-Djdk.util.zip.disableZip64ExtraFieldValidation=true',
+ str(apktool_path),
+ '--match-original',
+ '--frame-path',
+ gettempdir(),
+ '-f', '-s', 'd',
+ str(app_path),
+ '-o',
+ str(output_dir)]
+ logger.info('Converting AXML to XML with apktool')
+ with open(os.devnull, 'w') as fnull:
+ subprocess.run(
+ args,
+ stdout=fnull,
+ stderr=subprocess.STDOUT,
+ timeout=settings.JADX_TIMEOUT)
+ except Exception:
+ logger.warning('apktool failed to extract AndroidManifest.xml')
diff --git a/mobsf/StaticAnalyzer/views/android/db_interaction.py b/mobsf/StaticAnalyzer/views/android/db_interaction.py
index a6d03673f7..7a521d12bf 100755
--- a/mobsf/StaticAnalyzer/views/android/db_interaction.py
+++ b/mobsf/StaticAnalyzer/views/android/db_interaction.py
@@ -144,7 +144,7 @@ def get_context_from_analysis(app_dic,
'manifest_analysis': manifest_analysis,
'network_security': man_an_dic['network_security'],
'binary_analysis': bin_anal,
- 'file_analysis': app_dic['certz'],
+ 'file_analysis': app_dic['file_analysis'],
'android_api': code_an_dic['api'],
'code_analysis': code,
'niap_analysis': code_an_dic['niap'],
@@ -210,7 +210,7 @@ def save_or_update(update_type,
'MALWARE_PERMISSIONS': man_an_dic['malware_permissions'],
'MANIFEST_ANALYSIS': man_an_dic['manifest_anal'],
'BINARY_ANALYSIS': bin_anal,
- 'FILE_ANALYSIS': app_dic['certz'],
+ 'FILE_ANALYSIS': app_dic['file_analysis'],
'ANDROID_API': code_an_dic['api'],
'CODE_ANALYSIS': code_an_dic['findings'],
'NIAP_ANALYSIS': code_an_dic['niap'],
diff --git a/mobsf/StaticAnalyzer/views/android/icon_analysis.py b/mobsf/StaticAnalyzer/views/android/icon_analysis.py
index 3b9138d17e..ed58577441 100755
--- a/mobsf/StaticAnalyzer/views/android/icon_analysis.py
+++ b/mobsf/StaticAnalyzer/views/android/icon_analysis.py
@@ -5,10 +5,11 @@
import logging
import os
from shutil import copy2, copytree
-from xml.dom import minidom
from pathlib import Path
import subprocess
+from defusedxml.minidom import parseString
+
from lxml import etree
from django.conf import settings
@@ -22,6 +23,9 @@
from mobsf.StaticAnalyzer.tools.androguard4 import (
axml,
)
+from mobsf.StaticAnalyzer.views.common.shared_func import (
+ RESERVED_FILE_NAMES,
+)
logger = logging.getLogger(__name__)
@@ -145,39 +149,47 @@ def find_icon_path_zip(checksum, res_dir, icon_paths_from_manifest):
msg = 'Failed to find icon path'
logger.exception(msg)
append_scan_status(checksum, msg, repr(exp))
-# PNG icon lookup functions above ^
-# SVG/XML icon lookup functions below
-
-def get_icon_src(a, app_dic, res_dir):
- """
- Returns a dict with isHidden boolean and a relative path.
- path is a full path (not relative to resource folder)
- """
+def get_icon_apk_res(app_dic):
+ """Get icon path from APK resource."""
+ icon_src = ''
+ checksum = app_dic['md5']
try:
msg = 'Fetching icon path'
logger.info(msg)
- append_scan_status(app_dic['md5'], msg)
- icon_src = ''
+ append_scan_status(checksum, msg)
app_dir = Path(app_dic['app_dir'])
- icon_resolution = 0xFFFE - 1
+ res_path = app_dir / 'res'
+ apktool_res_path = app_dir / 'apktool_out' / 'res'
icon_name = None
- if a:
+
+ # If icon is found in androguard or aapt2
+ icon_name = app_dic.get('androguard_apk_icon') or app_dic.get(
+ 'apk_features', {}).get('application_icon')
+ if is_path_traversal(icon_name):
+ logger.warning('Path traversal detected in icon path')
+ icon_name = None
+ # Handle reserved file names case
+ if icon_name and any(
+ icon_name.startswith(x) for x in RESERVED_FILE_NAMES):
+ icon_name = str(Path('_conflict_') / icon_name)
+
+ # androguard/aapt2 cannot find icon file, fallback to res or apktool.
+ if not res_path.exists() and apktool_res_path.exists() and not icon_name:
+ logger.warning('Cannot find res directory,'
+ ' fallback to apktool res directory')
try:
- icon_name = a.get_app_icon(max_dpi=icon_resolution)
- if icon_name and is_path_traversal(icon_name):
- icon_name = None
+ copytree(apktool_res_path, res_path, dirs_exist_ok=True)
except Exception:
- logger.warning('Failed to get icon from parsed APK object')
- icon_name = None
- if not icon_name:
- # androguard cannot find icon file.
+ pass
+ if res_path.exists() and not icon_name:
icon_name = ''
- logger.warning('androguard cannot find icon resource')
- icon_name = guess_icon_path(res_dir)
+ logger.warning('androguard/aapt2 cannot find icon resource')
+ icon_name = guess_icon_path(str(res_path))
icon_src = icon_name
- if icon_name.endswith('.xml'):
+
+ if icon_name and icon_name.endswith('.xml'):
apktool_res = False
# Can be vector XML/XML pointing to vector files
# Convert AXML to XML for vector
@@ -198,16 +210,16 @@ def get_icon_src(a, app_dic, res_dir):
icon_path = ipath.as_posix()
else:
# When icon xml point to other vector files
- icon_path = get_icon_svg_from_xml(
- app_dir, icon_name)
+ icon_path = get_icon_svg_from_xml(app_dir, icon_name)
if icon_path:
icon_src = icon_path
else:
# if we cannot find from xml
- icon_src = guess_icon_path(res_dir)
+ icon_src = guess_icon_path(str(res_path))
else:
# We found png icon, the easy path
icon_src = (app_dir / icon_name).as_posix()
+
if icon_src.endswith('.xml'):
logger.warning('Cannot find icon file from xml')
icon_src = ''
@@ -221,40 +233,30 @@ def get_icon_src(a, app_dic, res_dir):
except Exception as exp:
msg = 'Failed to fetch icon path'
logger.exception(msg)
- append_scan_status(app_dic['md5'], msg, repr(exp))
+ append_scan_status(checksum, msg, repr(exp))
+ return icon_src
-def get_icon_apk(apk, app_dic):
+def get_icon_apk(app_dic):
"""Get/Guess icon from APK binary."""
- app_dir = Path(app_dic['app_dir'])
- icon_file = ''
-
- res_path = app_dir / 'res'
- if not res_path.exists():
- logger.warning('Cannot find res directory,'
- ' using apktool res directory')
- # If res directory is not found or named differently
- # piggyback on apktool decompiled resources
- try:
- apk_tool_res = app_dir / 'apktool_out' / 'res'
- copytree(apk_tool_res, res_path, dirs_exist_ok=True)
- except Exception:
- pass
- if res_path.exists():
+ app_dic['icon_path'] = ''
+ try:
# Icon lookup in res directory
- icon_file = get_icon_src(
- apk,
- app_dic,
- res_path.as_posix())
-
- if icon_file:
- src = Path(icon_file)
- # Copy PNG/SVG to Downloads
- icon = app_dic['md5'] + '-icon' + src.suffix.lower()
- out = Path(settings.DWD_DIR) / icon
- if src and src.exists() and src.is_file():
- copy2(src.as_posix(), out.as_posix())
- app_dic['icon_path'] = out.name
+ icon_file = get_icon_apk_res(app_dic)
+ if icon_file:
+ src = Path(icon_file)
+ # Copy PNG/SVG to Downloads
+ icon = app_dic['md5'] + '-icon' + src.suffix.lower()
+ out = Path(settings.DWD_DIR) / icon
+ if src and src.exists() and src.is_file():
+ copy2(src.as_posix(), out.as_posix())
+ app_dic['icon_path'] = out.name
+ except Exception:
+ logger.exception('Failed to get icon from APK')
+
+
+# PNG icon lookup functions above ^
+# SVG/XML icon lookup functions below
def transform_svg(fpath, bpath, output):
@@ -280,8 +282,7 @@ def get_icon_svg_from_xml(app_dir, icon_xml_file):
"""
try:
icon_xml = app_dir / 'apktool_out' / icon_xml_file
- parsed = minidom.parseString(
- icon_xml.read_text('utf8', 'ignore'))
+ parsed = parseString(icon_xml.read_text('utf8', 'ignore'))
foreground = parsed.getElementsByTagName('foreground')
background = parsed.getElementsByTagName('background')
ficon = foreground[0].getAttribute(
diff --git a/mobsf/StaticAnalyzer/views/android/jar_aar.py b/mobsf/StaticAnalyzer/views/android/jar_aar.py
index ec0d39d8ce..4f3e16b006 100644
--- a/mobsf/StaticAnalyzer/views/android/jar_aar.py
+++ b/mobsf/StaticAnalyzer/views/android/jar_aar.py
@@ -26,15 +26,12 @@
from mobsf.StaticAnalyzer.views.common.appsec import (
get_android_dashboard,
)
-from mobsf.StaticAnalyzer.views.android.app import (
- parse_apk,
-)
from mobsf.StaticAnalyzer.views.android.manifest_analysis import (
manifest_analysis,
)
from mobsf.StaticAnalyzer.views.android.manifest_utils import (
- get_manifest,
- manifest_data,
+ extract_manifest_data,
+ get_parsed_manifest,
)
from mobsf.StaticAnalyzer.views.android.strings import (
get_strings_metadata,
@@ -87,6 +84,7 @@ def common_analysis(request, app_dic, rescan, api, analysis_type):
app_dic['sha1'], app_dic['sha256'] = hash_gen(
checksum,
app_dic['app_path'])
+ app_dic['zipped'] = analysis_type
app_dic['files'] = unzip(
checksum,
app_dic['app_path'],
@@ -97,53 +95,24 @@ def common_analysis(request, app_dic, rescan, api, analysis_type):
request,
f'{analysis_type.upper()} file is invalid or corrupt',
api)
- app_dic['certz'] = get_hardcoded_cert_keystore(
- checksum,
- app_dic['files'])
+ get_hardcoded_cert_keystore(app_dic)
app_dic['playstore'] = {'error': True}
- # Parse APK with Androguard4
- apk = parse_apk(
- checksum,
- app_dic['app_path'])
if analysis_type == 'aar':
# AAR has manifest and sometimes certificate
- mani_file, ns, mani_xml = get_manifest(
- checksum,
- app_dic['app_path'],
- app_dic['app_dir'],
- app_dic['tools_dir'],
- 'aar',
- apk,
- )
- app_dic['manifest_file'] = mani_file
- app_dic['ns'] = ns
- app_dic['parsed_xml'] = mani_xml
- man_data_dic = manifest_data(
- checksum,
- app_dic['parsed_xml'],
- ns)
- man_an_dic = manifest_analysis(
- checksum,
- app_dic['parsed_xml'],
- ns,
- man_data_dic,
- '',
- app_dic['app_dir'],
- )
-
+ get_parsed_manifest(app_dic)
+ man_data_dic = extract_manifest_data(app_dic)
+ man_an_dic = manifest_analysis(app_dic, man_data_dic)
# Malware Permission check
mal_perms = permissions.check_malware_permission(
checksum,
man_data_dic['perm'])
man_an_dic['malware_permissions'] = mal_perms
- cert_dic = cert_info(
- apk,
- app_dic,
- man_data_dic)
+ cert_dic = cert_info(app_dic, man_data_dic)
else:
app_dic['manifest_file'] = None
- app_dic['parsed_xml'] = ''
+ app_dic['manifest_parsed_xml'] = None
+ app_dic['manifest_namespace'] = None
man_data_dic = {
'services': [],
'activities': [],
@@ -210,11 +179,8 @@ def common_analysis(request, app_dic, rescan, api, analysis_type):
code_an_dic)
# Get the strings and metadata
get_strings_metadata(
- checksum,
- apk,
- app_dic['app_dir'],
+ app_dic,
elf_dict['elf_strings'],
- APK_TYPE,
['.java'],
code_an_dic)
# Firebase DB Check
@@ -226,7 +192,6 @@ def common_analysis(request, app_dic, rescan, api, analysis_type):
checksum,
code_an_dic['urls_list'])
- app_dic['zipped'] = analysis_type
context = save_get_ctx(
app_dic,
man_data_dic,
diff --git a/mobsf/StaticAnalyzer/views/android/manifest_analysis.py b/mobsf/StaticAnalyzer/views/android/manifest_analysis.py
index 13100da50a..b0081b14bf 100755
--- a/mobsf/StaticAnalyzer/views/android/manifest_analysis.py
+++ b/mobsf/StaticAnalyzer/views/android/manifest_analysis.py
@@ -60,6 +60,8 @@
'32': '12L',
'33': '13',
'34': '14',
+ '35': '15',
+ '36': '16',
}
@@ -181,9 +183,14 @@ def get_browsable_activities(node, ns):
logger.exception('Getting Browsable Activities')
-def manifest_analysis(checksum, mfxml, ns, man_data_dic, src_type, app_dir):
+def manifest_analysis(app_dic, man_data_dic):
"""Analyse manifest file."""
# pylint: disable=C0301
+ checksum = app_dic['md5']
+ mfxml = app_dic['manifest_parsed_xml']
+ ns = app_dic['manifest_namespace']
+ src_type = app_dic['zipped']
+ app_dir = app_dic['app_dir']
try:
msg = 'Manifest Analysis Started'
logger.info(msg)
diff --git a/mobsf/StaticAnalyzer/views/android/manifest_utils.py b/mobsf/StaticAnalyzer/views/android/manifest_utils.py
index bf4ed68826..ae6fdd5a1a 100644
--- a/mobsf/StaticAnalyzer/views/android/manifest_utils.py
+++ b/mobsf/StaticAnalyzer/views/android/manifest_utils.py
@@ -1,22 +1,18 @@
# -*- coding: utf_8 -*-
"""Android manifest analysis utils."""
import logging
-import os
import re
-import subprocess
-import tempfile
from pathlib import Path
-from xml.dom import minidom
-from xml.parsers.expat import ExpatError
-from bs4 import BeautifulSoup
+from defusedxml.minidom import parseString
-from django.conf import settings
+from bs4 import BeautifulSoup
from mobsf.MobSF.utils import (
append_scan_status,
- find_java_binary,
- is_file_exists,
+)
+from mobsf.StaticAnalyzer.views.android.converter import (
+ run_apktool,
)
# pylint: disable=E0401
@@ -31,97 +27,79 @@
ANDROID_MANIFEST_FILE = 'AndroidManifest.xml'
-def get_manifest_file(app_dir, app_path, tools_dir, typ, apk):
- """Read the manifest file."""
+def get_manifest_file(app_dic):
+ """Get AndroidManifest.xml file path.
+
+ Used by get_parsed_manifest() and manifest_view.run()
+ """
+ manifest = None
try:
- manifest = ''
+ app_path = Path(app_dic['app_path'])
+ app_dir = Path(app_dic['app_dir'])
+ tools_dir = Path(app_dic['tools_dir'])
+ typ = app_dic['zipped']
+ checksum = app_dic['md5']
+ androguard_xml = app_dic.get('androguard_manifest_xml')
+
if typ == 'aar':
logger.info('Getting AndroidManifest.xml from AAR')
- manifest = os.path.join(app_dir, ANDROID_MANIFEST_FILE)
+ manifest = app_dir / ANDROID_MANIFEST_FILE
elif typ == 'apk':
logger.info('Getting AndroidManifest.xml from APK')
- manifest = get_manifest_apk(app_path, app_dir, tools_dir, apk)
+ manifest = app_dir / 'apktool_out' / ANDROID_MANIFEST_FILE
+ if manifest.exists():
+ return manifest
+
+ # Run apktool to extract AndroidManifest.xml
+ manifest.parent.mkdir(parents=True, exist_ok=True)
+ run_apktool(app_path, app_dir, tools_dir)
+
+ if not manifest.exists() and androguard_xml:
+ logger.warning(
+ 'apktool failed to extract AndroidManifest.xml,'
+ ' fallback to androguard')
+ manifest.write_bytes(androguard_xml)
+ elif not androguard_xml:
+ msg = ('Failed to extract AndroidManifest.xml'
+ ' from APK with apktool and androguard')
+ logger.error(msg)
+ append_scan_status(checksum, msg, 'apktool and androguard failed')
+ elif typ == 'eclipse':
+ logger.info('Getting AndroidManifest.xml'
+ ' from Eclipse project source code')
+ manifest = app_dir / ANDROID_MANIFEST_FILE
+ elif typ == 'studio':
+ logger.info('Getting AndroidManifest.xml'
+ ' from Android Studio project source code')
+ manifest = app_dir / 'app' / 'src' / \
+ 'main' / ANDROID_MANIFEST_FILE
else:
- logger.info('Getting AndroidManifest.xml from Source Code')
- if typ == 'eclipse':
- manifest = os.path.join(app_dir, ANDROID_MANIFEST_FILE)
- elif typ == 'studio':
- manifest = os.path.join(
- app_dir,
- f'app/src/main/{ANDROID_MANIFEST_FILE}')
- return manifest
+ logger.error('Unknown project type')
except Exception:
logger.exception('Getting AndroidManifest.xml file')
-
-
-def get_android_manifest_androguard(apk, app_dir):
- """Get AndroidManifest.xml using Androguard4."""
- try:
- logger.info('Extracting AndroidManifest.xml with Androguard')
- if not apk:
- logger.warning('Androgaurd APK parsing failed')
- return
- manifest = apk.get_android_manifest_axml()
- if not manifest:
- return
- manifest_file = Path(app_dir) / 'apktool_out' / ANDROID_MANIFEST_FILE
- manifest_file.write_bytes(manifest.get_xml())
- except Exception:
- logger.exception('Error Extracting AndroidManifest.xml with Androguard')
- return None
-
-
-def get_manifest_apk(app_path, app_dir, tools_dir, apk):
- """Get readable AndroidManifest.xml.
-
- Should be called before get_icon_apk() function
- """
- try:
- manifest = None
- if (len(settings.APKTOOL_BINARY) > 0
- and is_file_exists(settings.APKTOOL_BINARY)):
- apktool_path = settings.APKTOOL_BINARY
- else:
- apktool_path = os.path.join(tools_dir, 'apktool_2.10.0.jar')
- output_dir = os.path.join(app_dir, 'apktool_out')
- args = [find_java_binary(),
- '-jar',
- '-Djdk.util.zip.disableZip64ExtraFieldValidation=true',
- apktool_path,
- '--match-original',
- '--frame-path',
- tempfile.gettempdir(),
- '-f', '-s', 'd',
- app_path,
- '-o',
- output_dir]
- manifest = os.path.join(output_dir, ANDROID_MANIFEST_FILE)
- if is_file_exists(manifest):
- # APKTool already created readable XML
- return manifest
- logger.info('Converting AXML to XML')
- subprocess.check_output(args)
- except subprocess.CalledProcessError:
- # APK tool failed
- logger.warning('apktool failed to extract AndroidManifest.xml')
- get_android_manifest_androguard(apk, app_dir)
- except Exception:
- logger.exception('Getting Manifest file')
return manifest
def get_xml_namespace(xml_str):
"""Get namespace."""
- m = re.search(r'manifest (.{1,250}?):', xml_str)
- if m:
- return m.group(1)
- logger.warning('XML namespace not found')
- return None
+ match = re.search(r'manifest (.{1,250}?):', xml_str)
+ if not match:
+ logger.warning('XML namespace not found')
+ return None
+
+ namespace = match.group(1)
+
+ # Handle standard and non-standard namespaces
+ if namespace == 'xmlns':
+ namespace = 'android'
+ elif namespace != 'android':
+ logger.warning('Non-standard XML namespace: %s', namespace)
+
+ return namespace
def get_fallback():
- logger.warning('Using Fake XML to continue the Analysis')
- return minidom.parseString(
+ return parseString(
(r' tuple:
@@ -77,49 +90,122 @@ def hash_gen(checksum, app_path) -> tuple:
append_scan_status(checksum, msg, repr(exp))
+def is_reserved_file_conflict(file_path):
+ """Check for reserved file conflict."""
+ if any(file_path.startswith(i) and file_path != i for i in RESERVED_FILE_NAMES):
+ return True
+ return False
+
+
def unzip(checksum, app_path, ext_path):
+ """Unzip APK.
+
+ Unzip a APK archive while handling encrypted files, reserved file conflicts,
+ path traversal (Zip Slip), and permission adjustments. Some of the anti-analysis
+ techniques used by malware authors and packers are handled here.
+
+ Args:
+ checksum (str): The checksum of the file.
+ app_path (str): Path to the ZIP archive.
+ ext_path (str): Path to extract the files.
+
+ Returns:
+ list: A list of files extracted or an empty list if an error occurs.
+ """
msg = 'Unzipping'
logger.info(msg)
append_scan_status(checksum, msg)
+ files = []
+ original_ext_path = ext_path
try:
- files = []
with zipfile.ZipFile(app_path, 'r') as zipptr:
+ files = zipptr.namelist()
for fileinfo in zipptr.infolist():
- filename = fileinfo.filename
- if not isinstance(filename, str):
- filename = str(
- filename, encoding='utf-8', errors='replace')
- files.append(filename)
- zipptr.extract(filename, ext_path)
- return files
+ ext_path = original_ext_path
+
+ # Skip encrypted files
+ if fileinfo.flag_bits & 0x1:
+ msg = ('Skipping encrypted file '
+ f'{sanitize_for_logging(fileinfo.filename)}')
+ logger.warning(msg)
+ continue
+
+ file_path = fileinfo.filename.rstrip('/\\') # Remove trailing slashes
+
+ # Decode the filename
+ if not isinstance(file_path, str):
+ file_path = file_path.decode('utf-8', errors='replace')
+
+ # Check for reserved file conflict
+ if is_reserved_file_conflict(file_path):
+ ext_path = str(Path(ext_path) / '_conflict_')
+
+ # Handle Zip Slip
+ if is_path_traversal(file_path):
+ msg = ('Zip slip detected. skipped extracting'
+ f' {sanitize_for_logging(file_path)}')
+ logger.error(msg)
+ continue
+
+ # Fix permissions
+ if fileinfo.is_dir():
+ # Directories should have rwxr-xr-x (755)
+ # Skip creating directories
+ continue
+ else:
+ # Files should have rw-r--r-- (644)
+ fileinfo.external_attr = (0o100644 << 16) | (
+ fileinfo.external_attr & 0xFFFF)
+
+ # Extract the file
+ try:
+ zipptr.extract(file_path, ext_path)
+ except Exception:
+ logger.warning(
+ 'Failed to extract %s', sanitize_for_logging(file_path))
except Exception as exp:
msg = f'Unzipping Error - {str(exp)}'
logger.error(msg)
append_scan_status(checksum, msg, repr(exp))
+ # Fallback to OS unzip
+ ofiles = os_unzip(checksum, app_path, ext_path)
+ if not files:
+ files = ofiles
+ return files
+
+
+def os_unzip(checksum, app_path, ext_path):
+ """Unzip using OS utility."""
+ msg = 'Attempting to unzip with OS unzip utility'
+ logger.info(msg)
+ append_scan_status(checksum, msg)
+ try:
if platform.system() == 'Windows':
msg = 'Unzipping Error. Not yet implemented in Windows'
logger.warning(msg)
append_scan_status(checksum, msg)
- else:
- msg = 'Attempting to unzip with OS unzip utility'
- logger.info(msg)
+ return []
+ unzip_b = shutil.which('unzip')
+ if not unzip_b:
+ msg = 'OS Unzip utility not found'
+ logger.warning(msg)
append_scan_status(checksum, msg)
- try:
- unzip_b = shutil.which('unzip')
- subprocess.call(
- [unzip_b, '-o', '-q', app_path, '-d', ext_path])
- # Set permissions, packed files
- # may not have proper permissions
- set_permissions(ext_path)
- dat = subprocess.check_output([unzip_b, '-qq', '-l', app_path])
- dat = dat.decode('utf-8').split('\n')
- files_det = ['Length Date Time Name']
- files_det = files_det + dat
- return files_det
- except Exception as exp:
- msg = 'Unzipping Error with OS unzip utility'
- logger.exception(msg)
- append_scan_status(checksum, msg, repr(exp))
+ return []
+ subprocess.call(
+ [unzip_b, '-o', '-q', app_path, '-d', ext_path])
+ # Set permissions, packed files
+ # may not have proper permissions
+ set_permissions(ext_path)
+ # List files in the unzipped directory
+ dat = subprocess.check_output([unzip_b, '-qq', '-l', app_path])
+ dat = dat.decode('utf-8').split('\n')
+ files_det = ['Length Date Time Name']
+ return files_det + dat
+ except Exception as exp:
+ msg = 'Unzipping Error with OS unzip utility'
+ logger.exception(msg)
+ append_scan_status(checksum, msg, repr(exp))
+ return []
def lipo_thin(checksum, src, dst):
@@ -347,7 +433,7 @@ def strings_and_entropies(checksum, src, exts):
'secrets': set(),
}
try:
- if not src.exists():
+ if not (src and src.exists()):
return data
excludes = ('\\u0', 'com.google.')
eslash = ('Ljava', 'Lkotlin', 'kotlin', 'android')
diff --git a/mobsf/StaticAnalyzer/views/ios/ipa.py b/mobsf/StaticAnalyzer/views/ios/ipa.py
index 22e0766e07..93c296612d 100644
--- a/mobsf/StaticAnalyzer/views/ios/ipa.py
+++ b/mobsf/StaticAnalyzer/views/ios/ipa.py
@@ -55,6 +55,7 @@
)
from mobsf.StaticAnalyzer.views.common.async_task import (
async_analysis,
+ enqueued_task_init,
update_enqueued_task,
)
from mobsf.MalwareAnalyzer.views.MalwareDomainCheck import (
@@ -169,6 +170,7 @@ def ipa_analysis_task(checksum, app_dic, rescan, queue=False):
try:
if queue:
settings.ASYNC_ANALYSIS = True
+ enqueued_task_init(checksum)
append_scan_status(checksum, 'init')
msg = 'iOS Binary (IPA) Analysis Started'
logger.info(msg)
@@ -273,6 +275,7 @@ def ios_analysis_task(checksum, app_dic, rescan, queue=False):
try:
if queue:
settings.ASYNC_ANALYSIS = True
+ enqueued_task_init(checksum)
logger.info('iOS Source Code Analysis Started')
get_size_and_hashes(app_dic)
diff --git a/mobsf/StaticAnalyzer/views/ios/plist_analysis.py b/mobsf/StaticAnalyzer/views/ios/plist_analysis.py
index cca0e7194f..3fc59331cf 100755
--- a/mobsf/StaticAnalyzer/views/ios/plist_analysis.py
+++ b/mobsf/StaticAnalyzer/views/ios/plist_analysis.py
@@ -4,6 +4,7 @@
import logging
import os
from plistlib import (
+ FMT_XML,
dumps,
load,
loads,
@@ -13,12 +14,6 @@
from openstep_parser import OpenStepDecoder
-from biplist import (
- InvalidPlistException,
- readPlist,
- writePlistToString,
-)
-
from mobsf.MobSF.utils import (
append_scan_status,
find_key_in_dict,
@@ -48,7 +43,7 @@ def get_bundle_id(pobj, src):
Look up in Info.plist, entitlements, pbxproj
"""
possible_ids = set()
- skip_chars = {'$(', '${'}
+ skip_chars = ('$(', '${')
# From old Info.plist
bundle_id_og = pobj.get('CFBundleIdentifier', '')
@@ -101,10 +96,13 @@ def get_bundle_id(pobj, src):
def convert_bin_xml(bin_xml_file):
"""Convert Binary XML to Readable XML."""
try:
- plist_obj = readPlist(bin_xml_file)
- data = writePlistToString(plist_obj)
- return data
- except InvalidPlistException:
+ with open(bin_xml_file, 'rb') as fp:
+ plist_obj = load(fp)
+
+ # Serializing the plist object to a binary plist string
+ data = dumps(plist_obj, fmt=FMT_XML)
+ Path(bin_xml_file).write_bytes(data)
+ except Exception:
logger.warning('Failed to convert plist')
diff --git a/mobsf/StaticAnalyzer/views/ios/views/view_source.py b/mobsf/StaticAnalyzer/views/ios/views/view_source.py
index 4b0df1b3ff..7917fad1fb 100644
--- a/mobsf/StaticAnalyzer/views/ios/views/view_source.py
+++ b/mobsf/StaticAnalyzer/views/ios/views/view_source.py
@@ -5,10 +5,9 @@
import logging
import ntpath
import os
+import plistlib
from pathlib import Path
-import biplist
-
from django.conf import settings
from django.http import HttpResponseRedirect
from django.shortcuts import render
@@ -108,13 +107,17 @@ def run(request, api=False):
elif typ == 'plist':
file_format = 'json'
try:
- dat = biplist.readPlist(sfile)
- dat = json.dumps(dat, indent=4, sort_keys=True)
- except biplist.InvalidPlistException:
+ with open(sfile, 'rb') as f:
+ # Attempt to load the plist, binary or XML
+ dat = plistlib.load(f)
+ # Convert the plist data to JSON for output
+ dat = json.dumps(dat, indent=4, sort_keys=True)
+ except plistlib.InvalidFileException:
+ # Handle invalid plist files (e.g., if it isn't binary or XML)
file_format = 'xml'
dat = Path(sfile).read_text()
except Exception:
- pass
+ dat = None
elif typ == 'db':
file_format = 'asciidoc'
sql_dump = read_sqlite(sfile)
diff --git a/mobsf/templates/base/base_layout.html b/mobsf/templates/base/base_layout.html
index d9d16f2361..89ec10082b 100644
--- a/mobsf/templates/base/base_layout.html
+++ b/mobsf/templates/base/base_layout.html
@@ -26,6 +26,16 @@
body {
zoom: 0.8;
}
+ .modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1040; /* Ensure it is above most elements but below modal */
+ background-color: rgba(0, 0, 0, 0.5); /* Default backdrop color */
+ }
+
{% block extra_css %}
diff --git a/mobsf/templates/general/tasks.html b/mobsf/templates/general/tasks.html
index a7e11b4b0c..262a42371e 100644
--- a/mobsf/templates/general/tasks.html
+++ b/mobsf/templates/general/tasks.html
@@ -50,8 +50,7 @@ Scan Queue
Scan Task |
Filename |
- Queued At |
- Completed At |
+ Timeline |
Status |
@@ -123,25 +122,6 @@ Scan Queue
statusCell.appendChild(reportLink);
}
- function updateCellStatus(task, statusCell, completedCell) {
- statusCell.textContent = task.status.status || task.status;
-
- if (!task.completed_at) {
- addLoader(statusCell);
- } else {
- completedCell.textContent = new Date(task.completed_at).toLocaleString();
- if (task.status === "Success") {
- statusCell.classList.add("text-success");
- statusCell.classList.remove("text-warning");
- addReportLink(task, statusCell);
- } else if (task.app_name === "Failed") {
- statusCell.classList.add("text-danger");
- statusCell.classList.remove("text-warning");
- }
- }
- addAppName(task, statusCell);
- }
-
function buildTableRow(task) {
const row = document.createElement("tr");
row.setAttribute("data-task-id", task.task_id);
@@ -151,10 +131,14 @@ Scan Queue
const taskP = document.createElement("p");
const checksumP = document.createElement("p");
- taskP.innerHTML = 'Task ID: ';
+ const taskLabel = document.createElement("strong");
+ taskLabel.textContent = "Task ID: ";
+ taskP.appendChild(taskLabel);
taskP.appendChild(document.createTextNode(task.task_id));
- checksumP.innerHTML = 'Checksum: ';
+ const checksumLabel = document.createElement("strong");
+ checksumLabel.textContent = "Checksum: ";
+ checksumP.appendChild(checksumLabel);
checksumP.appendChild(document.createTextNode(task.checksum));
taskIdCell.appendChild(taskP);
@@ -164,33 +148,76 @@ Scan Queue
const fileNameCell = document.createElement("td");
fileNameCell.textContent = task.file_name;
- // Queued At
- const queuedAtCell = document.createElement("td");
- const date = new Date(task.created_at);
- queuedAtCell.textContent = date.toLocaleString();
-
- // Completed At
- const completedCell = document.createElement("td");
- completedCell.setAttribute("id", `completed-${task.task_id}`);
- completedCell.textContent = task.completed_at ? new Date(task.completed_at).toLocaleString() : null;
+ // Timeline
+ const timelineCell = document.createElement("td");
+ timelineCell.setAttribute("id", `timeline-${task.task_id}`);
// Status
const statusCell = document.createElement("td");
statusCell.setAttribute("id", `status-${task.task_id}`);
statusCell.classList.add(task.completed_at ? "text-success" : "text-warning");
- updateCellStatus(task, statusCell, completedCell);
+ updateCellStatus(task, statusCell, timelineCell);
// Append cells to row
row.appendChild(taskIdCell);
row.appendChild(fileNameCell);
- row.appendChild(queuedAtCell);
- row.appendChild(completedCell);
+ row.appendChild(timelineCell);
row.appendChild(statusCell);
return row;
}
+ function updateCellStatus(task, statusCell, timelineCell) {
+ // Update the status text
+ statusCell.textContent = task.status;
+
+ // Clear existing content in timelineCell
+ while (timelineCell.firstChild) {
+ timelineCell.removeChild(timelineCell.firstChild);
+ }
+
+ // Create and append "Queued At"
+ const queuedAtElement = document.createElement("p");
+ const queuedAtLabel = document.createElement("strong");
+ queuedAtLabel.textContent = "Queued At: ";
+ queuedAtElement.appendChild(queuedAtLabel);
+ queuedAtElement.appendChild(document.createTextNode(task.created_at ? new Date(task.created_at).toLocaleString() : "N/A"));
+ timelineCell.appendChild(queuedAtElement);
+
+ // Create and append "Started At"
+ const startedAtElement = document.createElement("p");
+ const startedAtLabel = document.createElement("strong");
+ startedAtLabel.textContent = "Started At: ";
+ startedAtElement.appendChild(startedAtLabel);
+ startedAtElement.appendChild(document.createTextNode(task.started_at ? new Date(task.started_at).toLocaleString() : "N/A"));
+ timelineCell.appendChild(startedAtElement);
+
+ // Create and append "Completed At"
+ const completedAtElement = document.createElement("p");
+ const completedAtLabel = document.createElement("strong");
+ completedAtLabel.textContent = "Completed At: ";
+ completedAtElement.appendChild(completedAtLabel);
+ completedAtElement.appendChild(document.createTextNode(task.completed_at ? new Date(task.completed_at).toLocaleString() : "N/A"));
+ timelineCell.appendChild(completedAtElement);
+
+ // Add loader or success indicators
+ if (!task.completed_at) {
+ addLoader(statusCell);
+ } else {
+ if (task.status === "Success") {
+ statusCell.classList.add("text-success");
+ statusCell.classList.remove("text-warning");
+ addReportLink(task, statusCell);
+ } else if (task.status === "Failed") {
+ statusCell.classList.add("text-danger");
+ statusCell.classList.remove("text-warning");
+ }
+ }
+
+ addAppName(task, statusCell);
+ }
+
async function renderTable() {
const tasks = await fetchTasks();
tasksTableBody.innerHTML = ""; // Clear existing rows
@@ -198,7 +225,7 @@ Scan Queue
if (tasks.length === 0) {
const noTasksRow = document.createElement("tr");
const noTasksCell = document.createElement("td");
- noTasksCell.setAttribute("colspan", "5");
+ noTasksCell.setAttribute("colspan", "4");
noTasksCell.textContent = "No tasks in queue.";
noTasksRow.appendChild(noTasksCell);
tasksTableBody.appendChild(noTasksRow);
@@ -217,11 +244,11 @@ Scan Queue
tasks.forEach((task) => {
const statusCell = document.getElementById(`status-${task.task_id}`);
- const completedCell = document.getElementById(`completed-${task.task_id}`);
+ const timelineCell = document.getElementById(`timeline-${task.task_id}`);
- if (statusCell && completedCell) {
- // Update existing task status
- updateCellStatus(task, statusCell, completedCell);
+ if (statusCell && timelineCell) {
+ // Update existing task status and timeline
+ updateCellStatus(task, statusCell, timelineCell);
} else if (!existingTaskIds.includes(task.task_id)) {
// Add a new row if the task ID is not found, at the top of the table
const newRow = buildTableRow(task);
@@ -231,6 +258,7 @@ Scan Queue
}
+
// Initial render
renderTable();
diff --git a/mobsf/templates/static_analysis/android_binary_analysis.html b/mobsf/templates/static_analysis/android_binary_analysis.html
index 647ba7847f..abd716d627 100755
--- a/mobsf/templates/static_analysis/android_binary_analysis.html
+++ b/mobsf/templates/static_analysis/android_binary_analysis.html
@@ -2354,9 +2354,11 @@
SBOM
+ {% if sbom %}
{% include 'base/list.html' with list=sbom.sbom_versioned type="Versioned Packages" limit=100 %}
{% include 'base/list.html' with list=sbom.sbom_packages type="Packages" limit=100 %}
-
+ {% endif %}
+
@@ -2555,11 +2557,13 @@ Suppression Rules
{% endblock %}
{% block scan_logs %}
+
Timestamp |
Event |
Error |
+
{% for log in logs %}
diff --git a/mobsf/templates/static_analysis/android_source_analysis.html b/mobsf/templates/static_analysis/android_source_analysis.html
index e7c0ff0bae..c9f06cd67e 100755
--- a/mobsf/templates/static_analysis/android_source_analysis.html
+++ b/mobsf/templates/static_analysis/android_source_analysis.html
@@ -1762,9 +1762,11 @@
SBOM
+ {% if sbom %}
{% include 'base/list.html' with list=sbom.sbom_versioned type="Versioned Packages" limit=100 %}
{% include 'base/list.html' with list=sbom.sbom_packages type="Packages" limit=100 %}
-
+ {% endif %}
+
@@ -1942,11 +1944,13 @@ Suppression Rules
{% endblock %}
{% block scan_logs %}
+
Timestamp |
Event |
Error |
+
{% for log in logs %}
diff --git a/mobsf/templates/static_analysis/ios_binary_analysis.html b/mobsf/templates/static_analysis/ios_binary_analysis.html
index 2e02963f4c..6a3d690fee 100755
--- a/mobsf/templates/static_analysis/ios_binary_analysis.html
+++ b/mobsf/templates/static_analysis/ios_binary_analysis.html
@@ -1921,12 +1921,14 @@ Suppression Rules
{% endblock %}
{% block scan_logs %}
-
- Timestamp |
- Event |
- Error |
-
-
+
+
+ Timestamp |
+ Event |
+ Error |
+
+
+
{% for log in logs %}
diff --git a/mobsf/templates/static_analysis/ios_source_analysis.html b/mobsf/templates/static_analysis/ios_source_analysis.html
index ba87d34cea..80c6057d89 100755
--- a/mobsf/templates/static_analysis/ios_source_analysis.html
+++ b/mobsf/templates/static_analysis/ios_source_analysis.html
@@ -1392,11 +1392,13 @@ Suppression Rules
{% endblock %}
{% block scan_logs %}
+
Timestamp |
Event |
Error |
+
{% for log in logs %}
diff --git a/mobsf/templates/static_analysis/windows_binary_analysis.html b/mobsf/templates/static_analysis/windows_binary_analysis.html
index d5caf235ef..da6a021100 100644
--- a/mobsf/templates/static_analysis/windows_binary_analysis.html
+++ b/mobsf/templates/static_analysis/windows_binary_analysis.html
@@ -463,11 +463,13 @@ Strings
{% endblock %}
{% block scan_logs %}
+
Timestamp |
Event |
Error |
+
{% for log in logs %}
diff --git a/poetry.lock b/poetry.lock
index db58b60326..9266b1cf20 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -229,16 +229,6 @@ files = [
{file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"},
]
-[[package]]
-name = "biplist"
-version = "1.0.3"
-description = "biplist is a library for reading/writing binary plists."
-optional = false
-python-versions = "*"
-files = [
- {file = "biplist-1.0.3.tar.gz", hash = "sha256:4c0549764c5fe50b28042ec21aa2e14fe1a2224e239a1dae77d9e7f3932aa4c6"},
-]
-
[[package]]
name = "blinker"
version = "1.9.0"
@@ -651,6 +641,17 @@ ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
+
[[package]]
name = "distro"
version = "1.9.0"
@@ -2666,4 +2667,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "47edc939d8d6d36767bbc3210b4440106dbda32948de859d18a100e458815e0c"
+content-hash = "b89f2cedf9ba39c6ec2d9ef024826fb8af034db78b2894597328aa292b419fdb"
diff --git a/pyproject.toml b/pyproject.toml
index 5812740482..0e07b36b4e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mobsf"
-version = "4.2.7"
+version = "4.2.8"
description = "Mobile Security Framework (MobSF) is an automated, all-in-one mobile application (Android/iOS/Windows) pen-testing, malware analysis and security assessment framework capable of performing static and dynamic analysis."
keywords = ["mobsf", "mobile security framework", "mobile security", "security tool", "static analysis", "dynamic analysis", "malware analysis"]
authors = ["Ajin Abraham "]
@@ -27,7 +27,6 @@ python = "^3.10"
django = ">=3.1.5"
lxml = ">=4.6.2"
rsa = ">=4.7"
-biplist = ">=1.0.3"
requests = ">=2.25.1"
bs4 = ">=0.0.1"
colorlog = ">=4.7.2"
@@ -61,6 +60,7 @@ lief = "^0.15.1"
packaging = ">=21.3"
django-ratelimit = "^4.1.0"
django-q2 = "^1.7.4"
+defusedxml = "^0.7.1"
[build-system]
requires = ["poetry-core"]
diff --git a/scripts/dependencies.sh b/scripts/dependencies.sh
index 93ffb60161..9b3bb897d8 100755
--- a/scripts/dependencies.sh
+++ b/scripts/dependencies.sh
@@ -1,4 +1,8 @@
#!/bin/bash
+JDK_FILE=openjdk-22.0.2_linux-x64_bin.tar.gz
+JDK_FILE_ARM=openjdk-22.0.2_linux-aarch64_bin.tar.gz
+WKH_FILE=wkhtmltox_0.12.6.1-3.bookworm_amd64.deb
+WKH_FILE_ARM=wkhtmltox_0.12.6.1-3.bookworm_arm64.deb
# For apktool
mkdir -p /home/mobsf/.local/share/apktool/framework
|