diff --git a/seashell/core/app.py b/seashell/core/app.py index 6d93453..24b62e3 100644 --- a/seashell/core/app.py +++ b/seashell/core/app.py @@ -32,6 +32,16 @@ from seashell.lib.config import Config +def _sanitize_app_name(name: str) -> str: + safe = os.path.basename(name) + if os.path.altsep: + safe = safe.replace(os.path.altsep, '') + safe = safe.replace(os.path.sep, '') + if safe in ('', '.', '..'): + return '' + return safe + + class App(Config): """ Subclass of seashell.core module. @@ -64,7 +74,9 @@ def set_name(self, name: str, bundle_id: str) -> None: """ if name: - self.app_name = name.lower().title() + safe = _sanitize_app_name(name) + if safe: + self.app_name = safe.lower().title() if bundle_id: self.bundle_id = bundle_id diff --git a/seashell/core/hook.py b/seashell/core/hook.py index 006f992..751dd68 100644 --- a/seashell/core/hook.py +++ b/seashell/core/hook.py @@ -25,6 +25,8 @@ import os import shutil import plistlib +import tempfile +import zipfile from typing import Optional @@ -34,6 +36,26 @@ from seashell.lib.config import Config +def _safe_extract_zip(archive_path: str, extract_dir: str) -> None: + base = os.path.realpath(extract_dir) + with zipfile.ZipFile(archive_path) as zf: + for member in zf.namelist(): + target = os.path.realpath(os.path.join(base, member)) + if not target.startswith(base + os.sep): + raise RuntimeError("Unsafe path in archive") + zf.extractall(extract_dir) + + +def _sanitize_executable(name: str) -> str: + if not name or name in ('.', '..'): + return '' + if os.path.sep in name or (os.path.altsep and os.path.altsep in name): + return '' + if '..' in name: + return '' + return name + + class Hook(Config): """ Subclass of seashell.core module. @@ -68,32 +90,39 @@ def patch_ipa(self, path: str) -> None: with alive_bar(monitor=False, stats=False, ctrl_c=False, receipt=False, title="Patching {}".format(path)) as _: - shutil.unpack_archive(path, format='zip') - app_files = [file for file in os.listdir('Payload') if file.endswith('.app')] + with tempfile.TemporaryDirectory() as tmp_dir: + _safe_extract_zip(path, tmp_dir) + payload = os.path.join(tmp_dir, 'Payload') + + if not os.path.isdir(payload): + return - if not app_files: - return + app_files = [file for file in os.listdir(payload) if file.endswith('.app')] + if not app_files: + return - bundle = '/'.join(('Payload', app_files[0] + '/')) - executable = self.get_executable(bundle + 'Info.plist') + bundle = os.path.join(payload, app_files[0]) + plist_path = os.path.join(bundle, 'Info.plist') + executable = self.get_executable(plist_path) - self.patch_plist(bundle + 'Info.plist') + if not executable: + return - shutil.move(bundle + executable, bundle + executable + '.hooked') - shutil.copy(self.main, bundle + executable) - shutil.copy(self.mussel, bundle + 'mussel') + self.patch_plist(plist_path) - os.chmod(bundle + executable, 777) - os.chmod(bundle + 'mussel', 777) + shutil.move( + os.path.join(bundle, executable), + os.path.join(bundle, executable + '.hooked') + ) + shutil.copy(self.main, os.path.join(bundle, executable)) + shutil.copy(self.mussel, os.path.join(bundle, 'mussel')) - app = path[:-4] - os.remove(path) + os.chmod(os.path.join(bundle, executable), 0o777) + os.chmod(os.path.join(bundle, 'mussel'), 0o777) - os.mkdir(app) - shutil.move('Payload', app) - shutil.make_archive(path, 'zip', app) - shutil.move(path + '.zip', path) - shutil.rmtree(app) + os.remove(path) + shutil.make_archive(path, 'zip', tmp_dir, 'Payload') + shutil.move(path + '.zip', path) @staticmethod def get_executable(path: str) -> str: @@ -107,7 +136,7 @@ def get_executable(path: str) -> str: plist_data = plistlib.load(f) if 'CFBundleExecutable' in plist_data: - return plist_data['CFBundleExecutable'] + return _sanitize_executable(plist_data['CFBundleExecutable']) return '' diff --git a/seashell/core/ipa.py b/seashell/core/ipa.py index 1a7c3ec..d5c14dc 100644 --- a/seashell/core/ipa.py +++ b/seashell/core/ipa.py @@ -25,6 +25,8 @@ import os import shutil import plistlib +import tempfile +import zipfile from PIL import Image from alive_progress import alive_bar @@ -33,6 +35,26 @@ from seashell.lib.config import Config +def _safe_extract_zip(archive_path: str, extract_dir: str) -> None: + base = os.path.realpath(extract_dir) + with zipfile.ZipFile(archive_path) as zf: + for member in zf.namelist(): + target = os.path.realpath(os.path.join(base, member)) + if not target.startswith(base + os.sep): + raise RuntimeError("Unsafe path in archive") + zf.extractall(extract_dir) + + +def _sanitize_app_name(name: str) -> str: + safe = os.path.basename(name) + if os.path.altsep: + safe = safe.replace(os.path.altsep, '') + safe = safe.replace(os.path.sep, '') + if safe in ('', '.', '..'): + return '' + return safe + + class IPA(Config): """ Subclass of seashell.core module. @@ -65,7 +87,9 @@ def set_name(self, name: str, bundle_id: str) -> None: """ if name: - self.app_name = name.lower().title() + safe = _sanitize_app_name(name) + if safe: + self.app_name = safe.lower().title() if bundle_id: self.bundle_id = bundle_id @@ -182,19 +206,24 @@ def check_ipa(self, path: str) -> bool: with alive_bar(monitor=False, stats=False, ctrl_c=False, receipt=False, title="Checking {}".format(path)) as _: - shutil.unpack_archive(path, format='zip') - app_files = [file for file in os.listdir('Payload') if file.endswith('.app')] + with tempfile.TemporaryDirectory() as tmp_dir: + _safe_extract_zip(path, tmp_dir) + payload = os.path.join(tmp_dir, 'Payload') + + if not os.path.isdir(payload): + return False - if not app_files: - return + app_files = [file for file in os.listdir(payload) if file.endswith('.app')] + if not app_files: + return False - bundle = '/'.join(('Payload', app_files[0] + '/')) - hash = self.get_hash(bundle + 'Info.plist') + bundle = os.path.join(payload, app_files[0]) + hash = self.get_hash(os.path.join(bundle, 'Info.plist')) - if os.path.exists(bundle + 'mussel') and hash: - return True + if os.path.exists(os.path.join(bundle, 'mussel')) and hash: + return True - return False + return False @staticmethod def get_hash(path: str) -> str: diff --git a/seashell/modules/apple_ios/hook.py b/seashell/modules/apple_ios/hook.py index fd7d637..4a59f0c 100644 --- a/seashell/modules/apple_ios/hook.py +++ b/seashell/modules/apple_ios/hook.py @@ -92,6 +92,9 @@ def run(self, args): hook.patch_plist(self.plist) executable = hook.get_executable(self.plist) + if not executable: + self.print_error("Invalid executable path in Info.plist!") + return self.print_information(F"Executable to replace: {executable}") if not self.session.upload(self.plist, path + '/Info.plist'): @@ -140,4 +143,4 @@ def run(self, args): self.print_error(f"Failed to give permissions to mussel!") return - self.print_success(f"{args[1]} patched successfully!") \ No newline at end of file + self.print_success(f"{args[1]} patched successfully!") diff --git a/seashell/modules/apple_ios/photos.py b/seashell/modules/apple_ios/photos.py index 91a0217..c6c8230 100644 --- a/seashell/modules/apple_ios/photos.py +++ b/seashell/modules/apple_ios/photos.py @@ -48,17 +48,29 @@ def recursive_walk(self, remote_path, local_path): file_type = self.mode_type(hash.get('st_mode', 0)) path = file.get_string(TLV_TYPE_PATH) + name = os.path.split(path)[1] + if not self._safe_component(name): + file = result.get_tlv(TLV_TYPE_GROUP) + continue if file_type == 'file': self.session.download( - path, local_path + '/' + os.path.split(path)[1]) + path, local_path + '/' + name) elif file_type == 'directory': self.recursive_walk( - path, local_path + '/' + os.path.split(path)[1]) + path, local_path + '/' + name) file = result.get_tlv(TLV_TYPE_GROUP) + @staticmethod + def _safe_component(name): + if not name or name in ('.', '..'): + return False + if os.path.sep in name or (os.path.altsep and os.path.altsep in name): + return False + return True + def run(self, args): if args[1] == 'icloud': path = '/var/mobile/Media/PhotoData/CPLAssets' diff --git a/seashell/modules/apple_ios/unhook.py b/seashell/modules/apple_ios/unhook.py index 6aa30ee..0b50302 100644 --- a/seashell/modules/apple_ios/unhook.py +++ b/seashell/modules/apple_ios/unhook.py @@ -91,6 +91,9 @@ def run(self, args): hook.patch_plist(self.plist, revert=True) executable = hook.get_executable(self.plist) + if not executable: + self.print_error("Invalid executable path in Info.plist!") + return if not self.session.upload(self.plist, path + '/Info.plist'): self.print_error("Failed to upload Info.plist!")